Java WebSocket 结合 Nginx 实现域名及 WSS 协议访问
由于安全需要,最近要对已存在的老项目进行协议升级,HTTP升级 https.
其中涉及到socket这块,总结下整个的改造过程。比较简单。
什么是 WebSocket
现在,很多网站为了实现推送技术,所用的技术都是轮询。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。
在这种情况下,HTML5定义了WebSocket协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。
WebSocket一种在单个 TCP 连接上进行全双工通讯的协议。使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
以上信息摘自维基百科
WebSocket 就是减小客户端与服务器端建立连接的次数,减小系统资源开销,只需要一次 HTTP 握手,整个通讯过程是建立在一次连接/状态中,也就避免了HTTP的非状态性,服务端会一直与客户端保持连接,直到你关闭请求,同时由原本的客户端主动询问,转换为服务器有信息的时候推送。当然,它还能做实时通信、更好的二进制支持、支持扩展、更好的压缩效果等这些优点。
很赞的知乎科普文,风格易懂。WebSocket 是什么原理?为什么可以实现持久连接?
ws 和 wss 是什么
Websocket使用 ws
或 wss
的统一资源标志符,类似于 HTTP 或 HTTPS,其中 wss 表示在 TLS 之上的 Websocket ,相当于 HTTPS 了。
ws://a.b.com/c/d
wss://a.b.com/c/d
默认情况下,Websocket 的 ws 协议使用 80 端口;
运行在TLS之上时,wss 协议默认使用 443 端口。
wss 就是 ws 基于 SSL 的安全传输,与 HTTPS 一样样的道理。
如果你的网站是 HTTPS 协议的,那你就不能使用 ws:// 了,浏览器会 block 掉连接,和 HTTPS 下不允许 HTTP 请求一样.
因此,如果是https的域名,那就必须要使用 wss:\\
安全协议。 仅仅单纯的更换前缀是无效的。
WebSocket connection to 'wss://IP地址:端口号/websocket' failed: Error in connection establishment: net::ERR_SSL_PROTOCOL_ERROR
我们的改造,其实也源于此。
改造开始,socket 源代码部分
maven 依赖
<!-- websocket dependency -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
Java 服务端 开启WebSocket支持
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* 开启WebSocket支持
*/
@Configuration
public class WebSocketConfig {
/**
* 注入对象ServerEndpointExporter,这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint
*
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
注册发送消息的类
/**
* 发送消息的
*/
@ServerEndpoint(value = "/...../{account}/{userId}")
@Component
public class WebSocketServer {
@PostConstruct
public void init() {
}
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session) throws IOException {
addOnlineCount(); //在线数加1
log.debug("连接成功, 有新窗口开始监听,当前在线人数为" + getOnlineCount());
// 私有方法,自行定义
sendMessage(...);
//触发链接事件
//.........
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose(Session session) {
subOnlineCount(); //在线数减1
log.debug("连接关闭,有一连接关闭!当前在线人数为" + getOnlineCount());
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, Session session) {
log.info("websocket recv,sessionId:{},key:{},message:{}", session.getId(), key,message);
}
/**
* 出现错误
*
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
String key = sessionIdMap.get(session.getId());
log.info("websocket onError,sessionId:{},key:{},message:{}", session.getId(), key,error.getMessage());
}
}
OPTIONS
@ServerEndpoint
通过这个注解就可以知道你暴露出去的 ws 应用的路径,有点类似我们经常用的@RequestMapping。比如你的启动端口是8080,而这个注解的值是ws,那我们就可以通过 ws://127.0.0.1:8080/ws 来连接你的应用@OnOpen
当 websocket 建立连接成功后会触发这个注解修饰的方法,注意它有一个 Session 参数@OnClose
当 websocket 建立的连接断开后会触发这个注解修饰的方法,注意它有一个 Session 参数@OnMessage
当客户端发送消息到服务端时,会触发这个注解修改的方法,它有一个 String 入参表明客户端传入的值@OnError
当 websocket 建立连接时出现异常会触发这个注解修饰的方法,注意它有一个 Session 参数
服务端如何发送消息给客户端
服务端发送消息必须通过上面说的 Session 类,通常是在@OnOpen 方法中,当连接成功后把 session 存入 Map 的 value,key 是与 session 对应的用户标识,当要发送的时候通过 key 获得 session 再发送.
这里可以通过
session.getBasicRemote_().sendText(_)
来对客户端发送消息。
对外服务地址
ws 对应 http 服务
此方式不需要特别的配置。
不论是采用ip还是域名,均可以直接使用
wss 对应 https 服务
建议采用域名的形式,对外提供服务。
当然,也可以在项目内通过tomcat TomcatServletWebServerFactory 或者 jetty 封装ssl协议的方式,直接以ip的形式提供https 服务。
参考文章
Nginx 配置域名支持 WSS
直接在配置 HTTPS 域名位置新增如下配置即可:
location /xxx {
......
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
......
}
拿域名再次连接试一下,看到 101 状态码, 也就是成功了。
Nginx 配置的说明
Nginx 自从 1.3 版本就开始支持 WebSocket 了,并且可以为 WebSocket 应用程序做反向代理和负载均衡。
WebSocket 和 HTTP 协议不同,但是 WebSocket 中的握手和 HTTP 中的握手兼容,它使用 HTTP 中的 Upgrade 协议头将连接从 HTTP 升级到 WebSocket,当客户端发过来一个 Connection: Upgrade
请求头时,Nginx 是不知道的,所以,当 Nginx 代理服务器拦截到一个客户端发来的 Upgrade
请求时,需要显式来设置 Connection Upgrade
头信息,并使用 101(交换协议)返回响应,在客户端和代理服务器、后端服务器之间建立隧道来支持 WebSocket。
WebSockets 仍然受到 Nginx 缺省为60秒的 proxy_read_timeout 的影响。
这意味着,如果你有一个程序使用了 WebSockets,但又可能超过60秒不发送任何数据的话,那你要么需要增加超时时间,要么实现一个 ping 的消息以保持联系。
使用 ping 的解决方法有额外的好处,可以发现连接是否被意外关闭。
Nginx 官方文档:《Nginx 官方文档》
~end