WebSockets
1. WebSocket入門
WebSocketは、クライアントとサーバー間に永続的なコネクションを提供し、リアルタイムで双方向の通信を可能にします。これは、リクエスト・レスポンスモデルに従う従来のHTTPとは根本的に異なります。
1.1 WebSocketの主なメリット
- リアルタイム更新: データを即座にクライアントへプッシュできます。
- 効率性: HTTPリクエストを繰り返す必要がありません。
- 双方向性: クライアントとサーバーの両方からメッセージを送信できます。
- 低レイテンシ: メッセージが即座に送受信されます。
1.2 WebSocket vs HTTP
リアルタイムアプリケーションを効果的に構築するには、WebSocketとHTTPの違いを理解することが不可欠です。
| 機能 | WebSocket | HTTP |
|---|---|---|
| コネクション | 永続的(単一のコネクション) | リクエストごとに新しいコネクション |
| 通信形態 | 双方向(フルデュプレックス) | 単方向(リクエスト・レスポンス) |
| オーバーヘッド | ハンドシェイク後は最小限 | すべてのリクエストにヘッダーが含まれる |
| ユースケース | リアルタイムアプリ | 従来のWebページ、API |
| 例 | チャット、ライブ配信 | ページ読み込み、フォーム送信 |
プロのヒント: WebSocketは、プロトコル(ws:// または wss://)にアップグレードする前に、まずHTTPハンドシェイク(ステータスコード 101)から始まります。
2. WebSocketのセットアップ
2.1 wsモジュールのインストール
まず、プロジェクト用の新しいディレクトリを作成して初期化します。
mkdir websocket-demo
cd websocket-demo
npm init -y次に、ws パッケージをインストールします。
npm install ws 注意: ws モジュールは、シンプルで高速、かつ徹底的にテストされたWebSocketのクライアントおよびサーバー実装です。
3. WebSocketサーバーの作成
受信したメッセージをそのまま送り返す(エコー)シンプルなWebSocketサーバーを作成してみましょう。server.js というファイルを作成します。
const WebSocket = require('ws');
// ポート8080でWebSocketサーバーを作成
const wss = new WebSocket.Server({ port: 8080 });
console.log('WebSocketサーバーが ws://localhost:8080 で実行中です');
// 接続イベントのハンドラー
wss.on('connection', (ws) => {
console.log('新しいクライアントが接続しました');
// クライアントにウェルカムメッセージを送信
ws.send('WebSocketサーバーへようこそ!');
// メッセージイベントのハンドラー
ws.on('message', (message) => {
console.log(`受信メッセージ: ${message}`);
// クライアントにメッセージをエコーバック(送り返す)
ws.send(`サーバーが受信しました: ${message}`);
});
// 切断イベントのハンドラー
ws.on('close', () => {
console.log('クライアントが切断されました');
});
});4. コードの理解
wsモジュールをインポートします。- ポート 8080 で新しいWebSocketサーバーを作成します。
- 接続(connection)、メッセージ(message)、切断(close)の各イベントハンドラーを設定します。
- 受信したメッセージをすべてクライアントにエコーバックします。
4.1 サーバーの起動方法
- 上記のコードを
server.jsとして保存します。 - サーバーを実行します:
node server.js - サーバーが起動し、
ws://localhost:8080で待機状態になります。
5. WebSocketクライアントの作成
サーバーができたので、接続するクライアントを作成しましょう。Node.js用とブラウザ用の2種類を作成します。
5.1 Node.jsクライアント
client.js というファイルを作成します:
const WebSocket = require('ws');
const readline = require('readline');
// ユーザー入力用のreadlineインターフェースを作成
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
// WebSocketサーバーに接続
const ws = new WebSocket('ws://localhost:8080');
// コネクションが開いた時の処理
ws.on('open', () => {
console.log('WebSocketサーバーに接続しました');
promptForMessage();
});
// サーバーからのメッセージをリッスン
ws.on('message', (message) => {
console.log(`サーバー: ${message}`);
});
// エラーハンドリング
ws.on('error', (error) => {
console.error('WebSocketエラー:', error);
});
// コネクションが閉じた時の処理
ws.on('close', () => {
console.log('サーバーから切断されました');
process.exit(0);
});
// ユーザーにメッセージ入力を促す関数
function promptForMessage() {
rl.question('メッセージを入力してください(終了するには "exit"): ', (message) => {
if (message.toLowerCase() === 'exit') {
ws.close();
rl.close();
return;
}
ws.send(message);
promptForMessage();
});
}5.2 ブラウザクライアント
HTMLとJavaScriptを使用して、サーバーに接続するシンプルなページを作成します。index.html を作成します:
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>WebSocketクライアント</title>
<style>
body { font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; }
#messages { height: 300px; border: 1px solid #ccc; overflow-y: auto; padding: 10px; margin-bottom: 10px; }
.message { margin: 5px 0; }
</style>
</head>
<body>
<h1>WebSocketクライアント</h1>
<div id="status">サーバーに接続中...</div>
<div id="messages"></div>
<div>
<input type="text" id="messageInput" placeholder="メッセージを入力">
<button onclick="sendMessage()">送信</button>
</div>
<script>
const status = document.getElementById('status');
const messages = document.getElementById('messages');
const messageInput = document.getElementById('messageInput');
// WebSocketサーバーに接続
const ws = new WebSocket('ws://localhost:8080');
// コネクションが開いた時
ws.onopen = () => {
status.textContent = 'サーバーに接続済み';
status.style.color = 'green';
};
// メッセージを受信した時
ws.onmessage = (event) => {
const message = document.createElement('div');
message.className = 'message';
message.textContent = event.data;
messages.appendChild(message);
messages.scrollTop = messages.scrollHeight;
};
// エラー発生時
ws.onerror = (error) => {
status.textContent = 'エラーが発生しました';
status.style.color = 'red';
};
// コネクションが閉じた時
ws.onclose = () => {
status.textContent = 'サーバーから切断されました';
status.style.color = 'red';
};
// メッセージ送信関数
function sendMessage() {
const message = messageInput.value.trim();
if (message) {
ws.send(message);
messageInput.value = '';
}
}
// Enterキーで送信
messageInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
sendMessage();
}
});
</script>
</body>
</html> 注意: ブラウザのセキュリティ制限により、このHTMLファイルはWebサーバー(http-server や live-server など)経由で提供する必要があります。
6. アプリケーションのテスト
- WebSocketサーバーを起動します:
node server.js - クライアントのHTMLページを複数のブラウザウィンドウで開きます。
- 異なるクライアントからメッセージを送信し、リアルタイムに反映されるか確認します。
- ブラウザクライアントと並行してNode.jsクライアントも実行できます。
7. 実装の理解
- サーバーは、接続されているすべてのクライアントのセットを保持します。
- あるクライアントからメッセージを受信すると、それを他へブロードキャスト(配信)できます。
- クライアントは接続、切断、およびエラーイベントを処理します。
- メッセージは受信した瞬間にリアルタイムで表示されます。
8. WebSocketイベント
WebSocketはイベント駆動型モデルを採用しています。主なイベントは以下の通りです:
| イベント | 説明 |
|---|---|
| connection (サーバー) | クライアントがサーバーに接続したときに発生 |
| open (クライアント) | コネクションが確立されたときに発生 |
| message | メッセージを受信したときに発生 |
| error | エラーが発生したときに発生 |
| close | コネクションが閉じられたときに発生 |
9. 実用的なユースケース
WebSocketは、多種多様なアプリケーションで使用されています:
- チャットアプリ: メッセージの即時配信
- ライブダッシュボード: メトリクスやデータのリアルタイム更新
- 共同編集ツール: 複数ユーザーによる同一ドキュメントの同時編集
- ゲーム: 高速なインタラクションが必要なオンラインマルチプレイヤーゲーム
- 金融プラットフォーム: リアルタイムの株価ティッカーや取引プラットフォーム
- IoTアプリケーション: 接続されたデバイスの監視と制御
10. 高度なWebSocket機能
10.1 バイナリデータの転送
WebSocketはバイナリデータの送信をサポートしており、特定の種類のデータではより効率的です。
// バイナリデータの送信(サーバー側)
const buffer = Buffer.from([0x48, 0x65, 0x6c, 0x6c, 0x6f]); // バイナリ形式の 'Hello'
ws.send(buffer, { binary: true });
// バイナリデータの受信(クライアント側)
ws.binaryType = 'arraybuffer';
ws.onmessage = (event) => {
if (event.data instanceof ArrayBuffer) {
const view = new Uint8Array(event.data);
console.log('バイナリデータを受信しました:', view);
}
};10.2 ハートビートと接続監視
切断を検知して処理するために、ハートビートを実装します。
// サーバー側ハートビート
function setupHeartbeat(ws) {
ws.isAlive = true;
ws.on('pong', () => { ws.isAlive = true; });
}
// 30秒ごとに全クライアントへPingを送信
const interval = setInterval(() => {
wss.clients.forEach((ws) => {
if (ws.isAlive === false) return ws.terminate();
ws.isAlive = false;
ws.ping();
});
}, 30000);
// サーバー停止時のクリーンアップ
wss.on('close', () => {
clearInterval(interval);
});11. セキュリティの考慮事項
11.1 認証(Authentication)
WebSocket接続は常に認証を行うべきです。以下はJWTを用いた例です。
const http = require('http');
const WebSocket = require('ws');
const jwt = require('jsonwebtoken');
const server = http.createServer();
const wss = new WebSocket.Server({ noServer: true });
// アップグレードリクエスト時の認証処理
server.on('upgrade', (request, socket, head) => {
try {
const token = request.url.split('token=')[1];
if (!token) throw new Error('トークンが提供されていません');
// JWTトークンの検証
jwt.verify(token, 'your-secret-key', (err, decoded) => {
if (err) {
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
socket.destroy();
return;
}
// WebSocketハンドシェイクの続行
wss.handleUpgrade(request, socket, head, (ws) => {
ws.user = decoded; // ユーザーデータをWebSocketに添付
wss.emit('connection', ws, request);
});
});
} catch (error) {
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
socket.destroy();
}
});11.2 レート制限(Rate Limiting)
不正利用を防ぐためにレート制限を導入します。
const rateLimit = require('ws-rate-limit');
// 1接続あたり1分間に100メッセージまで
const limiter = rateLimit({
windowMs: 60 * 1000, // 1分
max: 100,
message: 'メッセージ送信が多すぎます。少し時間を置いてください。',
});
wss.on('connection', (ws) => {
limiter(ws);
// ... その他の処理
});12. パフォーマンス最適化
12.1 圧縮(Compression)
per-message deflate を有効にすることで、帯域幅の使用量を削減できます。
const WebSocket = require('ws');
const wss = new WebSocket.Server({
port: 8080,
perMessageDeflate: {
zlibDeflateOptions: {
chunkSize: 1024,
memLevel: 7,
level: 3
},
zlibInflateOptions: {
chunkSize: 10 * 1024
},
clientNoContextTakeover: true,
serverNoContextTakeover: true,
concurrencyLimit: 10,
}
});ベストプラクティス: 本番環境のアプリケーションでは、WebSocketをサポートしていないブラウザへのフォールバック機能などを提供する Socket.IO のようなライブラリの使用も検討してください。