リアルタイム通信 — WebSocket・gRPC ストリーミング・WebRTC
WebSocket をバイトで読む
一言でいうと
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ライブラリのクライアントが自分のサーバーを受け入れるかを見ます。