banner
 Sayyiku

Sayyiku

Chaos is a ladder
telegram
twitter

WebSocket 心跳重連機制

WebSocket 是一種網絡通信協議,它使得客戶端和伺服器之間的數據交換變得更加簡單。最近在項目中使用 WebSocket 實現了一個簡單在線聊天室功能,在此探究下心跳重連的機制。

WebSocket#

WebSocket 允許伺服器主動向客戶端推送數據。之前很多網站為了實現推送技術,採用的技術都是輪詢,不僅效率低,也浪費了很多帶寬等資源。HTML5 定義了 WebSocket 協議,能更好的節省伺服器資源和帶寬,並且能夠更即時地進行通訊。

WebSocket 的優勢:

  • 較少的控制開銷
  • 更強的即時性
  • 保持連接狀態
  • 更好的二進制支持
  • 可以支持擴展
  • 更好的壓縮效果

WebSocket 最大的優勢就是能夠保持前後端消息的長連接,但是在某些情況下,長連接失效並不會得到及時的反饋,前端並不知道連接已斷開。例如用戶網絡斷開,並不會觸發 websocket 的任何事件函數,這個時候如果發送消息,消息便無法發送出去,瀏覽器會立刻或者一定短時間後(不同瀏覽器或者瀏覽器版本可能表現不同)觸發 onclose 函數。

為了避免這種情況,保證連接的穩定性,前端需要進行一定的優化處理,一般採用的方案就是心跳重連。前後端約定,前端按一定間隔發送一個心跳包,後端接收到心跳包後返回一個響應包,告知前端連接正常。如果一定時間內未接收到消息,則認為連接斷開,前端進行重連。

心跳重連#

通過以上分析,可以得到實現心跳重連的關鍵是按時發送心跳消息和檢測響應消息並判斷是否進行重連,所以首先設置 4 個小目標:

  • 可以按一定間隔發送心跳包
  • 連接錯誤或者關閉時能夠自動重連
  • 若在一定時間間隔內未接收消息,則視為斷連,自動進行重連
  • 可以自定義心跳消息並設置最大重連次數

0x01 初始化#

為了方便復用,這裡決定將 WebSocket 管理封裝為一個工具類 WebsocketHB,通過傳入配置對象來自定義心跳重連機制。

class WebsocketHB {
  constructor({
    url, // 連接客戶端地址
    pingTimeout = 8000, // 發送心跳包間隔,默認 8000 毫秒
    pongTimeout = 15000, // 最長未接收消息的間隔,默認 15000 毫秒
    reconnectTimeout = 4000, // 每次重連間隔
    reconnectLimit = 15, // 最大重連次數
    pingMsg // 心跳包的消息內容
  }) {
    // 初始化配置
    this.url = url
    this.pingTimeout = pingTimeout
    this.pongTimeout = pongTimeout
    this.reconnectTimeout = reconnectTimeout
    this.reconnectLimit = reconnectLimit
    this.pingMsg = pingMsg

    this.ws = null
    this.createWebSocket()
  }

  // 創建 WS
  createWebSocket() {
    try {
      this.ws = new WebSocket(this.url)
      this.ws.onclose = () => {
        this.onclose()
      }
      this.ws.onerror = () => {
        this.onerror()
      }
      this.ws.onopen = () => {
        this.onopen()
      }
      this.ws.onmessage = event => {
        this.onmessage(event)
      }
    } catch (error) {
      console.error('websocket 連接失敗!', error)
      throw error
    }
  }

  // 發送消息
  send(msg) {
    this.ws.send(msg)
  }
}

食用方式:

const ws = new WebsocketHB({
  url: 'ws://xxx'
})

ws.onopen = () => {
  console.log('connect success')
}
ws.onmessage = e => {
  console.log(`onmessage: ${e.data}`)
}
ws.onerror = () => {
  console.log('connect onerror')
}
ws.onclose = () => {
  console.log('connect onclose')
}
ws.send('Hello, chanshiyu!')

0x02 發送心跳包與重連#

這裡使用 setTimeout 模擬 setInterval 定時發送心跳包,避免定時器隊列阻塞,並且限制最大重連次數。需要注意的是每次進行重連時加鎖,避免進行無效重連,同時在每次接收消息時,清除最長間隔消息重連定時器,能接收消息說明連接正常,不需要重連。

