初探和实现websocket心跳重连(npm: websocket-heartbeat-js)

心跳重连缘由

websocket是前后端交互的长连接,前后端也都可能因为一些情况导致连接失效并且相互之间没有反馈提醒。因此为了保证连接的可持续性和稳定性,websocket心跳重连就应运而生。

在使用原生websocket的时候,如果设备网络断开,不会立刻触发websocket的任何事件,前端也就无法得知当前连接是否已经断开。这个时候如果调用websocket.send方法,浏览器才会发现链接断开了,便会立刻或者一定短时间后(不同浏览器或者浏览器版本可能表现不同)触发onclose函数。

后端websocket服务也可能出现异常,造成连接断开,这时前端也并没有收到断开通知,因此需要前端定时发送心跳消息ping,后端收到ping类型的消息,立马返回pong消息,告知前端连接正常。如果一定时间没收到pong消息,就说明连接不正常,前端便会执行重连。

为了解决以上两个问题,以前端作为主动方,定时发送ping消息,用于检测网络和前后端连接问题。一旦发现异常,前端持续执行重连逻辑,直到重连成功。

如何实现

在websocket实例化的时候,我们会绑定一些事件:

  1. var ws = new WebSocket(url);
  2. ws.onclose = function () {
  3. //something
  4. };
  5. ws.onerror = function () {
  6. //something
  7. };
  8.  
  9. ws.onopen = function () {
  10. //something
  11. };
  12. ws.onmessage = function (event) {
  13. //something
  14. }

如果希望websocket连接一直保持,我们会在close或者error上绑定重新连接方法。

  1. ws.onclose = function () {
  2. reconnect();
  3. };
  4. ws.onerror = function () {
  5. reconnect();
  6. };

这样一般正常情况下失去连接时,触发onclose方法,我们就能执行重连了。

那么针对断网情况的心跳重连,怎么实现呢,我们只需要定时的发送消息,去触发websocket.send方法,如果网络断开了,浏览器便会触发onclose。

简单的实现:

  1. var heartCheck = {
  2. timeout: 60000,//60ms
  3. timeoutObj: null,
  4. reset: function(){
  5. clearTimeout(this.timeoutObj);
         this.start();
  6. },
  7. start: function(){
  8. this.timeoutObj = setTimeout(function(){
  9. ws.send("HeartBeat");
  10. }, this.timeout)
  11. }
  12. }
  13.  
  14. ws.onopen = function () {
  15. heartCheck.start();
  16. };
  1. ws.onmessage = function (event) {
  2. heartCheck.reset();
  3. }

如上代码,heartCheck 的 reset和start方法主要用来控制心跳的定时。

什么条件下执行心跳:

当onopen也就是连接成功后,我们便开始start计时,如果在定时时间范围内,onmessage获取到了后端的消息,我们就重置倒计时,

距离上次从后端获取到消息超过60秒之后,执行心跳检测,看是不是断连了,这个检测时间可以自己根据自身情况设定。

判断前端websocket断开(断网但不限于断网的情况):

当心跳检测执行send方法之后,如果当前websocket是断开状态(或者说断网了),发送超时之后,浏览器的websocket会自动触发onclose方法,重连就会立刻执行(onclose方法体绑定了重连事件),如果当前一直是断网状态,重连会2秒(时间是自己代码设置的)执行一次直到网络正常后连接成功。

如此一来,判断前端断开websocket的心跳检测就实现了。为什么说是前端主动断开,因为当前这种情况主要是通过前端websocket.send来检测并触发的onclose,后面说后端断开的情况。

我本想测试websocket超时时间,又发现了一些新的问题

1. 在chrome中,如果心跳检测 也就是websocket实例执行send之后,15秒内没发送到另一接收端,onclose便会执行。那么超时时间是15秒。

2. 我又打开了Firefox ,Firefox在断网7秒之后,直接执行onclose。说明在Firefox中不需要心跳检测便能自动onclose。

