5月 082019
自作プロキシ (Nekoxy2) の WebSocket (RFC6455) 対応にあたっての私なりの理解をまとめました。
概要
WebSocket 自体はかなり単純で、TCP 上で投げっぱなし全二重通信をする方法と、HTTP/1.1 からのプロトコル アップグレード方法 (WebSocket の開始方法) が定義されているだけです。
実際に何を通信するかはサブプロトコル等のアプリケーション側にお任せとなります。
WebSocket over HTTP/2 (RFC8441) では、この内アップグレード方法が HTTP/2 向けに新しく定義されただけで、通信方法は同一となります。
※ 詳細は HTTP/2 理解メモ 参照。
注意点としては、接続・切断については到達保証がありますが、それ以外のメッセージには到達保証がないため、必要に応じてアプリケーション等で実装する必要があります。
また HTTP が利用するコネクションを再利用する形となるため、経路上に非対応の透過プロキシなどが存在する場合に通信が失敗する可能性があります。
目次
アーキテクチャ
- WebSocket は、HTTP/1.1 で開始された TCP コネクション上にてプロトコルのアップグレードを行うことで開始される [RFC6455 1.3]
- WebSocket コネクションは 1 つの TCP コネクションを専有する [RFC6455 1.1]
- HTTP/2 では代わりに 1 つの HTTP/2 ストリームを専有する
- WebSocket でやり取りされるメッセージの内容は、メッセージの種類とペイロードデータのみによって構成される
- メッセージは必要に応じてフレームに断片化される [RFC6455 1.2]
- WebSocket は拡張することができる [RFC6455 5.8, 9]
- 拡張対象と拡張をネゴシエーションする方法が定められている
- サブプロトコルはネゴシエーションする方法のみ定められている [RFC6455 1.3]
メッセージ
- 実際に通信されるのはフレームであるが、それを結合・解釈した結果のメッセージは意味的にはメッセージの種類とペイロードデータしか持たない
- 拡張情報が付与されることはある
メッセージの種類
- メッセージには大きく分けて制御用とデータ用がある
- 制御用メッセージは分割不可なため、制御用フレーム=制御用メッセージとなる
制御用メッセージ
参照: RFC6455 5.5
- フレームに分割することはできない
- ペイロード長は125バイト以下でなければならない
Close
- 切断することを示す
- 事由をペイロードに持っても良い
- 最初の2バイトはコード値となる
Ping
- ペイロードに任意のデータを持っても良い
- Close 前ならいつでも送信できる
Pong
- 対応する Ping と同じペイロードにする必要がある
- Ping を受信したら可能な限り早く応答しなければならない
- Ping がなくても Pong を送信して良い
- 応答は期待されない
データ用メッセージ
参照: RFC6455 5.6
- フレームに分割され得る
Text
- UTF-8 でエンコードされたデータをペイロードに持つ
- フレームに分割される場合、そのフレームのペイロードは UTF-8 として Valid である必要はない
Binary
- 任意のバイナリをペイロードに持つ
フレーム
- データ用メッセージは複数のフレームに分割され得る
- フレームの種類は、メッセージの種類に分割時用の継続(Continuation)フレームを追加した 6 種類となる
- 制御用
- Close
- Ping
- Pong
- データ用
- Text
- Binary
- Continuation
- 制御用
- フレームの種類は opcode で判別する
- 制御用フレームの種類は、メッセージの種類と一致する
- 分割されていないデータ用フレームの種類は、メッセージの種類と一致する
- 分割されているデータ用フレームの種類は、最初のフレームの種類がメッセージの種類と一致する
フレームの分割
参照: RFC6455 5.4
- 分割できるメッセージは Text or Binary のみ
- 分割されたメッセージは、FIN ビットが 0 な Text or Binary フレームから開始される
- 以下 Continuation フレームが続く
- FIN ビットが 1 な Continuation フレームで終了する
- 元のメッセージを復元するには、単に各フレームのペイロードデータを結合すれば良い
- 送信された順序通りに受信されなければならない
- 分割されたフレームの合間に制御用フレームが挿入されても良い
- 中継者は任意に分割・結合を行って良い
- 制御用フレームは不可
- 解釈できない拡張が適用されている場合は不可
マスク
- クライアントから送信されるすべてのフレームはマスクされなければならない [RFC6455 5.1]
- セキュリティ上の理由による [RFC6455 10.3]
- サーバーが送信するフレームはマスクしてはならない [RFC6455 5.1]
- マスクキーはランダムの 32 ビット(4 バイト)値 [RFC6455 5.3]
- マスク方法は、ペイロードデータに対して単純にマスクキーの 4 バイトずつ XOR していくだけ [RFC6455 5.3]
ペイロード長
参照: RFC6455 5.2
- WebSocket フレームヘッダーのペイロード長指定は妙にややこしい
- 以下の表現形式がある
- 7 bit : 最初の 7 bit が 0 ~ 125 の場合
- それがそのままペイロード長となる
- 制御用フレームはこの長さまでしか許されていない
- 7 + 16 bit : 最初の 7 bit が 126 の場合
- 16 bit 符号なし整数がペイロード長となる(最初の 7 bit は含まない)
- 7 + 64 bit : 最初の 7 bit が 127 の場合
- 64 bit 符号なし整数(最上位ビットは 0 でなければならない)がペイロード長となる(最初の 7 bit は含まない)
- 7 bit : 最初の 7 bit が 0 ~ 125 の場合
- 驚くべきことに、WebSocket フレーム(not メッセージ)の最大ペイロード長は 9 エクサバイトとなる
- フレーム分割に上限はないので、最大メッセージ長は無限大となる
拡張
- 拡張対象 [RFC6455 5.8]
- opcode の空き
- 拡張データフィールド
- 予約済みビット
- 拡張のネゴシエーション [RFC6455 9.1]
- 開始ハンドシェイク(プロトコルアップグレード)時に、
Sec-WebSocket-Extensions
ヘッダーをやり取りすることでネゴシエーションする - 拡張は複数適用することができる
- 拡張はヘッダー値の順序で適用する必要がある [RFC6455 9.1]
- 開始ハンドシェイク(プロトコルアップグレード)時に、
- 拡張内容は IANA に登録されなければならない [RFC6455 11]
- 2019/05 現在、登録されている拡張は
permessage-deflate
だけであるpermessage-deflate
は RFC7692 の Per-Message Compression Extensions (PMCEs) の具体実装の一つ (これも一つしかないが……)
Per-Message Compression Extensions (PMCEs)
参照: RFC7692
- WebSocket メッセージを圧縮する仕様を定義した拡張
Per-Message
とあるように、フレームではない点に注意
- データ用メッセージにのみ適用される
- 同 RFC 内にて deflate による具体実装として
permessage-deflate
が定義されている - 予約済みビットの
RSV1
を圧縮済みかどうかのフラグとして利用しているPer-Message Compressed
bit と呼ばれている- データ用メッセージの先頭データ用フレームの
RSV1
が1
の場合、圧縮されたメッセージとなる 0
の場合は圧縮されていないメッセージとなる
- 複数の PMCE や、引数の異なる同じ PMCE を指定することができる
- その場合、順序通りに処理すれば良い
※ .NET の DefalteStream
では permessage-deflate
の Decompress で an unsupported compression method
として失敗する事があるのが謎……
開始
参照: RFC6455 1.3, 4
- HTTP/1.1 の場合、Upgrade ヘッダーを用いて開始される [RFC7230 6.7]
- HTTP/2 の場合、CONNECT メソッドを用いて開始される
- Upgrade ヘッダーで用いられるトークンは IANA に登録されている
- Hypertext Transfer Protocol (HTTP) Upgrade Token Registry
websocket
とWebSocket
の2種類登録されているが、大文字小文字無視で判定する [RFC6455 4.2.1]
- プロキシがある場合は CONNECT メソッドを用いる
終了
参照: RFC6455 1.4, 5.5.1, 7
- 相互に Close メッセージが交換されたら終了とみなし、TCP コネクションも終了させる
- Close フレーム送信後はデータ用フレームを送信してはならない
- Close フレームを受信したら、可及的速やかに状態コードを含んだ応答用の (ACK的な) Close フレームを送信しなければならない (所謂 half-close となる)
- Close フレームを受信した後もメッセージを送信することはできる
- ただし相手が処理してくれる保証はない