【実務・中級編】CONTINUATIONフレームの役割 – HTTPプロトコル・通信規格実践ガイド

やあ、元気にしてるかい? 新人たちよ。
今日もまた、HTTP/2の深い森へと君たちを誘おう。

HTTP/2は、その導入以来、Webのパフォーマンスを劇的に向上させてくれた。マルチプレクシング、サーバープッシュ、そして何よりも「ヘッダー圧縮(HPACK)」の恩恵は計り知れない。HTTP/1.1時代に、何千バイトものクッキーやUser-AgentでTCPの輻輳窓が圧迫され、ボトルネックになっていたのを、歯を食いしばって見守った経験があるかい? HTTP/2はそんな苦悩を過去のものにしてくれたんだ。

しかし、その高速化の裏には、様々な巧妙なメカニズムが隠されている。今日焦点を当てるのは、普段あまり意識されないけれど、いざという時に君を救うかもしれない、「CONTINUATIONフレーム」だ。ちょっとニッチな話に聞こえるかもしれないが、Web APIの設計や、インフラのトラブルシューティングに携わるなら、その存在を知っておいて損はない。いや、むしろ知っておくべきだ。

HTTP/2のヘッダーとフレームの基本をおさらい

本題に入る前に、HTTP/2の基本的なフレーム構造とヘッダー圧縮について、ざっとおさらいしておこう。

HTTP/2では、HTTPメッセージを「フレーム」という小さな単位に分割して送受信する。これによって、一つのTCPコネクション上で複数のリクエスト・レスポンスを並行して処理できる(マルチプレクシング)。

ヘッダー情報は、主にHEADERフレームを使って送信される。このHEADERフレームのペイロードには、HPACKという圧縮アルゴリズムでエンコードされたヘッダーブロックが格納されるわけだ。HPACKは、静的テーブルと動的テーブルを駆使して、重複するヘッダーフィールドを効率的に圧縮し、通信量を大幅に削減してくれる。

しかし、ここで一つの疑問が浮かび上がらないかい?
「もし、ヘッダーブロックが途方もなく大きくなったらどうなるんだ?」

例えば、大量のクッキーをセットしたり、非常に長い認証トークンを持っていたり、あるいはプロキシが大量の`X-Forwarded-`ヘッダーを付与したりするケースだ。HTTP/2のフレームには、それぞれペイロードサイズの上限がある。デフォルトでは16,384バイト(16KB)だが、`SETTINGS_MAX_FRAME_SIZE`設定でこれを増やすこともできる(最大`2^24-1`バイト)。

もし、HEADERフレームのペイロードに収まりきらないほどヘッダーブロックが大きくなったら? その時に登場するのが、今日の主役、CONTINUATIONフレームなんだ。

CONTINUATIONフレーム:ヘッダーブロックを繋ぎ止める絆

CONTINUATIONフレーム(フレームタイプ `0x9`)は、その名の通り、「継続」を意味する。RFC 9113(旧 RFC 7540)で定義されている通り、これは前のHEADERフレームまたは別のCONTINUATIONフレームに続く、ヘッダーブロックの追加部分を運ぶために使われる。

なぜCONTINUATIONフレームが必要なのか?

最も重要なポイントは、HPACKの圧縮状態(動的テーブル)の整合性を維持するためだ。

HPACKは、送受信されるヘッダーフィールドを動的テーブルに記録し、それ以降の通信で同じヘッダーが出現した際に、そのテーブルのエントリへの参照として送ることで、さらなる圧縮を図る。この動的テーブルは、各HTTP/2コネクションごとに、クライアントとサーバーで同期して保持される。

もし、ヘッダーブロックが複数のフレームに分割された場合、各フレームが個別のヘッダーブロックとして扱われてしまうと、HPACKの動的テーブルは正しく更新されず、圧縮・解凍の整合性が崩壊してしまう。

CONTINUATIONフレームは、「これらは全て同じヘッダーブロックの断片である」ということを通信相手に明示する役割を担っているんだ。つまり、`HEADER`フレームで始まったヘッダーブロックが、複数の`CONTINUATION`フレームを経て、最終的に完全に受信されるまで、HPACKの解凍処理は中断される。そして、すべての断片が揃って初めて、完全なヘッダーブロックとしてHPACKデコーダーに渡され、動的テーブルが更新されるんだ。

`END_HEADERS`フラグの重要性

