TL

WebSocketのフレーミングとハンドシェイク内部

WebSocketが「なぜ確実に双方向で通る」のか、ビット単位で腑に落ちる。Upgradeハンドシェイク・フレーム構造・マスク・制御フレーム・permessage-deflateまで原理から理解できる。

応用WebSocketプロトコルフレーミングリアルタイム通信最終更新: 2026-06-21
TL;DR要点だけ先に
  • 1.WebSocketはHTTP UpgradeとSec-WebSocket-Accept(キー+固定GUIDのSHA-1)で接続を昇格させ、以後はHTTPを使わない独自フレームに切り替わる。
  • 2.フレームはFIN・opcode・MASK・ペイロード長で始まり、クライアント発フレームは必ず32ビットマスクキーでXORして送る規定がある。
  • 3.Ping/Pong/Closeは制御フレームでデータと多重化され、permessage-deflateは各メッセージをDEFLATE圧縮してフレームに載せる拡張。

なぜフレーミングが必要か

WebSocket(RFC 6455)は、TCP上に全二重のメッセージ境界付きチャネルを作るプロトコルです。TCPはバイト列を順序通りに運ぶだけで、「ここからここまでが1メッセージ」という区切りを持ちません。一方HTTPはリクエスト/レスポンスの単位で完結し、サーバーから自発的に送る手段がありません。WebSocketはこの隙間を、独自のフレーム形式で埋めます。フレームこそが、TCPの無構造なバイトストリームにメッセージ境界・データ種別・制御信号を与える本体です。

接続の確立だけはHTTPに乗りますが、ハンドシェイクが完了した瞬間に、同じTCP接続上の通信はHTTPを完全に捨ててWebSocketフレームへ切り替わります。

Upgradeハンドシェイク

クライアントは通常のHTTP/1.1 GETに、以下のヘッダを付けて送ります。

GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

Sec-WebSocket-Key は、クライアントが生成した16バイトのランダム値をBase64で符号化したものです。これは認証用ではなく、この応答が本当にWebSocketを理解したサーバーから返ったことを確認するためのチャレンジです。サーバーは受け取ったキー文字列に固定のGUID 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 を連結し、そのSHA-1ハッシュをBase64符号化して Sec-WebSocket-Accept として返します。

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

クライアントは自分が送ったキーから同じ計算を再現し、一致すれば接続を確立します。101 Switching Protocols が返った後は、このTCP接続は完全にWebSocketのものになります。

Sec-WebSocket-Acceptは暗号ではない

固定GUIDは公開値であり、SHA-1も暗号強度を狙ったものではありません。目的はあくまで「キャッシュやプロキシが古いHTTP応答を返したのではなく、ハンドシェイクを正しく処理したサーバーが応答した」ことの検証です。試験では「Sec-WebSocket-Acceptは認証/暗号化ではなくハンドシェイク完了の証明」と整理しましょう。

フレーム構造

ハンドシェイク後に流れる各フレームは、先頭2バイトに最小限のメタ情報を詰め込みます。ビットの並びは次の通りです。

フィールドビット幅意味
FIN1このフレームでメッセージが完結するか(分割時の最終フレームで1)
RSV1-3各1拡張用の予約ビット(permessage-deflateがRSV1を使う)
opcode4フレーム種別(テキスト/バイナリ/制御)
MASK1ペイロードがマスクされているか
Payload len7ペイロード長。126/127は拡張長への切り替え符号
拡張長16 or 64len=126なら16ビット、127なら64ビットで実長を表す
Masking-key32MASK=1のときのみ存在するマスクキー

ペイロード長は段階的に拡張されます。7ビットの値が0〜125ならそれがそのまま長さです。値が126なら続く16ビット、127なら続く64ビットが実際の長さを表します。これにより小さなフレームは2バイトのヘッダで済み、大きなフレームも64ビット長まで表現できます。

opcodeの主な値は以下です。

opcode種別役割
0x0継続フレーム分割メッセージの2片目以降
0x1テキストUTF-8として解釈されるデータ
0x2バイナリ任意のバイト列
0x8Close接続終了の合図(制御フレーム)
0x9Ping生存確認の問い合わせ(制御フレーム)
0xAPongPingへの応答(制御フレーム)

分割(フラグメンテーション)とテキスト/バイナリ

1つの論理メッセージは、複数フレームに分割できます。最初のフレームがopcode 0x1(テキスト)か 0x2(バイナリ)でFIN=0、続くフレームはopcode 0x0(継続)でFIN=0、最後のフレームがFIN=1です。これにより送信側は、全長が確定する前からストリーミング的にデータを流せます。受信側は同じopcodeのメッセージ系列として連結し、テキストなら全体を1つのUTF-8列として解釈します。

テキストフレームのペイロードは妥当なUTF-8でなければならず、不正なバイト列を受け取った側はステータスコード1007で接続を閉じる規定です。バイナリフレームにはこの制約がなく、ArrayBufferBlob として扱われます(TypedArrayとArrayBufferのメモリ表現 も参照)。

メッセージ境界はTCPセグメントと無関係

1つのWebSocketフレームが複数のTCPセグメントに割れることも、複数フレームが1セグメントに同梱されることもあります。受信側は到着したバイト列をフレームヘッダから順にパースして境界を復元するため、TCPの分割と、WebSocketのメッセージ境界・フラグメンテーションはまったく別の層の話です。ここを混同しないことが上級者の分かれ目です。

