【JS応用】Server Sent Events

Server-Sent Events (SSE) によるリアルタイム通信の完全理解と実装戦略

概要:なぜ今、SSEが見直されているのか

現代のWeb開発において、リアルタイム通信は避けて通れない要件です。チャットアプリケーション、株価の更新、通知システム、あるいはAIチャットボットのストリーミング表示など、サーバーからクライアントへ能動的にデータを送る仕組みが不可欠です。

一般的にリアルタイム通信といえばWebSocketが連想されますが、すべてのユースケースでWebSocketが最適解とは限りません。ここで注目すべき技術が「Server-Sent Events (SSE)」です。SSEは、HTTPプロトコル上でサーバーからクライアントへ一方向のストリーミングを行うための標準技術です。WebSocketが双方向のフルデュプレックス通信であるのに対し、SSEは「サーバーからクライアントへの単方向」に特化しています。

この「単方向」という制約こそが、SSEを極めて軽量かつ堅牢なソリューションに押し上げています。HTTPプロトコルをそのまま利用するため、ファイアウォールやプロキシとの親和性が高く、自動再接続機能やイベントIDによる状態管理といった、開発者が本来実装すべき機能を標準で備えています。本記事では、SSEの仕組みを深掘りし、実務で採用すべき理由と具体的な実装パターンを解説します。

詳細解説:SSEのメカニズムとプロトコル仕様

SSEの通信は、クライアントが標準的なHTTPリクエストをサーバーに送信することから始まります。サーバーはレスポンスのContent-Typeとして「text/event-stream」を指定し、接続を維持したままデータを送り続けます。

SSEの通信において重要なのは、そのデータフォーマットです。各メッセージは「データフィールド」を基本単位とし、空行(改行2つ)で区切られます。

・data: メッセージの内容
・event: イベント名(クライアント側でイベントリスナーを使い分ける際に使用)
・id: イベントの識別子(再接続時にLast-Event-IDヘッダーとして送信される)
・retry: 再接続までの待機時間(ミリ秒)

これらのフィールドを組み合わせることで、サーバーは「いつ、どのような種類のデータを、どのタイミングで」クライアントに届けるかを制御できます。

SSEがWebSocketと比較して優れている点は、その「HTTPネイティブ」な性質にあります。WebSocketはHTTPからアップグレードして別のプロトコルへと切り替わりますが、SSEはHTTPのままです。そのため、既存の負荷分散装置(ロードバランサー)、リバースプロキシ(Nginxなど)、認証メカニズムをそのまま活用可能です。また、HTTP/2を使用すれば、1つのTCP接続上で複数のストリームを多重化できるため、パフォーマンス上の懸念もほとんどありません。

サンプルコード:Node.jsとブラウザ側の実装例

以下に、Node.js(Express)を用いたサーバーサイドと、ブラウザ標準のEventSource APIを用いたクライアントサイドの実装例を示します。

サーバーサイド(Node.js / Express):


const express = require('express');
const app = express();

app.get('/events', (req, res) => {
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');

  // クライアントに定期的にデータを送信
  const intervalId = setInterval(() => {
    const data = { time: new Date().toLocaleTimeString() };
    res.write(`data: ${JSON.stringify(data)}\n\n`);
  }, 1000);

  // 接続が切断された場合のクリーンアップ
  req.on('close', () => {
    clearInterval(intervalId);
    res.end();
  });
});

app.listen(3000, () => console.log('SSE server running on port 3000'));

クライアントサイド(JavaScript):


const eventSource = new EventSource('/events');

eventSource.onmessage = (event) => {
  const data = JSON.parse(event.data);
  console.log('Received:', data.time);
};

eventSource.onerror = (err) => {
  console.error('EventSource failed:', err);
  // 自動再接続が試行されるが、必要に応じて明示的に閉じることも可能
  // eventSource.close();
};

eventSource.addEventListener('custom-event', (event) => {
  console.log('Custom event received:', event.data);
});

この実装において注目すべきは、ブラウザ側のEventSourceが標準で備えている「自動再接続機能」です。サーバーとの接続が切れた場合、ブラウザは自動的に再接続を試みます。さらに、Last-Event-IDをサーバーに送ることで、通信断絶中に失われたメッセージを再送するロジックを実装することも容易です。

実務アドバイス:SSEを採用すべき局面と注意点

実務においてSSEを選択する際の判断基準は、「双方向通信が本当に必要か?」という点に集約されます。

1. 採用すべきケース
・AIチャットの回答生成(ChatGPTのようなトークン単位のストリーミング)
・ダッシュボードのリアルタイム更新(株価、サーバー監視メトリクス)
・通知機能(サーバー側で発生したイベントを即座にUIへ反映)
・ログのストリーミング表示

2. 避けるべきケース
・オンラインゲーム(高速な双方向通信と低遅延が要求されるため、WebSocketが適している)
・ビデオチャット(WebRTCが最適)
・クライアントからサーバーへの頻繁なデータ送信が発生するアプリケーション

3. 実務上の注意点(Nginx等のプロキシ設定)
SSEを本番環境で運用する場合、Nginxの設定で「X-Accel-Buffering: no」を指定することが必須です。これを行わないと、Nginxがレスポンスをバッファリングしてしまい、クライアントへのリアルタイム配信が遅延します。また、HTTP/1.1を使用する場合、ブラウザごとの同時接続数制限(通常6接続程度)に注意してください。HTTP/2を使用すればこの制限は回避可能です。

4. 認証の取り扱い
EventSourceは標準のコンストラクタでカスタムヘッダー(Authorizationヘッダーなど)を送信できません。そのため、認証が必要な場合は、クエリパラメータにトークンを含めるか、事前にHTTP POSTで認証を行い、使い捨てのセッションIDをSSEのリクエスト時に渡すといった設計が必要になります。

まとめ:Webの「ストリーミング」を再定義する

Server-Sent Eventsは、決して古臭い技術ではありません。むしろ、複雑化する現代のWebアーキテクチャにおいて、そのシンプルさと堅牢性は極めて強力な武器となります。WebSocketのような複雑な状態管理を必要とせず、HTTPの恩恵を最大限に受けながらリアルタイム性を実現できるSSEは、特にサーバーからの一方的な情報配信において、最もコストパフォーマンスの高い選択肢です。

エンジニアとして重要なのは、技術の流行に流されることではなく、その技術が解決する課題とトレードオフを正確に把握することです。リアルタイム通信が必要な要件に直面したとき、まずは「SSEで解決できないか?」を検討してください。その結果、よりシンプルで保守性の高いシステムが構築できるはずです。

SSEの導入は、単なる機能実装を超え、Webアプリケーションにおけるデータ配信の思想をよりクリーンな方向へと導いてくれるでしょう。今すぐ、あなたのプロジェクトにSSEを導入し、その軽量かつ強力なパフォーマンスを体感してください。

コメント

タイトルとURLをコピーしました