4. Wire Format (线路格式)
4.1 Primitives (原语)
4.1.1 Prefixed Integers (前缀整数)
本文档广泛使用 [RFC7541] 第 5.1 节中的前缀整数. [RFC7541] 中的格式使用时未经修改. 但是请注意, QPACK 使用了一些 HPACK 中未实际使用的前缀大小.
QPACK 实现必须 (MUST) 能够解码长达并包括 62 位的整数.
4.1.2 String Literals (字符串字面量)
[RFC7541] 第 5.2 节定义的字符串字面量也在整个文档中使用. 此字符串格式包括可选的 Huffman 编码.
HPACK 定义字符串字面量从字节边界开始. 它们以单个位标志开始, 在本文档中表示为 'H' (指示字符串是否经过 Huffman 编码), 后跟编码为 7 位前缀整数的字符串长度, 最后是指示的字节数据数量. 当启用 Huffman 编码时, 使用 [RFC7541] 附录 B 中的 Huffman 表而不进行修改, 指示的长度是编码后字符串的大小.
本文档通过允许字符串字面量不从字节边界开始来扩展字符串字面量的定义. "N 位前缀字符串字面量" 从字节中间开始, 前 (8-N) 位分配给前一个字段. 该字符串使用一位作为 Huffman 标志, 后跟编码为 (N-1) 位前缀整数的已编码字符串的长度. 前缀大小 N 的值可以在 2 到 8 之间 (含). 字符串字面量的其余部分未修改.
没有注明前缀长度的字符串字面量是 8 位前缀字符串字面量, 并遵循 [RFC7541] 中的定义而不进行修改.
4.2 Encoder and Decoder Streams (编码器和解码器流)
QPACK 定义了两种单向流类型:
-
编码器流 (Encoder Stream) 是类型为 0x02 的单向流. 它承载从编码器到解码器的未分帧的编码器指令序列.
-
解码器流 (Decoder Stream) 是类型为 0x03 的单向流. 它承载从解码器到编码器的未分帧的解码器指令序列.
HTTP/3 端点包含 QPACK 编码器和解码器. 每个端点必须 (MUST) 最多启动一个编码器流和最多一个解码器流. 接收到任一流类型的第二个实例必须 (MUST) 被视为类型为 H3_STREAM_CREATION_ERROR 的连接错误.
发送方禁止 (MUST NOT) 关闭这两个流中的任何一个, 接收方禁止 (MUST NOT) 请求发送方关闭这两个流中的任何一个. 任一单向流类型的关闭必须 (MUST) 被视为类型为 H3_CLOSED_CRITICAL_STREAM 的连接错误.
如果编码器流不会被使用 (例如, 如果其编码器不希望使用动态表, 或者对等方允许的动态表的最大大小为零), 端点可以 (MAY) 避免创建编码器流.
如果解码器将动态表的最大容量设置为零, 端点可以 (MAY) 避免创建解码器流.
即使连接的设置阻止使用编码器流和解码器流, 端点也必须 (MUST) 允许其对等方创建它们.
4.3 Encoder Instructions (编码器指令)
编码器在编码器流上发送编码器指令以设置动态表的容量并添加动态表条目. 添加表条目的指令可以使用现有条目来避免传输冗余信息. 名称可以作为对静态表或动态表中现有条目的引用传输, 也可以作为字符串字面量传输. 对于已经存在于动态表中的条目, 完整的条目也可以通过引用使用, 从而创建重复条目.
4.3.1 Set Dynamic Table Capacity (设置动态表容量)
编码器使用以 '001' 3 位模式开始的指令通知解码器动态表容量的更改. 其后是表示为具有 5 位前缀的整数的新动态表容量; 参见第 4.1.1 节.
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| 0 | 0 | 1 | Capacity (5+) |
+---+---+---+-------------------+
Figure 5: Set Dynamic Table Capacity
图 5: 设置动态表容量
新容量必须 (MUST) 低于或等于第 3.2.3 节中描述的限制. 在 HTTP/3 中, 此限制是从解码器接收的 SETTINGS_QPACK_MAX_TABLE_CAPACITY 参数 (第 5 节) 的值. 解码器必须 (MUST) 将超过此限制的新动态表容量值视为类型为 QPACK_ENCODER_STREAM_ERROR 的连接错误.
减少动态表容量可能导致条目被驱逐; 参见第 3.2.2 节. 这禁止 (MUST NOT) 导致不可驱逐的条目被驱逐; 参见第 2.1.1 节. 更改动态表的容量不会被确认, 因为此指令不插入条目.
4.3.2 Insert with Name Reference (使用名称引用插入)
编码器使用以 '1' 1 位模式开始的指令向动态表添加条目, 其中字段名称与存储在静态表或动态表中的条目的字段名称匹配. 第二个 ('T') 位指示引用是对静态表还是动态表的引用. 后面的 6 位前缀整数 (第 4.1.1 节) 用于定位字段名称的表条目. 当 T=1 时, 该数字表示静态表索引; 当 T=0 时, 该数字是动态表中条目的相对索引.
字段名称引用后跟表示为字符串字面量的字段值; 参见第 4.1.2 节.
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| 1 | T | Name Index (6+) |
+---+---+-----------------------+
| H | Value Length (7+) |
+---+---------------------------+
| Value String (Length bytes) |
+-------------------------------+
Figure 6: Insert Field Line -- Indexed Name
图 6: 插入字段行 -- 索引名称
4.3.3 Insert with Literal Name (使用字面名称插入)
编码器使用以 '01' 2 位模式开始的指令向动态表添加条目, 其中字段名称和字段值都表示为字符串字面量.
其后是表示为 6 位前缀字符串字面量的名称和表示为 8 位前缀字符串字面量的值; 参见第 4.1.2 节.
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| 0 | 1 | H | Name Length (5+) |
+---+---+---+-------------------+
| Name String (Length bytes) |
+---+---------------------------+
| H | Value Length (7+) |
+---+---------------------------+
| Value String (Length bytes) |
+-------------------------------+
Figure 7: Insert Field Line -- New Name
图 7: 插入字段行 -- 新名称
4.3.4 Duplicate (复制)
编码器使用以 '000' 3 位模式开始的指令复制动态表中的现有条目. 其后是表示为具有 5 位前缀的整数的现有条目的相对索引; 参见第 4.1.1 节.
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| 0 | 0 | 0 | Index (5+) |
+---+---+---+-------------------+
Figure 8: Duplicate
图 8: 复制
现有条目被重新插入到动态表中, 而无需重新发送名称或值. 这对于避免添加对较旧条目的引用很有用, 因为这可能会阻止插入新条目.
4.4 Decoder Instructions (解码器指令)
解码器在解码器流上发送解码器指令, 以通知编码器有关字段区段的处理和表更新, 以确保动态表的一致性.
4.4.1 Section Acknowledgment (区段确认)
在处理声明的必需插入计数不为零的已编码字段区段后, 解码器发出区段确认指令. 该指令以 '1' 1 位模式开始, 后跟编码为 7 位前缀整数的字段区段的关联流 ID; 参见第 4.1.1 节.
此指令按第 2.1.4 节和第 2.2.2 节中的描述使用.
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| 1 | Stream ID (7+) |
+---+---------------------------+
Figure 9: Section Acknowledgment
图 9: 区段确认
如果编码器接收到引用流的区段确认指令, 该流上具有非零必需插入计数的每个已编码字段区段都已被确认, 则必须 (MUST) 将其视为类型为 QPACK_DECODER_STREAM_ERROR 的连接错误.
区段确认指令可能会增加已知接收计数; 参见第 2.1.4 节.
4.4.2 Stream Cancellation (流取消)
当流被重置或读取被放弃时, 解码器发出流取消指令. 该指令以 '01' 2 位模式开始, 后跟编码为 6 位前缀整数的受影响流的流 ID.
此指令按第 2.2.2 节中的描述使用.
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| 0 | 1 | Stream ID (6+) |
+---+---+-----------------------+
Figure 10: Stream Cancellation
图 10: 流取消
4.4.3 Insert Count Increment (插入计数增量)
插入计数增量指令以 '00' 2 位模式开始, 后跟编码为 6 位前缀整数的增量. 此指令将已知接收计数 (第 2.1.4 节) 增加增量参数的值. 解码器应该发送一个增量值, 该值将已知接收计数增加到到目前为止处理的动态表插入和复制的总数.
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| 0 | 0 | Increment (6+) |
+---+---+-----------------------+
Figure 11: Insert Count Increment
图 11: 插入计数增量
接收到增量字段等于零或将已知接收计数增加到超出编码器已发送的值的编码器必须 (MUST) 将其视为类型为 QPACK_DECODER_STREAM_ERROR 的连接错误.
4.5 Field Line Representations (字段行表示)
已编码的字段区段由前缀和本节中定义的可能为空的表示序列组成. 每个表示对应于单个字段行. 这些表示引用处于特定状态的静态表或动态表, 但它们不修改该状态.
已编码的字段区段在封闭协议定义的流上的帧中承载.
4.5.1 Encoded Field Section Prefix (已编码字段区段前缀)
每个已编码的字段区段都以两个整数为前缀. 必需插入计数编码为使用第 4.5.1.1 节中描述的编码的具有 8 位前缀的整数. 基准编码为符号位 ('S') 和具有 7 位前缀的增量基准值; 参见第 4.5.1.2 节.
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| Required Insert Count (8+) |
+---+---------------------------+
| S | Delta Base (7+) |
+---+---------------------------+
| Encoded Field Lines ...
+-------------------------------+
Figure 12: Encoded Field Section
图 12: 已编码字段区段
4.5.1.1 Required Insert Count (必需插入计数)
必需插入计数标识处理已编码字段区段所需的动态表状态. 阻塞解码器使用必需插入计数来确定何时安全地处理字段区段的其余部分.
编码器在编码之前按如下方式转换必需插入计数:
if ReqInsertCount == 0:
EncInsertCount = 0
else:
EncInsertCount = (ReqInsertCount mod (2 * MaxEntries)) + 1
这里 MaxEntries 是动态表可以拥有的最大条目数. 最小的条目具有空名称和值字符串, 大小为 32. 因此, MaxEntries 的计算如下:
MaxEntries = floor( MaxTableCapacity / 32 )
MaxTableCapacity 是解码器指定的动态表的最大容量; 参见第 3.2.3 节.
此编码限制了长期连接上前缀的长度.
解码器可以使用如下算法重建必需插入计数. 如果解码器遇到不可能由符合标准的编码器产生的 EncodedInsertCount 值, 它必须 (MUST) 将其视为类型为 QPACK_DECOMPRESSION_FAILED 的连接错误.
TotalNumberOfInserts 是插入到解码器动态表中的总数.
FullRange = 2 * MaxEntries
if EncodedInsertCount == 0:
ReqInsertCount = 0
else:
if EncodedInsertCount > FullRange:
Error
MaxValue = TotalNumberOfInserts + MaxEntries
# MaxWrapped is the largest possible value of
# ReqInsertCount that is 0 mod 2 * MaxEntries
MaxWrapped = floor(MaxValue / FullRange) * FullRange
ReqInsertCount = MaxWrapped + EncodedInsertCount - 1
# If ReqInsertCount exceeds MaxValue, the Encoder's value
# must have wrapped one fewer time
if ReqInsertCount > MaxValue:
if ReqInsertCount <= FullRange:
Error
ReqInsertCount -= FullRange
# Value of 0 must be encoded as 0.
if ReqInsertCount == 0:
Error
例如, 如果动态表是 100 字节, 则必需插入计数将以模 6 编码. 如果解码器已接收 10 次插入, 则编码值 4 表示字段区段的必需插入计数为 9.
4.5.1.2 Base (基准)
基准用于解析动态表中的引用, 如第 3.2.5 节所述.
为了节省空间, 基准使用单位符号 ('S' 在图 12 中) 和增量基准值相对于必需插入计数编码. 符号位为 0 表示基准大于或等于必需插入计数的值; 解码器将增量基准的值添加到必需插入计数以确定基准的值. 符号位为 1 表示基准小于必需插入计数; 解码器从必需插入计数中减去增量基准的值, 并再减去一以确定基准的值. 即:
if Sign == 0:
Base = ReqInsertCount + DeltaBase
else:
Base = ReqInsertCount - DeltaBase - 1
单遍编码器在编码字段区段之前确定基准. 如果编码器在编码字段区段时在动态表中插入了条目并正在引用它们, 则必需插入计数将大于基准, 因此编码的差值为负, 符号位设置为 1. 如果字段区段未使用引用表中最近条目的表示进行编码且未插入任何新条目, 则基准将大于必需插入计数, 因此编码的差值为正, 符号位设置为 0.
基准的值禁止 (MUST NOT) 为负. 尽管协议可能使用负基准和 Post-Base 索引正确运行, 但这是不必要和低效的. 如果必需插入计数的值小于或等于增量基准的值, 端点必须 (MUST) 将符号位为 1 的字段块视为无效.
在编码字段区段之前产生表更新的编码器可能会将基准设置为必需插入计数的值. 在这种情况下, 符号位和增量基准都将设置为零.
不引用动态表编码的字段区段可以对基准使用任何值; 将增量基准设置为零是最有效的编码之一.
例如, 如果必需插入计数为 9, 解码器接收到符号位 1 和增量基准 2. 这将基准设置为 6 并为三个条目启用 Post-Base 索引. 在此示例中, 相对索引 1 指的是添加到表中的第五个条目; Post-Base 索引 1 指的是第八个条目.
4.5.2 Indexed Field Line (索引字段行)
索引字段行表示标识静态表中的条目或动态表中绝对索引小于基准值的条目.
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| 1 | T | Index (6+) |
+---+---+-----------------------+
Figure 13: Indexed Field Line
图 13: 索引字段行
此表示以 '1' 1 位模式开始, 后跟 'T' 位, 指示引用是对静态表还是动态表的引用. 后面的 6 位前缀整数 (第 4.1.1 节) 用于定位字段行的表条目. 当 T=1 时, 该数字表示静态表索引; 当 T=0 时, 该数字是动态表中条目的相对索引.
4.5.3 Indexed Field Line with Post-Base Index (带 Post-Base 索引的索引字段行)
带 Post-Base 索引的索引字段行表示标识动态表中绝对索引大于或等于基准值的条目.
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| 0 | 0 | 0 | 1 | Index (4+) |
+---+---+---+---+---------------+
Figure 14: Indexed Field Line with Post-Base Index
图 14: 带 Post-Base 索引的索引字段行
此表示以 '0001' 4 位模式开始. 其后是匹配字段行的 Post-Base 索引 (第 3.2.6 节), 表示为具有 4 位前缀的整数; 参见第 4.1.1 节.
4.5.4 Literal Field Line with Name Reference (带名称引用的字面字段行)
带名称引用的字面字段行表示编码一个字段行, 其中字段名称与静态表中条目的字段名称或动态表中绝对索引小于基准值的条目的字段名称匹配.
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| 0 | 1 | N | T |Name Index (4+)|
+---+---+---+---+---------------+
| H | Value Length (7+) |
+---+---------------------------+
| Value String (Length bytes) |
+-------------------------------+
Figure 15: Literal Field Line with Name Reference
图 15: 带名称引用的字面字段行
此表示以 '01' 2 位模式开始. 以下位 'N' 指示是否允许中间方在后续跳上将此字段行添加到动态表. 当设置了 'N' 位时, 已编码的字段行必须 (MUST) 始终使用字面表示进行编码. 特别是, 当对等方发送它接收到的表示为带 'N' 位设置的字面字段行的字段行时, 它必须 (MUST) 使用字面表示来转发此字段行. 此位旨在保护不希望通过压缩它们而置于风险中的字段值; 有关更多详细信息, 参见第 7.1 节.
第四个 ('T') 位指示引用是对静态表还是动态表的引用. 后面的 4 位前缀整数 (第 4.1.1 节) 用于定位字段名称的表条目. 当 T=1 时, 该数字表示静态表索引; 当 T=0 时, 该数字是动态表中条目的相对索引.
仅从动态表条目中获取字段名称; 字段值编码为 8 位前缀字符串字面量; 参见第 4.1.2 节.
4.5.5 Literal Field Line with Post-Base Name Reference (带 Post-Base 名称引用的字面字段行)
带 Post-Base 名称引用的字面字段行表示编码一个字段行, 其中字段名称与动态表条目的字段名称匹配, 该条目的绝对索引大于或等于基准值.
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| 0 | 0 | 0 | 0 | N |NameIdx(3+)|
+---+---+---+---+---+-----------+
| H | Value Length (7+) |
+---+---------------------------+
| Value String (Length bytes) |
+-------------------------------+
Figure 16: Literal Field Line with Post-Base Name Reference
图 16: 带 Post-Base 名称引用的字面字段行
此表示以 '0000' 4 位模式开始. 第五位是如第 4.5.4 节所述的 'N' 位. 其后是编码为具有 3 位前缀的整数的动态表条目的 Post-Base 索引 (第 3.2.6 节); 参见第 4.1.1 节.
仅从动态表条目中获取字段名称; 字段值编码为 8 位前缀字符串字面量; 参见第 4.1.2 节.
4.5.6 Literal Field Line with Literal Name (带字面名称的字面字段行)
带字面名称的字面字段行表示将字段名称和字段值编码为字符串字面量.
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| 0 | 0 | 1 | N | H |NameLen(3+)|
+---+---+---+---+---+-----------+
| Name String (Length bytes) |
+---+---------------------------+
| H | Value Length (7+) |
+---+---------------------------+
| Value String (Length bytes) |
+-------------------------------+
Figure 17: Literal Field Line with Literal Name
图 17: 带字面名称的字面字段行
此表示以 '001' 3 位模式开始. 第四位是如第 4.5.4 节所述的 'N' 位. 名称紧随其后, 表示为 4 位前缀字符串字面量, 然后是值, 表示为 8 位前缀字符串字面量; 参见第 4.1.2 节.