3.  同一代码, reconnect方法 在chrome 执行了一次,Firefox执行了两次。当然我们在几处地方(代码逻辑处和websocket事件处)绑定了reconnect(),

所以保险起见,我们还是给reconnect()方法加上一个锁,保证只执行一次

目前来看不同的浏览器,有不同的机制,无论浏览器websocket自身会不会在断网情况下执行onclose,加上心跳重连后,已经能保证onclose的正常触发。  其实这是由于socket本身就有底层的心跳,socket消息发送不出去的时候,会等待一定时间看是否能在这个时间之内再次连接上,如果超时便会触发onclose。

判断后端断开:

如果后端因为一些情况断开了ws,是可控情况下的话,会下发一个断连的通知,这样会触发前端weboscket的onclose方法,我们便会重连。

如果因为一些异常断开了连接,前端是不会感应到的,所以如果前端发送了心跳一定时间之后,后端既没有返回心跳响应消息,前端也没有收到任何其他消息的话,我们就能断定后端发生异常断开了。

一点特别重要的发送心跳到后端,后端收到消息之后必须返回消息,否则超过60秒之后会判定后端主动断开了。再改造下代码:

  1. var heartCheck = {
  2. timeout: 60000,//60ms
  3. timeoutObj: null,
  4. serverTimeoutObj: null,
  5. reset: function(){
  6. clearTimeout(this.timeoutObj);
  7. clearTimeout(this.serverTimeoutObj);
  8.      this.start();
  9. },
  10. start: function(){
  11. var self = this;
  12. this.timeoutObj = setTimeout(function(){
  13. ws.send("HeartBeat");
  14. self.serverTimeoutObj = setTimeout(function(){
  15. ws.close();//如果onclose会执行reconnect,我们执行ws.close()就行了.如果直接执行reconnect 会触发onclose导致重连两次
  16. }, self.timeout)
  17. }, this.timeout)
  18. },
  19. }
  20.  
  21. ws.onopen = function () {
  22. heartCheck.start();
  23. };
  24. ws.onmessage = function (event) {
  25. heartCheck.reset();
  26. }
  1. ws.onclose = function () {
  2. reconnect();
  3. };
  4. ws.onerror = function () {
  5. reconnect();
  6. };
  1.  

PS:

因为目前我们这种方式会一直重连如果没连接上或者断连的话,如果有两个设备同时登陆并且会踢另一端下线,一定要发送一个踢下线的消息类型,这边接收到这种类型的消息,逻辑判断后就不再执行reconnect,否则会出现一只相互挤下线的死循环。

结语

由于断开等原因可能会导致发送的数据没有发送出去,要保证数据不丢失的话,可以做消息回执,也就是a给b发送消息id=1,b返回收到id=1的消息,如果没有回执a可以再次发送消息id=1。

由上文可以看到,我们使用了前端发送ping,后端返回pong的这样一种心跳的方式。也有一种方式是后端主动发送心跳,前端判断是否超时。因为ws链接必须是前端主动请求建立连接,因此重连肯定是给前端来做,所以判断重连逻辑都是写在前端。

上面所说第二种方式是让服务端发送心跳,前端来接收,这样的方式会多节约一点带宽,因为如果是前端发送心跳,后端需要返回心跳,也就是ping pong的过程会有两次数据传递。  而后端来发送心跳的话,就只需要发送ping,前端不需要回应。但是这样造成了一个问题。前端需要和后端约定好心跳间隔,比如后端设置10秒发送一次心跳,那前端就需要设置一个安全值,比如距离上次收到心跳超过12秒还没收到下一个心跳就重连。这种方式的问题在于调节时间就变得不那么灵活了,需要双方都同时确定一个时间约定。后端的逻辑也会比较多一点。
而如果前端来发送ping 后端返回pong的话,那么间隔时间就只需要前端自己控制了。加上我的代码把收到的任何后端信息都可以当作是连接正常,从而重置心跳时间,这样也节约了一些请求次数。
使用我这样的方式,后端比较轻松,只需要在 onmessage 写一段代码,大概如下:

  1. if(msg=='heartbeat') socket.send(anything);