CONTINUATIONフレームを理解する上で、`END_HEADERS`フラグの存在は不可欠だ。

  • `HEADER`フレームには、そのフレームでヘッダーブロックが完結するかどうかを示す`END_HEADERS`フラグがある。
  • もし`HEADER`フレーム単体でヘッダーブロックが完結するなら、このフラグはセットされる。
  • もしヘッダーブロックが大きすぎて分割される場合、`HEADER`フレームでは`END_HEADERS`フラグはセットされず、後続の`CONTINUATION`フレームが必要であることを示す。
  • そして、ヘッダーブロックの最後の断片を運ぶ`CONTINUATION`フレームには、必ず`END_HEADERS`フラグがセットされる。これにより、通信相手は「これでヘッダーブロックの送信は終わりだ、HPACKデコードを完了していいぞ」と判断できるわけだ。

もしこの`END_HEADERS`フラグが正しくセットされなかったり、途中で欠落したりすると、どうなると思う? そう、HPACKデコーダーは永遠にヘッダーブロックの終わりを待ち続け、タイムアウトするか、プロトコルエラーでコネクションが切断されるだろう。これは、Web APIのクライアントやサーバーが予期せぬエラーを吐き出す、厄介なトラブルの原因になり得るんだ。

通信フロー(シーケンス)のイメージ

実際の通信では、以下のようなシーケンスでヘッダーブロックが送信される。

1. [送信側] HEADERフレームを送信(`END_HEADERS`フラグはクリア)

  • ストリームID:特定のHTTPリクエスト/レスポンスに紐づくID
  • ペイロード:ヘッダーブロックの最初の断片(HPACKエンコード済み)

2. [送信側] CONTINUATIONフレームを送信(`END_HEADERS`フラグはクリア)

  • ストリームID:上記のHEADERフレームと同じID
  • ペイロード:ヘッダーブロックの次の断片

3. [送信側] CONTINUATIONフレームを送信(`END_HEADERS`フラグはクリア、必要に応じて繰り返す)

  • …

4. [送信側] 最後のCONTINUATIONフレームを送信(`END_HEADERS`フラグはセット)

  • ストリームID:上記のHEADERフレームと同じID
  • ペイロード:ヘッダーブロックの最後の断片

受信側は、同じストリームIDを持つこれらのフレームのペイロードを連結し、`END_HEADERS`フラグがセットされたフレームを受け取った時点で、連結された完全なヘッダーブロックをHPACKデコーダーに渡す、という流れになる。

実務でCONTINUATIONフレームを「見る」方法

さて、RFCの仕様は理解できたとして、実際に君たちがWeb APIのデバッグやインフラ運用で、このCONTINUATIONフレームの存在をどうやって確認するのか、具体的な方法を見ていこう。

1. Wiresharkでパケットキャプチャ

これが最も直接的で確実な方法だ。パケットは嘘をつかないからね。

1. キャプチャの準備:

  • 対象となるクライアント(ブラウザ、curlなど)とサーバー間の通信をWiresharkでキャプチャする。
  • HTTPS通信の場合は、SSL/TLSの復号設定が必要になることが多い。例えば、Webブラウザ(Chromeなど)であれば、`SSLKEYLOGFILE`環境変数を設定してセッションキーをファイルに出力し、Wiresharkにそのファイルを指定することで復号できる。

2. フィルタリング:

  • HTTP/2のトラフィックをフィルタリングする。
  • 特にCONTINUATIONフレームだけを見たい場合は、`http2.type == 0x9`というフィルタを使うと良い。
  • HEADERフレームとセットで見たい場合は、`http2.type == 0x1 || http2.type == 0x9`を使う。

3. 解析:

  • キャプチャされたパケットリストから、該当するHTTP/2フレームを選択する。
  • 「展開」すると、フレームの種類、ストリームID、フラグ(`END_HEADERS`がセットされているか否か)など、詳細な情報が表示されるはずだ。
  • ペイロード部分にはHPACKエンコードされたヘッダーブロックの断片が確認できる。

もし、意図的に大きなヘッダーを送信するテストを行うなら、例えば以下のような`curl`コマンドで試すことができる。

適当なWebサーバーに向けて、非常に長いCookieを持つリクエストを送る
注意: 実際のサーバーでこんな長いCookieを使うことは稀だが、テスト用として
curl -v –http2-prior-knowledge \
-H “Cookie: $(python -c ‘print(“a”4000 + “;” + “b”4000 + “;” + “c”4000 + “;” + “d”4000)’)” \
http://localhost:8080/path/to/api

