2. Compression Process Overview (压缩过程概述)
与 HPACK 类似, QPACK 使用两个表将字段行 ("headers") 与索引关联. 静态表 (Static Table, 第 3.1 节) 是预定义的, 包含常见的头部字段行 (其中一些具有空值). 动态表 (Dynamic Table, 第 3.2 节) 在连接过程中构建, 编码器可以使用它来索引已编码字段区段中的头部和尾部字段行.
QPACK 定义了单向流 (Unidirectional Streams), 用于从编码器向解码器发送指令, 反之亦然.
2.1 Encoder (编码器)
编码器通过为列表中的每个字段行发出索引表示 (Indexed Representation) 或字面表示 (Literal Representation), 将头部或尾部区段转换为一系列表示; 参见第 4.5 节. 索引表示通过用静态表或动态表的索引替换字面名称和可能的值来实现高压缩率. 对静态表的引用和字面表示不需要任何动态状态, 且永远不会导致队头阻塞风险. 如果编码器尚未收到指示该条目在解码器处可用的确认 (Acknowledgment), 则对动态表的引用存在队头阻塞风险.
编码器可以 (MAY) 向动态表中插入其选择的任何条目; 它不限于正在压缩的字段行.
QPACK 保留每个字段区段内字段行的顺序. 编码器必须 (MUST) 按照字段表示在输入字段区段中出现的顺序发出它们.
QPACK 旨在将可选状态跟踪的负担放在编码器上, 从而产生相对简单的解码器.
2.1.1 Limits on Dynamic Table Insertions (动态表插入限制)
如果表中包含无法驱逐 (Evict) 的条目, 则可能无法向动态表中插入条目.
动态表条目在插入后不能立即被驱逐, 即使它从未被引用过. 一旦动态表条目的插入已被确认, 并且在未确认的表示中没有对该条目的未完成引用, 该条目就变为可驱逐 (Evictable). 请注意, 编码器流上的引用永远不会阻止条目的驱逐, 因为这些引用保证在驱逐该条目的指令之前被处理.
如果动态表没有足够的空间容纳新条目而不驱逐其他条目, 并且将被驱逐的条目不可驱逐, 则编码器禁止 (MUST NOT) 将该条目插入到动态表中 (包括现有条目的副本). 为了避免这种情况, 使用动态表的编码器必须跟踪每个字段区段引用的每个动态表条目, 直到这些表示被解码器确认; 参见第 4.4.1 节.
2.1.1.1 Avoiding Prohibited Insertions (避免禁止插入)
为了确保编码器不会被阻止添加新条目, 编码器可以避免引用接近驱逐的条目. 编码器可以发出复制 (Duplicate) 指令 (第 4.3.4 节) 并引用副本, 而不是引用此类条目.
确定哪些条目太接近驱逐而无法引用是编码器的偏好. 一种启发式方法是在动态表中针对固定数量的可用空间: 未使用的空间或可以通过驱逐非阻塞条目来回收的空间. 为此, 编码器可以维护一个排空索引 (Draining Index), 它是动态表中将要发出引用的最小绝对索引 (第 3.2.4 节). 随着新条目的插入, 编码器增加排空索引以维护它不会引用的表部分. 如果编码器不创建对绝对索引低于排空索引的条目的新引用, 则对这些条目的未确认引用数量最终将变为零, 从而允许它们被驱逐.
<-- Newer Entries Older Entries -->
(Larger Indices) (Smaller Indices)
+--------+---------------------------------+----------+
| Unused | Referenceable | Draining |
| Space | Entries | Entries |
+--------+---------------------------------+----------+
^ ^ ^
| | |
Insertion Point Draining Index Dropping
Point
Figure 1: Draining Dynamic Table Entries
图 1: 排空动态表条目
2.1.2 Blocked Streams (阻塞流)
因为 QUIC 不保证不同流上数据之间的顺序, 解码器可能会遇到引用尚未收到的动态表条目的表示.
每个已编码的字段区段包含一个必需插入计数 (Required Insert Count, 第 4.5.1 节), 即可以解码字段区段的插入计数的最低可能值. 对于使用对动态表的引用编码的字段区段, 必需插入计数比所有引用的动态表条目的最大绝对索引大一. 对于不引用动态表编码的字段区段, 必需插入计数为零.
当解码器收到必需插入计数大于其自身插入计数的已编码字段区段时, 该流无法立即处理, 并被视为 "阻塞" (Blocked); 参见第 2.2.1 节.
解码器使用 SETTINGS_QPACK_BLOCKED_STREAMS 设置指定可以被阻塞的流数量的上限; 参见第 5 节. 编码器必须 (MUST) 始终将可能被阻塞的流数量限制为 SETTINGS_QPACK_BLOCKED_STREAMS 的值. 如果解码器遇到的阻塞流数量超过其承诺支持的数量, 它必须 (MUST) 将其视为类型为 QPACK_DECOMPRESSION_FAILED 的连接错误.
请注意, 解码器可能不会在每个有阻塞风险的流上都被阻塞.
编码器可以决定是否冒流被阻塞的风险. 如果 SETTINGS_QPACK_BLOCKED_STREAMS 的值允许, 通过引用仍在传输中的动态表条目, 通常可以提高压缩效率, 但如果存在丢包或重新排序, 流可能会在解码器处被阻塞. 编码器可以通过仅引用已确认的动态表条目来避免阻塞风险, 但这可能意味着使用字面量. 由于字面量会使已编码的字段区段更大, 这可能导致编码器在拥塞或流量控制限制上被阻塞.
2.1.3 Avoiding Flow-Control Deadlocks (避免流量控制死锁)
在受流量控制限制的流上写入指令可能会产生死锁.
解码器可能会停止在承载已编码字段区段的流上发放流量控制信用, 直到在编码器流上收到必要的更新. 如果在编码器流 (或整个连接) 上授予流量控制信用取决于承载已编码字段区段的流上数据的消费和释放, 则可能导致死锁.
更一般地, 如果解码器在完全收到指令之前保留流量控制信用, 则包含大型指令的流可能会陷入死锁.
为了避免这些死锁, 编码器不应该 (SHOULD NOT) 写入指令, 除非有足够的流和连接流量控制信用可用于整个指令.
2.1.4 Known Received Count (已知接收计数)
已知接收计数 (Known Received Count) 是解码器确认的动态表插入和复制的总数. 编码器跟踪已知接收计数以识别哪些动态表条目可以在不可能阻塞流的情况下被引用. 解码器跟踪已知接收计数以便能够发送插入计数增量 (Insert Count Increment) 指令.
区段确认 (Section Acknowledgment) 指令 (第 4.4.1 节) 暗示解码器已接收到解码字段区段所需的所有动态表状态. 如果已确认字段区段的必需插入计数大于当前已知接收计数, 则将已知接收计数更新为该必需插入计数值.
插入计数增量指令 (第 4.4.3 节) 将已知接收计数增加其增量 (Increment) 参数. 参见第 2.2.2.3 节以获取指导.
2.2 Decoder (解码器)
与 HPACK 中一样, 解码器处理一系列表示并发出相应的字段区段. 它还处理在编码器流上接收的修改动态表的指令. 请注意, 已编码的字段区段和编码器流指令在单独的流上到达. 这与 HPACK 不同, 在 HPACK 中, 已编码的字段区段 (头部块) 可以包含修改动态表的指令, 并且没有专用的 HPACK 指令流.
解码器必须 (MUST) 按照其表示在已编码字段区段中出现的顺序发出字段行.
2.2.1 Blocked Decoding (阻塞解码)
收到已编码的字段区段后, 解码器检查必需插入计数. 当必需插入计数小于或等于解码器的插入计数时, 可以立即处理字段区段. 否则, 接收字段区段的流变为阻塞.
在阻塞期间, 已编码的字段区段数据应该 (SHOULD) 保留在阻塞流的流量控制窗口中. 在流解除阻塞之前, 此数据不可用, 过早释放流量控制会使解码器容易受到内存耗尽攻击. 当插入计数大于或等于解码器已开始从流中读取的所有已编码字段区段的必需插入计数时, 流解除阻塞.
在处理已编码的字段区段时, 解码器期望必需插入计数等于可以解码字段区段的插入计数的最低可能值, 如第 2.1.2 节所规定. 如果遇到小于预期的必需插入计数, 它必须 (MUST) 将其视为类型为 QPACK_DECOMPRESSION_FAILED 的连接错误; 参见第 2.2.3 节. 如果遇到大于预期的必需插入计数, 它可以 (MAY) 将其视为类型为 QPACK_DECOMPRESSION_FAILED 的连接错误.
2.2.2 State Synchronization (状态同步)
解码器通过在解码器流上发出解码器指令 (第 4.4 节) 来发出以下事件的信号.
2.2.2.1 Completed Processing of a Field Section (完成字段区段处理)
在解码器完成解码使用包含动态表引用的表示编码的字段区段后, 它必须 (MUST) 发出区段确认指令 (第 4.4.1 节). 在中间响应、尾部和推送请求的情况下, 流可能携带多个字段区段. 编码器将每个区段确认指令解释为确认在给定流上发送的包含动态表引用的最早未确认字段区段.
2.2.2.2 Abandonment of a Stream (放弃流)
当端点在流结束之前或在处理该流上的所有已编码字段区段之前接收到流重置, 或者当它放弃流的读取时, 它生成流取消 (Stream Cancellation) 指令; 参见第 4.4.2 节. 这向编码器发出信号, 表明该流上对动态表的所有引用不再未完成. 最大动态表容量 (第 3.2.3 节) 等于零的解码器可以 (MAY) 省略发送流取消, 因为编码器不能有任何动态表引用. 编码器无法从此指令推断出已接收到对动态表的任何更新.
区段确认和流取消指令允许编码器删除对动态表中条目的引用. 当绝对索引低于已知接收计数的条目具有零引用时, 则认为它是可驱逐的; 参见第 2.1.1 节.
2.2.2.3 New Table Entries (新表条目)
在编码器流上接收到新表条目后, 解码器选择何时发出插入计数增量指令; 参见第 4.4.3 节. 在添加每个新动态表条目后发出此指令将向编码器提供最及时的反馈, 但可能与其他解码器反馈冗余. 通过延迟插入计数增量指令, 解码器可能能够合并多个插入计数增量指令, 或用区段确认完全替换它们; 参见第 4.4.1 节. 但是, 如果编码器在使用条目之前等待确认条目, 延迟太久可能会导致压缩效率低下.
2.2.3 Invalid References (无效引用)
如果解码器在字段行表示中遇到对已被驱逐的动态表条目的引用, 或对绝对索引大于或等于声明的必需插入计数 (第 4.5.1 节) 的引用, 它必须 (MUST) 将其视为类型为 QPACK_DECOMPRESSION_FAILED 的连接错误.
如果解码器在编码器指令中遇到对已被驱逐的动态表条目的引用, 它必须 (MUST) 将其视为类型为 QPACK_ENCODER_STREAM_ERROR 的连接错误.