WebSocketのハンドシェイクとフレーム構造の詳解

Tech

本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。

WebSocketのハンドシェイクとフレーム構造の詳解

背景

従来のHTTP/1.1は、Webアプリケーションにおけるリアルタイム性が求められる要件に対して、いくつかの課題を抱えていました。HTTP/1.1はステートレスなリクエスト/レスポンスモデルに基づいており、サーバーからクライアントへのプッシュ通信や、低レイテンシでの双方向通信を実現するためには、ロングポーリングやCometといったワークアラウンドが必要でした。これらはHTTPヘッダのオーバーヘッドが大きく、効率的な通信とは言えませんでした。

このような背景から、Webブラウザとサーバー間で永続的な双方向通信パスを提供する新しいプロトコルの必要性が高まり、RFC 6455として「The WebSocket Protocol」が標準化されました。

設計目標

WebSocketプロトコルは、以下の設計目標を達成することを目指しています。

  1. 双方向通信: クライアントとサーバーが任意のタイミングでデータを送受信できるフルデュプレックス通信チャネルの提供。

  2. 低レイテンシとオーバーヘッド削減: HTTP/1.1の接続確立やヘッダ交換のオーバーヘッドを削減し、アプリケーションデータに集中できる効率的な通信。

  3. 既存インフラとの互換性: HTTP/1.1のポート80/443を利用し、アップグレードメカニズムを用いることで、既存のプロキシやファイアウォールを透過できる設計。

  4. メッセージ指向: バイトストリームではなく、メッセージ単位でのデータ送受信をサポート。

  5. 拡張性: 新しい機能やサブプロトコル、圧縮メカニズムなどをネゴシエートするための拡張メカニズム。

詳細

ハンドシェイク

WebSocketのハンドシェイクは、既存のHTTP/1.1プロトコル上に構築されています。クライアントは通常のHTTP GETリクエストを送信し、HTTPの「Upgrade」メカニズムを利用してプロトコルをWebSocketに切り替えることを要求します。サーバーがこの要求を受け入れると、プロトコルはHTTPからWebSocketへと昇格し、以降の通信はWebSocketフレーム形式で行われます。

ハンドシェイクシーケンス

sequenceDiagram
    participant C as Client
    participant S as Server

    C ->> S: |HTTP GET Request (Upgrade)|
    Note over C,S: HTTP/1.1 Upgrade Request (RFC 6455 Section 4.1)
    C ->> S: GET /chat HTTP/1.1
    C ->> S: Host: example.com
    C ->> S: Upgrade: websocket
    C ->> S: Connection: Upgrade
    C ->> S: Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
    C ->> S: Sec-WebSocket-Version: 13
    C ->> S: Origin: http://example.com
    C ->> S: Sec-WebSocket-Protocol: chat, superchat

    S ->> C: |HTTP 101 Switching Protocols|
    Note over C,S: HTTP/1.1 Upgrade Response (RFC 6455 Section 4.2)
    S ->> C: HTTP/1.1 101 Switching Protocols
    S ->> C: Upgrade: websocket
    S ->> C: Connection: Upgrade
    S ->> C: Sec-WebSocket-Accept: s3pPLMBiTxaQ9GUZcgNWSQ==
    S ->> C: Sec-WebSocket-Protocol: chat

    C -->> S: |WebSocket Data Frames|
    S -->> C: |WebSocket Data Frames|
    Note over C,S: Full-duplex WebSocket communication

ハンドシェイクヘッダの詳細 (RFC 6455 Section 4.1, 4.2)

