websocket心跳重連 websocket-heartbeat-js


 

初探和實現websocket心跳重連(npm: websocket-heartbeat-js)

心跳重連緣由

 

websocket是前后端交互的長連接,前后端也都可能因為一些情況導致連接失效並且相互之間沒有反饋提醒。因此為了保證連接的可持續性和穩定性,websocket心跳重連就應運而生。

在使用原生websocket的時候,如果設備網絡斷開,不會立刻觸發websocket的任何事件,前端也就無法得知當前連接是否已經斷開。這個時候如果調用websocket.send方法,瀏覽器才會發現鏈接斷開了,便會立刻或者一定短時間后(不同瀏覽器或者瀏覽器版本可能表現不同)觸發onclose函數。

后端websocket服務也可能出現異常,造成連接斷開,這時前端也並沒有收到斷開通知,因此需要前端定時發送心跳消息ping,后端收到ping類型的消息,立馬返回pong消息,告知前端連接正常。如果一定時間沒收到pong消息,就說明連接不正常,前端便會執行重連。

為了解決以上兩個問題,以前端作為主動方,定時發送ping消息,用於檢測網絡和前后端連接問題。一旦發現異常,前端持續執行重連邏輯,直到重連成功。

 

 

如何實現

在websocket實例化的時候,我們會綁定一些事件:

復制代碼
var ws = new WebSocket(url);
ws.onclose = function () {
    //something
};
ws.onerror = function () {
    //something
};
        
ws.onopen = function () {
   //something
};
ws.onmessage = function (event) {
   //something
}
復制代碼

如果希望websocket連接一直保持,我們會在close或者error上綁定重新連接方法。

復制代碼
ws.onclose = function () {
    reconnect();
};
ws.onerror = function () {
    reconnect();
};
    
復制代碼

這樣一般正常情況下失去連接時,觸發onclose方法,我們就能執行重連了。

 

那么針對斷網情況的心跳重連,怎么實現呢,我們只需要定時的發送消息,去觸發websocket.send方法,如果網絡斷開了,瀏覽器便會觸發onclose。

簡單的實現:

復制代碼
var heartCheck = {
    timeout: 60000,//60ms
    timeoutObj: null,
    reset: function(){
        clearTimeout(this.timeoutObj);
     this.start(); }, start: function(){ this.timeoutObj = setTimeout(function(){ ws.send("HeartBeat"); }, this.timeout) } } ws.onopen = function () { heartCheck.start(); };
ws.onmessage = function (event) {
    heartCheck.reset();
}
復制代碼

如上代碼,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秒之后會判定后端主動斷開了。再改造下代碼:

 

復制代碼
var heartCheck = {
    timeout: 60000,//60ms
    timeoutObj: null,
    serverTimeoutObj: null,
    reset: function(){
        clearTimeout(this.timeoutObj);
        clearTimeout(this.serverTimeoutObj);
     this.start();
    },
    start: function(){
        var self = this;
        this.timeoutObj = setTimeout(function(){
            ws.send("HeartBeat");
            self.serverTimeoutObj = setTimeout(function(){
                ws.close();//如果onclose會執行reconnect,我們執行ws.close()就行了.如果直接執行reconnect 會觸發onclose導致重連兩次
            }, self.timeout)
        }, this.timeout)
    },
}

ws.onopen = function () {
   heartCheck.start();
};
ws.onmessage = function (event) {
    heartCheck.reset();
}
ws.onclose = function () {
    reconnect();
};
ws.onerror = function () {
    reconnect();
};
 
復制代碼

 

PS:

    因為目前我們這種方式會一直重連如果沒連接上或者斷連的話,如果有兩個設備同時登陸並且會踢另一端下線,一定要發送一個踢下線的消息類型,這邊接收到這種類型的消息,邏輯判斷后就不再執行reconnect,否則會出現一只相互擠下線的死循環。

 

結語

由於斷開等原因可能會導致發送的數據沒有發送出去,要保證數據不丟失的話,可以做消息回執,也就是a給b發送消息id=1,b返回收到id=1的消息,如果沒有回執a可以再次發送消息id=1。

由上文可以看到,我們使用了前端發送ping,后端返回pong的這樣一種心跳的方式。也有一種方式是后端主動發送心跳,前端判斷是否超時。因為ws鏈接必須是前端主動請求建立連接,因此重連肯定是給前端來做,所以判斷重連邏輯都是寫在前端。

