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')
}
屬性#
屬性 | 必填 | 類型 | 默認值 | 描述 |
---|---|---|---|---|
url | true | string | none | websocket 伺服器接口地址 |
pingTimeout | false | number | 8000 | 心跳包發送間隔 |
pongTimeout | false | number | 15000 | 15 秒內沒收到後端消息便會認為連接斷開 |
reconnectTimeout | false | number | 4000 | 嘗試重連的間隔時間 |
reconnectLimit | false | number | 15 | 重連嘗試次數 |
pingMsg | false | string | "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 ฅ●ω●ฅ