このレッスンのロックを解除するには、まず「`ws`ライブラリで最初のWebSocketサーバーを構築する」を完了してください。前のレッスンに戻る

はじめに

リアルタイム Node.js アプリケーションにおいて、WebSocket 接続は単なる一本の「開かれたパイプ」ではありません。それは、開かれ、データを送り、気づかないうちに死に、予期せぬ形で失敗する、生きた存在です。ライフサイクルの各フェーズを明示的に管理しなければ、サーバーはメモリをリークし、ゾンビ接続を蓄積し、最終的にはクラッシュします。このレッスンでは、open イベント、close イベント、error イベント、そして死んだピアを検出するためのハートビート ping を含む、ライフサイクル全体をカバーします。

接続のライフサイクル

Node.js におけるすべての WebSocket 接続は 4 つの観測可能な状態を経ます。使用するライブラリ(ここでは人気の `ws` パッケージ)では、リッスン可能なイベントが発行されます:`connection`、`message`、`close`、`error`。一般的なパターンは、すべてのハンドラーを単一の関数にまとめ、すべての新規クライアントが同じライフサイクル管理を受け取るようにすることです。

javascript
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws, req) => {
console.log('Client connected:', req.socket.remoteAddress);
ws.on('message', (data) => {
console.log('Received:', data.toString());
ws.send(`Echo: ${data}`);
});
ws.on('close', (code, reason) => {
console.log(`Client disconnected. Code: ${code}, Reason: ${reason}`);
});
ws.on('error', (err) => {
console.error('Socket error:', err.message);
});
});
WebSocket 接続の 4 状態ライフサイクル図:CONNECTING → OPEN → CLOSING → CLOSED、および対応するイベント

close イベントの `code` は WebSocket 仕様に従います。一般的な値:`1000` はどちらかの側から開始された正常な終了を意味し、`1001` はクライアントが離れる(ページ遷移など)ことを意味し、`1006` は悪名高い「異常終了」で、多くの場合ネットワークが予期せず切断されたことを示します。

ハートビート:死んだクライアントの検出

TCP はクライアントが静かに消えたことを教えてくれません。接続は他に何か気づくまでは、ただ中途半端に開かれた状態でそこにあります。WebSocket プロトコルは ping(オペコード 0x9)と pong(オペコード 0xA)という制御フレームでこの問題を解決します。どちらの側からも ping を送信でき、相手側は同じペイロードを含む pong で応答しなければなりません。タイムアウトウィンドウ内に pong が返ってこなければ、接続は死んでいると見なされ、終了させるべきです。

`ws` ライブラリは `ws.ping()`、`ws.pong()`、そして作業の大部分を行う組み込みの `heartbeat` オプションを公開しています。以下のパターンは正規の実装です。

javascript
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
function heartbeat() {
this.isAlive = true;
}
wss.on('connection', (ws) => {
ws.isAlive = true;
ws.on('pong', heartbeat);
ws.on('message', (data) => {
ws.send(`Echo: ${data}`);
});
ws.on('close', () => {
console.log('Connection closed, cleanup done');
});
ws.on('error', (err) => {
console.error('Connection error:', err.message);
});
});
const interval = setInterval(() => {
wss.clients.forEach((ws) => {
if (ws.isAlive === false) {
console.log('Terminating dead connection');
return ws.terminate();
}
ws.isAlive = false;
ws.ping();
});
}, 30000);
wss.on('close', () => clearInterval(interval));

実際の動作:サーバー起動時に、すべての新規クライアントに対して `isAlive` が `true` に設定されます。30 秒ごとに、サーバーはすべてのクライアントを反復処理します。前のラウンドで既に死んだとマークされたクライアントには `terminate()` が呼ばれ、基礎となる TCP ソケットを強制的に閉じます。そうでなければフラグをリセットし、ping フレームを送信します。健全なクライアントは自動的に pong で応答し、`pong` ハンドラーをトリガーして `isAlive` を再び `true` に設定します。死んだクライアントは pong を返さないため、次のラウンドで terminate されます。

エラーの適切な処理

`error` イベントは、プロトコル違反、無効なフレーム、低レベルのソケット障害に対して発生します。リスナーがないと、未処理の error イベントによって Node.js プロセスがクラッシュします。常にリスナーを設定し、ほとんどの場合 error の直後に close が続くため、`close` イベントと組み合わせてください。便利なルール:診断のためにエラーをログ出力しますが、error ハンドラーの中で `send()` を呼ばないでください。ソケットは既に使用不能になっている可能性があります。

javascript
ws.on('error', (err) => {
console.error(`Socket error code=${err.code} message=${err.message}`);
});

error の `code` プロパティには、しばしば `ECONNRESET` や `EPIPE` のようなシステムレベルの値が含まれます。これらはデバッグのヒントであり、反応すべきアプリケーションロジックではありません。

まとめ

WebSocket ライフサイクルの管理は、3 つの習慣の規律です。常に `close` と `error` をリッスンして、ゾンビや失敗がクリーンアップされるようにしましょう。30 秒が標準的な間隔ですが、合理的な間隔で ping/pong ハートビートループを実装し、サイレント切断を検出してください。そして、未処理の `error` イベントを Node.js のデフォルトのクラッシュ動作に到達させてはいけません。これらパターンを組み合わせることで、不安定なネットワーク状況や数千の同時接続クライアントの下でも、長時間動作するリアルタイムサーバーを安定して稼働させることができます。

レッスンの練習

レッスンのチェックポイント

学んだ内容を復習し、回答へのフィードバックを受けましょう。

0/1
回答済み · 0/1
接続のライフサイクル、エラー、ハートビートの管理