はじめに (対象読者・この記事でわかること)
この記事は、WebSocketを使用したリアルタイム通信を実装している開発者、特に接続が定期的に切断される問題に直面している開発者を対象としています。また、Node.jsでサーバーを構築している方にも役立つ内容です。
この記事を読むことで、WebSocket接続が切断される根本的な原因を理解し、サーバー側とクライアント側で実装すべき具体的な対策方法を習得できます。ハートビート機能の実装やタイムアウト設定の調整といった実践的なテクニックを学び、安定したWebSocket通信を実現するための知識を得ることができます。
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。 - WebSocketの基本的な概念と仕組み - JavaScript/TypeScriptの基本的な知識 - Node.jsの基本的な知識 - HTTP/HTTPS通信の基本的な理解
WebSocket接続切断の問題概要
WebSocketを使用してリアルタイム通信を実装している際、数秒ごとにメッセージを送受信しているにもかかわらず、接続が30秒ほどで切断される問題に遭遇することがあります。これは特に、ロードバランサー、プロキシ、ファイアウォールなどのネットワーク機器が長時間アイドル状態の接続を自動的に切断する設定になっている場合に発生しやすい問題です。
多くのネットワーク機器では、セキュリティやリソース管理の理由から、一定時間通信が行われない接続を自動的に切断するタイムアウト機構が搭載されています。このタイムアウト時間はデフォルトで30秒に設定されていることが多く、これが原因でWebSocket接続が切断されてしまうのです。
具体的な原因と解決策
WebSocket接続切断の原因
WebSocket接続が切断される主な原因は以下の通りです。
-
ネットワーク機器のタイムアウト設定 ロードバランサーやプロキシ、ファイアウォールなどのネットワーク機器は、長時間アイドル状態の接続を自動的に切断することがあります。多くの場合、このタイムアウト時間は30秒に設定されています。
-
サーバー側のタイムアウト設定 WebSocketサーバー自身がアイドル状態の接続をタイムアウトで切断する設定になっている場合があります。
-
クライアント側のタイムアウト設定 クライアント側のWebSocket実装がタイムアウト処理を実装している場合があります。
-
ネットワークの不安定さ ネットワークの不安定さや一時的な切断によって接続が失われることがあります。
サーバー側での対策
タイムアウト設定の確認と変更
Node.jsのWebSocketライブラリ(例:ws)を使用している場合、サーバー側でタイムアウト時間を設定できます。
Javascriptconst WebSocket = require('ws'); const wss = new WebSocket.Server({ port: 8080, clientTracking: true, // タイムアウト時間を60秒に設定 handshakeTimeout: 60000, // 心拍間隔を30秒に設定 pingInterval: 30000, // 心拍応答タイムアウトを5秒に設定 pingTimeout: 5000 }); wss.on('connection', (ws) => { console.log('クライアントが接続されました'); ws.on('message', (message) => { console.log('メッセージを受信:', message); }); ws.on('pong', () => { console.log('pongを受信'); }); });
ハートビート(Ping/Pong)の実装
WebSocketプロトコルには、ハートビートを実装するためのPing/Pongメカニズムが組み込まれています。これを利用して、定期的に接続が生きているか確認できます。
Javascriptconst WebSocket = require('ws'); const wss = new WebSocket.Server({ port: 8080 }); wss.on('connection', (ws) => { console.log('クライアントが接続されました'); // クライアントからメッセージを受信したとき ws.on('message', (message) => { console.log('メッセージを受信:', message); ws.send('メッセージを受信しました'); }); // 30秒ごとにpingを送信 const interval = setInterval(() => { if (ws.readyState === WebSocket.OPEN) { ws.ping(); } }, 30000); // 接続が閉じられたらintervalをクリア ws.on('close', () => { console.log('接続が閉じられました'); clearInterval(interval); }); });
クライアント側での対策
ハートビートの実装
クライアント側でも定期的にPingを送信する実装を追加することで、接続を維持できます。
Javascript// クライアント側のJavaScript const socket = new WebSocket('ws://localhost:8080'); socket.onopen = () => { console.log('WebSocket接続が確立されました'); // 30秒ごとにpingを送信 setInterval(() => { if (socket.readyState === WebSocket.OPEN) { socket.send(JSON.stringify({ type: 'ping' })); } }, 30000); }; socket.onmessage = (event) => { const data = JSON.parse(event.data); if (data.type === 'pong') { console.log('pongを受信'); } else { console.log('メッセージを受信:', data); } }; socket.onclose = () => { console.log('WebSocket接続が閉じられました'); // 再接続のロジックを実装 setTimeout(() => { socket = new WebSocket('ws://localhost:8080'); }, 5000); }; socket.onerror = (error) => { console.error('WebSocketエラー:', error); };
再接続ロジックの実装
接続が切断された場合に自動的に再接続するロジックを実装することも重要です。
Javascriptfunction connectWebSocket() { const socket = new WebSocket('ws://localhost:8080'); socket.onopen = () => { console.log('WebSocket接続が確立されました'); // 再接続カウンターをリセット reconnectAttempts = 0; }; socket.onmessage = (event) => { const data = JSON.parse(event.data); console.log('メッセージを受信:', data); }; socket.onclose = () => { console.log('WebSocket接続が閉じられました'); // 再接続ロジック reconnectAttempts++; // 再接続試行間隔の計算(指数バックオフ) const timeout = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000); console.log(`${timeout/1000}秒後に再接続を試みます...`); setTimeout(() => { connectWebSocket(); }, timeout); }; socket.onerror = (error) => { console.error('WebSocketエラー:', error); }; return socket; } let reconnectAttempts = 0; let socket = connectWebSocket();
ネットワーク機器での対策
ロードバランサー/プロキシの設定
ロードバランサーやプロキシを使用している場合、それらの設定でWebSocket接続のタイムアウト時間を調整できます。
-
Nginxの場合:
nginx proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 86400s; proxy_send_timeout 86400s; -
AWS ELB/ALBの場合: ELB/ALBのコンソールから、
Idle timeoutの値を調整できます。デフォルトは60秒ですが、より長い値に設定できます。 -
Cloudflareの場合: CloudflareのWebSocket設定を有効にし、タイムアウト時間を調整できます。
ハマった点やエラー解決
タイムアウト時間の不一致問題
サーバー側とクライアント側でタイムアウト時間の設定が不一致な場合、意図せず接続が切断されることがあります。例えば、サーバー側では60秒のタイムアウトを設定しているのに、クライアント側では30秒で接続を再確立しようとすると、競合状態が発生します。
解決策: サーバー側とクライアント側でタイムアウト時間を統一し、クライアント側はサーバー側のタイムアウト時間より短めに設定することをお勧めします。
Javascript// サーバー側 const TIMEOUT = 60000; // 60秒 // クライアント側 const PING_INTERVAL = 30000; // 30秒(サーバー側の半分)
ハートビートの実装漏れ
WebSocketライブラリによっては、ハートビート機能が自動で実装されている場合がありますが、すべてのライブラリでこの機能が提供されているわけではありません。ハートビートを実装せずに長時間メッセージのやり取りがない場合、接続が切断されてしまうことがあります。
解決策: 使用しているWebSocketライブラリのドキュメントを確認し、ハートビート機能が実装されているか確認してください。実装されていない場合は、自前で実装する必要があります。
ネットワーク機器の設定の見落とし
ロードバランサーやプロキシ、ファイアウォールなどのネットワーク機器の設定を見落としている場合、サーバーとクライアントの両方で適切な対策を講じていても接続が切断されることがあります。
解決策: ネットワーク機器の設定を確認し、WebSocket接続のタイムアウト時間を適切に設定してください。特に、社内ネットワークやクラウド環境を使用している場合は、この点に注意が必要です。
まとめ
本記事では、WebSocket接続が30秒で切断される問題の原因と具体的な解決策を紹介しました。
- ネットワーク機器のタイムアウト設定が主な原因
- サーバー側でのハートビート実装とタイムアウト設定の調整
- クライアント側でのハートビート実装と再接続ロジックの導入
- ネットワーク機器での設定変更による対策
この記事を通して、WebSocket接続を安定して維持するための具体的な対策を理解できたかと思います。WebSocketはリアルタイム通信に非常に強力な技術ですが、適切な対策を講じないと接続が不安定になることがあります。本記事で紹介した対策を適用することで、より安定したWebSocket通信を実現できるでしょう。
今後は、WebSocketを大規模なシステムで使用する際のパフォーマンスチューニングや負荷分散についても記事にする予定です。