このcurlコマンドの実行中にWiresharkでキャプチャし、`http2.type == 0x1 || http2.type == 0x9`でフィルタリングしてみると、複数のCONTINUATIONフレームが送信されている様子が確認できるはずだ。`END_HEADERS`フラグがセットされているのは最後のCONTINUATIONフレームだけ、ということも明確に見て取れるだろう。

2. `curl`のトレースオプションを活用する

Wiresharkほど低レイヤーではないが、`curl`の`-v`(verbose)や`–trace-ascii`オプションも、HTTP/2のフレームレベルの挙動を垣間見るのに役立つ。ただし、`curl`自体はHTTP/2のフレームを直接ダンプするわけではないので、CONTINUATIONフレームの存在を直接的に確認することは難しい。しかし、ヘッダーの送受信が複数回に分かれるような挙動が見られた場合、裏でCONTINUATIONフレームが使われている可能性を示唆するヒントにはなる。

curlで冗長な情報を表示し、詳細な通信状況を確認
–http2-prior-knowledge は、HTTP/2を直接喋ることを指示 (ALPNネゴシエーションなし)
-H で非常に長いカスタムヘッダーを付与し、CONTINUATIONフレームの発生を促す
curl -v –http2-prior-knowledge \
-H “X-Long-Header: $(python -c ‘print(“X”10000)’)” \
http://localhost:8080/some/endpoint

この出力では、生のHTTP/2フレームは見えないが、ヘッダーが正しく送受信されているか、途中で切れていないか、といった高レベルな視点での確認は可能だ。もしサーバー側でヘッダーが途中で切れているような挙動が見られる場合、CONTINUATIONフレームの処理に問題がある可能性を疑うきっかけになるだろう。

3. Pythonの`h2`ライブラリで低レベルなプロトコル操作

より深くプロトコルを理解したいなら、Pythonの`h2`ライブラリを使って、低レベルでHTTP/2のフレームを操作してみるのも良い練習になる。これにより、CONTINUATIONフレームがどのように組み立てられ、送受信されるのかをコードレベルで体験できる。

import socket
import ssl
from h2.connection import H2Connection
from h2.events import RequestReceived, DataReceived, StreamEnded, WindowUpdated, ResponseReceived
import logging

ロギング設定
logging.basicConfig(level=logging.DEBUG)
log = logging.getLogger(__name__)

大きなヘッダーブロックを生成する関数
def generate_large_headers(size_kb):
headers = [
(‘:method’, ‘GET’),
(‘:authority’, ‘localhost:8080’),
(‘:scheme’, ‘http’), # または https
(‘:path’, ‘/large-header-test’),
]
# 指定されたサイズになるまでダミーヘッダーを追加
# HPACK圧縮されることを考慮し、少し多めに生成
dummy_header_value = “A” 200 # 1つのダミーヘッダーの値
for i in range(size_kb 5): # ざっくりとした繰り返し回数
headers.append((f’x-dummy-header-{i}’, dummy_header_value))
if sum(len(k) + len(v) for k, v in headers) > size_kb 1024:
break
log.info(f”Generated total header size: {sum(len(k) + len(v) for k, v in headers)} bytes”)
return headers

class H2Client:
def __init__(self, host, port, use_tls=False):
self.host = host
self.port = port
self.use_tls = use_tls
self.conn = H2Connection()
self.sock = None

def connect(self):
if self.use_tls:
context = ssl.create_default_context()
context.set_alpn_protocols([‘h2’]) # HTTP/2をALPNでネゴシエート
self.sock = context.wrap_socket(socket.create_connection((self.host, self.port)), server_hostname=self.host)
else:
self.sock = socket.create_connection((self.host, self.port))

self.conn.initiate_connection()
self.sock.sendall(self.conn.data_to_send())

def send_request_with_large_headers(self, headers):
stream_id = self.conn.get_next_available_stream_id()
# ヘッダーを送信。h2ライブラリが内部でHEADERフレームとCONTINUATIONフレームを適切に処理してくれる
self.conn.send_headers(stream_id, headers)
self.conn.end_stream(stream_id) # リクエストボディがない場合はストリームを閉じる
self.sock.sendall(self.conn.data_to_send())