クライアントリクエストヘッダ例:

  • Upgrade: websocket: プロトコルのアップグレード要求。

  • Connection: Upgrade: HTTPの汎用ヘッダで、接続のアップグレードを指示。

  • Sec-WebSocket-Key: 16バイトのランダムなBase64エンコードされた値。サーバーはこの値と固定のGUID “258EAFA5-E914-47DA-95CA-C5AB0DC85B11” (RFC 6455 Section 5.3) を結合しSHA-1ハッシュを計算、Base64エンコードして Sec-WebSocket-Accept ヘッダに含めて返します。これはハンドシェイクが意図されたものであり、中間ノードによる誤用を防ぐためのセキュリティメカニズムです。

  • Sec-WebSocket-Version: サポートするWebSocketプロトコルバージョン(現在の標準は13)。

  • Origin: ウェブブラウザからの接続の場合、リクエスト元のオリジンを示し、サーバーが同オリジンポリシーを適用するために利用できます。

  • Sec-WebSocket-Protocol: クライアントがサポートするサブプロトコルのリスト。サーバーはリストから一つを選択し、レスポンスで返します。

  • Sec-WebSocket-Extensions: クライアントがサポートする拡張機能のリスト。

サーバーレスポンスヘッダ例:

  • HTTP/1.1 101 Switching Protocols: プロトコル切り替え成功を示すステータスコード。

  • Upgrade: websocket, Connection: Upgrade: クライアントからの要求を承認。

  • Sec-WebSocket-Accept: Sec-WebSocket-Keyから計算された値。これが一致しない場合、クライアントは接続を閉じるべきです。

  • Sec-WebSocket-Protocol: サーバーが選択したサブプロトコル。

  • Sec-WebSocket-Extensions: サーバーが選択した拡張機能。

フレーム構造

WebSocketプロトコルは、アプリケーションデータをフレームに分割して送信します。各フレームには、そのデータの種類や特性を示すヘッダが付与されます。

WebSocketフレーム形式 (RFC 6455 Section 5.2)

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
|N|V|V|V|       |S|             |   (if Payload len is 126/127) |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+-------------------------------+
|     Extended payload length continued, if 64 bits             |
+---------------------------------------------------------------+
|           Masking-key (4 bytes) if MASK is set                |
+---------------------------------------------------------------+
|                         Payload Data                          |
+---------------------------------------------------------------+
  • FIN (1ビット): これがメッセージの最後のフラグメントであることを示す。1の場合、最後のフラグメント。0の場合、後続のフラグメントが存在する。

  • RSV1, RSV2, RSV3 (各1ビット): 将来の拡張のために予約されています。現在は0でなければなりません。拡張機能が使用される場合、これらのビットは意味を持つことがあります。

  • Opcode (4ビット): ペイロードデータの種類を定義します。

    • %x0: 継続フレーム (Continuation Frame)

    • %x1: テキストフレーム (Text Frame)

    • %x2: バイナリフレーム (Binary Frame)

    • %x3-7: 非制御フレーム用として予約済み

    • %x8: 接続終了フレーム (Close Frame)

    • %x9: Pingフレーム (Ping Frame)

    • %xA: Pongフレーム (Pong Frame)

    • %xB-F: 制御フレーム用として予約済み

  • MASK (1ビット): ペイロードデータがマスキングされているかどうかを示します。クライアントからサーバーへのすべてのフレームは、プロキシキャッシュポイズニングやメッセージインジェクションを防ぐため、必ずマスキングされなければなりません。サーバーからクライアントへのフレームはマスキングされません。

  • Payload len (7ビット/16ビット/64ビット): ペイロードデータの長さをバイト単位で示します。

    • 0-125: 直接長さを示す。

    • 126: 次の2バイトが16ビット符号なし整数として長さを示す。

    • 127: 次の8バイトが64ビット符号なし整数として長さを示す(最上位ビットは0)。

  • Masking-key (0または4バイト): MASKビットが1の場合に存在し、ペイロードデータをデマスキングするための32ビット(4バイト)のキー。

  • Payload Data: 実際のアプリケーションデータ。マスキングされている場合、Payload Data[i] = Original Data[i] XOR Masking-key[i MOD 4] のようにバイトごとにXORされます。

断片化 (Fragmentation)

