DEEP DIVE: WEBSOCKET

WebSocket:サーバーが自ら話しかけるためのプロトコル

HTTPはクライアントが尋ね、サーバーが答えるという一方通行のモデルの上に築かれています。チャットや通知、リアルタイム同期のようにサーバー側から自発的にデータを届けたい用途では、この構造が長年ボトルネックになってきました。ハンドシェイクからフレーム構造、P2Pのシグナリングチャネルとしての役割、そして次世代の代替となるWebTransportとの関係まで、双方向通信プロトコルWebSocketを詳説します。

なぜ生まれたか:リクエスト/レスポンスモデルの限界

Webはもともと「クライアントが要求し、サーバーがそれに応答する」というリクエスト/レスポンス型のモデルの上に築かれています。この構造では、サーバー側に新しいデータが発生しても、クライアントから次のリクエストが送られてくるまでそれを届ける手段がありません。チャットや株価の更新、通知のようにサーバー起点でデータを届けたい用途では、クライアントが数秒おきに問い合わせを繰り返すポーリングや、リクエストを保留したままサーバー側が応答を意図的に遅らせるロングポーリング(Comet)といった苦肉の策が広く使われてきました。しかしこれらはいずれも、新規接続のたびに発生するHTTPヘッダのオーバーヘッド、コネクションの確立・切断の繰り返し、応答までの遅延の大きさといった問題を抱えていました。この非効率を根本から解決するために、2011年にRFC 6455として標準化されたのがWebSocketです。1本のTCP接続の上で、クライアントとサーバーの双方が任意のタイミングで自由にメッセージを送り合える全二重(full-duplex)の通信路を提供します。

ハンドシェイクと接続確立

WebSocket接続は、いきなり独自プロトコルで始まるのではなく、通常のHTTPリクエストとして始まります。クライアントはHTTP/1.1のGETリクエストにUpgrade: websocketConnection: Upgradeヘッダを付与し、ランダムな16バイトをBase64エンコードしたSec-WebSocket-Keyを添えて送信します。サーバーがWebSocketへの切り替えに同意すると、ステータスコード101 Switching ProtocolsとともにSec-WebSocket-Acceptヘッダを返します。この値は、受け取ったキーに固定のGUID文字列258EAFA5-E914-47DA-95CA-C5AB0DC85B11を連結してSHA-1ハッシュを取り、Base64エンコードしたものであり、クライアントは自分でも同じ計算をして突き合わせることで、応答が確かにWebSocketを理解するサーバーから返ってきたものであることを確認します。ハンドシェイクが完了すると、同じTCP接続上でHTTPのセマンティクスは失われ、以降は後述するフレーム形式でのバイト列のやり取りに切り替わります。URLスキームは平文のws://(既定ポート80)と、TLSで保護されたwss://(既定ポート443、HTTPSと同様に証明書検証を伴う)の2種類が用いられます。

フレーム構造:メッセージをどう区切って送るか

ハンドシェイク後のWebSocket通信は、メッセージをそのまま流すのではなく、決まった形式のフレーム単位に区切って送受信します。1つの論理的なメッセージが複数のフレームに分割(フラグメンテーション)されて送られることもあります。

FINビット
そのフレームがメッセージの最後の断片かどうかを示す1ビット。0であれば後続のフレームが続くことを意味し、受信側はopcodeが「continuation」(0x0)の断片を連結してもとのメッセージを再構成する。
opcode
フレームの種別を示す4ビットの値。テキスト(0x1、UTF-8でエンコードされたペイロード)やバイナリ(0x2)といったデータフレームに加え、接続終了を告げるclose(0x8)、死活監視に使うping(0x9)・pong(0xA)などの制御フレームがある。
ペイロード長
可変長のエンコーディングを採用しており、7ビットの基本フィールドに収まらない大きさの場合は、値126で16ビットの拡張長を、値127で64ビットの拡張長を後続に置く。これにより小さなメッセージのオーバーヘッドを抑えつつ、大きなペイロードにも対応できる。
マスキング
クライアントからサーバーへ送るフレームは、4バイトのマスキングキーでペイロード全体をXORすることが必須とされている(逆にサーバーからクライアントへのフレームはマスクしてはならない)。これは通信内容を秘匿するためではなく、悪意あるWebページが細工したペイロードによって、経路上にある透過プロキシ(クライアントに意識されないままリクエストを中継・キャッシュするプロキシサーバー)のキャッシュを汚染する攻撃を防ぐための、RFC上の要求事項です。

死活監視・サブプロトコル・オリジンモデル

  • ping/pong: opcode 0x9のpingフレームを送ると、受信側は同じペイロードを付けたpongフレーム(0xA)を返すことが期待される。応答がなければ接続が切れていると判断でき、また定期的なping/pongのやり取り自体が、アイドル状態のTCP接続をプロキシやNATが勝手にタイムアウトさせてしまうのを防ぐ役割も果たす。
  • サブプロトコル: ハンドシェイク時にSec-WebSocket-Protocolヘッダで候補を列挙し、サーバーが対応するものを1つ選んで応答することで、同じWebSocketの上でアプリケーション固有のメッセージフォーマットを取り決められる(例: MQTT over WebSocket、GraphQL subscriptions用のgraphql-wsなど)。
  • オリジンモデル: XMLHttpRequestやfetchと異なり、WebSocket接続にはSame-Origin Policy(同一オリジンポリシー)が適用されない。ブラウザはハンドシェイクのリクエストにOriginヘッダを付与するが、それを検証してリクエストを拒否するかどうかはサーバー実装者の責任に委ねられている。この検証を怠ると、認証済みセッションを悪用されるCSWSH(Cross-Site WebSocket Hijacking)と呼ばれる攻撃を許してしまう。

