HTTP/3入門

第3章詳解HTTP/3 ~ いかにしてQUICを活用し、いかにして高速化を実現したか

本章では、HTTP/3がどのようにQUICを利用し、効率良くHTTPメッセージをやりとりするかを説明します。

HTTP/3通信の構成要素

本節では、HTTP/3通信の構成要素であるストリームとフレームについて説明します。

ストリーム ─⁠─ HTTP/3における通信管理の仮想単位

HTTP/3では、QUICストリームを利用してメッセージをやりとりします。HTTP/3のストリームは、QUICの単方向ストリームと双方向ストリームをどのように使うかを定義したものです。

単方向ストリーム ─⁠─ 片方からのみデータを送る

QUICの単方向ストリームを使うHTTP/3のストリームには、表1の5つのタイプがあります。Pushストリームはサーバからしかオープンできませんが、それ以外のストリームはどちらのエンドポイントからもオープンできます。単方向ストリームですので、オープンした側からのみデータを送信できます。

表1 単方向ストリームのタイプ
タイプ名 タイプ値 説明
Control 0x00 通信の制御情報を送信する。通信に関わるパラメータの送信やエラーの通知などに使用する
Push 0x01 サーバプッシュでリソースをプッシュする際に使用する
QPACK Encoder 0x02 QPACK で動的テーブルの更新操作に使用する
QPACK Decoder 0x03 QPACK で動的テーブルの更新操作に対するフィードバックに使用する
Reserved (0x1f * N) + 0x21 不明なストリームタイプは無視するという HTTP/3 の要件を相手が守っているかの確認に使用する

双方向ストリーム ─⁠─ 双方からデータを送る

QUICの双方向ストリームを使うHTTP/3のストリームは、HTTPメッセージの送受信に使うRequestストリームのみが定義されています。常にクライアントからオープンし、リクエストとレスポンスを1つずつ送受信したらクローズされ、再利用はされません。

フレーム ─⁠─ HTTP/3におけるメッセージの単位

HTTP/2と同様に、HTTP/3でもバイナリ形式のフレームというメッセージ形式を定義しています。HTTP/3のフレームは、HTTPメッセージの送信や通信制御に使用します。HTTP/3のフレームは、QUICのSTREAMフレームの中に格納されます図1⁠。

図1 HTTP/3のストリーム
画像

HTTP/3のフレームはタイプを持ち、表2の7つがあります。各フレームはストリームで送受信されますが、フレームのタイプによって使用できるストリームタイプは決められています。

表2 フレームのタイプ
タイプ名 タイプ値 説明
DATA 0x0 コンテンツの送信に使用する
HEADERS 0x1 フィールドの送信に使用する
CANCEL_PUSH 0x3 サーバプッシュのキャンセルに使用する
SETTINGS 0x4 通信に関わるパラメータであるSETTINGSパラメータの送信に使用する
PUSH_PROMISE 0x5 サーバプッシュの送信に使用する
GOAWAY 0x6 エラーを通知し、通信を切断する際に使用する
MAX_PUSH_ID 0xD プッシュ可能な最大Push IDの通知に使用する

HTTP/3通信の開始から終了まで

本節では、HTTP/3通信の開始から終了までを解説します。

HTTP/3に対応しているかどうかの確認

HTTP/3のクライアントは、サーバがHTTP/3に対応しているかどうかを確認したうえでHTTP/3通信を開始します。加えて、HTTP/3で使用するUDPのポート番号は仕様で決められていないため、クライアントは使用するポート番号も確認する必要があります[1]⁠。

クライアントは、いったんHTTP/1.1もしくはHTTP/2でリクエストを送信します。このリクエストは、通常のHTTP/1.1もしくはHTTP/2のリクエストと同じです。

サーバはレスポンスにおいて、自身がHTTP/3をサポートしていることを、RFC 7838のHTTP Alternative Servicesで定義されるAlt-Svcヘッダでクライアントに通知します。

Alt-Svcヘッダの例
Alt-Svc: h3=":50781" ; ma=3600

上記では、UDPの50781番ポートでHTTP/3を提供できることを示しています。maはこの情報の有効期間です。3600秒間は、このサーバはHTTP/3に対応しているものとして確認作業を省略できます。

QUICコネクションの確立

