エンジニアのための WebSocket 実践ガイド
1. WebSocket 概要
WebSocket は RFC 6455 で策定されたフルデュープレックス通信プロトコルです。最初に HTTP/1.1 の Upgrade: websocket
握手を経て、以降は独立した軽量フレームでやりとりするため、毎リクエストごとにヘッダを積む REST‐HTTP とくらべてレイテンシ・帯域の両面で効率的です。HTTP/2 環境でも RFC 8441 (Extended CONNECT) により 1 本の TCP で多重化でき、HTTP/3 では派生技術の WebTransport が UDP-ライクな通信を補完します。
WebSocket フレームは最小 2 byte のヘッダに OPCODE
, FIN
, MASK
などを持ち、クライアント→サーバは必ずマスクが義務づけられる設計です。 Ping/Pong
・Close
など制御フレームが標準化されているため、TCP Keep-Alive では測れないアプリ層レベルの生存確認や Graceful Shutdown を自前で制御できます。
2. WebSocket が真価を発揮する場面
-
リアルタイム双方向同期
- チャット、共同ドキュメント編集、オンラインホワイトボード。
-
高頻度 Push
- 株価ティッカー、IoT センサストリーム、ゲーム状態のブロードキャスト。
-
近似リアルタイムで大量接続
- ライブメトリクス監視、大規模スポーツ配信のコメントストリーム。
「サーバ → クライアント片方向」かつ接続数が限定的なら SSE(Server-Sent Events)、順序保証より低遅延を優先する UDP 系なら WebTransport/Datagram の検討がベターです。要件が“低レイテンシ双方向”なら WebSocket 一択と覚えてください。
3. FastAPI + Uvicorn で組むミニチャット
Python 3.11/FastAPI 0.110 系で動作確認
# app.py
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from collections import defaultdict
import asyncio, json, uuid
app = FastAPI(title="Realtime Chat over WebSocket")
rooms: dict[str, set[WebSocket]] = defaultdict(set)
async def broadcast(room: str, payload: dict):
dead = set()
msg = json.dumps(payload, ensure_ascii=False)
for ws in rooms[room]:
try:
await ws.send_text(msg)
except Exception: # 送信失敗=切断
dead.add(ws)
rooms[room] -= dead
@app.websocket("/ws/{room}")
async def ws_endpoint(ws: WebSocket, room: str):
await ws.accept()
rooms[room].add(ws)
cid = uuid.uuid4().hex[:8]
try:
await broadcast(room, {"sys": f"🟢 {cid} joined"})
while True:
txt = await asyncio.wait_for(ws.receive_text(), timeout=30)
await broadcast(room, {"user": cid, "msg": txt})
except (WebSocketDisconnect, asyncio.TimeoutError):
await broadcast(room, {"sys": f"🔴 {cid} left"})
finally:
rooms[room].discard(ws)
if not rooms[room]:
rooms.pop(room, None)
pip install fastapi "uvicorn[standard]"
uvicorn app:app --host 0.0.0.0 --port 8000
-
Ping/Pong は
uvicorn --ws ping-interval 20 --ws ping-timeout 10
で自動化。 - 水平スケール時は
rooms
を Redis PUB/SUB や NATS に置換し、Pod 間でブロードキャストを共有。
4. HTTP(Web API) との対比
観点 | REST/GraphQL over HTTP | WebSocket |
---|---|---|
接続モデル | リクエスト → レスポンス/ステートレス | 常時接続フルデュープレックス |
オーバーヘッド | TLS + ヘッダを毎回 | 初回 Upgrade 後は数 byte |
負荷分散 | コネクション短命で容易 | Sticky 必須 or 外部 Pub/Sub |
キャッシュ | CDN/SWR 等が有効 | 基本不可 |
セキュリティ機構 | CORS, CSRF, OAuth 充実 | Origin 検証と TLS のみ。再認証不可 |
代表ユースケース | CRUD API, バッチ, 公開エンドポイント | チャット, ゲーム同期, ティッカー |
REST API が“短命 & ステートレス”でスケールを担保するのに対し、WebSocket は“長寿命 & 状態保持”でリアルタイムを達成します。
5. 実装・運用時の留意点
-
接続ライフサイクル
-
ping_interval 15–30 s / ping_timeout 5–10 s
で死活を速断。 - Close フレーム(1000,1001,1008) → 相手の Close 受信 → TCP FIN の順で Graceful。
-
-
バックプレッシャ
-
asyncio.Queue
送信バッファに上限を設け、溢れたらドロップ or 切断。 - 1 接続あたりのメモリと FD を監視 (
ulimit -n
)。
-
-
スケールアウト
- Sticky session が効かない LB では Redis Stream/NATS でメッセージをファンアウトし、サーバをステートレス化。
- AWS ALB の Idle Timeout を Ping 間隔の 2–3 倍に設定。
-
セキュリティ
-
wss://
強制、握手時に Cookie or JWT を検証。 - 接続中に権限が変わる可能性がある場合は セッション寿命を短くして再接続で更新。
-
permessage-deflate
は CPU DoS を誘発するためmax_window_bits=15
など制限を入れる。
-
-
クライアント再接続戦略
- 指数バックオフ + ジッター (
1s,2s,4s…max30s
)。 - Close Code 1006/1011 は UI に「一時的障害」と表示し自動再試行。
- 指数バックオフ + ジッター (
-
モニタリング
-
websocket_open_connections
、ping_rtt_seconds
、send_queue_size
を Prometheus で収集。 - 異常時に Cut-off しても “サービス全体が詰まらない” 設計を徹底。
-
6. まとめ
WebSocket は「低レイテンシ × 双方向 × ブラウザ互換」を満たす唯一の標準プロトコルであり、HTTP エコシステムの枠を超えたリアルタイム機能を提供します。その一方で 接続維持によるリソース圧迫 や 既存 HTTP セキュリティモデルの不在 がトレードオフです。
上級エンジニアが WebSocket を採用する際は、
- 要件が本当に双方向か?
- 水平スケールの出口(Pub/Sub)を用意したか?
- Ping/Pong, Close, 再接続, Back-pressure をコードレベルで実装したか?
をチェックリストに据え、プロトコルの特性と運用コストを天秤に掛けましょう。この記事が設計・実装・運用の各フェーズでの指針となれば幸いです。