封装了一个npm包,欢迎使用

https://github.com/zimv/websocket-heartbeat-js

https://www.npmjs.com/package/websocket-heartbeat-js

Introduction

The websocket-heartbeat-js is base on WebSocket of browser javascript, whose main purpose is to ensure web client and server connection, and it has a mechanism of heartbeat detection and automatic reconnection. When client device has network outage or server error which causes websocket to disconnect, the program will automatically reconnect until reconnecting is successful again.

Why

When we use the native websocket, if network disconnects, any event function not be executed. So front-end program doesn't know that websocket was disconnected. But if program is now executing ***WebSocket.send()***, browser must discover that message signal is failed, so the onclose function will execute.

Back-end websocket service is likely to happen error, when websocket disconnected that front-end not notice message received. So need to send ping message by timeout. Server return pong message to client when server received ping message. Because received pong message, client know connection normal. If client not received pong message, it is connection abnormal, client will reconnect.

In summary, for solve above two problems. Client should initiative send ping message for check connect status.

How

1.close websocket connection

If websocket need to disconnect, client must execute ***WebsocketHeartbeatJs.close()***. If server wants to disconnect, it should send a close message to client. When client received close message that it to execute ***WebsocketHeartbeatJs.close()***.

Example:

  1. websocketHeartbeatJs.onmessage = (e) => {
  2. if(e.data == 'close') websocketHeartbeatJs.close();
  3. }

2.ping & pong

Server should to return pong message when the client sends a ping message. Pong message can be of any value. websocket-heartbeat-js will not handle pong message, instead it will only reset heartbeat after receiving any message, as receiving any message means that the connection is normal.

Usage

install

  1. npm install websocket-heartbeat-js

import

  1. import WebsocketHeartbeatJs from 'websocket-heartbeat-js';
  2. let websocketHeartbeatJs = new WebsocketHeartbeatJs({
  3. url: 'ws://xxxxxxx'
  4. });
  5. websocketHeartbeatJs.onopen = function () {
  6. console.log('connect success');
  7. websocketHeartbeatJs.send('hello server');
  8. }
  9. websocketHeartbeatJs.onmessage = function (e) {
  10. console.log(`onmessage: ${e.data}`);
  11. }
  12. websocketHeartbeatJs.onreconnect = function () {
  13. console.log('reconnecting...');
  14. }

use script

  1. <script src="./node_modules/websocket-heartbeat-js/dist/index.js"></script>
  2. let websocketHeartbeatJs = new window.WebsocketHeartbeatJs({
  3. url: 'ws://xxxxxxx'
  4. });

API

websocketHeartbeatJs.ws (WebSocket)

This websocketHeartbeatJs.ws is native Websocket instance. If you need more native Websocket features, operate the websocketHeartbeatJs.ws.

  1. websocketHeartbeatJs.ws == WebSocket(websocketHeartbeatJs.opts.url);

websocketHeartbeatJs.opts (Object)

Attribute required type default description
url true string none websocket service address
pingTimeout false number 15000 A heartbeat is sent every 15 seconds. If any backend message is received, the timer will reset
pongTimeout false number 10000 After the Ping message is sent, the connection will be disconnected without receiving the backend message within 10 seconds
reconnectTimeout false number 2000 The interval of reconnection
pingMsg false string "heartbeat" Ping message value
repeatLimit false number null The trial times of reconnection。default: unlimited
  1. const options = {
  2. url: 'ws://xxxx',
  3. pingTimeout: 15000,
  4. pongTimeout: 10000,
  5. reconnectTimeout: 2000,
  6. pingMsg: "heartbeat"
  7. }
  8. let websocketHeartbeatJs = new WebsocketHeartbeatJs(options);