上述のレスポンスヘッダを受け取ったクライアントは、指定されたポートに対してQUICコネクションの確立を試みます。QUICコネクションの確立時に、前章で説明したアプリケーションプロトコルのネゴシエーションにのっとり、HTTP/3通信を行うことに合意します。

コネクションの確立がうまくいかない場合は、実装依存ですがHTTP/2などにフォールバックします。

通信パラメータの設定

QUICコネクションが確立したら、通信に関するパラメータをお互いに設定します。

クライアントとサーバは、それぞれ単方向ストリームのControlストリームをオープンします図2⁠。Controlストリームはそれぞれが必ずただ1つオープンしている必要があり、HTTP/3通信が終了するまでクローズしてはいけません。

図2 SETTINGSフレームの送信
画像

Controlストリームがオープンしたら、クライアントとサーバは、それぞれControlストリームでSETTINGSフレームを送信し、通信に関するパラメータを通知します。パラメータには、MAX_FIELD_SECTION_SIZEというフィールドサイズの上限や、QPACKで利用するものなどがあります。

クライアントは、SETTINGSフレームの送信が終われば、サーバからのSETTINGSフレームの受信を待たずに、次項で解説するHTTPメッセージを送信できます。

HTTPメッセージの送受信

HTTP/3で送信されるHTTPメッセージのフィールドは、HEADERSフレームに格納されます。HTTPメッセージにコンテンツがある場合は、1つ以上のDATAフレームに分けて格納されます。

クライアントは、リクエストが格納されたHEADERSフレームとDATAフレームを、双方向ストリームのRequestストリームで送信します。サーバは、同じRequestストリームで、レスポンスが格納されたHEADERSフレームとDATAフレームを返します。

HTTPメッセージは、QUICコネクション上で複数のRequestストリームを使用して並列的にやりとりされます。前章で説明したとおり、QUICパケットのロスや順番の入れ替わりは回復されます。その間、ほかのストリームは影響を受けません。

疑似ヘッダ ─⁠─ セマンティクス上ヘッダではないもの

フィールドをどのようにHEADERSフレームに格納するかは、ヘッダ圧縮のしくみであるQPACKで定義されています。ヘッダ圧縮を用いて効率良く表現する都合上、HTTP/1.1ではステータスラインにあったHTTPメソッドやHTTPステータスコードは、HTTP/3では擬似ヘッダとして表されます。

リクエストには、次の擬似ヘッダがあります。

:method
リクエストメソッド
:scheme
リクエストURLのスキーム
:authority
リクエスト先のドメイン。Hostヘッダ相当
:path
リクエストURLのパス

レスポンスには、次の擬似ヘッダがあります。

:status
レスポンスのステータスコード

HTTPメッセージの例

HTTP/1.1とHTTP/3のメッセージを比較してみましょう。

まず、リクエストの比較です。

HTTP/1.1のリクエスト例
GET /resource HTTP/1.1
Host: example.org
Accept: image/jpeg
HTTP/3のリクエスト例
:method = GET
:scheme = https
:path = /resource
:authority = example.org
accept = image/jpeg

HTTP/3では、擬似ヘッダも用いて表現されています。これらはHEADERSフレームに格納されています。この例ではコンテンツがないのでDATAフレームはありませんが、POSTやPUTのリクエストでは、コンテンツは別途DATAフレームに格納されて送信されます。

また、HTTP/2およびHTTP/3では、すべてのHTTPヘッダ名は小文字を使用します。

続いて、レスポンスの比較です。

HTTP/1.1のレスポンス例
HTTP/1.1 200 OK
Age: 234274
Cache-Control: max-age=604800
Content-Type: text/html; charset=UTF-8

コンテンツデータ
HTTP/3のレスポンス例
:status = 200
Age = 234274
Cache-Control = max-age=604800
Content-Type = text/html; charset=UTF-8

リクエストと同様に、HTTP/3では擬似ヘッダも用いて表現され、HEADERSフレームに格納されます。コンテンツデータは、別途DATAフレームに格納されて送信されます。

なお、実際にはヘッダはQPACKの仕様にのっとりエンコードされるため、上記の文字列どおりに送られているわけではありません。

通信の終了

HTTPのやりとりが終わるか、通信を継続できない不具合が発生した場合、エラーハンドリングとして通信を終了します。

