ネットワーク●●●○○

WebSocket

ブラウザとサーバの双方向・常時接続を実現するプロトコル。

概要

WebSocketは、ブラウザとサーバの間に「つなぎっぱなしの双方向通信路」を作るプロトコルです。HTTPが「クライアントが聞き、サーバが答えたら終わり」の往復であるのに対し、WebSocketは一度接続を確立すると、クライアントとサーバのどちらからでも、好きなタイミングで何度でもメッセージを送り合えます。

この性質が活きるのは「サーバ側で起きた出来事を、即座にブラウザへ届けたい」場面です。チャットで誰かが発言した、株価が動いた、共同編集で他の人がカーソルを動かした、対戦ゲームで相手が移動した——いずれも、クライアントが尋ねる前にサーバから知らせる必要があります。SlackやGoogle Docsのようなリアルタイム性の高いWebアプリの裏では、ほぼ確実にWebSocket(またはその同類)が動いています。

名前の由来どおり、OSのネットワーク通信の口であるソケットに近い「生の双方向チャネル」を、ブラウザの世界に持ち込んだものと理解できます。ブラウザからはnew WebSocket(url)という素朴なJavaScript APIで使えます。

なぜ生まれたか

もともとのWebは文書を取り寄せる仕組みなので、HTTPは「クライアントから始まる往復」しか持たず、サーバから自発的に話しかける手段がありませんでした。それでもチャットや通知をWebで作りたい開発者たちは、2000年代に力技で乗り切っていました。定期的に「何かあった?」と聞き続けるポーリングは、更新がなくても大量の無駄なリクエストが飛び、それでいてポーリング間隔ぶんの遅延が残ります。改良版のロングポーリング(サーバが返事を保留し、出来事が起きた瞬間に応答する)は遅延を減らせるものの、応答のたびに接続を張り直すうえ、リクエストごとにヘッダやCookieを送り直すオーバーヘッドも重く、サーバ側の実装も複雑でした。こうした一連の苦肉の策は「Comet」と総称されていました。

WebSocketは、この不毛な工夫を根本から解決するために設計され、2011年にRFC 6455として標準化されました。設計の妙は「最初の握手だけHTTPを使う」ことです。HTTPのアップグレード機構を利用して既存のポート443/80とWebインフラをそのまま通り抜け、握手が済んだらHTTPの衣を脱ぎ捨てて素の双方向チャネルに切り替わる——新しいポートも新しいインフラも要求しなかったことが、普及の決め手になりました。

詳細

アップグレードハンドシェイク

WebSocket接続は、普通のHTTPリクエストとして始まります。クライアントはUpgrade: websocketヘッダを付けた特別なGETリクエストを送り、「この接続をWebSocketに切り替えたい」と申し出ます。サーバが応じる場合はステータスコード101(Switching Protocols)を返し、その瞬間から、このTCP/IP接続の上を流れるのはHTTPではなくWebSocketのフレームになります。

サーバクライアントサーバクライアントここでプロトコルがWebSocketに切り替わるどちらかがCloseフレームを送るまで接続は維持GET リクエスト + Upgrade websocket ヘッダ101 Switching Protocolsメッセージメッセージサーバ発のプッシュも任意のタイミングで可能メッセージ

ハンドシェイクにはSec-WebSocket-Keyというヘッダも含まれ、サーバが決められた計算で応答することで「相手が本当にWebSocketを理解している」ことを確かめます。確立後の通信は「フレーム」という軽量な単位で流れ、テキスト(JSONを載せることが多い)とバイナリの両方を扱えます。リクエストのたびにヘッダ一式を送り直すHTTPと違い、フレームのオーバーヘッドは数バイトで済むため、小さなメッセージを高頻度で交換する用途に向いています。暗号化にはTLSを使い、URLスキームはhttp/httpsに対応してws/wssとなります。本番でwss以外を使う理由はまずありません。

HTTPとの使い分け

双方向で高機能だからといって、何でもWebSocketにすべきではありません。通常のページ遷移やAPI呼び出しのような「要求→応答」で完結する通信は、REST APIなどHTTPのほうが適しています。HTTPにはキャッシュ、ステータスコードによるエラー表現、リトライの容易さといった成熟したエコシステムがあり、ステートレスなのでスケーリングも簡単だからです。WebSocketを選ぶ基準は「サーバ発のプッシュが必要か」「メッセージの頻度・遅延要件がHTTP往復では満たせないか」です。また、サーバ→クライアントの一方向プッシュで足りるなら、HTTPの枠内で実現できるSSE(Server-Sent Events)というより軽い選択肢もあります。通知やフィード更新程度ならSSE、チャットやゲームのような双方向ならWebSocket、という住み分けが目安です。

運用の落とし穴 — 状態を持つ接続の扱い

WebSocketの難しさは、プロトコルよりも運用に現れます。最大の特徴は「接続が長生きし、状態を持つ」ことです。まず経路の問題として、間に挟まるリバースプロキシロードバランサがアップグレードを正しく中継する設定になっているか、アイドル接続を勝手に切るタイムアウト値になっていないかを確認する必要があります。「60秒でなぜか切断される」はこの層の設定が原因の定番トラブルです。プロトコル自体にPing/Pongフレームによる死活確認が用意されており、接続維持と切断検知に使います。

次にクライアント側では、モバイル回線の切り替わりなどで接続は必ず切れるものとして、自動再接続と「切断中に取りこぼしたメッセージの補償」を設計に織り込む必要があります。サーバ側では、接続がサーバのメモリ上の状態になるため、複数台にスケーリングした途端に「サーバAに繋がっている人へ、サーバBが受けたメッセージをどう届けるか」という問題が生じます。定石はRedis Pub/Subやメッセージキューをサーバ間の伝言板として挟むことです。こうした定番の面倒事(再接続、フォールバック、ルーム管理)をまとめて面倒見てくれるSocket.IOのようなライブラリや、Pusher・Ablyといったマネージドサービスが広く使われているのは、裏を返せばWebSocketの素の運用がそれだけ大変だということでもあります。

なお、ブラウザの同一オリジン制約(CORSの仕組み)はWebSocketには適用されず、サーバがOriginヘッダを自分で検証しなかった場合、悪意あるサイトからの接続を受け入れてしまう脆弱性(Cross-Site WebSocket Hijacking)につながります。ハンドシェイク時のOrigin検証と認証チェックは忘れずに実装すべきポイントです。