原创

Java WebSocket 结合 Nginx 实现域名及 WSS 协议访问

由于安全需要,最近要对已存在的老项目进行协议升级,HTTP升级 https.
其中涉及到socket这块,总结下整个的改造过程。比较简单。

什么是 WebSocket

现在,很多网站为了实现推送技术,所用的技术都是轮询。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。
在这种情况下,HTML5定义了WebSocket协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。
WebSocket一种在单个 TCP 连接上进行全双工通讯的协议。使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

以上信息摘自维基百科

WebSocket 就是减小客户端与服务器端建立连接的次数,减小系统资源开销,只需要一次 HTTP 握手,整个通讯过程是建立在一次连接/状态中,也就避免了HTTP的非状态性,服务端会一直与客户端保持连接,直到你关闭请求,同时由原本的客户端主动询问,转换为服务器有信息的时候推送。当然,它还能做实时通信、更好的二进制支持、支持扩展、更好的压缩效果等这些优点。

很赞的知乎科普文,风格易懂。WebSocket 是什么原理?为什么可以实现持久连接?

ws 和 wss 是什么

Websocket使用 wswss 的统一资源标志符,类似于 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 请求一样.
file

因此,如果是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 状态码, 也就是成功了。
file

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

正文到此结束
广告是为了更好的提供数据服务
本文目录