websocketHeartbeatJs.send(msg) (function)

Send the message to the back-end service

  1. websocketHeartbeatJs.send('hello server');

websocketHeartbeatJs.close() (function)

The front end manually disconnects the websocket connection. This method does not trigger reconnection.

hook function and event function

websocketHeartbeatJs.onclose (function)

  1. websocketHeartbeatJs.onclose = () => {
  2. console.log('connect close');
  3. }

websocketHeartbeatJs.onerror (function)

  1. websocketHeartbeatJs.onerror = () => {
  2. console.log('connect onerror');
  3. }

websocketHeartbeatJs.onopen (function)

  1. websocketHeartbeatJs.onopen = () => {
  2. console.log('open success');
  3. }

websocketHeartbeatJs.onmessage (function)

  1. websocketHeartbeatJs.onmessage = (e) => {
  2. console.log('msg:', e.data);
  3. }

websocketHeartbeatJs.onreconnect (function)

  1. websocketHeartbeatJs.onreconnect = (e) => {
  2. console.log('reconnecting...');
  3. }

demo

demo show

blog

初探和实现websocket心跳重连

RFC 6455 - The WebSocket Protocol https://tools.ietf.org/html/rfc6455#section-5.5

5.5.2. Ping

  1.  
  2. The Ping frame contains an opcode of 0x9.
  3.  
  4. A Ping frame MAY include "Application data".
  5.  
  6. Upon receipt of a Ping frame, an endpoint MUST send a Pong frame in
  7. response, unless it already received a Close frame. It SHOULD
  8. respond with Pong frame as soon as is practical. Pong frames are
  9. discussed in Section 5.5.3.
  10.  
  11. An endpoint MAY send a Ping frame any time after the connection is
  12. established and before the connection is closed.
  13.  
  14. NOTE: A Ping frame may serve either as a keepalive or as a means to
  15. verify that the remote endpoint is still responsive.

5.5.3. Pong

  1.  
  2. The Pong frame contains an opcode of 0xA.
  3.  
  4. Section 5.5.2 details requirements that apply to both Ping and Pong
  5. frames.
  6.  
  7. A Pong frame sent in response to a Ping frame must have identical
  8. "Application data" as found in the message body of the Ping frame
  9. being replied to.
  10.  
  11. If an endpoint receives a Ping frame and has not yet sent Pong
  12. frame(s) in response to previous Ping frame(s), the endpoint MAY
  13. elect to send a Pong frame for only the most recently processed Ping
  14. frame.

Writing WebSocket servers - Web APIs | MDN https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers#Pings_and_Pongs_The_Heartbeat_of_WebSockets

Pings and Pongs: The Heartbeat of WebSockets

At any point after the handshake, either the client or the server can choose to send a ping to the other party. When the ping is received, the recipient must send back a pong as soon as possible. You can use this to make sure that the client is still connected, for example.

A ping or pong is just a regular frame, but it's a control frame. Pings have an opcode of 0x9, and pongs have an opcode of 0xA. When you get a ping, send back a pong with the exact same Payload Data as the ping (for pings and pongs, the max payload length is 125). You might also get a pong without ever sending a ping; ignore this if it happens.

If you have gotten more than one ping before you get the chance to send a pong, you only send one pong.

Closing the connection

To close a connection either the client or server can send a control frame with data containing a specified control sequence to begin the closing handshake (detailed in Section 5.5.1). Upon receiving such a frame, the other peer sends a Close frame in response. The first peer then closes the connection. Any further data received after closing of connection is then discarded.

Sending and receiving heartbeat messages — django-websocket-redis 0.5.2 documentation https://django-websocket-redis.readthedocs.io/en/latest/heartbeats.html

Sending and receiving heartbeat messages