通信を終了する際は、まずControlストリームでGOAWAYフレームを送信し、切断の準備をします。GOAWAYにはどこまで処理したかの情報が含まれます。また、受信中のHTTPメッセージはやりとりが完了するのを待ちます。

そのあと、前章で解説したQUICでの即時クローズを行います。QUICのCONNECTION_CLOSEフレームを送信することで、QUICコネクションを終了します。このとき、アプリケーションによる終了を示す0x1dタイプのCONNECTION_CLOSEフレームが使用されます。

CONNECTION_CLOSEフレームは、エラーコードを持ちます。HTTP/3で定義されているエラーコードは以下です。

H3_NO_ERROR
正常終了
H3_GENERAL_PROTOCOL_ERROR
具体的には示さないが、仕様違反により切断する
H3_INTERNAL_ERROR
HTTPスタック内のエラー
H3_MISSING_SETTINGS
必要なSETTINGSフレームが送られてこなかった
H3_VERSION_FALLBACK
HTTP/3でリクエストに答えられない

QPACK ─⁠─ ヘッダを圧縮するしくみ

本節では、転送量を削減するためにヘッダ領域の圧縮を行うヘッダ圧縮について説明します。ヘッダ圧縮はHTTP/2で導入されましたが、HTTP/3では届いたパケットから処理するQUICの利点を活かすように考慮されています。

HTTP/2で使用していたヘッダ圧縮のしくみは、RFC 7541のHPACKです。HTTP/3では、QPACKを使用します。⁠QPACK: Header Compression for HTTP/3」という仕様の名称からヘッダ圧縮と呼ばれることが多いですが、ヘッダだけでなく、もちろんトレーラの圧縮にも使用できます。

HPACKやQPACKでは、以降で説明するハフマン符号と辞書データを組み合わせて利用し、データ量の削減を行います。

ハフマン符号 ─⁠─ フィールドを少ないビットで表現する

HPACKやQPACKでは、ハフマン符号を用いてフィールド名やフィールド値の文字列を表現します。後述する辞書データ内でのフィールドの表現にも、ハフマン符号を利用しています。

ハフマン符号では、各文字の出現頻度に応じてビット表現を変更します。たとえば出現頻度の多い「a」には、0b00011を割り当て5ビットで表現します。出現頻度の少ない「q」には、0b1110110を割り当て7ビットで表現します。これにより、フィールド名やフィールド値の文字列を短く表現できます。

英数字や記号を含む各文字とハフマン符号の対応表は、HPACKの仕様中に定義されています。QPACKでも、HPACKの定義をそのまま利用します。

なお、実装の簡略化のために、ハフマン符号を使わず、フィールドを文字列として表現することもできます。

辞書データ ─⁠─ フィールドをインデックス番号で表現する

辞書データであるテーブルを使ってフィールドを表現できます。たとえば、後述の静的テーブルのインデックス番号の31を送るだけで、accept-encoding: gzip, deflate, brを表現できます。

テーブルの各行をエントリと呼びます。各エントリには、インデックス番号、フィールド名、フィールド値があります。ただし、フィールド値は格納されていないこともあります。インデックス番号は0番から始まります。

テーブルを利用してフィールドを表現する際、フィールド名のみテーブルのインデックス番号を参照し、フィールド値は文字列もしくはハフマン符号で指定するという使い方もできます。そのため、格納されているフィールド値とは異なる値を指定することもできます。フィールド値が格納されていないエントリの場合、必然的にこの使い方になります。

テーブルには、静的テーブルと動的テーブルの2種類があります。

静的テーブル
QPACKの仕様で定義されているテーブル
動的テーブル
1つのHTTP/3通信中に更新されていくテーブル

順に解説します。

静的テーブル ─⁠─ 変わらない辞書データ

静的テーブルは、QPACKの仕様で表3のものが定義されており、よく使う汎用的なフィールドが格納されています。HPACKも静的テーブルを持ちますが、当時とはよく使うフィールドが異なるため、刷新されています。静的テーブルには汎用的なものしか入っておらず、たとえばuser-agentヘッダの値といったクライアントによって値が異なるものは入っていません。

