4月 262019
自作プロキシ (Nekoxy2) の HTTP/2 (RFC7540) 対応にあたっての私なりの理解をまとめました。
HTTP/2 の解説自体は他サイトのほうがわかりやすかったり正確だったりすると思うので、あくまでも参考程度に。
RFC においては SHOULD とか MUST とかの違いは重要ですが、ここに書くの面倒なので必要に応じて調べましょう。
HPACK(RFC7541) については RFC も別だし長いし記事を分けます。
→ HPACK 理解メモ – CAT EARS
目次
概要
- HTTP/1.1 からは、通信の仕方が変わっているだけで中身はほぼ同じ
- RFC7230(Message Syntax and Routing) 相当の部分が大きく置き換えられる [RFC7540 8]
- chunked エンコーディング廃止 [RFC7540 8.1]
- 101 Switching Protocols 廃止 [RFC7540 8.1.1]
- ヘッダー名が小文字に [RFC7540 8.1.2]
- リクエストライン/ステータスラインは擬似ヘッダーとして表現される [RFC7540 8.1.2.1, 8.1.2.3, 8.1.2.4]
- Connection ヘッダー等、コネクション管理機能廃止 [RFC7540 8.1.2.2]
- Cookie ヘッダーが分割できるように [RFC7540 8.1.2.5]
- REFUSED_STREAM エラーコードにより、切断時の安全な再試行が可能に [RFC7540 8.1.4]
- PUSH_PROMISE フレームにより、サーバープッシュが可能に [RFC7540 8.2]
- CONNECT メソッドはストリーム上のトンネル作成に [RFC7540 8.3]
- RFC7231~7235 は HTTP/2 にも適用可能 [RFC7540 8]
- RFC7231 Semantics and Content
- RFC7232 Conditional Requests
- RFC7233 Range Requests
- RFC7234 Caching
- RFC7235 Authentication
- RFC7230(Message Syntax and Routing) 相当の部分が大きく置き換えられる [RFC7540 8]
- 実際に意識してみると HTTP/2 はすでに結構な割合で使われている
アーキテクチャ
- HTTP/2 コネクションは可能な限り長く持続される [RFC7540 9.1]
- HTTP/2 コネクションとそれに紐付く TCP コネクションは、サーバーあたり1つとなる [RFC7540 9.1]
- HTTP/2 コネクションは複数のストリームを持つ [RFC7540 5]
- HTTP/2 コネクション内で送受信される HTTP/2 フレームにて通信する
- フレームはすべてバイナリ形式
- 各フレームの共通ヘッダーにストリーム ID があり、それでフレームが所属するストリームを識別する [RFC7540 4.1]
- ID 0 のストリームは、コネクション制御用に用いられる [RFC7540 5.1.1]
- HTTP/2 Frame Type, HTTP/2 Settings, Error Code の仕様は拡張可能 [RFC7540 5.5]
※ストリームの依存関係(Stream Dependency)と優先度(Weight)はややこしいので省略
フレーム
参照: RFC7540 6
種類 | 概要 | 制御用 | 制御用以外 | ヘッダー内包 | エラーコード | ストリーム終端 | コネクション終端 | 要 ACK | 優先度変更 |
---|---|---|---|---|---|---|---|---|---|
DATA | HTTP メッセージボディーを転送する | × | ○ | × | × | △* | × | × | × |
HEADERS | HTTP メッセージヘッダーを転送する | × | ○ | ○ | × | △* | × | × | × |
PRIORITY | ストリーム優先度を変更する | × | ○ | × | × | × | × | × | ○ |
RST_STREAM | ストリームを切断する | × | ○ | × | ○ | ○ | × | × | × |
SETTINGS | 通信設定を共有する | ○ | × | × | × | × | × | ○ | × |
PUSH_PROMISE | サーバープッシュ用ストリームの作成指示と共に予想リクエストヘッダーを転送する | × | ○ | ○ | × | × | × | × | × |
PING | Ping | ○ | × | × | × | × | × | ○ | × |
GOAWAY | コネクションの終了や重大なエラー通知 | ○ | × | × | ○ | × | ○ | × | × |
WINDOW UPDATE | フロー制御ウインドウサイズを変更 | ○ | ○ | × | × | × | × | × | × |
CONTINUATION | ヘッダー内容の続きを転送する | × | ○ | ○ | × | △** | × | × | × |
*…フラグによる制御
**…先行する HEADERS フレームのフラグ次第
フレーム・メッセージ変換
- 各種ヘッダーフレームを結合することで、ヘッダーブロックが得られる
- ヘッダーブロックの内容は擬似ヘッダーと通常のヘッダーに分けられる
- 擬似ヘッダーはリクエストライン/ステータスラインに対応する
- :scheme 擬似ヘッダーが追加され、プロキシ等からもプロトコル スキームが判別できるようになった [RFC7540 8.1.2.3]
- :authority 擬似ヘッダーが追加され、Host ヘッダーが廃止された [RFC7540 8.1.2.3]
- reason-phrase (OK とか Not Found とか) [RFC7230 3.1.2] は廃止された
- データフレームを結合することで、メッセージボディーに対応したペイロードボディーが得られる
ヘッダーフレームの結合
参照: RFC7540 4.3, 6.2, 8.1
- HEADERS フレームか PUSH_PROMISE フレームから開始される
- 続きがある場合は CONTINUATION フレームが後続する
- END_HEADERS フラグを持ったフレームが来るまで、Header Block Fragment を結合していく
- 結合されたヘッダーブロックは HPACK [RFC7541] で処理する必要がある
- HEADERS フレームに END_STREAM フラグが設定されていて DATA フレームが来ていない場合は、ボディが空なメッセージとなる
- END_STREAM フラグが設定されていても CONTINUATION フレームが後続することはある(CONTINUATION フレームは HEADERS フレームの一部という考え)
- 1つのストリーム(リクエスト/レスポンス)に付き、1xx レスポンスヘッダー、メッセージヘッダー、トレイラーヘッダーの3種類のヘッダーフレームが送信される場合がある
データフレームの結合
参照: RFC7540 6.1, 8.1
- END_STREAM フラグを持ったフレームが来るまで Data を結合していくだけ
- ボディーが空の場合は DATA フレームは送信されない
- PUSH_PROMISE フレームから開始されるサーバープッシュリクエストは、リクエストボディーを持つことができない
通信手順
HTTP/2 開始手順
参照: RFC7540 3
HTTP/2 開始手順は以下の 3 種類。
- http の場合、まず HTTP/1.1 で開始し Upgrade ヘッダーによるプロトコル アップグレード [RFC7230 6.7] を用いる
- https の場合、TLS 拡張機能の ALPN [RFC7301] を用いる
- Alt-Svc ヘッダーなどで、事前に HTTP/2 対応していることを知っておく (平文でのみ使用可)
大半の場合は TLS の ALPN によるネゴシエーションにて HTTP/2 が選択された場合に開始されることとなるでしょう。
リクエスト・レスポンス シーケンス
初期化
- まずはコネクション プリフェイスの送信から開始される [RFC7540 3.5]
- クライアントのコネクション プリフェイスは文字列 “PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n” (0x505249202a20485454502f322e300d0a0d0a534d0d0a0d0a) 固定
- 直後に SETTINGS フレームを送信する
- サーバーのコネクション プリフェイスは、初回 SETTINGS フレーム
送受信
- 1つのリクエスト/レスポンスの交換で、1つのストリームを完全に消費する [RFC7540 8.1]
- 複数のストリームは並列してやり取りすることができる [RFC7540 5]
- 1つのストリーム内のフレーム順序は直列である必要がある [RFC7540 5]
- 1つのコネクション内で送受信されるフレームは、送受信された順序通りに処理する必要がある
- HPACK の仕組みの都合上。HTTP/3 & QPACK にて改善される予定。
- クライアント側から開始されたストリームの ID は奇数となる [RFC7540 5.1.1]
- サーバー側から開始されたストリームの ID は偶数となる [RFC7540 5.1.1]
終了
- エンドポイントはいつでもコネクションを終了できる [RFC7540 5.4.1]
- コネクション終了時には可能な限り GOAWAY フレームを送信すべき [RFC7540 5.4.1, 9.1]
- コネクションエラー発生時には、最後のストリーム ID とエラーコードが含まれた GOAWAY フレームを送信し、コネクションを終了する [RFC7540 5.4.1]
- GOAWAY フレームが複数回送信されることはある [RFC7540 6.8]
- ストリームIDを使い切ったら GOAWAY で切断して再接続する [RFC7540 5.1.1]
サーバープッシュ シーケンス
サーバープッシュの目的
- サーバープッシュは主に、クライアントがリクエストしたページに紐付くリソース(js や css)を、ページ返却前に先行してサーバーが送信する目的で使用される [RFC7540 8.2]
- ただしサーバーに権限のないリソース(例えば https での異なるサーバーのもの)をプッシュするような事はできない [RFC7540 8.2.2]
- RFC7540 では上記目的の場合のような先行するリクエストが必須とは書かれていない気がするので、サーバー側から突然送信することも可能かもしれない
- サーバープッシュ機能は設定で無効化できる [RFC7540 8.2]
サーバープッシュの実現方法
- サーバープッシュは、予想されるリクエスト(予約リクエスト)をサーバーが PUSH_PROMISE フレームで送信し、それに続き対応するレスポンスを送信することで実現される [RFC7540 8.2.1, 8.2.2]
- リクエストボディーが必要な内容をプッシュすることはできない [RFC7540 8.2.1]
- PUSH_PROMISE フレームはヘッダーしか持つことができず、リクエストボディーに対応する DATA フレームを送信する仕組みも無いため
- 予約リクエストは、安全かつキャッシュ可能なメソッドである必要がある [RFC7540 8.2]
- つまり GET or HEAD
- Web ブラウザの場合は恐らくサーバープッシュされたリクエスト/レスポンスをキャッシュしておき、実際にリクエストが発生した際にそのキャッシュを利用するという挙動を取っていると想像できる
- 権限のないリソースのサーバープッシュが禁止されているのも、キャッシュ汚染を防ぐためである [RFC7540 10.4]
- クライアントは RST_STREAM フレームを送信することでプッシュされたレスポンスの受信を拒否することができる [RFC7540 8.2.2]
WebSocket over HTTP/2 シーケンス
参照: RFC8441
- HTTP/2 の CONNECT メソッドによるストリーム トンネルを利用して WebSocket フレームをやり取りできるようにしたもの
- HTTP/1.1 では Upgrade ヘッダーによるプロトコル アップグレードにてネゴシエーションしていたものを、HTTP/2 CONNECT メソッドを拡張してネゴシエーションできるようにした
- 接続開始・ネゴシエーションの方法以外は WebSocket のまま
HTTP/2 での CONNECT メソッド
- CONNECT メソッドは、HTTP/1.1 では 1 つの HTTP コネクション上にトンネルを作らせ、TLS や他プロトコルを通すためのものだった [RFC7231 4.3.6]
- HTTP/2 における CONNECT メソッドは、1 つの HTTP/2 ストリーム上にトンネルを作るために用いられる [RFC7540 8.3]
- トンネル内の通信には DATA フレームが用いられ、そのペイロードで TCP コネクション上のように通信する [RFC7540 8.3]
- トンネル内には DATA フレーム以外は送信してはいけない [RFC7540 8.3]
- トンネル内の DATA フレームの END_STREAM フラグは、TCP の FIN ビットのように扱われ、トンネル接続を終了させる [RFC7540 8.3]
- END_STREAM フラグを受け取ったピアは、END_STREAM フラグを設定した DATA フレームを送信する [RFC7540 8.3]
- トンネル内の DATA フレームの RST_STREAM フラグは、TCP の RST ビットのように扱われ、接続エラーを通知する [RFC7540 8.3]
WebSocket over HTTP/2 のための CONNECT メソッド拡張
- RFC8441 にて、CONNECT トンネル内で用いるプロトコルをネゴシエーションするための :protocol 擬似ヘッダーが拡張追加された
- やり取りされる値は HTTP/1.1 の Upgrade の場合と同じ
-> Hypertext Transfer Protocol (HTTP) Upgrade Token Registry - 同時に :protocol 擬似ヘッダーの使用を有効化するための設定も追加された(既定値は無効)
- やり取りされる値は HTTP/1.1 の Upgrade の場合と同じ