マスキングの意義

クライアントからサーバーへ送るフレームは、必ずマスクしなければならない(MASK=1)と規定されています。送信側は32ビットのランダムなマスクキーを生成し、ペイロードの各バイトを payload[i] XOR maskkey[i mod 4] で変換します。受信側は同じキーで再度XORすれば元に戻ります(XORは可逆)。

変換: transformed[i] = original[i] XOR maskkey[i mod 4]
復元: original[i]    = transformed[i] XOR maskkey[i mod 4]

この一見奇妙な要件は、キャッシュポイズニング攻撃の防止が目的です。マスクが無ければ、攻撃者はブラウザのWebSocketからプロキシをHTTPリクエストに見える形のバイト列を送り込み、中間のキャッシュやプロキシを騙せる恐れがありました。ペイロードを毎回ランダムキーでマスクすることで、攻撃者がネットワーク上に出る具体的なバイト列を制御できなくなるため、こうした注入が成立しません。逆にサーバーからクライアントへのフレームはマスクしてはなりません。

マスクはセキュリティ機能であって暗号化ではない

マスクキーはフレーム内に平文で同梱されるため、盗聴者は即座に復号できます。マスキングは中間装置を騙されにくくするための対策であり、機密性は提供しません。盗聴・改ざんへの防御は wss:// のTLSが担います。両者の役割を混同しないでください。

制御フレームとClose

opcodeの最上位ビットが立つ 0x80xF制御フレームです。制御フレームはペイロードが125バイト以下に制限され、分割できません。これは、データメッセージの長い分割の合間にも、Ping/Pong/Closeを即座に割り込ませて処理できるようにするためです。

  • Ping/Pong:生存確認に使います。Pingを受けた側は、同じペイロードを載せたPongを速やかに返さねばなりません。アイドル接続の死活監視や、中間装置によるタイムアウト切断の防止に使われます。
  • Close:opcode 0x8 で送り合います。ペイロード先頭2バイトにステータスコード(1000=正常終了、1001=離脱、1002=プロトコル違反など)を入れ、続けて理由文字列を載せられます。片方がCloseを送ったら相手もCloseで応じ、双方がTCPを閉じるクローズハンドシェイクを経て終了します。

permessage-deflate圧縮

Sec-WebSocket-Extensions ヘッダでハンドシェイク時に折衝する拡張が permessage-deflate(RFC 7692)です。これはメッセージ単位でDEFLATE圧縮を適用し、圧縮済みフレームではフレームヘッダのRSV1ビットを1にして「このメッセージは圧縮されている」と示します。

Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

DEFLATEは前のデータを辞書として参照するため、JSONのようにキー名が繰り返される反復的なメッセージで効果が大きいのが特徴です。client_max_window_bits などのパラメータで圧縮ウィンドウのサイズを折衝し、server_no_context_takeover を指定するとメッセージごとに圧縮辞書をリセットします。辞書を持ち越す(context takeover)方が圧縮率は上がりますが、接続ごとに辞書ぶんのメモリを保持し続けるトレードオフがあります。

圧縮はCPUとメモリを消費する

小さく非反復的なメッセージ(バイナリの乱数列、既に圧縮済みの画像など)では、圧縮しても縮まらずCPUを浪費するだけになります。多数の同時接続を抱えるサーバーでは、context takeoverの辞書メモリが接続数ぶん積み上がる点も無視できません。permessage-deflateは「テキスト中心・反復的・帯域が貴重」な場面で選択的に使うのが定石です。

まとめ

WebSocketは、HTTP UpgradeとSec-WebSocket-Acceptで接続を昇格させた後、HTTPを捨てて独自フレームに切り替わります。フレームはFIN・opcode・MASK・段階的ペイロード長という最小構造でメッセージ境界とデータ種別を表し、クライアント発フレームはキャッシュポイズニング対策として必ずマスクされます。制御フレームでPing/Pong/Closeをデータと多重化し、permessage-deflateで反復データを圧縮できます。確立前のHTTP側の挙動は HTTP/2の多重化とHPACKFetch APIの内部 が、オリジン検証の前提は 同一オリジンセキュリティモデル が補完します。

Web/フロントエンド Article

WebSocketのフレーミングとハンドシェイク内部を実務で読む

TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。

解決すること

WebSocket

比較で見る軸

難易度: advanced / カテゴリ: Web/フロントエンド / タグ数: 5

導入後に効く点

フレームはFIN・opcode・MASK・ペイロード長で始まり、クライアント発フレームは必ず32ビットマスクキーでXORして送る規定がある。

先に潰すリスク

用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。

数字・仕様の読み方
難易度
advanced
カテゴリ
Web/フロントエンド
タグ数
5

判断チェックリスト

  • 自社の用途が「WebSocket / プロトコル」に近いか確認する。
  • 強みである「WebSocketはHTTP UpgradeとSec-WebSocket-Accept(キー+固定GUIDのSHA-1)で接続を昇格させ、以後はHTTPを使わない独自フレームに切り替わる。」が本当に評価軸になるか確認する。
  • 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
  • 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
  • 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
  • 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。

次に確認する観点

WebSocketプロトコルフレーミングリアルタイム通信WebSocketプロトコルフレーミング
参考: 公式情報