Node.jsにおけるWebSocketを超えた効率的な単方向リアルタイム通信:SSEを利用して
Min-jun Kim
Dev Intern · Leapcell

はじめに
今日の相互接続された世界では、リアルタイム通信はもはや贅沢ではなく、多くのWebアプリケーションにとって基本的な要件となっています。ライブスポーツスコアや株価ティッカーからニュースフィードや分析ダッシュボードまで、絶え間ないポーリングなしにサーバーからクライアントへ更新をプッシュする能力は、ユーザーエクスペリエンスとアプリケーションの応答性を大幅に向上させます。WebSocketは全二重(双方向)通信のための主要なソリューションとして登場しましたが、通信フローが主に単方向(サーバーからクライアントへ)であるシナリオでは、そのオーバーヘッドが過剰になることがあります。ここで、Server-Sent Events(SSE)が、特にNode.jsエコシステム内で、説得力があり、しばしばより効率的な代替手段を提供します。この記事では、Node.jsでSSEを使用して高性能な片方向リアルタイムデータストリーミングを行うことの実用的な利点と実装の詳細を掘り下げます。
コアコンセプトと実装
SSEの詳細に入る前に、関連するいくつかの主要なテクノロジーを簡単に定義しましょう。
- HTTP/1.1およびHTTP/2: Web通信の基盤となるプロトコル。SSEは標準HTTP接続を利用します。
 - 永続接続: クライアントとサーバー間で長時間開いたままになる接続。これにより、複数のリクエスト/レスポンスまたは連続したデータストリームが可能になります。
 - Server-Sent Events (SSE): WebページがHTTP接続を介してサーバーから更新を取得できるようにするW3C標準。サーバーからクライアントへの単方向データストリーミングのために設計されています。クライアントは標準HTTPリクエストを開始し、サーバーはレスポンスを開いたままにして、定期的にデータを送信します。
 - WebSocket: 単一のTCP接続を介した全二重通信プロトコル。クライアントとサーバーの両方がいつでも互いにデータを送信できる永続的な接続を提供します。
 
Server-Sent Eventsの理解
SSEは基本的に、サーバーが新しい情報が利用可能になるたびにクライアントにデータをプッシュする永続的なHTTP接続を確立することによって機能します。WebSocketとは異なり、SSEは標準HTTP上に構築されており、特別なハンドシェイクやプロトコルアップグレードを必要としません。データは特定のtext/event-stream形式で送信され、ブラウザが簡単に解析および処理できるようにします。各「イベント」は通常、eventタイプ、id、およびdata自体で構成されます。
単方向通信のためにWebSocketよりもSSEを選択する理由
通信が主に一方向である場合、SSEはいくつかの利点を提供します。
- シンプルさ: SSEはWebSocketよりも実装と管理が簡単です。標準HTTPを使用するため、個別のWebSocketサーバーや複雑なプロトコル処理が不要になります。
 - 組み込み再接続: ブラウザはSSE接続の自動再接続をネイティブにサポートしています。これは、WebSocketで手動で実装するか、クライアントサイドライブラリで処理する必要がある機能です。
 - HTTP/2多重化: HTTP/2を介して使用する場合、SSEはストリーム多重化の恩恵を受けることができます。これにより、単一のTCP接続上で複数のSSE接続(または他のHTTPリクエスト)が可能になり、オーバーヘッドが削減されます。
 - 低オーバーヘッド: 単純なサーバーからクライアントへのデータプッシュの場合、SSEはWebSocketハンドシェイク、オペコード処理、またはフレームマスキングを必要としないため、通常、WebSocketよりもオーバーヘッドが低くなります。
 - 少ない状態管理: クライアントは通常データを返さないため、サーバーは全二重WebSocket接続と比較して管理する必要のある接続状態が少なくなることがよくあります。
 
