Node.jsのWebSocket:サーバー、クライアント、ハートビート、再接続
Node.jsのWebSocket:wsサーバー、組み込みクライアント、ハートビート | We.Inc
wsパッケージでNode.jsのWebSocketサーバーを構築し、Node組み込みのWebSocketクライアントで接続し、pingとpongで死んだソケットを検出し、バックオフで再接続する方法を解説します。
Node.jsには組み込みのWebSocketサーバーがないため、多くのプロジェクトはwsパッケージをインストールしてWebSocketServerを作成します。クライアント側については、Nodeにブラウザー互換のグローバルWebSocketが用意されています。v21.0.0とv20.10.0で追加され、v22.0.0でフラグ不要になり、v22.4.0で安定版になりました。本番公開の前に、pingとpongのハートビートと、バックオフ付きの再接続を追加しましょう。
それぞれの役割
- 接続を受け付ける
- new WebSocketServer({ port })
- wsパッケージ(npm install ws)
- Nodeから接続する
- new WebSocket(url)
- Node 22以降のグローバル
- ブラウザーから接続する
- new WebSocket(url)
- すべての最新ブラウザーに組み込み
- 死んだソケットを見つける
- ws.ping() と pong リスナー
- wsパッケージ
- 再起動を乗り切る
- 指数バックオフで再接続
- 自分のクライアントコード
このガイドでは、小さなチャット風サーバーとそれに接続するクライアントを順番に作ります。エコーとブロードキャストを行うサーバー、依存関係なしで接続するNodeスクリプト、気づかないうちに切れた接続を破棄するハートビート、サーバー再起動後に復帰するクライアントです。下の計算ツールでは、コードに書く前にリトライの待ち時間がどう伸びるかを確認できます。
- package.jsonに"type": "module"があるプロジェクトでnpm install wsを実行し、例のimport構文が動くようにします。
- node server.jsでサーバーファイルを起動します。ポート8080で待ち受け、受け取ったメッセージをログに出します。
- 2つ目のターミナルでNode 22以降を使ってクライアントファイルを実行するか、ブラウザーのコンソールを開いて同じコードを貼り付けます。
- Ctrl+Cでサーバーを止めて再び起動し、再接続するクライアントが待機、再試行、復帰する様子を確認します。
- サーバー側の各ソケットには必ずerrorリスナーを付けましょう。wsはerrorイベントを発生させ、処理されないとプロセスがクラッシュすることがあります。
- connectionハンドラーでreq.headers.originを確認し、対象外のオリジンからのソケットは閉じましょう。ブラウザーは、攻撃者のページからでもこのヘッダーを送ります。
- リバースプロキシはアイドル状態のソケットを切断します。nginxはデフォルトで、バックエンドが60秒何も送らないとプロキシ中のWebSocketを閉じるため、ping間隔はそれより短くしましょう。
- 再接続の待ち時間にランダムなジッターを加えましょう。ないと、再起動後にすべてのクライアントが同じ瞬間に再試行し、一斉にサーバーへ押し寄せます。
- ブロードキャストのループでは、readyStateがOPENでないクライアントをスキップしましょう。閉じかけのソケットに送っても意味がないからです。
よくある質問
Node.jsにWebSocketは組み込まれていますか?
クライアントだけです。グローバルのWebSocketクラスはv21.0.0とv20.10.0で--experimental-websocketフラグ付きで登場し、v22.0.0でフラグが不要になり、v22.4.0で実験的機能ではなくなりました。組み込みのサーバーは今もないため、wsが定番の選択肢であり続けています。
wsとSocket.IOのどちらを使うべきですか?
wsは標準のWebSocketプロトコルを使うため、ブラウザーやNodeのグローバルを含むあらゆる標準クライアントが接続できます。Socket.IOはその上に独自プロトコルを載せ、ルームや自動再接続を提供しますが、相手側にも専用のクライアントライブラリが必要です。
closeイベントなしに接続が切れるのはなぜですか?
ノートPCのふたを閉じたときのようにネットワーク経路が消えると、closeフレームは送られず、サーバーには半開きのソケットが残ります。ハートビートはこれを見つけます。前回のpingに応答しなかったソケットは、次の周回で強制終了されます。
ブラウザーのクライアントからpingを送れますか?
いいえ。ブラウザーのAPIにはsend()とclose()しかありません。ただしブラウザーはサーバーからのpingに自動でpongを返すため、サーバー側のハートビートはブラウザーのクライアントでも機能します。
HTTPサーバーとポートを共有するには?
Expressやnode:httpなどでHTTPサーバーを自分で作成し、new WebSocketServer({ server })として渡します。アップグレードのリクエストはwsに、通常のリクエストはアプリに届きます。
接続はどう始まるか
- WebSocketは、Upgrade: websocketを付けたHTTP GETとして始まります。wsサーバーがそれを確認し、101 Switching Protocolsで応答して、新しいソケットと元のリクエストを渡してconnectionイベントを発生させます。クエリ文字列のトークンやOriginヘッダーなど、ハンドシェイクから得たい情報はそのリクエストから読み取ります。
ハートビートをわかりやすく
- プロトコルでは、pingを受け取った側は同じデータを載せたpongで応答しなければならないと定められています。サーバーはpongが届いたソケットを生存とマークし、タイマーでそのマークを消してから再びpingを送ります。次の周回でマークが付いていないソケットは応答しなかったということなので、サーバーが強制終了します。
- wsのREADMEでは、このタイマーに30秒を使っています。前段にあるプロキシのアイドルタイムアウトより十分短い値を選びましょう。
サーバーを叩きすぎずに再接続する
- 良いクライアントは、最初の切断後は少しだけ待ち、失敗するたびに上限まで待ち時間を延ばし、接続が開いたらカウンターをリセットします。待ち時間をランダムにばらつかせれば、大勢のクライアントが一斉に戻ってくるのを防げます。
ステップ1:wsでサーバーを作る
server.js:エコーとブロードキャスト
各メッセージは開いているすべてのクライアントに送り返されます。errorリスナーにより、1つの不良ソケットでプロセスがクラッシュするのを防ぎます。
ステップ2:依存関係なしのクライアント
client.js:NodeのグローバルWebSocket
Node 22以降とブラウザーのコンソールの両方が同じWebSocketクラスを持つため、どちらでもそのまま動きます。
ステップ3:pingとpongで死んだ接続を切る
wsのREADMEにあるパターンを当社なりに書いたものです。pongで生存をマークし、前回の周回に応答しなかったものはすべて強制終了します。
ステップ4:再接続するクライアント
reconnect.js:ジッター付き指数バックオフ
待ち時間は失敗するたびに上限まで倍になり、ランダムなばらつきが加わり、接続が開くとリセットされます。
nginxの背後で使う
nginxは、UpgradeとConnectionのヘッダーを自分で転送したときだけアップグレードを通します。nginx 1.29.7より前では、proxy_http_version 1.1も必要です。
ソケット用のnginx location
proxy_read_timeoutで60秒のアイドル上限を延ばします。いずれにしても、上のハートビートが通信を流し続けます。
再接続の待ち時間計算ツール
最初の待ち時間、倍率、上限を設定すると、クライアントが各リトライ前に待つ時間と、ジッターを加えたときの最短と最長の待ち時間がわかります。
最初の待ち時間(ms)
倍率
上限(ms)
表示する試行回数
ジッターを加える(待ち時間の半分から全体の間で待つ)
各再接続試行前の待ち時間
試行
待ち時間
ジッターあり
累計待ち時間
{n}〜{v} 秒
同じ設定をコードで
無料で始める · クレジットカード不要
関連: ガイド|Webマーケティングと集客を学ぶ
製品
ご利用いただける方
機能
リソース
会社情報
サイトマップを見る