The Websocket protocol implements so called PING/PONG messages to keep Websockets alive, even behind proxies, firewalls and load-balancers. The server sends a PING message to the client through the Websocket, which then replies with PONG. If the client does not reply, the server closes the connection.

The client part

Unfortunately, the Websocket protocol does not provide a similar method for the client, to find out if it is still connected to the server. This can happen, if the connection simply disappears without further notification. In order to have the client recognize this, some Javascript code has to be added to the client code responsible for the Websocket:

  1. var ws = new WebSocket('ws://www.example.com/ws/foobar?subscribe-broadcast');
  2. var heartbeat_msg = '--heartbeat--', heartbeat_interval = null, missed_heartbeats = 0;
  3.  
  4. function on_open() {
  5. // ...
  6. // other code which has to be executed after the client
  7. // connected successfully through the websocket
  8. // ...
  9. if (heartbeat_interval === null) {
  10. missed_heartbeats = 0;
  11. heartbeat_interval = setInterval(function() {
  12. try {
  13. missed_heartbeats++;
  14. if (missed_heartbeats >= 3)
  15. throw new Error("Too many missed heartbeats.");
  16. ws.send(heartbeat_msg);
  17. } catch(e) {
  18. clearInterval(heartbeat_interval);
  19. heartbeat_interval = null;
  20. console.warn("Closing connection. Reason: " + e.message);
  21. ws.close();
  22. }
  23. }, 5000);
  24. }
  25. }

The heartbeat message, here --heartbeat-- can be any magic string which does not interfere with your remaining logic. The best way to achieve this, is to check for that magic string inside the receive function, just before further processing the message:

  1. function on_message(evt) {
  2. if (evt.data === heartbeat_msg) {
  3. // reset the counter for missed heartbeats
  4. missed_heartbeats = 0;
  5. return;
  6. }
  7. // ...
  8. // code to further process the received message
  9. // ...
  10. }

The server part

The main loop of the Websocket server is idle for a maximum of 4 seconds, even if there is nothing to do. After that time interval has elapsed, this loop optionally sends a magic string to the client. This can be configured using the special setting:

  1. WS4REDIS_HEARTBEAT = '--heartbeat--'

The purpose of this setting is twofold. During processing, the server ignores incoming messages containing this magic string. Additionally the Websocket server sends a message with that magic string to the client, about every four seconds. The above client code awaits these messages, at least every five seconds, and if too many were not received, it closes the connection and tries to reestablish it.

By default the setting WS4REDIS_HEARTBEAT is None, which means that heartbeat messages are neither expected nor sent.