上面所說第二種方式是讓服務端發送心跳,前端來接收,這樣的方式會多節約一點帶寬,因為如果是前端發送心跳,后端需要返回心跳,也就是ping pong的過程會有兩次數據傳遞。  而后端來發送心跳的話,就只需要發送ping,前端不需要回應。但是這樣造成了一個問題。前端需要和后端約定好心跳間隔,比如后端設置10秒發送一次心跳,那前端就需要設置一個安全值,比如距離上次收到心跳超過12秒還沒收到下一個心跳就重連。這種方式的問題在於調節時間就變得不那么靈活了,需要雙方都同時確定一個時間約定。后端的邏輯也會比較多一點。
而如果前端來發送ping 后端返回pong的話,那么間隔時間就只需要前端自己控制了。加上我的代碼把收到的任何后端信息都可以當作是連接正常,從而重置心跳時間,這樣也節約了一些請求次數。
使用我這樣的方式,后端比較輕松,只需要在 onmessage 寫一段代碼,大概如下:

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:

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

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

npm install websocket-heartbeat-js

import

import WebsocketHeartbeatJs from 'websocket-heartbeat-js';
let websocketHeartbeatJs = new WebsocketHeartbeatJs({
    url: 'ws://xxxxxxx'
});
websocketHeartbeatJs.onopen = function () {
    console.log('connect success');
    websocketHeartbeatJs.send('hello server');
}
websocketHeartbeatJs.onmessage = function (e) {
    console.log(`onmessage: ${e.data}`);
}
websocketHeartbeatJs.onreconnect = function () {
    console.log('reconnecting...');
}

use script

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

API

websocketHeartbeatJs.ws (WebSocket)

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

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
const options = {
    url: 'ws://xxxx',
    pingTimeout: 15000, 
    pongTimeout: 10000, 
    reconnectTimeout: 2000,
    pingMsg: "heartbeat"
}
let websocketHeartbeatJs = new WebsocketHeartbeatJs(options);

websocketHeartbeatJs.send(msg) (function)

Send the message to the back-end service

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)

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

websocketHeartbeatJs.onerror (function)

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

websocketHeartbeatJs.onopen (function)

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

websocketHeartbeatJs.onmessage (function)

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

websocketHeartbeatJs.onreconnect (function)

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

demo

demo show

blog

初探和實現websocket心跳重連

 

 

 

 

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

5.5.2. Ping



   The Ping frame contains an opcode of 0x9.

   A Ping frame MAY include "Application data".

   Upon receipt of a Ping frame, an endpoint MUST send a Pong frame in
   response, unless it already received a Close frame.  It SHOULD
   respond with Pong frame as soon as is practical.  Pong frames are
   discussed in Section 5.5.3.

   An endpoint MAY send a Ping frame any time after the connection is
   established and before the connection is closed.

   NOTE: A Ping frame may serve either as a keepalive or as a means to
   verify that the remote endpoint is still responsive.

5.5.3. Pong

 The Pong frame contains an opcode of 0xA. Section 5.5.2 details requirements that apply to both Ping and Pong frames. A Pong frame sent in response to a Ping frame must have identical "Application data" as found in the message body of the Ping frame being replied to. If an endpoint receives a Ping frame and has not yet sent Pong frame(s) in response to previous Ping frame(s), the endpoint MAY elect to send a Pong frame for only the most recently processed Ping 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:

var ws = new WebSocket('ws://www.example.com/ws/foobar?subscribe-broadcast'); var heartbeat_msg = '--heartbeat--', heartbeat_interval = null, missed_heartbeats = 0; function on_open() { // ... // other code which has to be executed after the client // connected successfully through the websocket // ... if (heartbeat_interval === null) { missed_heartbeats = 0; heartbeat_interval = setInterval(function() { try { missed_heartbeats++; if (missed_heartbeats >= 3) throw new Error("Too many missed heartbeats."); ws.send(heartbeat_msg); } catch(e) { clearInterval(heartbeat_interval); heartbeat_interval = null; console.warn("Closing connection. Reason: " + e.message); ws.close(); } }, 5000); } } 

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:

function on_message(evt) { if (evt.data === heartbeat_msg) { // reset the counter for missed heartbeats missed_heartbeats = 0; return; } // ... // code to further process the received message // ... } 

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:

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.

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM