服务器推送技术--spring websocket + 心跳保活重连 简易聊天室demo(二)

上一篇文章简单说了spring websocket搭建简易聊天室demo的过程,spring的文档比较详细因此过程比较顺利.

这篇文章主要记录一下对心跳保活和断线重连的一点思考和尝试,不保证观点的正确性,如有发现错误,希望能留言指教。

具体代码实现见 index.js

重连

一般断线时都会触发websocket的onclose方法,因此,只需在此方法中重新发起一个websocket连接即可

        websocket.onclose = function (event) {
            document.getElementById("info").innerHTML = "连接关闭,10秒后重新连接……";
            console.log("Disconnected from WebSocket server. It will reconnect after 10 seconds...");
            // 10秒后重新连接,实际效果:每10秒重连一次,直到连接成功
            setTimeout(function () {
                connect(url);
            }, 10000);
        };

注意:最好不要在onerror事件中进行重连,若此时进行重连,原来的连接可能还来不及close,随着时间积累会有越来越多未close的连接,一旦网络通信恢复了正常,该页面会同时存在很多个重复的websocket连接,占用大量服务器资源。

而在实际环境中,可能会出现如物理性的断网(拔网线),这时候websocket的连接已经断开,却不会触发浏览器执行onclose方法,我们无法知道是否断开连接,也就无法进行重连操作。因此,需要用到心跳检测重连。

websocket规范定义了心跳机制,一方可以通过发送ping(协议头opcode 0x9)消息给另一方,另一方收到ping后应该按照协议要求,尽可能快的返回pong(协议头opcode 0xA);

然而,各个浏览器并没有为websocket的发送/接收ping/pong消息提供JavaScript API,但各个浏览器都有各自的缺省检测方案和处理方法,如chrome一旦收到ping包,会马上自动答复一个相同的包作为pong,再如某些浏览器在一段时间没有发送数据传输时,会自动发送一个ping包到服务器检测是否还在线/保持连接。因此,完善的机制需要开发者自己实现并且对各个浏览器进行测试和调试,这里只针对chrome62的情况进行尝试:

Java服务端向客户端发送一条pingMessage:

byte[] bs = new byte[1];
bs[0] = 'i';
ByteBuffer byteBuffer = ByteBuffer.wrap(bs);
PingMessage pingMessage = new PingMessage(byteBuffer);
webSocketSession.sendMessage(pingMessage);
System.out.println("已发送一个ping包:【" + pingMessage.toString() + "】");

则chrome会自动答复一个【java.nio.HeapByteBuffer[pos=0 lim=1 cap=1]】,且关于收到这条pingMessage的信息不会显示在控制台中;其它浏览器未测试。

回到客户端的断线重连检测,设计方案如下:

打开websocket连接的同时,启动 heartcheck机制,每次收到服务器的推送(说明此时连接正常),将计时器归零。当计时器到达timeout时,主动向服务器发起一个heartbeat,若得不到答复,则说明连接已经断开,采取相应措施:重连或者关闭连接。

        /**
         * 心跳检测
         * 若30秒内没有接收到任何来自服务器的信息,则向服务器发起一个ping包
         * @type {{timeout: number, timeoutObj: null, serverTimeoutObj: null, reset: reset, start: start}}
         */
        var heartCheck = {
            timeout: 20000, //计时器设定为20s
            timeoutObj: null,
            serverTimeoutObj: null,
            reset: function() {
                clearTimeout(this.timeoutObj);
                clearTimeout(this.serverTimeoutObj);
                this.start();
            },
            start: function() {
                var self = this;
                this.timeoutObj = setTimeout(function() {
                    //向服务器发送ping消息
                    pingToServer();
                    //计算答复的超时时间
                    self.serverTimeoutObj = setTimeout(function() {
                        //如果调用onclose会执行reconnect,导致重连两次,因此直接调用close()关闭连接
                        websocket.close();
                    }, self.timeout);
                }, this.timeout);
            }
        };

        websocket.onclose = function (event) {
            document.getElementById('chat').onkeydown = null;

            if (curTryNum <= maxTryNum) {
                document.getElementById("state-info").innerHTML = "连接关闭,10秒后重新连接……";
                console.log("Disconnected from WebSocket server. It will reconnect after 10 seconds...");


                // 10秒后重新连接,实际效果:每10秒重连一次,直到连接成功
                setTimeout(function () {
                    connect(url);
                }, 10000);
            } else {
                document.getElementById("state-info").innerHTML = "连接关闭,且已超过最大重连次数,不再重连";
                console.log("Disconnected from WebSocket server. It won't reconnect anymore");
            }


        };

        websocket.onmessage = function(message) {
            // 无论收到什么信息,说明当前连接正常,将心跳检测的计时器重置
            heartCheck.reset();


            console.log("client received a message.data: " + message.data);
            if (message.data !== "hb_ok") {
                // 不要将ping的答复信息输出
                output.log(message.data);
            }


        };

