HTTPの接続再利用と多重化
curlによるHTTP/1.1の二回の取得から始め、同じ接続が使われた記録を確認します。続いてHTTP/2フレームとHTTP/3を運ぶ復号後のQUIC STREAMフレームの固定試料を開き、識別子・本文の区切り・欠落時の待機範囲を比較します。
説明
複数の資源を取得するとき、接続を毎回作り直す必要があるとは限りません。HTTP/1.1はメッセージの終端を使って同じ接続を再利用し、HTTP/2とHTTP/3は要求ごとのストリームを識別して扱います。
curlによるHTTP/1.1の二回の取得から始め、同じ接続が使われた記録を確認します。続いてHTTP/2フレームとHTTP/3を運ぶ復号後のQUIC STREAMフレームの固定試料を開き、識別子・本文の区切り・欠落時の待機範囲を比較します。
1一つのcurlで二つの資源を取得
同じ配信先の/part/aと/part/bを続けて取得します。
curl -sS -i二つの応答をヘッダー付きで表示 http://127.0.0.1:18080/part/a http://127.0.0.1:18080/part/b同じ接続先の二つの資源どちらも200で、本文はAと改行、Bと改行の各2バイトです。HTTP/1.1では持続的接続が既定で、本文の終端が分かれば次のメッセージへ進めます。
2要求が使った接続を確認する
配信側の記録から、要求行と接続番号を表示します。
python3 inspect_http.py connections要求ごとの接続番号二つともconnection 1です。同じTCP接続で二回の要求が行われています。これは今回のcurlの逐次取得で、同時に複数の要求を流したことを示す記録ではありません。
3本文長で次の応答を見つける
同じ二応答を連続したバイト列にした試料を読み、ヘッダー終端と本文長から次の開始位置を求めます。
python3 streams.py h1HTTP/1.1のメッセージ境界を計算各応答の空行の後から2バイトだけを本文として取り、その直後を次の応答の開始位置にします。今回の試料はContent-Lengthによる区切りで、接続を閉じることを本文終端としていません。
4HTTP/2のストリームを対応付ける
接続確立後のHEADERSとDATAの試料を解析します。クライアントはstream 1と3を開始し、応答は3が先に完了する例です。
python3 streams.py h2フレームとHPACKの固定試料を解析HEADERSのtypeは1、DATAは0です。要求と応答は同じstream IDで対応します。要求のflags=0x05はEND_HEADERSとEND_STREAM、応答DATAの0x01はEND_STREAMです。本文の完了順が要求の開始順と異なっても、識別子で対応を保てます。
5HTTP/2の9バイトヘッダー
サーバー側フレームの実バイト列を表示します。
xxd -g 1 h2-server.binフレームを1バイトずつ表示先頭の00 00 01がペイロード長1、次の01がHEADERS、04がEND_HEADERS、続く00 00 00 03がstream 3です。その後の88はHPACKの静的表で:status 200を表します。次のフレームの本文は42 0a、つまりBと改行です。
6HTTP/3では外側のQUICストリームで対応付ける
QUICの復号後STREAMフレームを解析します。要求用の双方向ストリーム0と4へ、サーバーが応答を返す部分の試料です。
python3 streams.py h3QUICとHTTP/3の二層を解析HTTP/3のHEADERSはtype 1、DATAはtype 0ですが、そのフレーム内にstream ID欄はありません。外側のQUIC STREAMのid=4または0で要求と対応します。FINはQUIC側の送信終了を表し、HTTP/2のEND_STREAMフラグとは配置が異なります。
7QUICとHTTP/3の境界をバイトで確認
復号後のQUIC STREAMフレームのバイト列を表示します。
xxd -g 1 h3-quic.bin外側と内側のフレーム境界先頭0fはoffset・length・FINを含むSTREAM型、04はstream ID、00はoffset、09はデータ長です。その9バイト内の01 03がHTTP/3 HEADERSと長さ3、00 00 d9がQPACKの状態200です。続く00 02 42 0aがHTTP/3 DATAと2バイトの本文です。
8欠落した範囲を待つ単位
受信済みの断片から、先頭から連続して渡せる範囲を計算するモデルです。HTTP/2を運ぶTCPの途中と、QUIC stream 0の途中をそれぞれ欠落させます。
python3 streams.py loss受信断片と連続して渡せる範囲を計算TCPでは先行する欠落範囲より後のバイトをアプリケーションへ渡せず、後ろにあるstream 3の本文も待ちます。QUICではstream 0が未完了でも、stream 4に必要なバイトが揃えばBを渡せます。欠落が同じストリームにある場合は、そのストリーム内で順序を待ちます。
9HTTP/3の本文が揃う条件
HTTP/3試料の本文をストリームごとに取り出し、FINと合わせて表示します。
python3 - <<'PYCODE'
from pathlib import Path
from streams import quicframes外側のストリームを取得,h3frames
for _,sid,offset,fin,data in quicframes(Path("h3-quic.bin").read_bytes()):
body = b"".join(payload for kind,length,payload in h3frames(data)内側のHTTPフレーム if kind==0DATAの本文だけを集める)
print(sid,repr(body),"FIN",fin)本文とストリーム終了
PYCODEstream 4のB、stream 0のAをそれぞれ取得できます。ストリームが独立していても、接続全体の輻輳制御やヘッダー圧縮の依存など、他の待機要因がなくなるわけではありません。今回の比較は、転送の順序保証がどの単位にかかるかを示しています。
まとめ
HTTP/1.1は本文終端が分かれば接続を再利用できます。今回の二要求は同じ接続で逐次処理されました。
HTTP/2はフレームヘッダーのstream ID、HTTP/3は外側のQUICストリームで要求と応答を対応付けます。
順序待ちの単位がTCP接続全体かQUICの各ストリームかで、欠落時に後続を渡せる範囲が異なります。
操作履歴と解説を振り返れます。
シェル / curl・HTTP/1.1
curl -sS -i http://127.0.0.1:18080/part/a http://127.0.0.1:18080/part/bHTTP/1.1 200 OK Content-Type: text/plain Content-Length: 2 A HTTP/1.1 200 OK Content-Type: text/plain Content-Length: 2 B
python3 inspect_http.py connectionsconnection 1 GET /part/a HTTP/1.1 connection 1 GET /part/b HTTP/1.1
python3 streams.py h1response 1 start 0 body 64 length 2 value b'A\n' response 2 start 66 body 130 length 2 value b'B\n' stream bytes 132 consumed 132
python3 streams.py h2C>S offset=0 length=19 type=1 flags=0x05 stream=1
[(':method', 'GET'), (':scheme', 'http'), (':path', '/part/a'), (':authority', 'a.test')]
C>S offset=28 length=19 type=1 flags=0x05 stream=3
[(':method', 'GET'), (':scheme', 'http'), (':path', '/part/b'), (':authority', 'a.test')]
S>C offset=0 length=1 type=1 flags=0x04 stream=3
[(':status', '200')]
S>C offset=10 length=2 type=0 flags=0x01 stream=3
b'B\n'
S>C offset=21 length=1 type=1 flags=0x04 stream=1
[(':status', '200')]
S>C offset=31 length=2 type=0 flags=0x01 stream=1
b'A\n'xxd -g 1 h2-server.bin00000000: 00 00 01 01 04 00 00 00 03 88 00 00 02 00 01 00 ................ 00000010: 00 00 03 42 0a 00 00 01 01 04 00 00 00 01 88 00 ...B............ 00000020: 00 02 00 01 00 00 00 01 41 0a ........A.
python3 streams.py h3QUIC STREAM file=0 id=4 offset=0 length=9 FIN=True HTTP/3 type=1 length=3 QPACK required-insert-count=0, delta-base=0, :status=200 HTTP/3 type=0 length=2 b'B\n' QUIC STREAM file=13 id=0 offset=0 length=9 FIN=True HTTP/3 type=1 length=3 QPACK required-insert-count=0, delta-base=0, :status=200 HTTP/3 type=0 length=2 b'A\n'
xxd -g 1 h3-quic.bin00000000: 0f 04 00 09 01 03 00 00 d9 00 02 42 0a 0f 00 00 ...........B.... 00000010: 09 01 03 00 00 d9 00 02 41 0a ........A.
python3 streams.py lossTCP missing [10,21) delivered 10 B complete False TCP after recovery delivered 42 B complete True QUIC stream 0 delivered 5 A complete False QUIC stream 4 delivered 9 B complete True QUIC stream 0 after recovery 9 A complete True
python3 - <<'PYCODE'
from pathlib import Path
from streams import quicframes,h3frames
for _,sid,offset,fin,data in quicframes(Path("h3-quic.bin").read_bytes()):
body = b"".join(payload for kind,length,payload in h3frames(data) if kind==0)
print(sid,repr(body),"FIN",fin)
PYCODE4 b'B\n' FIN True 0 b'A\n' FIN True
演習完了