Node.jsでのSSEの実装
Node.jsでのSSEサーバーの設定と、クライアントサイドJavaScriptアプリケーションでのその使用方法の実際的な例を見ていきましょう。
Node.jsサーバー
Webアプリケーションの一般的な選択肢であるExpress.jsをサーバーに使用します。
// server.js const express = require('express'); const cors = require('cors'); // クロスオリジンリクエスト用 const app = express(); const PORT = process.env.PORT || 3000; app.use(cors()); // クライアントサイドアクセス用にCORSを有効にする /** * アクティブなSSE接続のレスポンスオブジェクトを格納するための一時的なクライアントリスト。 * 本番環境では、Redis Pub/Subのような、より堅牢なメッセージングシステムを使用して * 複数のサーバーインスタンス間でイベントをブロードキャストすることになるでしょう。 */ let clients = []; let eventId = 0; // シンプルなイベントIDカウンター app.get('/events', (req, res) => { // SSEに必要なヘッダーを設定 res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); res.setHeader('X-Accel-Buffering', 'no'); // Nginxのバッファリングを無効にする(該当する場合) // クライアントのレスポンスオブジェクトを保存 clients.push(res); // 接続確認のための初期イベントを送信 res.write('event: connected\n'); res.write(`data: ${JSON.stringify({ message: 'Connected to SSE stream' })}\n\n`); console.log('New SSE client connected.'); // 接続が閉じられたらクライアントを削除 req.on('close', () => { console.log('SSE client disconnected.'); clients = clients.filter(client => client !== res); }); }); // 接続中のクライアントにデータを送信することをシミュレートするエンドポイント app.post('/send-update', express.json(), (req, res) => { const { message } = req.body; if (!message) { return res.status(400).send('Message is required'); } eventId++; const eventData = { id: eventId, timestamp: new Date().toISOString(), message: message }; clients.forEach(client => { // SSE仕様に従ってイベントデータをフォーマット client.write(`id: ${eventData.id}\n`); client.write('event: new_update\n'); client.write(`data: ${JSON.stringify(eventData)}\n\n`); }); res.status(200).send(`Update sent to ${clients.length} clients.`); }); // 基本的なテストのためのシンプルなルートエンドポイント app.get('/', (req, res) => { res.send('SSE server is running. Connect to /events to receive updates.'); }); app.listen(PORT, () => { console.log(`Server listening on port ${PORT}`); });
このサーバー実装では:
Content-Typeをtext/event-streamに設定し、重要なキャッシュ制御ヘッダーを設定します。- 後でメッセージをブロードキャストするために、接続中のクライアントの 
resオブジェクトを保存します。 - クライアントが接続すると、初期の 
connectedイベントを送信します。 - クライアントの切断を処理して 
clients配列をクリーンアップします。 /send-updatePOSTエンドポイントは、外部トリガーまたは内部ロジックが新しいデータをプッシュすることをシミュレートします。アクティブなすべてのクライアント接続を反復処理し、new_updateイベントを送信します。
クライアントサイドJavaScript
次に、これらのイベントを消費するためのクライアントサイドJavaScriptアプリケーションを備えたシンプルなHTMLページを作成しましょう。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>SSE Client</title> <style> body { font-family: sans-serif; margin: 20px; } #messages { border: 1px solid #ccc; padding: 10px; min-height: 200px; max-height: 400px; overflow-y: auto; background-color: #f9f9f9; } .message { margin-bottom: 5px; padding: 5px; border-bottom: 1px dashed #eee; } label { display: block; margin-top: 10px; } input[type="text"] { width: 300px; padding: 8px; } button { padding: 8px 15px; margin-top: 5px; cursor: pointer; } </style> </head> <body> <h1>Server-Sent Events Client</h1> <p>This page connects to a Node.js SSE server and displays real-time updates.</p> <h2>Live Updates</h2> <div id="messages"> <p>Waiting for updates...</p> </div> <h2>Send Test Update (via POST to server)</h2> <div> <label for="messageInput">Message:</label> <input type="text" id="messageInput" placeholder="Enter message to send via POST"> <button id="sendButton">Send Update</button> <p id="sendStatus"></p> </div> <script> const messageContainer = document.getElementById('messages'); const messageInput = document.getElementById('messageInput'); const sendButton = document.getElementById('sendButton'); const sendStatus = document.getElementById('sendStatus'); // EventSourceインスタンスを作成 const eventSource = new EventSource('http://localhost:3000/events'); eventSource.onopen = function() { console.log('SSE connection opened.'); addMessage('System', 'Connected to SSE stream.'); }; // カスタム'connected'イベントをリッスン eventSource.addEventListener('connected', function(event) { const data = JSON.parse(event.data); console.log('Received initial connection event:', data); addMessage('System', data.message); }); // カスタム'new_update'イベントをリッスン eventSource.addEventListener('new_update', function(event) { const data = JSON.parse(event.data); console.log('Received new update:', data); addMessage(`Event ID ${data.id} (${new Date(data.timestamp).toLocaleTimeString()})`, data.message); }); eventSource.onerror = function(error) { console.error('SSE Error:', error); addMessage('System', 'SSE connection error. Trying to reconnect...'); // EventSourceは自動的に再接続を試みます。 }; function addMessage(sender, text) { const div = document.createElement('div'); div.className = 'message'; div.innerHTML = ``; messageContainer.appendChild(div); messageContainer.scrollTop = messageContainer.scrollHeight; // 自動スクロール } // --- サーバーアップデートをトリガーするためのクライアントサイドPOST機能 --- sendButton.addEventListener('click', async () => { const message = messageInput.value.trim(); if (!message) { sendStatus.textContent = 'Please enter a message.'; sendStatus.style.color = 'red'; return; } try { const response = await fetch('http://localhost:3000/send-update', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message }) }); if (response.ok) { const result = await response.text(); sendStatus.textContent = `Success: ${result}`; sendStatus.style.color = 'green'; messageInput.value = ''; // 入力をクリア } else { const errorText = await response.text(); throw new Error(`Server error: ${response.status} - ${errorText}`); } } catch (error) { console.error('Failed to send update:', error); sendStatus.textContent = `Error sending update: ${error.message}`; sendStatus.style.color = 'red'; } }); </script> </body> </html>
クライアント側では:
- SSEを消費するためのネイティブブラウザインターフェイスである
EventSourceAPIを使用します。 onopen、new_update(カスタムイベントタイプ)、およびonerrorイベントをリッスンします。EventSourceクライアントは、接続が失われた場合の再接続を自動的に処理します。sendButton機能により、サーバーのPOSTエンドポイントをトリガーでき、それがSSE経由でメッセージをブロードキャストします。
例の実行
- サーバー:
npm init -y npm install express cors node server.js - クライアント:
index.htmlファイルをWebブラウザで開きます。 - ブラウザに「Connected to SSE stream.」と表示されるはずです。
 - クライアントページの入力フィールドとボタンを使用してメッセージを送信すると、「Live Updates」セクションにリアルタイムで表示されるのがわかります。リフレッシュは不要です。
 
