Server-Sent Eventsの仕組みとWebSocketとの設計上の違い
サーバー起点の通知が、特別なプロトコルなしに普通のHTTPだけで実装できる。EventSourceのストリーミング・自動再接続・Last-Event-IDの取りこぼし防止と、双方向WebSocketとの使い分けを原理から整理する。
- 1.SSEはサーバーが閉じない1本のHTTPレスポンスに text/event-stream を流し続ける単方向ストリーム。EventSourceがそれをパースし、空行区切りのイベントを message として配信する。
- 2.切断時はブラウザが自動で再接続し、直前に受け取った id を Last-Event-ID ヘッダで送り返すため、サーバーが続きから再送でき取りこぼしを防げる。WebSocketではこれを全て自前で実装する。
- 3.サーバー → クライアントの一方向で足りるなら、普通のHTTP・CORS・認証がそのまま効くSSEが手軽。クライアントからの高頻度送信や低遅延の双方向が要るならWebSocketを選ぶ。
リアルタイム更新というと WebSocket がまず思い浮かびますが、用途が「サーバーからクライアントへ通知を流すだけ」なら、専用プロトコルを持ち出さずとも普通の HTTP で完結します。それが Server-Sent Events(SSE) です。SSE は「閉じない 1 本の HTTP レスポンス」というだけの素朴な仕組みでありながら、自動再接続と取りこぼし防止までブラウザが面倒を見てくれます。ここでは EventSource の内部動作、text/event-stream のフォーマット、再接続と Last-Event-ID、そして WebSocket との設計上の分かれ目を原理から見ていきます。
SSE の正体は「閉じない HTTP レスポンス」
通常の HTTP レスポンスは、ボディを送り終えると接続を完了します。SSE はここを変え、サーバーがレスポンスを完了させずにボディを少しずつ書き続けるだけです。新しいプロトコルではなく、HTTP/1.1 のチャンク転送やストリーミングをそのまま使った応用にすぎません。クライアントは Fetch のボディストリーム と同じく、到着した分を逐次読みます。
サーバー側は Content-Type: text/event-stream を返し、接続を開いたまま保持します。最小のレスポンスは次のようになります。
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
data: hello
data: {"price": 100}
ボディは空行(改行 2 つ)で区切られたイベントの並びです。各イベントは field: value 形式の行で構成され、空行に達した時点でそのイベントが確定(dispatch)されます。Content-Length は付けず、接続が続く限り無限にイベントを追記していきます。
EventSource のパースとイベント配信
ブラウザ側は EventSource がこのストリームを受け取り、フィールドごとに解釈します。扱うフィールドは 4 つだけです。
| フィールド | 意味 |
|---|---|
| data | イベント本体。複数行書くと改行で連結される |
| event | イベント名。指定すると message ではなく同名のイベントとして配信 |
| id | イベントID。次回再接続時の Last-Event-ID になる |
| retry | 再接続待ち時間(ミリ秒)。サーバーから調整できる |
クライアントのコードは購読するだけで、フレーミングやパースは標準が肩代わりします。
const es = new EventSource("/api/stream");
// event フィールド無し、または event: message のイベント
es.onmessage = (e) => {
console.log(e.data, e.lastEventId);
};
// event: price と書かれたイベントだけを受ける
es.addEventListener("price", (e) => {
const v = JSON.parse(e.data);
});
es.onerror = () => {
// 切断時。ブラウザが自動で再接続を試みる(自前で close する場合を除く)
};
ここで重要なのは、data: で始まる行のうちコロン直後の 1 個の空白だけが剥がされる、という規則です。つまり data: {"a":1} の値は {"a":1} になります。また行頭がコロンで始まる行(例 : keep-alive)はコメントとして無視されます。一定間隔でこのコメント行を送ると、プロキシのアイドルタイムアウトによる切断を防ぐハートビートになります。
イベント本体を持たせるには data: 行が要ります。data: が 1 行も無いイベントは dispatch されません。本体に改行を入れたいときは値の中に改行文字を書くのではなく、data: 行を複数並べます。受信側ではそれらが改行(LF)で連結されて 1 つの data になります。
自動再接続と Last-Event-ID による取りこぼし防止
SSE が WebSocket に対して持つ最大の利点が、再接続と再送ポイントの標準化です。WebSocket はこれを全て自前で書く必要があります。
接続が切れると、EventSource は自動的に再接続を試みます。待ち時間の初期値は実装依存ですが、サーバーは retry: フィールドで上書きできます。さらにブラウザは、直前に受け取ったイベントの id を内部に保持しており、再接続のリクエストに Last-Event-ID ヘッダとして自動付与します。
GET /api/stream HTTP/1.1
Accept: text/event-stream
Last-Event-ID: 42
サーバーはこのヘッダを見て「ID 42 の次から再送すればよい」と判断できます。つまり切断中に発生したイベントを欠落させずに済むわけです。これが成り立つには、サーバー側が各イベントに単調増加する id を付け、かつ一定範囲のイベントを再送できるようバッファや永続ログを持っておく設計が前提になります。
id を一度も送らないと Last-Event-ID が空のまま再接続され、サーバーは「どこから続けるか」を知る手がかりを失います。少なくとも欠落を許容できないストリームでは、毎イベントに ID を振るのが定石です。逆に ID を空文字でリセットしたい場合は id:(値なし)を送ります。
なお、サーバーが意図的にストリームを終わらせたい場合は HTTP ステータスを 204 No Content で返すか、text/event-stream 以外を返すと、EventSource は再接続をやめます。普通の 200 で接続を閉じると、クライアントは「事故的な切断」とみなして再接続し続ける点に注意が必要です。
WebSocket との設計上の違い
両者は「サーバー起点でリアルタイムに届ける」という目的が重なりますが、設計思想は対照的です。
| 観点 | SSE(EventSource) | WebSocket |
|---|---|---|
| 方向 | サーバー → クライアントの単方向 | 全二重の双方向 |
| プロトコル | 普通のHTTP(text/event-stream) | 専用プロトコル(ws / wss) |
| データ形式 | UTF-8テキストのみ | テキストとバイナリ両方 |
| 自動再接続 | 標準で内蔵 | 自前で実装 |
| 再送の足場 | Last-Event-ID が標準 | アプリ層で独自設計 |
| CORS・認証・プロキシ | 通常のHTTPの仕組みがそのまま効く | ハンドシェイク以降は独自で要対応 |
設計上の本質的な違いは通信路の対称性です。WebSocket は Upgrade で接続を双方向の通信路に昇格させるため、クライアントからも自由に送れますが、その代わり HTTP の上に乗っていた仕組み(CORS のブラウザ自動防御、Cookie の自動付与、HTTP キャッシュ、既存のリバースプロキシ設定)から外れ、認証もオリジン検証も自前になります。SSE は最後まで「ただの GET レスポンス」なので、これらがそのまま流用できます。EventSource のリクエストにも CORS が通常どおり適用され、withCredentials で資格情報の送信を制御します。
制約と使い分けの指針
SSE には明確な制約もあります。仕様上 UTF-8 テキスト専用でバイナリを直接は流せません(送るならテキスト符号化が要る)。また HTTP/1.1 では、同一オリジンへのブラウザの同時接続数上限(一般に 6)に SSE 接続が 1 本食い込むため、多数のタブ・多数のストリームを開くと枯渇しやすい弱点があります。これは複数ストリームを 1 接続に 多重化する HTTP/2 を使うと大幅に緩和され、SSE は HTTP/2 環境で特に扱いやすくなります。
選択の指針はシンプルです。
- サーバー → クライアントの通知で足りる(株価・スコア・進捗・フィード・サーバー発のイベント通知)なら SSE。実装が薄く、再接続と取りこぼし対策まで標準で付く。
- クライアントからも高頻度に送る/低遅延の往復が要る(チャット・同時編集・カーソル共有・オンラインゲーム)なら WebSocket。
- クライアント送信がたまにしか無いなら、SSE を受信に使い、送信は普通の HTTP リクエストで補う構成が軽量で堅実です。
「双方向が本当に要るか」が最初の分岐です。要らないなら SSE が、HTTP の資産(CORS・認証・プロキシ・再接続)をそのまま活かせる分だけ手間が少なく事故りにくい、と覚えておくと選定で迷いません。
Web/フロントエンド Article
Server-Sent Eventsの仕組みとWebSocketとの設計上の違いを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
SSE
比較で見る軸
難易度: advanced / カテゴリ: Web/フロントエンド / タグ数: 5
導入後に効く点
切断時はブラウザが自動で再接続し、直前に受け取った id を Last-Event-ID ヘッダで送り返すため、サーバーが続きから再送でき取りこぼしを防げる。WebSocketではこれを全て自前で実装する。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- Web/フロントエンド
- タグ数
- 5
判断チェックリスト
- 自社の用途が「SSE / EventSource」に近いか確認する。
- 強みである「SSEはサーバーが閉じない1本のHTTPレスポンスに text/event-stream を流し続ける単方向ストリーム。EventSourceがそれをパースし、空行区切りのイベントを message として配信する。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。