保活

websocket长连接有默认的超时时间(1分钟),也就是说,超过一定的时间客户端和服务器之间没有发生任何消息传输,连接会自动断开;除此之外,服务器或防火墙一般也会在一段时间不活动并超时之后终止外部的长连接。因此,若需要使客户端一直保持连接,就需要设置心跳保活机制了。

疑问和思考

WebSocket底层的工作/实现都是基于TCP协议,所以连接的保活机制是跟TCP一样的,就是通过”TCP Keep-Alive”心跳包来保证连接始终处于有效状态。
这里写图片描述
(图片来自https://www.cnblogs.com/wilber2013/p/4792788.html

那么疑问来了:
    既然有TCP的keep-alive保活机制,为什么还需要我们再去实现?为什么websocket还会超过一分钟就断开?
思考:
    TCP的keep-alive保活机制是为了确认端到端的连接是否都仍然可达,用以通知浏览器和服务器这个连接链路出了问题了,从而触发onclose/onerror和服务器的相应函数;
    而在相对高层的websocket中还定义了超时时间,就不是为了检测该连接是否还可达,而是为了不要让无用连接占用带宽–某些连接虽然两端的连接没问题,但事实上在应用层暂时/已经没有作用了,完全可以先关闭掉。

心跳保活机制设计

    回到应用层websocket心跳保活机制的设计:
    首先想到的可能是在此连接被关闭后,在onclose中进行重连,但这就是上一节的内容了,本节的目的是保活,也就是要不让连接关闭。
    因此,最直接的保活机制可以这样:
    让浏览器每隔一定时间(要小于超时时间)发送一个心跳信息,也就是说让此websocket连接通路上有消息传输发生即可:

window.setInterval(function(){ //每隔5秒钟发送一次心跳
    if (websocket.readyState == WebSocket.OPEN) {
        websocket.send(‘ ’);
    }
},5000);

    这样的做法优点是逻辑简单,但缺点是可能会导致心跳消息频繁发送甚至淹没了真正要传输的消息,同时也浪费了很多网络流量,对移动设备太不友好。

实际上,每当客户端收到一个消息,无论这个消息是什么性质的,都足以证明此时的连接是有效存活的;
因此,可以设计另一种保活机制,能对客户端状态进行检测,决定发心跳包时机的保活机制,搭配断线重连,该机制的逻辑流程图如下:

  • 3
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,下面是一个简单的 Spring Boot WebSocket 入门 demo: 1. 首先,在 pom.xml 文件中引入 `spring-boot-starter-websocket` 依赖: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> ``` 2. 创建一个简单的 WebSocket 处理器: ```java @Component public class WebSocketHandler extends TextWebSocketHandler { private final List<WebSocketSession> sessions = new CopyOnWriteArrayList<>(); @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { sessions.add(session); session.sendMessage(new TextMessage("连接成功!")); } @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { for (WebSocketSession webSocketSession : sessions) { webSocketSession.sendMessage(new TextMessage("客户端说:" + message.getPayload())); } } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { sessions.remove(session); } } ``` 3. 创建 WebSocket 配置类: ```java @Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Autowired private WebSocketHandler webSocketHandler; @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(webSocketHandler, "/ws").setAllowedOrigins("*"); } } ``` 4. 编写一个简单的页面来测试 WebSocket: ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>WebSocket</title> </head> <body> <h1>WebSocket Demo</h1> <div> <input type="text" id="input"/> <button onclick="send()">发送</button> </div> <div id="output"></div> <script> var socket = new WebSocket("ws://localhost:8080/ws"); socket.onmessage = function(event) { var output = document.getElementById("output"); output.innerHTML += "<p>" + event.data + "</p>"; }; function send() { var input = document.getElementById("input"); socket.send(input.value); input.value = ""; } </script> </body> </html> ``` 5. 运行程序,访问 http://localhost:8080/index.html,打开浏览器控制台,输入命令 `socket.send("Hello, WebSocket!")`,即可看到页面上显示出 "客户端说:Hello, WebSocket!"。 希望这个 demo 能帮助到你。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值