大きなメッセージは複数のフレームに断片化して送信できます。最初のフラグメントはOpcodeでメッセージタイプを指定し、FINビットは0です。続くフラグメントはOpcodeが%x0 (継続フレーム) となり、FINビットが最後のフラグメントで1に設定されます。これにより、メッセージ全体の送信中に別のメッセージをインターリーブすることが可能になりますが、通常は一つのメッセージを完全に送信し終えてから次のメッセージを送信します。

相互運用

WebSocketはHTTP/1.1のアップグレードメカニズムを利用するため、既存のHTTPプロキシやファイアウォールと互換性があります。しかし、プロキシによってはWebSocketのアップグレードヘッダを正しく処理できない場合があり、その場合は接続が確立できません。WSS (WebSocket Secure) を利用する場合、TLSトンネル内でWebSocketトラフィックが暗号化されるため、中間ノードはWebSocketプロトコルの詳細にアクセスできません。

サブプロトコルや拡張機能はハンドシェイク時にネゴシエートされ、相互運用性を確保します。これにより、特定のアプリケーション層プロトコル(例: STOMP over WebSocket)や圧縮(例: permessage-deflate)を標準化された方法で利用できます。

セキュリティ

WebSocketプロトコルには、いくつかのセキュリティ考慮事項が含まれています。

  • Originベースのアクセス制御: クライアントからの Origin ヘッダを用いて、サーバーは接続元が許可されたドメインであるかを確認し、クロスサイトリクエストフォージェリ (CSRF) 攻撃を防ぐことができます。

  • TLS/WSSの利用: ws:// の代わりに wss:// を使用することで、TCP接続上でTLSプロトコルが利用され、通信が暗号化・認証されます。これにより、盗聴、改ざん、なりすましを防ぎます。

  • ハンドシェイク時のダウングレード攻撃: Sec-WebSocket-Version ヘッダは、プロトコルのダウングレード攻撃を防ぎます。サーバーはサポートするバージョンのみを受け入れ、古いバージョンでの接続要求を拒否します。

  • フレームマスキング: クライアントからサーバーへのすべてのフレームのペイロードは、ランダムなマスクキーによってマスキングされます。これにより、悪意のあるプロキシがメッセージを改ざんしたり、キャッシュポイズニングやメッセージインジェクション攻撃を行ったりするのを防ぎます。サーバーからのフレームはマスキングされません。

  • リプレイアタック: WebSocketプロトコル自体には、リプレイアタックに対する組み込みの保護メカニズムはありません。これは通常、上位のアプリケーション層プロトコルやTLSレベルのセッション管理によって対処されます。

  • キー更新: 基盤となるTLS接続は、セッション中に鍵を更新するメカニズム (Renegotiation) を提供します。これにより、長期接続における鍵漏洩のリスクを軽減できます。

  • 0-RTT: WebSocketプロトコルはHTTP/1.1のアップグレードに基づいているため、TLS 1.3やQUICのようなプロトコルが提供する0-RTT (Zero Round-Trip Time) のメリットは直接受けられません。WebSocketのハンドシェイクは常に1-RTT以上の時間が必要です。0-RTTは接続確立の高速化に寄与しますが、リプレイアタックのリスクを伴うため、データ保護には注意が必要です。WebSocketの再接続シーケンスは常に完全なハンドシェイクを伴います。

再接続シーケンス (0-RTTなし)

WebSocketは、切断された場合に新しいTCP接続と新しいHTTPハンドシェイクを確立するため、0-RTT再開は行いません。

sequenceDiagram
    participant C as Client
    participant S as Server

    C ->> S: |WebSocket Data Frames|
    S ->> C: |WebSocket Data Frames|
    Note over C,S: Active WebSocket connection

    C-x S: |Connection Lost (e.g., Network Error)|

    C ->> S: |New TCP SYN|
    Note over C,S: Client attempts to re-establish connection
    S ->> C: |TCP SYN-ACK|
    C ->> S: |TCP ACK|

    C ->> S: |HTTP GET Request (Upgrade)|
    Note over C,S: Full WebSocket handshake required for re-establishment
    S ->> C: |HTTP 101 Switching Protocols|

    C -->> S: |WebSocket Data Frames|
    S -->> C: |WebSocket Data Frames|
    Note over C,S: Re-established WebSocket communication