P2Pの文脈での役割

WebSocket自体はクライアント・サーバー型の技術だが、P2Pシステムの周辺でも重要な役回りを担っています。

  • WebRTCのシグナリングチャネル: NAT越えで扱ったように、WebRTCはピア間で直接メディアやデータを送るための仕様だが、接続を確立する前に交換しなければならないSDPオファー/アンサーやICE候補をやり取りする「シグナリング」の手段そのものは規定していない。多くの実装は、この事前交渉のためのシグナリングサーバーとの通信にWebSocketを用いる。
  • Nostrのリレー通信: Nostrでは、クライアントとリレーの間でEVENTREQCLOSEといったJSONメッセージをやり取りする通信手段としてWebSocketがそのまま採用されており、プロトコル仕様の中核をなしている。
  • ブロックチェーンノードの購読API: Ethereumノードなどが提供するJSON-RPCでは、新しいブロックやログをリアルタイムに受け取るためのeth_subscribeのような購読系エンドポイントがWebSocket経由で提供され、ポーリングに頼らずに最新の状態を追い続けられる。
  • P2Pとメタバースのように、サーバーを介した多人数のリアルタイム状態同期が必要な場面でも、WebSocketは低遅延な双方向通信を実現する定番の選択肢として広く使われている。

運用上の課題

HTTPリクエストのような短命でステートレスなやり取りと異なり、WebSocketはステートフルで長時間張られっぱなしの接続を大量に保持し続けることを前提とします。この性質は、運用の現場に固有の課題を持ち込みます。

  • スケーリング: 一般的なL7ロードバランサーは短命な接続を前提に設計されていることが多く、長時間の接続を適切に分散できないことがある。1台のサーバーが同時に保持できる接続数はファイルディスクリプタなどのOSリソースに制約され、デプロイやネットワーク障害の直後に大量のクライアントが一斉に再接続を試みる「再接続ストーム」への対策(指数バックオフとジッター)も欠かせない。
  • プロキシ・企業ファイアウォールとの相性: 古いプロキシやファイアウォールの中にはUpgradeヘッダを正しく扱えなかったり、アイドル状態の接続を短いタイムアウトで強制的に切断したりするものもあり、企業ネットワーク環境での接続性がしばしば問題になる。
  • HTTP/2・HTTP/3時代の位置づけ: HTTP/2は複数のリクエストを1本の接続上で多重化する設計だが、当初の仕様ではWebSocketのUpgrade機構とうまく両立しなかった。この隙間を埋めたのがRFC 8441(2018年)で、HTTP/2の拡張CONNECTメソッドを使い、HTTP/2の1本のストリーム内にWebSocketをトンネリングする方法を定義している。QUICを土台とするHTTP/3、そしてWebTransportは、この延長線上に位置づけられる新しい選択肢である。

なお、しばしば混同されるがSocket.IOはWebSocketそのものではありません。独自のハンドシェイクとメッセージフレーミングを持つ上位ライブラリであり、WebSocketが使えない環境ではHTTPロングポーリングへ自動的にフォールバックし、切断時の再接続や部屋(room)単位の配信といった機能を備えています。素のWebSocketクライアントとは相互接続できない点に注意が必要です。

ポーリング・SSE・WebSocket・WebTransportの比較

サーバーからクライアントへリアルタイムにデータを届ける手段は、WebSocketだけではありません。用途に応じて、より単純な仕組みや、より新しい仕組みが選ばれることも多くあります。

観点ポーリング/ロングポーリングSSEWebSocketWebTransport
方向性クライアント起点で、実質的にサーバー→クライアントの片方向サーバー→クライアントの単方向クライアント・サーバー双方向の全二重双方向。複数ストリームとデータグラムを併用可能
多重化接続ごとにリクエストを都度発行単一のHTTP接続で単方向のイベントを流し続ける単一のTCP接続上に1つの論理ストリームQUIC上で複数ストリーム・データグラムを多重化
信頼性・順序リクエスト単位で保証されるTCPの順序保証に加え、Last-Event-IDによる再開が可能TCPの順序保証。切断後の再接続はアプリ側で実装する必要がある信頼性のあるストリームと、順序保証のない軽量なデータグラムを選択できる
トランスポートHTTP/TCPHTTP/TCPTCP(HTTPからのUpgrade経由)QUIC/UDP
ブラウザ対応の成熟度最も枯れている主要ブラウザで対応済みだが利用は限定的全ブラウザで成熟しており、最も普及している対応は比較的新しく発展途上。詳細はWebTransportを参照

関連ページ

WebSocketの考え方は、このサイトで扱う他の技術とも随所でつながっています。QUICの上でストリームとデータグラムを扱う次世代の代替であるWebTransport、リレーとの通信手段としてWebSocketをそのまま採用するNostr、シグナリングの相手先を見つけるための土台となるNAT越え、そしてリアルタイム性が求められるP2Pとメタバースは、いずれもWebSocketが提供する双方向通信という性質の上に成り立っています。

トップページへ戻る