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('接続成功')
}
ws.onmessage = e => {
console.log(`onmessage: ${e.data}`)
}
ws.onerror = () => {
console.log('接続エラー')
}
ws.onclose = () => {
console.log('接続切断')
}
ws.send('こんにちは、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('接続成功')
}
ws.onmessage = e => {
console.log(`onmessage: ${e.data}`)
}
ws.onerror = () => {
console.log('接続エラー')
}
ws.onclose = () => {
console.log('接続切断')
}
プロパティ#
プロパティ | 必須 | 型 | デフォルト値 | 説明 |
---|---|---|---|---|
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('こんにちは、世界')
接続を切断#
ws.destroyed()
Just enjoy it ฅ●ω●ฅ