# サーバーからの応答を待機
while True:
data = self.sock.recv(4096)
if not data:
break
events = self.conn.receive_data(data)
for event in events:
if isinstance(event, ResponseReceived):
log.info(f”Received response headers for stream {event.stream_id}: {event.headers}”)
elif isinstance(event, DataReceived):
log.info(f”Received data for stream {event.stream_id}: {len(event.data)} bytes”)
elif isinstance(event, StreamEnded):
log.info(f”Stream {event.stream_id} ended.”)
return # ストリーム終了で処理を終える

def close(self):
self.conn.close_connection()
self.sock.sendall(self.conn.data_to_send())
self.sock.close()

if __name__ == “__main__”:
host = ‘localhost’
port = 8080 # テスト用のHTTP/2サーバーが動いているポート

# 意図的に大きなヘッダーを生成 (例: 64KB分のダミーヘッダー)
# これによりh2ライブラリが内部でCONTINUATIONフレームを生成するはず
large_headers = generate_large_headers(64)

client = H2Client(host, port, use_tls=False) # TLSが必要な場合はTrueに
client.connect()
client.send_request_with_large_headers(large_headers)
client.close()

このコードを実行し、同時にWiresharkでキャプチャすれば、`h2`ライブラリが自動的に大きなヘッダーを複数のフレーム(HEADERフレームとCONTINUATIONフレーム)に分割して送信している様子を観察できるだろう。

Web API設計・インフラ運用における考慮点

CONTINUATIONフレームの存在は、単なるプロトコルの実装詳細ではない。君たちの設計や運用にも影響を与える。

1. ヘッダーサイズの意識:

  • Web APIの設計において、送信するヘッダーのサイズは常に意識すべきだ。特にカスタムヘッダーや認証トークン、セッション情報などを大量に持たせる場合、それがHTTP/2のフレームサイズ上限を超え、CONTINUATIONフレームを多用することにならないか確認しよう。
  • 過剰なヘッダーは、たとえ圧縮されても、処理オーバーヘッドを増やし、動的テーブルの容量を圧迫し、パフォーマンスの低下につながる可能性がある。CDNやリバースプロキシによっては、ヘッダーサイズに独自の制限を設けている場合もあるから注意が必要だ。

2. プロキシ・ロードバランサーの挙動:

  • 古いプロキシや一部のロードバランサー(特にHTTP/1.1とHTTP/2の変換を行うもの)は、HTTP/2のフレーム処理、特にCONTINUATIONフレームのハンドリングにバグを抱えていることがある。
  • もし、大きなヘッダーを持つリクエスト・レスポンスで通信が途切れたり、ヘッダーが不完全に届いたりするような問題に遭遇したら、中継しているネットワーク機器が原因である可能性も疑ってみるべきだ。

3. セキュリティ(HTTP/2DoS攻撃):

  • CONTINUATIONフレームは、HTTP/2のDoS攻撃にも悪用されることがある。例えば、`END_HEADERS`フラグがセットされないまま大量のCONTINUATIONフレームを送りつけたり、非常に長いヘッダーブロックを送信してサーバーのリソース(メモリなど)を枯渇させたりする攻撃手法が存在する。
  • そのため、多くのHTTP/2実装は、ヘッダーブロックの最大サイズや、連続するCONTINUATIONフレームの数に制限を設けている。君たちのWebサーバーやプロキシの設定で、これらの制限が適切に設定されているか確認しておくことを強く推奨する。

まとめ

CONTINUATIONフレームは、HTTP/2のヘッダー圧縮(HPACK)の整合性を維持し、巨大なヘッダーブロックを効率的に、かつ安全に送受信するための、まさに「縁の下の力持ち」だ。普段は意識されないかもしれないが、このフレームが存在するおかげで、我々はHTTP/2の高速性を享受できている。

もし、君たちがHTTP/2環境でヘッダー関連の奇妙な挙動やパフォーマンス問題に直面したら、`Wireshark`でパケットを覗き込み、CONTINUATIONフレームの有無、`END_HEADERS`フラグの状態、そして各フレームのペイロードを注意深く確認してみるんだ。パケットは決して嘘をつかない。そこに真実が隠されているはずだ。

プロトコルの深い理解は、トラブルシューティングの強力な武器になる。今日の話が、君たちのネットワーク探求の一助となれば幸いだ。また次回、ネットワークの奥深い話で会おう!

コメント

タイトルとURLをコピーしました