表3 静的テーブル(一部)
インデックス番号 フィールド名 フィールド値
0 :authority
1 :path /
2 age 0
3 content-disposition
4 content-length 0
(省略) (省略) (省略)
31 accept-encoding gzip, deflate, br
(省略) (省略) (省略)
98 x-frame-options sameorigin

動的テーブル ─⁠─ 変わる辞書データ

動的テーブルは、1つの通信中に更新されていくテーブルです。静的テーブルにないフィールドを個々にエントリとして追加していきます。動的テーブルのエントリは追加と削除のみが行え、既存のエントリが更新されることはありません。動的テーブルはHTTP/3通信ごとに管理されます。通信が違えば状態は異なりますし、通信が切断されれば状態は失われます。

動的テーブルの更新手順

本項では、動的テーブルがどのように更新されるかを解説します。

動的テーブルは、RequestストリームでやりとりするHEADERSフレームを送信する側と受信する側で状態が一致している必要があります。HEADERSフレームで参照するエントリがずれると、フィールドを正しく解釈できません。HPACKではHEADERSフレームの中で動的テーブルを更新しますが、QPACKではHEADERSフレームとは独立した別のストリームで動的テーブルを更新します。

QPACKでは、フィールドをエンコードするエンコーダと、デコードするデコーダの立場があります。クライアントとサーバはそれぞれ両方の立場を持ちます。リクエストの場合、クライアントがエンコーダ、サーバがデコーダです。レスポンスの場合、サーバがエンコーダ、クライアントがデコーダです。

エンコーダとデコーダで齟齬そごなく動的テーブルを更新するために、単方向ストリームのQPACK Encoderストリーム、QPACK Decoderストリームをそれぞれ1つだけ使用します。1つの単方向ストリームで動的テーブルの更新を指示するため、指示の順番が入れ替わることはなく、エンコーダが送った順番どおりにデコーダで処理されます図3⁠。

図3 QPACKのやりとり
画像

QPACK Encoderストリーム ─⁠─ 動的テーブルの更新命令を送る

エンコーダは、QPACK EncoderストリームでEncoder instructionsというメッセージを送信し、動的テーブルにエントリを追加します。動的テーブルのエントリは、エンコーダが送信するRequestストリームのHEADERSフレームのHeader Block Representationsから参照して使えます。

QPACK EncoderストリームとRequestストリームではストリームが異なるため、Encoder instructionsとHeader Block Representationsは、送信した順番で受信されるとは限りません。そのため、Encoder instructionsを運ぶパケットがロスし、デコーダが受け取ったHEADERSフレームを処理する際に参照先のエントリが存在しないことがあります。このような場合、デコーダは処理に必要なEncoder instructionsが届くまで待ちます。

動的テーブルはキャパシティに達すると、古いエントリから順に削除されます。

QPACK Decoderストリーム ─⁠─ エンコーダにフィードバックを送る

前述したように動的テーブルの古いエントリは削除されます。そのため、たとえばエンコーダ側が参照したエントリが、デコーダ側で削除されていては、処理を継続できません。このような不整合が起きないように、QPACK Decoderストリームで次のDecoder instructionsをフィードバックとして送信します。

Header Acknowledgement
動的テーブルを参照するストリームを処理した際に、そのストリームIDを示す
Insert Count Increment
以前に送信してから、動的テーブルに追加されたエントリの増分を示す
Stream Cancellation
Requestストリームをキャンセルする。送られたHEADERSフレームが参照した動的テーブルのエントリは使用しなかったことを示す

エンコーダはこのフィードバックにより、特定のエントリを参照するHEADERSフレームがデコーダに処理されたことがわかるので、動的テーブルのエントリを削除できます。また、デコード側で追加されたエントリがわかるので、以後HEADERSフレームで参照しても、デコーダ側で待ち時間が発生しないことを理解できます。

QPACKを使ったフィールドの表現

フィールド群は、ハフマン符号やテーブルへの参照を使ってHeader Block Representationsという形式で表され、HEADERSフレームに格納されます。1つのフィールドは、次のいずれかの方式で表現されます。