アプリケーションシナリオ
SSEは、サーバーがリアルタイムイベントの主なソースであり、クライアントがそれを消費するだけの場合に、その真価を発揮します。一般的なユースケースは次のとおりです。
- ニュースフィードとライブブログ: 新しい記事や更新情報を発生時にプッシュする。
 - 株価ティッカーと仮想通貨価格: リアルタイムの市場データを表示する。
 - スポーツスコアとイベント更新: スコア、ゲーム統計、またはライブ解説を送信する。
 - 分析ダッシュボード: リアルタイムメトリクスとデータビジュアライゼーションをストリーミングする。
 - 通知システム: ユーザーに即時通知を配信する(例:新しいメール、友達リクエスト)。
 - 進捗インジケーター: 時間のかかるサーバーサイドタスクのステータスを表示する。
 
これらのすべてのケースにおいて、WebSocketを使用することも可能ですが、SSEは単方向データフローに対して、より軽量で簡単なソリューションを提供します。
結論
Server-Sent Eventsは、Node.jsアプリケーションで単方向リアルタイム通信機能を構築するための非常に効率的でエレガントにシンプルなソリューションを提供します。標準HTTPと、自動再接続のためのネイティブブラウザサポートを活用することで、SSEは複雑さとオーバーヘッドを削減し、サーバーからクライアントへのデータストリーミングに最適な選択肢となります。クライアントからの頻繁な通信を期待せずにサーバーがクライアントに更新をプッシュすることが主なニーズである場合、SSEはWebSocketよりも優れた、より適切な代替手段となります。これにより、開発労力が最小限で、堅牢なパフォーマンスにより、応答性があり、魅力的なユーザーエクスペリエンスが可能になります。

