QUICをゆっくり解説(7):アプリケーションデータとストリーム
2021年09月10日 金曜日
CONTENTS
これまでの記事で、QUICのコネクションを確立する手順について説明しました。今回は、いよいよコネクションを使って、アプリケーションデータをやりとりします。
ストリーム
ストリームとは、QUICコネクションの中で、順番を守って配送されるバイト列のことです。アプリケーションのデータが、ストリームとして運ばれます。たとえば、あるHTTPの要求とそれに対する応答は、QUICでは1つのストリームとして扱われます。
QUICコネクションの中では、複数のストリームが利用できます。ストリーム間ではタイミングを気にする必要はありません。HTTP/1.1のように応答が返ってきた後に次の要求を出すのではなく、複数の要求をあたかも同時に送り、それぞれの応答を受け取ることができます。
ストリームという用語は、HTTP/2由来です。HTTP/2のストリームがHTTPに特化していたのに対し、QUICのストリームは汎用的でどんなアプリケーションデータでも運べます。
QUICを使って通信するプログラムを実装することを考えましょう。ストリームを1つだけ用いるのであれば、単に逐次的なプログラミングで十分です。複数のストリームを非同期に使用する場合は、以下の2つのプログラミングスタイルの内、いずれかを選択することになるでしょう。
- イベント駆動プログラミング:複数の要求は逐次的に送信し、それぞれの応答は登録したイベントハンドラで処理する
- スレッドプログラミング:複数のスレッドが、それぞれ1つずつストリームを利用する。スレッド内は、単なる逐次なプログラミングとなる
分割と再構成
アプリケーションデータが大きければ、1つのQUICパケットには収まりません。一方で、QUICフレームは、QUICパケットを跨ってはならないという規則があります。そのため、アプリケーションデータは、適切な大きさに分割され、フレームに納められたあとに、パケットに格納される必要があります。
アプリケーションデータを収めるフレームは、STREAMフレームです。第3回の記事で説明したように、STREAMフレームは以下のような構造を持ち、フレーム型の値の後に配置されるのでした。
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+
| | | | | |O|L|F|
|0|0|0|0|1|F|E|I|
| | | | | |F|N|N|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Stream ID (i) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| [Offset (i)] ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| [Length (i)] ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Stream Data (*) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
オフセット(Offset)と長さ(Length)のフィールドがあるため、分割や再構成できることが分かります。送信側では、アプリケーションデータを適切な大きさに分割して、オフセットと長さを指示します。受信側では、相手の指示したオフセットと長さから、アプリケーションデータを再構成します。QUICパケットは通信途中で欠落する可能性があるため、STREAMフレームは順番を守って到着しない場合があります。再構成のアルゴリズムは、こういった事態に対応できなければなりません。
オフセットと長さのフィールドは、省略可能です。フィールドの有無は、フレーム型を表す値に符号化されているのでした。フィールドがあれば対応するフラグが1、なければ0になります。
オフセットのフィールドが省略された場合、オフセットの値は0となります。長さのフィールドが省略された場合、QUICパケットの残りの部分全部がアプリケーションデータとなります。
ストリームの生成と終了
TCPでは、ストリームの生成にSYNフラグ、終了にFINフラグが用いられます。一方QUICでは、ストリームの生成にオフセット0、終了にFINフラグが用いられます。
QUICの実装者のコミュニティでは、HTTP/0.9が最初のアプリケーションプロトコルとして選ばれました。HTTP/0.9では、要求が一行でメソッドは GET しかなく、バージョンは指定されません。応答には、ステータス行も応答ヘッダもなく、単に要求されたリソースが返ってきます。リソースの大きさは、コネクションが切れる(End of file が返る)ことで分かります。
QUIC上でのHTTP/0.9の例を以下に図示します。QUICでは、残念ながら共通のAPIは定義されていません。しかし、イメージがつき易いように、この図にはソケットAPIを模倣した擬似APIも書き添えてあります。
黒色は、アプリケーションデータ鍵で暗号化されたSTREAMフレームを表しています。クライアントは、長さ17バイトの文字列を送信した後、FINを送っています。この2つのSTREAMフレームを1つにまとめる実装もあります。サーバは、応答として大きさ3,000バイトのファイルを送り返しています。この例では、ファイルは1,024バイトの単位で分割されています。
ストリームを読み取るAPIは、ストリームの再構成が完了しFINが到着したことを伝えられるように設計されるべきです。たとえば、End of file を返したり、空のバイト列を返したりすることで、ストリームの片方向の終了を伝えることができます。
また、解説していませんが、通信相手が受け取ったQUICパケットに対してはACKが返ってきます。ですので、相手がFINまで受け取ったこと、すなわち送信したアプリケーションデータをすべて受け取ったことを送信側(のQUIC層)が確認できます。
ストリームID
ストリームには以下のように4種類あり、ストリームIDによって区別されます。
- クライアントから作成する双方向のストリーム (ストリームID は 4n)
- サーバから作成する双方向のストリーム (ストリームID は 4n + 1)
- クライアントから作成する一方向のストリーム (ストリームID は 4n + 2)
- サーバから作成する一方向のストリーム (ストリームID は 4n + 3)
1)は、一番一般的なストリームです。たとえば、HTTPの要求+応答に利用できます。ストリームIDは、0、4、8、12… となります。2)の利用例は今のところありません。
3)は、クライアントからサーバへデータを送るだけのストリームです。サーバからの返答はありません。同様に4)は、サーバからクライアントへデータを送るだけのストリームです。HTTP/3では制御用のストリームとして、3)と4)をそれぞれ3本ずつ使います。また、サーバのプッシュには4)が利用されます。
宣伝
9月末発行予定のIIR(Internet Infrastructure Review) Vol.52 に、「HaskellによるQUICの実装」という記事を書きました。Haskellのように軽量スレッドが気軽に利用できるプログラミング言語では、QUICのAPIがどのように与えられるかについて説明しています。発行されたらお知らせしますので、興味があればお読みください。