QUICをゆっくり解説(12):確認応答(ACK)

2021年10月19日 火曜日


【この記事を書いた人】
山本 和彦

Haskellコミュニティでは、ネットワーク関連を担当。 4児の父であり、家庭では子供たちと、ジョギング、サッカー、スキー、釣り、クワガタ採集をして過ごす。

「QUICをゆっくり解説(12):確認応答(ACK)」のイメージ

この連載も12回目を迎え、トランスポート層の魔境であるロス検知と輻輳制御に踏み入る時期が来たようです。私はこの分野の専門家ではないため、表面的な説明になってしまうと思うのですが、ご容赦ください。今回は、ロス検知と輻輳制御の前哨戦として、QUICの確認応答について説明します。

TCPのACK

ご存知のようにTCPでは、アプリケーションデータが送信者の送った順番どおりに再構成され、受信者のアプリケーションに渡されます。アプリケーションデータのそれぞれのバイトには、シーケンス番号(通し番号)がつきます。

先頭のシーケンス番号は安全性のために、3-Wayハンドシェイクの際に、乱数的に値が決まります。ここでは、仮想的に先頭のシーケンス番号を0と考えましょう。(Wiresharkでもそう表示されますね。) そうすると、シーケンス番号とは、単に先頭からのオフセットと考えることができます。

TCPが返す確認応答の値は、次に受け取るべきデータのオフセット(届いていないデータの最小のオフセット)です。確認応答は、データを受け取ったらすぐに返すのではなく、いくつか溜めて返すことができます。このため、TCPの確認応答は累積確認応答と呼ばれています。以下の例を見てください。

この図では、棒線の矢印が届いたデータを表し、破線の矢印が欠落したデータを表しています。したがって、各データの送信結果は以下のとおりです。

  1. データ0-999を含むパケットは届いた
  2. データ1000-1999を含むパケットは届いた
  3. データ2000-2999を含むパケットは欠落した
  4. データ3000-3999を含むパケットは届いた

この場合、1番目と2番目に対する確認応答は累積され、確認応答の値は2000となります(A)。送信側が、「データ2000-2999を含むパケット」が欠落したと判断した場合は、そのパケットを再送します。シーケンス番号は変わらないことに注意してください。

また、「データ3000-3999を含むパケット」が届いた際も、届いていないデータの最小のオフセットは2000なので、返す確認応答は2000となります(B)。そのため、送信者は「データ3000-3999を含むパケット」が届いたのか判断できません。これをTCPの確認応答の曖昧性と言います。

QUICのACK

QUICでは、確認応答の曖昧性を排除するために、単調に増加するパケット番号を使います。(TCPのシーケンス番号に相当する情報は、STREAMフレームのオフセットと長さで伝えられます。) 「単調に増加する」とは同じパケット番号が再利用されることはないことを意味します。「コネクションの終了」で説明したように、QUICの場合再送されるのはパケットではなく情報です。再送される情報を格納するパケットには、新しいパケット番号が与えられます。

QUICの確認応答には、パケット番号に対する複数の範囲(range)を指定できます。(ACKフレームの書式は、差分を多用するので初見では理解できない程度に複雑です。今回は説明を割愛します。)TCPに詳しい方は、SACK(Selective Ack)を使えば、TCPの確認応答でも範囲を指定できると主張したくなるかもしれません。TCPのSACKと比べると、QUICの確認応答では、以下の2点が決定的に異なります。

  • SACKで指定できる範囲は3つまでであるが、QUICではパケットに収まるだけの範囲を指定できる (ただし大量の範囲を格納することは推奨されていない)
  • SACKはあくまでヒントである。データの受信側はSACKで指定したデータを破棄してもよい(reneging)。QUICでは、確認応答で指定したデータは、本当に受信したことを意味する

QUICの確認応答の例を以下の図を用いて説明します。

先ほどの図と同様に、棒線の矢印が届いたデータを表し、破線の矢印が欠落したデータを表しています。この図ではパケット12のみが欠落しています。

また、範囲を <start>〜<end> で表現し、<start><end> が同一の場合は単に <start> と書いています。また、複数個の範囲は角括弧の中にカンマで区切って列挙しています。

(a)ではパケット10と11が受信されたことが分かり、(b)ではパケット10、11、および13が受信されたことが分かります。このように、確認応答の情報は重複して送っても構いません。

確認応答に対して、確認応答が戻って来れば、対応する範囲は捨てていけます。たとえば、(a)に対してサーバが確認応答を受け取ると、次からの確認応答には 10〜11 を含める必要がなくなります。

QUICでは、どのパケットに対する確認応答かはっきり分かるので、RTTも計測しやすくなっています。

パケット番号空間

ハンドシェイク」で説明したように、暗号のレベルには、Initial、0-RTT、Handshake、1-RTTがありました。暗号のレベルのそれぞれでロスを検知するため、パケット番号の空間も分かれています。ただし、0-RTTと1-RTTは同じ空間です。

空間名 暗号レベル
Initial空間 Initial
Handshake空間 Handshake
Application data空間 0-RTTと1-RTT

0-RTTパケットに対する確認応答は1-RTTパケットで返されます。

それぞれの空間のパケット番号は0から始まります。パケット番号は単調に増加します。以下にハンドシェイクの例を示します。

実は、パケット番号の増加は1でなくても構いません。これは、どういう目的なのでしょうか?受信側での(相手の)パケット番号の管理は、空間ごとにする必要があります。しかし、送信側での(自分の)パケット番号は、空間を分けずに1つで管理しても構いません。なぜなら、増加は1でなくてもよく、ある空間の最初のパケット番号を0以外から始めても欠落と区別できないからです。

パケット番号空間は、標準化の途中で導入されました。それまでの実装に対して、送信側の変更は不要で、受信側だけ対応すれば仕様を満たすように配慮されたのです。実際に実装によっては、暗号レベルに関係なく、送信するパケットごとにパケット番号を1つずつ増やしていきます。そのような実装間のハンドシェイクの様子を以下に図示します。

山本 和彦

2021年10月19日 火曜日

Haskellコミュニティでは、ネットワーク関連を担当。 4児の父であり、家庭では子供たちと、ジョギング、サッカー、スキー、釣り、クワガタ採集をして過ごす。

Related
関連記事