class WebsocketHB {
  constructor() {
    // ...
    // 實例變量
    this.ws = null
    this.pingTimer = null // 心跳包定時器
    this.pongTimer = null // 接收消息定時器
    this.reconnectTimer = null // 重連定時器
    this.reconnectCount = 0 // 當前的重連次數
    this.lockReconnect = false // 鎖定重連
    this.lockReconnectTask = false // 鎖定重連任務隊列

    this.createWebSocket()
  }

  createWebSocket() {
    // ...
    this.ws = new WebSocket(this.url)
    this.ws.onclose = () => {
      this.onclose()
      this.readyReconnect()
    }
    this.ws.onerror = () => {
      this.onerror()
      this.readyReconnect()
    }
    this.ws.onopen = () => {
      this.onopen()

      this.clearAllTimer()
      this.heartBeat()
      this.reconnectCount = 0
      // 解鎖,可以重連
      this.lockReconnect = false
    }
    this.ws.onmessage = event => {
      this.onmessage(event)

      // 超時定時器
      clearTimeout(this.pongTimer)
      this.pongTimer = setTimeout(() => {
        this.readyReconnect()
      }, this.pongTimeout)
    }
  }

  // 發送心跳包
  heartBeat() {
    this.pingTimer = setTimeout(() => {
      this.send(this.pingMsg)
      this.heartBeat()
    }, this.pingTimeout)
  }

  // 準備重連
  readyReconnect() {
    // 避免循環重連,當一個重連任務進行時,不進行重連
    if (this.lockReconnectTask) return
    this.lockReconnectTask = true
    this.clearAllTimer()
    this.reconnect()
  }

  // 重連
  reconnect() {
    if (this.lockReconnect) return
    if (this.reconnectCount > this.reconnectLimit) return

    // 加鎖,禁止重連
    this.lockReconnect = true
    this.reconnectCount += 1
    this.createWebSocket()
    this.reconnectTimer = setTimeout(() => {
      // 解鎖,可以重連
      this.lockReconnect = false
      this.reconnect()
    }, this.reconnectTimeout)
  }}

  // 清空所有定時器
  clearAllTimer() {
    clearTimeout(this.pingTimer)
    clearTimeout(this.pongTimer)
    clearTimeout(this.reconnectTimer)
  }
}

0x03 實例銷毀#

最後給工具類加一個銷毀方法,在實例銷毀的時候設置一個禁止重連鎖,避免銷毀的時候還在嘗試重連,並且清空所有定時器,關閉長連接。

class WebsocketHB {
  // 重連
  reconnect() {
    if (this.forbidReconnect) return
    //...
  }

  // 銷毀 ws
  destroyed() {
    // 如果手動關閉連接,不再重連
    this.forbidReconnect = true
    this.clearAllTimer()
    this.ws && this.ws.close()
  }
}

封裝 npm 包#

到這裡,WebSocket 工具類心跳重連功能基本封裝完成,可以嘗試開始食用。這裡將最終完成代碼上傳到 Github,詳見 websocket-heartbeat。並將其封裝上傳到 npm 以便今後在項目中使用,有興趣可以嘗試一下 websockethb

安裝#

npm install websockethb

引入與使用#

import WebsocketHB from 'websockethb'

const ws = new WebsocketHB({
  url: 'ws://xxx'
})

ws.onopen = () => {
  console.log('connect success')
}
ws.onmessage = e => {
  console.log(`onmessage: ${e.data}`)
}
ws.onerror = () => {
  console.log('connect onerror')
}
ws.onclose = () => {
  console.log('connect onclose')
}

屬性#

屬性必填類型默認值描述
urltruestringnonewebsocket 伺服器接口地址
pingTimeoutfalsenumber8000心跳包發送間隔
pongTimeoutfalsenumber1500015 秒內沒收到後端消息便會認為連接斷開
reconnectTimeoutfalsenumber4000嘗試重連的間隔時間
reconnectLimitfalsenumber15重連嘗試次數
pingMsgfalsestring"heartbeat"心跳包消息
const opts = {
  url: 'ws://xxx',
  pingTimeout: 8000, // 發送心跳包間隔,默認 8000 毫秒
  pongTimeout: 15000, // 最長未接收消息的間隔,默認 15000 毫秒
  reconnectTimeout: 4000, // 每次重連間隔
  reconnectLimit: 15, // 最大重連次數
  pingMsg: 'heartbeat' // 心跳包的消息內容
}
const ws = new WebsocketHB(opts)

發送消息#

ws.send('Hello World')

斷開連接#

ws.destroyed()

Just enjoy it ฅ●ω●ฅ

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。