4 分で読了 #networking #HTTP
このページの目次

WebSocket と SSE

HTTP のリクエスト・レスポンスモデルはリアルタイムシーンには不向き——WebSocket はフルデュプレックスチャネルにアップグレードされ、SSE は単方向ストリームで簡潔さを保ちます。どちらを選ぶかは「サーバーがプッシュを必要とするか」「バイナリフレームを必要とするか」にかかっています。

概要

WebSocket(RFC 6455, 2011)は HTTP ハンドシェイクに基づいて永続的な双方向 TCP チャネルを作成します——ブラウザとサーバーはいつでも互いにメッセージを送信できます。これは HTTP の「リクエスト・レスポンス」という単方向の制限を解決し、リアルタイムアプリケーション(チャット、ゲーム、ライブ配信の弾幕コメント、Grafana Live など)を可能にします。SSE(Server-Sent Events)はよりシンプルな単方向の代替手段です:サーバーはイベントストリームをクライアントにプッシュし、純粋な HTTP に基づいており、ファイアウォールとの互換性がより優れています。両方ともハートビートメカニズム(ping/pong vs SSE の自動再接続)を持っており、NAT/proxy 環境でロングコネクションが切れずに保たれます。

WebSocket アップグレード

Client → Server: GET /ws HTTP/1.1
  Upgrade: websocket
  Connection: Upgrade
  Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==   ← ランダム、base64
  Sec-WebSocket-Version: 13
  Sec-WebSocket-Protocol: chat                    ← オプションのサブプロトコル

Server → Client: HTTP/1.1 101 Switching Protocols
  Upgrade: websocket
  Connection: Upgrade
  Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=  ← SHA1(Key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
  Sec-WebSocket-Protocol: chat

  → ハンドシェイク完了 → TCP 接続が双方向 WebSocket にアップグレード

WebSocket フレーム

 0                   1                   2                   3
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |        (16/64)              |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                   Masking-key (if MASK set, 4 bytes)          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Payload Data (可変、クライアントからの場合はマスク済み)   |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

FIN(1): 1 = これはメッセージの最後のフレーム
opcode(4): 0=continuation, 1=text(UTF-8), 2=binary, 8=close, 9=ping, 10=pong
MASK(1): クライアント→サーバーはマスク必須、サーバー→クライアントはマスク不可

Masking (クライアント → サーバー):
  1. クライアント: 4バイトのマスクキーをランダム生成
  2. ペイロードの各バイトに対して: payload[i] = original[i] XOR key[i % 4]
  3. 目的: 中間プロキシによるキャッシュポイズニングを防ぐ(攻撃者がブラウザに作成された WS ペイロードを送信させ、
     → プロキシが誤って HTTP レスポンスとして解析し、キャッシュを破損させるのを防ぐ)

Close フレーム (opcode=8):
  [ステータスコード (2バイト、ビッグエンディアン)][理由 (UTF-8、オプション)]
  1000: 正常なクローズ
  1001: 終了(サーバーシャットダウン、ページ遷移)

Ping/Pong (ハートビート)

Ping フレーム (opcode=9): オプションのペイロード (アプリケーション固有)
Pong フレーム (opcode=10): ping のペイロードをコピー必須

→ アプリケーション層のハートビート (TCP keepalive とは異なる)
→ 検出: 死んだ接続 (TCP は生きていてもリモートアプリがハングしている場合)

プロキシ/サーバーのタイムアウト: nginx の proxy_read_timeout は WebSocket に適用される →
  ping/pong がなければ、接続はプロキシのタイムアウトで切断される可能性がある

SSE (Server-Sent Events)

単方向 (サーバー → クライアント) ストリーム、HTTP chunked encoding に基づく:

HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache

id: 42                         ← 最後のイベント ID (この ID から自動再接続)
event: alert                   ← オプションのイベントタイプ (指定しない場合は "message")
data: {"status":"firing"}      ← イベントデータ

data: 複数行のデータ
data: 2行目
                               ← 空行 → イベントディスパッチ

クライアント API: EventSource (HTML5 組み込み):
  const es = new EventSource("/events");
  es.onmessage = (e) => { console.log(e.data); };
  es.addEventListener("alert", (e) => { ... });

WebSocket vs SSE

WebSocketSSE
方向双方向サーバー → クライアント
プロトコルWebSocket (アップグレード)HTTP chunked
バイナリデータ標準 (opcode=2)base64 エンコードが必要
ファイアウォール互換性低い (一部のプロキシはアップグレードをサポートしない)高い (純粋な HTTP)
自動再接続手動EventSource が自動 (last-event-id から)
適した用途チャット、ゲーム、リアルタイム共同作業通知、フィード、リアルタイムデータプッシュ

参考

  • RFC: 6455
  • SSE: html.spec.whatwg.org/multipage/server-sent-events.html
  • WS テスト⁠: websocat (CLI WebSocket クライアント)

キーワード: WebSocket, アップグレード, フレーム, マスキング, ping/pong, SSE, EventSource, ハートビート