提示:文章最下方有倉庫地址前端
心跳重連原因git
websocket是先後端交互的長鏈接,先後端也均可能由於一些狀況致使鏈接失效而且相互之間沒有反饋提醒。所以爲了保證鏈接的可持續性和穩定性,websocket心跳重連就應運而生。github
在使用原生websocket的時候,若是設備網絡斷開,不會馬上觸發websocket的任何事件,前端也就沒法得知當前鏈接是否已經斷開。這個時候若是調用websocket.send方法,瀏覽器纔會發現連接斷開了,便會馬上或者必定短期後(不一樣瀏覽器或者瀏覽器版本可能表現不一樣)觸發onclose函數。web
後端websocket服務也可能出現異常,形成鏈接斷開,這時前端也並無收到斷開通知,所以須要前端定時發送心跳消息ping,後端收到ping類型的消息,立馬返回pong消息,告知前端鏈接正常。若是必定時間沒收到pong消息,就說明鏈接不正常,前端便會執行重連。chrome
爲了解決以上兩個問題,之前端做爲主動方,定時發送ping消息,用於檢測網絡和先後端鏈接問題。一旦發現異常,前端持續執行重連邏輯,直到重連成功。npm
如何實現後端
在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
那麼針對斷網狀況的心跳重連,怎麼實現呢,咱們只須要定時的發送消息,去觸發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);