TT Lab
はじめる
学ぶ 学習パス コース

リアルタイム通信 — WebSocket・gRPC ストリーミング・WebRTC

WebSocket をバイトで読む

TT Labで続きを見る

一言でいうと

WebSocketはHTTPで始まり、101に乗り換えたあと、クライアントだけがマスキングする小さなフレームで双方向のメッセージを運び、クローズコードで切れた理由を残します。

なぜ必要なのか

HTTPは、クライアントが尋ねてサーバーが答える形なので、サーバーのほうから話しかける機能は、長いあいだ、回避策で埋められてきました。ロングポーリングは、応答を握っておく方式なので、メッセージごとにリクエストを新しく送り、ヘッダーのほうがメッセージより大きくなりました。RFC 6455のWebSocketは、接続を1つ開いておいて、双方がいつでもメッセージを送れるようにします。そして、既存のHTTPインフラ(80・443ポート、プロキシ、Cookie)をそのまま通れるように、HTTPリクエストで始まります。

ライブラリがこのすべてを代わりにしてくれますが、障害の記録には、たいてい「1006で切れた」のような1行だけが残ります。その1行を読むには、バイトがどんな形をしているかを知る必要があります。

どう動くのか

オープニングハンドシェイク(4節)は、普通のHTTP/1.1のGETです。クライアントは、Upgrade: websocket、Connection: Upgrade、Sec-WebSocket-Version: 13と、ランダムな16バイトをbase64で書いたSec-WebSocket-Keyを送ります。サーバーは、そのキー文字列の後ろに、固定のGUID258EAFA5-E914-47DA-95CA-C5AB0DC85B11を付けてSHA-1でハッシュし、その20バイトをbase64にした値をSec-WebSocket-Acceptに入れて、101 Switching Protocolsで答えます。1.3節の例のキーdGhlIHNhbXBsZSBub25jZQ==は、s3pPLMBiTxaQ9kYGzzhZRbK+xOo=になります。この計算は、秘密を守るためのものではなく、相手がWebSocketを本当に理解しているサーバーかを確認するためのものです。バージョンが合わなければ、サーバーは426とともに、サポートするバージョンを知らせます。

そのあとはフレームです(5.2節)。

 0               1               2               3
|F|R|R|R| opcode|M| 길이(7)     | 확장 길이(0·2·8바이트) ...
|I|S|S|S|  (4)  |A|             |
|N|V|V|V|       |S|             | 마스크 열쇠(마스킹할 때 4바이트) | 본문 ...

opcodeは、0(継続)・1(テキスト)・2(バイナリ)・8(クローズ)・9(ping)・10(pong)です。長さが125以下なら7ビットにそのまま、126なら後ろの2バイト、127なら後ろの8バイトに書き、8バイトの長さの最上位ビットは0でなければなりません。RSVビットは、拡張(例: 圧縮のpermessage-deflate)に合意したときにだけ立てられ、合意なしに立てられたフレームを受け取ったら、接続を失敗させる必要があります。

クライアントが送るフレームは必ずマスキングし、サーバーが送るフレームはマスキングしません(5.1節)。マスキングは、本文のi番目のバイトを、キーのi mod 4番目のバイトとXORするだけで、キーはフレームにそのまま載って運ばれます。目的は秘密の保持ではなく、10.3節が説明する中間キャッシュ汚染攻撃を防ぐことです。攻撃者が選んだバイトがそのままネットワークに出ると、WebSocketを知らない透過プロキシが、それをHTTPリクエストと応答だと誤解して、キャッシュに入れてしまうことがあるからです。サーバーは、マスキングされていないクライアントフレームを受け取ったら、接続を閉じる必要があります。

制御フレーム(クローズ・ping・pong)は、本文が125バイト以下で、断片化されていてはならず、断片化されたデータメッセージの断片の間に割り込めます。pongは、受け取ったpingの本文をそのまま返します。テキストメッセージは、UTF-8でなければなりません。

クローズ(7節)は、2バイトのステータスコードと、任意のUTF-8の理由からなります。片方がcloseを送ると、もう片方もcloseで答え、そのあと、サーバーがTCPを先に閉じます。

コード 意味
1000 正常終了
1001 去っていく(サーバーの終了、ページの移動)
1002 プロトコルエラー
1006 closeなしで切断。受け取る側が付ける名前で、送ってはいけません
1007 メッセージの内容が形式に合っていない(UTF-8ではないテキスト)
1008 ポリシー違反
1009 メッセージが大きすぎる
1011 サーバーの内部エラー
4000–4999 アプリケーションが使うために残された範囲

1012(サービスの再起動)と1013(あとでもう一度試す)は、RFC本文ではなく、IANAのWebSocketクローズコード登録簿に載っている値です。

現場での姿

「プロキシの後ろでだけ接続できない」は、たいてい、プロキシがUpgradeとConnectionヘッダーを渡さないので、ハンドシェイクが400や200で終わる場合です。nginxでは、2つのヘッダーを明示的に渡す設定をする必要があります。ブラウザーのWebSocket APIは、ユーザー定義のリクエストヘッダーを付けられないので、認証は、Cookieや最初のメッセージ、またはURLの使い捨てトークンで行います。このラボプラットフォームのWebターミナルが、Cookieで身元を確認するのも、同じ理由です。

次のラボですること

acceptキーの計算から、フレームのエンコード・解析、ハンドシェイクの応答、エコーサーバー、クローズコードまで、標準ライブラリで作ります。採点ツールは、生のソケットで接続して、サーバーフレームのMASKビットとクローズコードをバイトで確認し、最後に、websocketsライブラリのクライアントが自分のサーバーを受け入れるかを見ます。