Indexed Field Line
テーブルのインデックス番号か相対インデックス番号のみでフィールドが表現される
Indexed Field Line With Post-Base Index
テーブルのPost-Baseインデックス番号のみでフィールドが表現される
Literal Field Line With Name Reference
フィールド名のみテーブルのインデックス番号か相対インデックス番号で表現され、フィールド値は文字列もしくはハフマン符号で表現される
Literal Field Line With Post-Base Name Reference
フィールド名のみテーブルのPost-Baseインデックス番号で表現され、フィールド値は文字列もしくはハフマン符号で表現される
Literal Field Line With Literal Name
フィールド名とフィールド値ともに、それぞれ文字列もしくはハフマン符号で表現される

上記のうちテーブルを参照するものは、静的テーブルと動的テーブルのどちらを参照するかを指定します。

静的テーブルのエントリを参照するときは、インデックス番号を使います。これは、表3で紹介したものです。

動的テーブルのエントリ参照するときは、相対インデックス番号もしくはPost-Baseインデックス番号を使います。相対インデックス番号は、エンコード時にエンコーダが決めたインデックス番号上の基準点よりいくつ小さいかで表されます。Post-Baseインデックス番号は、逆に基準点よりいくつ大きいかで表されます。動的テーブルの参照方法および基準点は、エンコーダの都合により決まります。

サーバプッシュ ─⁠─ レスポンスを先に送るしくみ

本節では、クライアントからのリクエストより先にサーバがレスポンスを送るサーバプッシュについて説明します。サーバプッシュはHTTP/2で導入されましたが、HTTP/3では届いたパケットから処理するQUICの利点を活かすように考慮されています。

なお、サーバプッシュの有用性については議論があり、一概にWebページの表示が速くなるとは限らないため注意が必要です。また、Chromeはサーバプッシュをサポートしないことを表明しています。そのためプロトコルとしてはしくみを持ちますが、実装が対応していないケースもあるため注意してください。

サーバプッシュの考え方

HTTP/1.1ではリクエストがないと、サーバからレスポンスを返すことはできませんでした。サーバプッシュは、リクエストがなくてもレスポンスを送る機能です。これにより、クライアントが必要なファイルをサーバから先行して送れます。

たとえば、次のHTMLファイルにリクエストがあった場合のことを考えます。

<html>
  <body>
    <img src=”./1.jpg” />
    <img src=”./2.jpg” />
    <img src=”./3.jpg” />
    <img src=”./4.jpg” />
  </body>
</html>

サーバは、このHTMLファイルへのリクエストを受け取ると、クライアントが次にimgタグで指定した画像類を要求してくることが予想できます。そういった場合に、サーバ側から画像などのレスポンスを先んじて送信することで、クライアントはより速くWebページを表示できます。

サーバプッシュの手順

本項では、サーバプッシュの手順を説明します。この手順は、1つのファイルごとに行われます。

クライアントからRequestストリームでHEADERSフレームを受け取ったサーバは、同じRequestストリームでPUSH_PROMISEフレームを送信します。PUSH_PROMISEフレームには、次の内容が含まれます。

Push ID
次に送るPushストリームと紐付けるためのID
Encoded Field Section
QPACKでエンコードされたリクエストヘッダ。リクエストヘッダには:authority:pathといった擬似ヘッダが含まれており、サーバプッシュするレスポンスが、どのURLへのリクエストに対するものなのかが記述される

サーバは続けて、単方向ストリームのPushストリームでPush IDを送信し、このPushストリームがどのPUSH_PROMISEフレームに紐付くのかを示します。そして、同じストリームでHEADERSフレームとDATAフレームを用いて、サーバプッシュするデータをレスポンスします。

サーバプッシュのレスポンスを受け取ったクライアントは、利用するとともに、キャッシュ領域に格納します。以降、ブラウザがそのリソースを必要とするとき、リクエストは送信せず、キャッシュ領域に保存したものを利用します。

なお、クライアントは自身のリソースを節約するために、サーバが送信するサーバプッシュの数を制限できます。クライアントはPUSH_PROMISEフレームを受け取り、制限を行うと判断した場合、ControlストリームでMAX_PUSH_IDフレームを送信します。サーバは指定された数以上のサーバプッシュは行いません。

また、クライアントは、サーバプッシュをキャンセルすることもできます。クライアントはPUSH_PROMISEフレームを受け取り、Encoded Field Sectionに記述されたURLのキャッシュをすでに持っている場合などにキャンセルします。キャンセルは、ControlストリームでCANCEL_PUSHフレームを送ることで行います。サーバは、サーバプッシュとして送信途中のデータがあっても、送信を中止します。