実装メモ

WebSocketプロトコルを実装する際には、以下の点に注意が必要です。

  • MTU/Path MTU: 基盤のTCPがMTU(Maximum Transmission Unit)やPath MTUを考慮してセグメンテーションを行いますが、アプリケーション層ではWebSocketフレームのペイロードサイズを適切に管理することが重要です。大きなメッセージを送信する際には、フレームの断片化 (Fragmentation) を利用して、ネットワークの帯域幅や遅延を考慮したチャンクサイズで送信することで、ネットワーク輻輳を軽減し、送受信バッファのオーバーフローを防ぐことができます。

  • HOL blocking回避: WebSocket自体は単一のTCPストリーム上で動作するため、理論的にはHead-of-Line (HOL) blockingの影響を受けやすいです。これは、特定のフレームの処理が遅延すると、その後ろのすべてのフレームがブロックされる可能性があるためです。この問題への対策は、多くの場合、上位のアプリケーション層プロトコル(例えば、メッセージキューイングや並行処理)で行われます。ただし、WebSocketの断片化は、メッセージ全体の処理を待たずに最初のフラグメントを受信次第処理を開始するといった最適化を可能にする場合があります。

  • キュー制御と優先度: WebSocketプロトコル自体には、メッセージレベルのキュー制御や優先度付けのメカニズムは提供されていません。これはアプリケーション層で実装する必要があります。緊急性の高いメッセージ(例: リアルタイムゲームの操作入力)とそうでないメッセージ(例: ログデータ)を区別し、異なるキューやスレッドで処理したり、アプリケーションプロトコルレベルで優先度情報を付与したりすることが考えられます。

  • HTTP/2との比較: WebSocketはHTTP/2のストリーム多重化の恩恵を直接受けません。WebSocket接続は単一のHTTP/1.1アップグレードされたTCP接続を使用するため、HTTP/2で複数の論理ストリームが1つのTCP接続上で多重化されるのとは異なります。HTTP/2のフレームベースの多重化やヘッダ圧縮(HPACK)はWebSocketには適用されません。

  • HTTP/3との比較: HTTP/3はQUICプロトコル上で動作し、TCPのHOL blocking問題を解消し、ハンドシェイクの0-RTT再開をサポートします。WebSocketに代わるリアルタイム通信技術として、HTTP/3上で動作するWebTransportがIETFで策定中です。WebTransportは、信頼性のあるストリームと信頼性のないデータグラムの両方を提供し、より高度な多重化と流量制御を可能にすることで、WebSocketのいくつかの制限を克服することを目指しています。

まとめ

WebSocketプロトコルは、HTTP/1.1のアップグレードメカニズムを利用して、Webブラウザとサーバー間の永続的な双方向通信を実現します。そのシンプルながらも効率的なフレーム構造と、既存のWebインフラとの互換性により、リアルタイムWebアプリケーション開発の基盤として広く利用されています。

RFC 6455で定義されたハンドシェイクとフレーム構造は、低レイテンシかつ低オーバーヘッドでの通信を可能にする一方で、Originチェックやマスキングといったセキュリティ機能も内包しています。実装においては、MTUやHOL blocking、アプリケーション層でのキュー制御と優先度付けの考慮が重要です。将来的には、HTTP/3ベースのWebTransportのような新しいプロトコルが、WebSocketのいくつかの課題を解決し、より高度なリアルタイム通信を提供することが期待されています。ネットワークエンジニアとしては、これらのプロトコルの特性を理解し、適切な場面で最適な技術を選択することが求められます。

ライセンス:本記事のテキスト/コードは特記なき限り CC BY 4.0 です。引用の際は出典URL(本ページ)を明記してください。
利用ポリシー もご参照ください。

コメント

タイトルとURLをコピーしました