websocket心跳重连 websocket-heartbeat-js的更多相关文章

  1. 初探和实现websocket心跳重连

    心跳重连缘由 在使用websocket过程中,可能会出现网络断开的情况,比如信号不好,或者网络临时性关闭,这时候websocket的连接已经断开, 而浏览器不会执行websocket 的 onclos ...

  2. 初探和实现websocket心跳重连(npm: websocket-heartbeat-js)

    提示:文章最下方有仓库地址 心跳重连缘由 websocket是前后端交互的长连接,前后端也都可能因为一些情况导致连接失效并且相互之间没有反馈提醒.因此为了保证连接的可持续性和稳定性,websocket ...

  3. websocket 心跳重连

    websocket 的基本使用: var ws = new WebSocket(url); ws.onclose = function () { //something reconnect(); // ...

  4. 161114、websocket实现心跳重连

    心跳重连缘由 在使用websocket过程中,可能会出现网络断开的情况,比如信号不好,或者网络临时性关闭,这时候websocket的连接已经断开, 而浏览器不会执行websocket 的 onclos ...

  5. WebSocket心跳检测和重连机制

    1. 心跳重连原由 心跳和重连的目的用一句话概括就是客户端和服务端保证彼此还活着,避免丢包发生. websocket连接断开有以下两证情况: 前端断开 在使用websocket过程中,可能会出现网络断 ...

  6. 【简记】前端对接WebSocket与心跳重连

    前言 最近又在忙着开发别的模块,其中包含了即时通讯这一块,上一次做即时通讯时还是去年年底,一时间代码都在自己的笔记本里,还没带--这里就记录一下前端对接WebSocket的实现,包含心跳重连,简记之. ...

  7. js websocket断线重连

    js websocket断开重连实例代码,请根据自己需求做出相应改动Vue中使用websocket $(function() { var lockReconnect = false;//避免重复连接 ...

  8. 理解WebSocket心跳及重连机制(五)

    理解WebSocket心跳及重连机制 在使用websocket的过程中,有时候会遇到网络断开的情况,但是在网络断开的时候服务器端并没有触发onclose的事件.这样会有:服务器会继续向客户端发送多余的 ...

  9. vue使用 封装websocket心跳包

    ---恢复内容开始--- 这套代码可以拿过去直接用 一些注意我会在下面代码中加上注释: 谢谢支持 核心代码 //这里需要引入vuex import store from './store'; let ...

随机推荐

  1. sklearn: CountVectorize处理及一些使用参数

    sklearn: CountVectorize处理及一些使用参数 CountVectorizer是属于常见的特征数值计算类,是一个文本特征提取方法.对于每一个训练文本,它只考虑每种词汇在该训练文本中出 ...

  2. 记录第一次使用Vivado——以全加器为例子

    从altera转战xilinx,经典的FPGA到ZYNQ系列,第一站就是先熟悉编译软件Vivado.我就直接跳过软件安装部分了,如有疑问,可以在评论区提出来,我看到了就帮你解答. 首先是是打开界面 然 ...

  3. Spring循环依赖解决方式源码解析

    1. 什么是循环依赖? 循环依赖其实就是循环引用,也就是两个或则两个以上的bean互相持有对方,最终形成闭环.比如A依赖于B,B依赖于A我们直接上代码 先创建一个类ServiceA依赖于Service ...

  4. [leetcode]49. Group Anagrams重排列字符串分组

    是之前的重排列字符串的延伸,判断是重排列后存到HashMap中进行分组 这种HashMap进行分组的方式很常用 public List<List<String>> groupA ...

  5. Dreamoon Likes Coloring 【CF 1329 A】

    传送门 思路:"Dreamoon will choose a number pipi from range [1,n−li+1](inclusive) and will paint all ...

  6. 关于HashSet

    HashSet存储数据原理: 当HashSet调用add方法时,有返回值,返回值是boolean类型,表示是否添加成功(如果对象不存在,则添加成功,否则添加失败) 但是,添加的过程并不是一个个去遍历去 ...

  7. Net/NetCore/.NET5 ORM 六大查询体系 - SqlSugar 高级篇

    框架介绍 SqlSugar ORM是一款老牌国产ORM框架,生命力也比较顽强,从早期ORM不成熟阶段,一直存活到现在,我为什么要一直坚持,那是因为还有很多用户在使用,本来我能够较早推出新开源框架 ,可 ...

  8. Oracle RedoLog-基本概念和组成

    Oracle 数据库恢复操作最关键的依据就是 redo log,它记录了对数据库所有的更改操作.在研究如何提取 redolog 中 DML 操作的过程可谓一波三折,因为介绍 redolog 结构细节的 ...

  9. node使用xlsx导入导出excel

    1.安装和引入xlsx 安装  npm install xlsx 引入:let xlsx = require('xlsx');2.读取excel数据function readFile(file) {  ...

  10. 风炫安全Web安全学习第十节课 数字型的Sql注入

    数字型的Sql注入 风炫安全Web安全学习第十一节课 字符型和搜索型的sql注入 风炫安全Web安全学习第十二节课 mysql报错函数注入 风炫安全Web安全学习第十三节课 CUD类型的sql注入 风 ...