WebSocketとSSE - リアルタイム通信の仕組みと使い分け
通常のHTTPリクエストはクライアントからの要求に対してサーバーが応答する一方向のモデル。リアルタイム通信を実現する手段として WebSocket と SSE(Server-Sent Events) がある。
HTTP ポーリングとの比較
リアルタイム通信の実現方法は複数ある。
| 方式 | 仕組み | 課題 |
|---|---|---|
| 短時間ポーリング | クライアントが定期的にリクエストを送る | 無駄なリクエストが多い・遅延がある |
| ロングポーリング | レスポンスを保留してデータが来たら返す | 接続が増えるとサーバー負荷が高い |
| SSE | サーバーからクライアントへの一方向ストリーミング | クライアントからの送信はHTTPで別途行う |
| WebSocket | 双方向のリアルタイム通信 | HTTP/2との相性・プロキシ設定が必要な場合がある |
SSE(Server-Sent Events)
概要
HTTPの仕組みを利用して、サーバーからクライアントへデータをプッシュし続ける技術。接続を維持したまま、サーバーが任意のタイミングでイベントを送信できる。
接続フロー
sequenceDiagram
participant C as クライアント
participant S as サーバー
C->>S: GET /events<br/>Accept: text/event-stream
S-->>C: 200 OK<br/>Content-Type: text/event-stream
Note over S,C: 接続を維持
S-->>C: data: {"message": "更新1"}
S-->>C: data: {"message": "更新2"}
S-->>C: data: {"message": "更新3"}
Note over C: 接続切断時は自動再接続
データフォーマット
SSEは text/event-stream という特定のテキスト形式で送信される。
data: シンプルなメッセージ\n\n
event: update
data: {"id": 1, "message": "更新データ"}\n\n
id: 42
event: notification
data: {"type": "alert", "text": "新しいメッセージ"}\n\n
: これはコメント(クライアントには届かない)\n\n
retry: 3000\n\n
| フィールド | 説明 |
|---|---|
data | 送信するデータ。複数行にわたる場合は data: を繰り返す |
event | イベント名。省略時は message |
id | イベントのID。再接続時に Last-Event-ID ヘッダーで送られる |
retry | 再接続までの待機時間(ミリ秒) |
: | コメント行(ハートビート用途にも使われる) |
クライアント実装(JavaScript)
const evtSource = new EventSource('/events')
// デフォルトイベント(event フィールドなし)
evtSource.onmessage = (event) => {
console.log(event.data)
}
// カスタムイベント
evtSource.addEventListener('update', (event) => {
const data = JSON.parse(event.data)
console.log(data)
})
// エラー処理
evtSource.onerror = (err) => {
console.error('SSE error:', err)
}
// 切断
evtSource.close()
SSEは接続が切れると自動で再接続する(retry フィールドで間隔を制御)。
サーバー実装(Node.js / 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 sendEvent = (data, event = 'message') => {
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`)
}
// ハートビート(接続維持)
const heartbeat = setInterval(() => {
res.write(': ping\n\n')
}, 30000)
// 接続切断時のクリーンアップ
req.on('close', () => {
clearInterval(heartbeat)
})
})
SSEの制約
- 一方向のみ: サーバー → クライアントの送信だけ。クライアントからのメッセージはHTTPリクエストで別途送る
- テキストのみ: バイナリデータは Base64 エンコードが必要
- ブラウザの同時接続数制限: HTTP/1.1では同一ドメインへの接続数制限(通常6)があり、SSEで消費される。HTTP/2を使えば解消される
WebSocket
概要
HTTP接続をアップグレードして確立する、双方向のリアルタイム通信プロトコル。一度接続が確立されると、サーバーとクライアントのどちらからもメッセージを送り合える。
接続フロー
sequenceDiagram
participant C as クライアント
participant S as サーバー
C->>S: HTTP GET /chat<br/>Upgrade: websocket<br/>Connection: Upgrade<br/>Sec-WebSocket-Key: xxx
S-->>C: 101 Switching Protocols<br/>Upgrade: websocket<br/>Sec-WebSocket-Accept: yyy
Note over C,S: WebSocket接続確立(双方向)
C->>S: メッセージ送信
S->>C: メッセージ送信
C->>S: メッセージ送信
S->>C: Ping
C-->>S: Pong
C->>S: Close フレーム
S-->>C: Close フレーム
ハンドシェイク
WebSocket接続はHTTPのアップグレードリクエストで始まる。
リクエスト:
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
レスポンス:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Accept の値は Sec-WebSocket-Key と固定のGUID 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 を結合してSHA-1ハッシュを取り、Base64エンコードしたもの。
クライアント実装(JavaScript)
const ws = new WebSocket('wss://example.com/chat')
// 接続確立
ws.onopen = () => {
console.log('接続完了')
ws.send(JSON.stringify({ type: 'join', room: 'general' }))
}
// メッセージ受信
ws.onmessage = (event) => {
const data = JSON.parse(event.data)
console.log(data)
}
// エラー処理
ws.onerror = (err) => {
console.error('WebSocket error:', err)
}
// 接続終了
ws.onclose = (event) => {
console.log(`切断: code=${event.code}, reason=${event.reason}`)
}
// メッセージ送信
ws.send('テキストメッセージ')
ws.send(JSON.stringify({ type: 'message', text: 'こんにちは' }))
// 切断
ws.close(1000, '正常終了')
接続状態
ws.readyState
// 0: CONNECTING 接続中
// 1: OPEN 接続確立済み
// 2: CLOSING 切断処理中
// 3: CLOSED 切断済み
サーバー実装(Node.js / ws)
const { WebSocketServer } = require('ws')
const wss = new WebSocketServer({ port: 8080 })
wss.on('connection', (ws, req) => {
console.log('クライアント接続')
// メッセージ受信
ws.on('message', (data) => {
const message = JSON.parse(data.toString())
// 全クライアントにブロードキャスト
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(message))
}
})
})
// 切断
ws.on('close', () => {
console.log('クライアント切断')
})
// ハートビート(Ping/Pong)
ws.isAlive = true
ws.on('pong', () => { ws.isAlive = true })
})
// 定期的にゾンビ接続をクリーンアップ
const heartbeat = setInterval(() => {
wss.clients.forEach((ws) => {
if (!ws.isAlive) { ws.terminate(); return }
ws.isAlive = false
ws.ping()
})
}, 30000)
クローズコード
| コード | 意味 |
|---|---|
1000 | 正常終了 |
1001 | エンドポイント離脱(ページ遷移など) |
1002 | プロトコルエラー |
1003 | 受け入れられないデータ型 |
1006 | 異常切断(CloseフレームなしでTCPが切れた) |
1007 | 不正なデータ |
1011 | サーバー内部エラー |
WebSocket vs SSE 比較
| 項目 | WebSocket | SSE |
|---|---|---|
| 通信方向 | 双方向 | サーバー → クライアントのみ |
| プロトコル | ws:// / wss:// | HTTP |
| データ形式 | テキスト・バイナリ | テキストのみ |
| 自動再接続 | 実装が必要 | ブラウザが自動で行う |
| 接続確立 | HTTPアップグレード | 通常のHTTPリクエスト |
| HTTP/2との相性 | 別プロトコルのため無関係 | HTTP/2ストリームを活用できる |
| プロキシ・CDN | 対応が必要な場合がある | HTTPなので基本的にそのまま動く |
| ブラウザサポート | 全モダンブラウザ対応 | 全モダンブラウザ対応(IEは除く) |
| 実装の複雑さ | やや複雑 | シンプル |
使い分けのガイド
flowchart TD
A[リアルタイム通信の選定] --> B{通信方向}
B --> C["サーバー → クライアント<br/>のみ"]
B --> D["双方向が必要"]
C --> E{更新頻度}
E --> E1["低〜中<br/>(通知・フィード)"] --> R1[SSE]
E --> E2[高頻度・バイナリも必要] --> R2[WebSocket]
D --> F{用途}
F --> F1["チャット<br/>コラボレーション"] --> R3[WebSocket]
F --> F2["ゲーム<br/>リアルタイム操作"] --> R4[WebSocket]
F --> F3[IoTデバイス制御] --> R5[WebSocket]
SSEが向いているケース
- ニュースフィード・SNSのタイムライン更新
- 株価・スポーツスコアなどのデータ配信
- 進捗バー・バックグラウンドジョブの状態通知
- ダッシュボードのメトリクス更新
WebSocketが向いているケース
- チャットアプリ・コメント機能
- 共同編集(Google Docs のようなリアルタイム協調)
- オンラインゲーム
- ライブ配信のコメント・リアクション
- IoTデバイスとの双方向制御
参考リンク
- RFC 6455 - The WebSocket Protocol - WebSocket公式仕様(IETF)
- Server-Sent Events 仕様 - WHATWG - SSE公式仕様
- WebSocket API - MDN - MDN(日本語)
- Server-sent events - MDN - MDN(日本語)