最近またネットワークの問題を調査していて、ついでにHTTPの各バージョンの進化を整理し直した。HTTP/1.1はすでに20年以上使われており、HTTP/2は2015年に標準化された。HTTP/3は、ここ2年でようやく主要サイトにも広まった。単にフィールドを数個追加したのではなく、下層の伝送モデルそのものを変えている。
このエントリではHTTP/1.1、HTTP/2、HTTP/3 (QUIC)をまとめて、「なぜ足りないのか → どう変更したのか → 変更後に何が残るか」という流れで見ていく。
関連ノート
- [[笔记/web/计算机网络/TCP 重传、滑动窗口、流量控制与拥塞控制]]
- [[笔记/web/计算机网络/计算机网络框架]]
- [[笔记/web/计算机网络/HTTP 1.1 与 HTTP 2 协议对比]]
1. HTTP/1.0の歩み
HTTPはTim Berners-Leeが1989年に作った。最初の要望はきわめて単純で、世界中に散らばった物理学者どうしが文書をお互いに引用できるようにすればよかった。1つの文書は1つのファイルで、ブラウザがファイル名をサーバーに送り、サーバーがその内容を返す。
最初の正式バージョンであるHTTP/1.0(RFC 1945、1996年)で中心になるのは1つだけ。リクエストを1回送り、レスポンスを1回受信したらTCPを切断する。
1つのページに画像が10枚あれば、TCP接続を10回確立する必要がある。TCPのハンドシェイクで1.5 RTT、さらにTLSにも数RTTかかる。ダイヤルアップ網の时代には、ハンドシェイクが全体の大きな部分を占めていた。
HTTP/1.0はこの問題を解決しなかった。しかし、拡張可能なプロトコルの骨格を定めた。メソッド、ステータスコード、ヘッダーフィールド、ボディ。その骨格はその後も変わらず、HTTP/2が変えたのは「这些东西をどう運ぶか」であって、「这些东西が何を意味するか」ではない。
3. ブラウザはなぜ6接続を開くのか
HTTP/1.1では1つの接続でリクエストを直列に処理するため、並行処理するにはTCP接続を複数開くしかない。
Chromeは同じドメインに対して既定で6接続を並行利用し、Firefoxも6接続前後だ。バグではなく、HTTP/1.1時代に必要な妥協だった。
副作用は次の3つ。
- 各接続が独立したTCPの輻輳制御を実行し、互いに協調しない。
- 各接続がメモリ、ファイルディスクリプタ、ポートを消費する。
- フロントエンドエンジニアは「リクエスト数を減らす」ために、スプライトシート、CSS/JSの結合、ドメインシャーディング(sharding)といった奇技淫巧に凝った。
HTTP/2の導入後は、この手の工夫はいらなくなった。
5. バイナリフレーミング
HTTP/1.xのメッセージはテキスト形式だ。
GET /index.html HTTP/1.1\r\n
Host: example.com\r\n
\r\n
テキストプロトコルの利点は可読性があり、デバッグもできること。代償はパースが遅く、曖昧さが多いこと(改行が\nなのか、\r\nなのか、別の形式なのか)。
HTTP/2ではバイナリ形式に変わった。各フレームには9バイトの固定ヘッダーがある。
Length:フレームペイロードのバイト数Type:フレーム種別(HEADERS、DATA、PRIORITYなど)Flags:フラグビット(ENDSTREAM、ENDHEADERS)Stream Identifier:ストリームID
常见帧类型:
| Type | 役割 |
|---|---|
| HEADERS | リクエスト/レスポンスのヘッダーを運ぶ |
| DATA | ボディを運ぶ |
| PRIORITY | ストリームの優先度を宣言する |
| RST_STREAM | 1つのストリームをキャンセルする |
| SETTINGS | 接続パラメータをネゴシエートする |
| WINDOW_UPDATE | フロー制御 |
| PING | ハートビートとRTT測定 |
| GOAWAY | 接続を閉じる意思を通知する |
| PUSH_PROMISE | サーバープッシュの予告 |
HEADERSとDATAは別のフレームである。つまり、1つのリクエストでは最初にHEADERSを送り、そのあと複数のDATAフレームに分けてボディを送信できる。各DATAフレームの長さは任意だ。
7. HPACKによるヘッダー圧縮
HTTPヘッダーはWebパフォーマンスの古びた難題だ。
- 通常のリクエストのヘッダーは約600-800バイト。
Cookie、User-Agent、Accept、Refererなどのフィールドは、リクエストごとにおおむね同じ。- HTTP/1.xはヘッダーを圧縮しない(gzipはボディを圧縮するもの)。
- 1ページに50個のリクエストがあると、ヘッダーだけでも30-40 KBになる。
HTTP/2はHPACK(RFC 7541)を導入した。
静的テーブルと動的テーブル
中心となるのは2つのテーブルだ。
- 静的テーブル:規格で固定された61個の一般的なヘッダー名と値のペア。たとえば
:method GET、:scheme https、:authority example.com。 - 動的テーブル:接続する双方がそれぞれ維持する、変更可能なテーブル。
ヘッダーを送信するとき:
- 静的テーブルにある場合 → 1バイトのインデックスだけを送る。
- 動的テーブルにある場合 → インデックスと指定方法を送る。
- どちらにもない場合 → リテラル値を送り、必要に応じて動的テーブルへ挿入する。
1回目の送信:
同じドメインへの2回目のリクエスト:
Huffman符号
リテラル値には、HTTPヘッダーに現れる文字の頻度に合わせて設計されたHuffman符号をもう一度適用して圧縮する。復号はO(n)のビット演算で、性能も高い。
HPACKがgzipよりHTTPSに適した理由
「gzipで圧縮すればいいのでは」と思うかもしれない。
重要なのは安全性だ。
- gzipは文脈に基づく圧縮で、繰り返しパターンに依存する。
- 攻撃者は圧縮後の長さを観察して平文を推測できる。これがCRIME攻撃とBREACH攻撃だ。
- HPACKは辞書インデックスに基づく事前定義の符号化方式で、攻撃者は長さから平文を推測できない。
HPACKは「HTTPSで圧縮しながら安全性を確保する」ために专门設計された。
9. HTTP/2が解けなかった問題:TCP層のヘッドオブラインブロッキング
HTTP/2はアプリケーション層のヘッドオブラインブロッキングを解決したが、トランスポート層では残った。
なぜだろう。HTTP/2の多重化は論理的な仕組みで、下層には1本のTCP接続がある。TCPはデータを順序付きバイトストリームとして扱い、規格では次のように定めている。
1つのパケットでも失われた場合、TCPは後続のパケットをすべて保持し、失われたパケットが再送されて配信されるまで上位アプリケーションに渡してはならない。
そのため、TCPパケットが1つでも失われると、その接続上のすべてのHTTP/2ストリームが止まる。他のストリームのフレームがすでに届いていても同じだ。
パケットロスの少ない有線ネットワークでは問題ない。しかし、モバイルネットワーク、衛星ネットワーク、地下鉄の車内のような環境では、HTTP/2の優位性が大幅に弱まる。
HTTP/2には未解決の問題があと2つある。
- ハンドシェイクのオーバーヘッド:TCPの3-way handshakeとTLS 1.2を合わせて、少なくとも3-RTTでなければ最初のバイトを送れない。
- 接続の移行:TCP接続は4タプル(IP + ポート)に固定されるため、ネットワークの切り替え(Wi-Fi → 4G)で切れる。
これらを解決するため、Googleは索性TCPも置き換え、UDPを土台にQUICを作った。それがHTTP/3だ。
11. ハンドシェイクの比較:なぜHTTP/3は0-RTTなのか
HTTP/3で最も見落とされがちな改善点だ。まず比較する。
HTTP/1.1 + TLS 1.2
HTTP/2 + TLS 1.2
HTTP/1.1と同じハンドシェイクのコストになるが、ALPNの交渉でh2を宣言するだけだ。
HTTP/2 + TLS 1.3
TLS 1.3はハンドシェイクを2-RTTから1-RTTに短縮した。
HTTP/3 + QUIC
QUICはTCPハンドシェイクとTLSハンドシェイクを統合した。1-RTTハンドシェイク + 0-RTTでの接続再開。
0-RTTにも代償がある。リプレイ攻撃のリスクがあるため、通常はGETやHEADなど冪等なリクエストにのみ使う。POSTや送金など金銭に関わる操作では0-RTTを使えない。
13. 実践:NginxでHTTP/2とHTTP/3を有効にする
HTTP/2
http2はlistenの引数で、単独の設定項目ではない。HTTPSとの組み合わせが必須だ。ブラウザは平文のHTTP/2に対応していない。
HTTP/3
Nginx 1.25.0以降ではQUICをネイティブに対応している(以前は実験的モジュールだった):
重要な点は次の通り。
- 443番ポートではTCPとUDPを同時に待ち受ける必要がある。HTTP/3はUDPを使い、古いクライアントはTCP/HTTP/2を使う。
add_header Alt-Svcで、ブラウザにこのサイトがHTTP/3に対応していると伝えられる。reuseportで、複数のワーカープロセスが同じUDPポートを共有できる。- ファイアウォールでUDP 443を許可する必要がある(多くの企業の社内ネットワークではTCP 443しか開けておらず、HTTP/3は利用できない)。
検証
Chrome DevTools → Network → Protocolの列を見れば、h2ならHTTP/2、h3ならHTTP/3だと確認できる。
15. 参考文献
[1] RFC 1945 - HTTP/1.0. https://datatracker.ietf.org/doc/html/rfc1945
[2] RFC 7230 - HTTP/1.1: Message Syntax and Routing. https://datatracker.ietf.org/doc/html/rfc7230
[3] RFC 7540 - HTTP/2. https://datatracker.ietf.org/doc/html/rfc7540
[4] RFC 7541 - HPACK. https://datatracker.ietf.org/doc/html/rfc7541
[5] RFC 8446 - TLS 1.3. https://datatracker.ietf.org/doc/html/rfc8446
[6] RFC 9000 - QUIC. https://datatracker.ietf.org/doc/html/rfc9000
[7] RFC 9114 - HTTP/3. https://datatracker.ietf.org/doc/html/rfc9114
[8] High Performance Browser Networking. Ilya Grigorik. https://hpbn.co/
[9] HTTP/2入門. Google Developers. https://developers.google.com/web/fundamentals/performance/http2
[10] Chrome Status - HTTP/2 Server Pushの削除. https://chromestatus.com/feature/5762410658996224
[11] Nginx HTTP/2モジュール. https://nginx.org/en/docs/http/ngx_http_v2_module.html
[12] 図解ネットワーク. 小林. https://xiaolincoding.com/network/