Preload ─⁠─ PHPやRailsからプッシュを指示する

HTTP/3のサーバプッシュをサーバサイドアプリケーションから使うための仕様に、Preloadがあります。この仕様は、Web技術の標準化を行っているW3CWorld Wide Web Consortiumが定義しています。

Preloadを用いると、PHPやRuby on Railsといったサーバサイドアプリケーションから、nginxなどのミドルウェアに対してサーバプッシュを指示できます。サーバサイドアプリケーションは、生成したレスポンスにLinkヘッダを付与してプッシュを指示します。Linkヘッダを解釈できるミドルウェアは、指定されたリソースをサーバプッシュします。

次の例では、/app/style.css/app/script.jsがクライアントにプッシュされます。

Linkヘッダを用いたpreloadの例
Link: </app/style.css>; rel=preload; as=style
Link: </app/script.js>; rel=preload; as=script

もちろん、ほかのドメインのリソースを勝手にプッシュすることはできません。

優先度制御 ─⁠─ 速く届けたいもの、遅くてもよいもの

本節では、クライアントがリクエストの優先度を設定する優先度制御について説明します。優先度制御はHTTP/2で導入されましたが、HTTP/3では届いたパケットから処理するQUICの利点を活かすように考慮されています。

なお、優先度制御は、HTTP/3の標準化段階で、HTTPのバージョンによらない別の拡張仕様であるExtensible Prioritization Scheme for HTTPとして分離されました。そのため、正確にはHTTP/3の仕様ではありません。

優先度制御の考え方

Webサイトを閲覧する際、クライアントは速く欲しいものと、遅くてもよいものがあります。たとえば、ブラウザがWebページをレンダリングする際、CSSCascading Style Sheetsはレンダリングの開始に重要ですが、画像はあとからでもよい場合があります。

このようなケースでクライアントからサーバにリソースが必要な順番を伝えることで、Webページを速く表示できる可能性があります。

ただし、サーバはクライアントの優先度を必ずしも尊重する必要はありません。サーバにとって都合の良い順にレスポンスすることも可能です。

複雑だったHTTP/2の優先度制御

HTTP/2では、各ストリームに依存関係と重みを指定して優先度を表現します。このしくみを使うと、⁠ストリームAの処理が終わったらストリームBとストリームCの処理を行ってほしい。ストリームBとストリームCは2対3の割合でサーバリソースを利用してほしい」といったことをサーバに指示できます。ストリームの依存関係は1階層だけということはなく、多階層となります。

この方法は複雑であり、複雑さに見合うメリットが得られるか疑問視されました。クライアントの提示した優先度を尊重しないサーバ実装も多くあったようです。

そのような中で、新しい優先度制御が検討されました。

バージョンによらない新しい優先度制御

HTTPの新しい優先度制御では、新しく定義されたpriorityヘッダを用いて、0~7の8段階でリクエストの優先度を表現します。

priorityヘッダの例
priority: u=5, i

ヘッダの値としては、次のパラメータが指定されます。

u
Urgencyを意味し、値が小さいほど優先度が高いことを示す
i
Incrementalを意味し、たとえば画像など、受信しながら逐次処理可能であることを示す

サーバが複数のリクエストを受け付けた際、このヘッダの値を見て優先度の高いものからレスポンスを返します。

クライアントは、レスポンスを受け取っている最中に優先度を変更することもできます。HTTP/3の場合は、この仕様で新しく定義されたPRIORITY_UPDATEフレームをControlストリームで送信し、優先度を変更します。たとえば、ブラウザのタブの切り替えにより見えなくなったタブの通信は優先度を下げるといったことが行えます。

なお、この新しい優先度制御は、HTTP/1.1やHTTP/2でも使用できます。

補足情報

この仕様は、2022年6月にRFCとして出版されました。仕様の内容については、誌面での説明から変更はありません。

RFCは次のリンクから参照できます。

まとめ

本章では、HTTP/3ではどのようにQUICを利用し効率良くHTTPメッセージをやりとりするかを紹介しました。HTTP/3が使うQUICでは、届いたパケットから処理できます。その特徴を活かすように、HTTP/3でも届いたパケットから処理できるように工夫されています。

おすすめ記事

記事・ニュース一覧