| 互联网工程任务组 (IETF) | C. Krasic |
| 请求评论: 9204 | M. Bishop |
| 类别:标准轨道 | Akamai Technologies |
| ISSN: 2070-1721 | A. Frindell, Editor |
| 2022年6月 |
QPACK:用于 HTTP/3 的字段压缩
摘要
本规范定义了 QPACK:一种用于在 HTTP/3 中高效表示 HTTP 字段的压缩格式。它是 HPACK 压缩的变体,旨在减少首部阻塞(head-of-line blocking)。
本备忘录的状态
这是一个互联网标准轨道文档。
本文档是 Internet Engineering Task Force (IETF) 的产物,代表 IETF 社区的共识。它已通过公开评审并已获 Internet Engineering Steering Group (IESG) 批准发表。有关互联网标准的更多信息,请参见 RFC 7841 第 2 节。
有关本文档当前状态、任何勘误以及如何提供反馈的信息,可在 https://www.rfc-editor.org/info/rfc9204 获取。
Copyright Notice
Copyright (c) 2022 IETF Trust and the persons identified as the document authors. All rights reserved.
This document is subject to BCP 78 and the IETF Trust's Legal Provisions Relating to IETF Documents (https://trustee.ietf.org/license-info) in effect on the date of publication of this document. Please review these documents carefully, as they describe your rights and restrictions with respect to this document. Code Components extracted from this document must include Revised BSD License text as described in Section 4.e of the Trust Legal Provisions and are provided without warranty as described in the Revised BSD License.
1. 引言
QUIC 传输协议([QUIC-TRANSPORT]) 的设计旨在支持 HTTP 语义,其设计包含了许多 HTTP/2([HTTP/2])的特性。HTTP/2 使用 HPACK([RFC7541]) 来压缩头和尾部节。如果在 HTTP/3([HTTP/3])中直接使用 HPACK,由于其基于对所有流上帧的全序的内在假设,会导致字段节出现首部阻塞(head-of-line blocking)。
QPACK 重用了 HPACK 的核心概念,但进行了重新设计以在存在乱序交付的情况下仍能保证正确性,并为实现提供在抵抗首部阻塞与获得最佳压缩率之间进行权衡的灵活性。设计目标是在相同丢包条件下,以明显更少的首部阻塞接近 HPACK 的压缩比。
1.1. 约定与定义
本文档中关键字 "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY" 和 "OPTIONAL" 应按 BCP 14 中的描述来解释(仅当这些词以全部大写形式出现时,如此处所示)。参见 [RFC2119] 和 [RFC8174]。
本文档使用下列术语:
- HTTP fields:
-
作为 HTTP 消息一部分发送的元数据。该术语包括头部字段和尾部字段。通俗地讲,“headers” 常被用于指代 HTTP 头字段和尾部字段;本文档为更一般的用途采用“fields”。
- HTTP field line:
- HTTP field value:
-
与字段名相关联的数据,由该节中具有该字段名的所有字段行值组合而成,并用逗号分隔连接。
- Field section:
-
与 HTTP 消息关联的有序 HTTP 字段行集合。字段节可以包含多个具有相同名称的字段行,也可以包含重复字段行。HTTP 消息可以同时包含头部和尾部节。
- Representation:
-
表示字段行的指令,可能通过引用动态表或静态表来表示。
- Encoder:
-
将字段节编码的实现。
- Decoder:
-
对编码后的字段节进行解码的实现。
- Absolute Index:
-
动态表中每个条目的唯一索引。
- Base:
-
相对索引和后基索引的参考点。引用动态表条目的表示以 Base 为参考。
- Insert Count:
-
动态表中已插入条目的总数。
注意:QPACK 是一个名称,而非缩写。
1.2. 符号约定
本文档中的图表采用 RFC2360 第 3.1 节 中描述的格式,并增加以下约定:
- x (A)
-
表示 x 为 A 位长。
- x (A+)
-
表示 x 使用第 4.1.1 节 带前缀整数 中定义的带前缀整数编码,起始于 A 位前缀。
- x ...
-
表示 x 为可变长度,并延伸到该区域的末尾。
2. 压缩过程概述
与 HPACK 相似,QPACK 使用两张表将字段行(“headers”)与索引关联。静态表(第 3.1 节)是预定义的,包含常见的头字段行(其中一些具有空值)。动态表(第 3.2 节)在连接过程中逐步构建,编码器可以使用它对编码的字段节中的头部和尾部字段行进行索引。
QPACK 定义了用于在编码器与解码器之间发送指令的单向流。
2.1. 编码器
编码器将头部或尾部节转换为一系列表示,为列表中的每个字段行发出索引表示或字面表示;见 第 4.5 节。索引表示通过将字面名称并(可选地)值替换为静态表或动态表的索引来实现高压缩比。对静态表的引用和字面表示不需要任何动态状态,并且永远不会引起首部阻塞。对动态表的引用如果编码器尚未收到指示该条目在解码器处可用的确认,则有可能导致首部阻塞。
编码器 MAY 将任意条目插入动态表;它不必仅限于其正在压缩的字段行。
QPACK 保留每个字段节内字段行的顺序。编码器 MUST 按输入字段节中出现的顺序发出字段表示。
QPACK 的设计将可选状态跟踪的负担放在编码器上,从而使解码器相对简单。
2.1.1. 动态表插入限制
如果动态表包含无法被逐出的条目,插入新条目可能不可行。
动态表条目在插入后并不立即可被逐出,即便它从未被引用也是如此。一旦动态表条目的插入被确认,并且在未确认的表示中没有未完成的对该条目的引用,该条目就变为可逐出。注意,对编码器流上的引用永远不会阻止条目的逐出,因为那些引用保证会在逐出该条目的指令之前被处理。
如果动态表在不逐出其他条目的情况下没有足够空间放置新条目,且将被逐出的那些条目不可逐出,则编码器 MUST NOT 将该条目(包括现有条目的重复)插入动态表。为避免此情况,使用动态表的编码器必须跟踪每个字段节所引用的动态表条目,直到这些表示被解码器确认;参见 第 4.4.1 节。
2.1.1.1. 避免被禁止的插入
为了确保编码器不会被阻止添加新条目,编码器可以避免引用接近被逐出的条目。与其引用这样的条目,编码器可以发出 Duplicate 指令(第 4.3.4 节)并引用该复制条目。
确定哪些条目过于接近逐出以致不可引用是编码器的偏好设置。一个启发式方法是目标动态表中的固定可用空间:要么未使用的空间,要么可以通过逐出非阻塞条目回收的空间。为实现此目标,编码器可以维护一个 draining index,即它将发出引用的动态表中最小的绝对索引(参见 第 3.2.4 节)。随着新条目的插入,编码器增加 draining index 以维持其不会引用的表部分。如果编码器不再为绝对索引低于 draining index 的条目创建新引用,则这些条目的未确认引用数最终会变为零,从而允许它们被逐出。
<-- Newer Entries Older Entries -->
(Larger Indices) (Smaller Indices)
+--------+---------------------------------+----------+
| Unused | Referenceable | Draining |
| Space | Entries | Entries |
+--------+---------------------------------+----------+
^ ^ ^
| | |
Insertion Point Draining Index Dropping
Point
图 1:动态表条目萃取示意
2.1.2. 阻塞流
由于 QUIC 不保证不同流上的数据有序,解码器可能遇到引用其尚未收到的动态表条目的表示。
每个编码的字段节都包含一个 Required Insert Count(第 4.5.1 节),它是解码该字段节所需的最低 Insert Count。对于使用动态表引用编码的字段节,Required Insert Count 等于所有被引用动态表条目中最大绝对索引再加一。对于未引用动态表的字段节,Required Insert Count 为零。
当解码器接收到一个 Required Insert Count 大于自身 Insert Count 的编码字段节时,该流无法立即处理,被视为“阻塞”;参见 第 2.2.1 节。
解码器通过设置 SETTINGS_QPACK_BLOCKED_STREAMS 来指定可被阻塞流的上限;参见 第 5 节。编码器 MUST 始终将可能被阻塞的流数量限制在 SETTINGS_QPACK_BLOCKED_STREAMS 的值之内。如果解码器遇到超出其承诺支持的被阻塞流数量,必须将其视为类型为 QPACK_DECOMPRESSION_FAILED 的连接错误。
注意,解码器未必会在每个有可能被阻塞的流上实际阻塞。
编码器可以决定是否冒流被阻塞的风险。如果 SETTINGS_QPACK_BLOCKED_STREAMS 允许,通常可以通过引用仍在传输中的动态表条目来提高压缩效率,但如果发生丢失或重排,流可能在解码器处被阻塞。编码器可以通过仅引用已被确认的动态表条目来避免阻塞风险,但这可能意味着使用字面值。由于字面值会使编码字段节变大,这可能导致编码器在拥塞或流量控制限制下自身被阻塞。
2.1.3. 避免流控制死锁
在受流量控制限制的流上写入指令可能产生死锁。
解码器可能会停止对承载编码字段节的流发放流量控制配额,直到必要的更新在编码器流上收到为止。如果对编码器流(或整个连接)发放流量控制配额依赖于对承载编码字段节的流中数据的消费与释放,则可能会导致死锁。
更一般地说,如果解码器在完全接收某个大型指令前一直拒绝发放流量控制配额,则承载该指令的流可能被死锁。
为避免这些死锁,编码器 SHOULD NOT 在没有足够流与连接流量控制配额来容纳整个指令时写入该指令。
2.1.4. 已知接收计数
Known Received Count 是解码器确认的动态表插入与复制的总数。编码器跟踪 Known Received Count 以识别哪些动态表条目可以在不阻塞流的情况下被引用。解码器跟踪 Known Received Count 以便能够发送 Insert Count Increment 指令。
节确认指令(第 4.4.1 节)意味着解码器已接收到解码该字段节所需的所有动态表状态。如果被确认的字段节的 Required Insert Count 大于当前 Known Received Count,则 Known Received Count 更新为该 Required Insert Count 值。
Insert Count Increment 指令(第 4.4.3 节)通过其 Increment 参数增加 Known Received Count。有关指导请参见 第 2.2.2.3 节。
2.2. 解码器
与 HPACK 相同,解码器处理一系列表示并发出相应的字段节。它还处理在编码器流上接收的修改动态表的指令。注意,编码的字段节和编码器流指令在不同的流上到达。这与 HPACK 不同,HPACK 中编码的字段节(头块)可以包含修改动态表的指令,且没有专用的 HPACK 指令流。
解码器 MUST 按表示在编码字段节中出现的顺序发出字段行。
2.2.1. 阻塞解码
接收到编码字段节后,解码器检查 Required Insert Count。当 Required Insert Count 小于或等于解码器的 Insert Count 时,字段节可以立即处理。否则,接收该字段节的流将变为阻塞状态。
在阻塞期间,编码字段节数据 SHOULD 保持在被阻塞流的流量控制窗口内。该数据在流解锁前不可用,过早释放流量控制会使解码器易受内存耗尽攻击。当 Insert Count 对解码器已开始从该流读取的所有编码字段节都大于或等于其 Required Insert Count 时,该流才会被解除阻塞。
2.2.2. 状态同步
解码器通过在解码器流上发出解码器指令(第 4.4 节)来表示以下事件。
2.2.2.1. 字段节处理完成
在解码器完成对包含动态表引用的表示编码的字段节的解码后,解码器 MUST 发出节确认指令(第 4.4.1 节)。在中间响应、尾部以及推送请求的情况下,一个流可能承载多个字段节。编码器将每个节确认指令解释为对给定流上最早未确认且包含动态表引用的字段节的确认。
2.2.2.2. 流的放弃
当端点在流结束之前或在该流上所有编码字段节被处理之前收到流重置,或放弃读取该流时,它会生成一个流取消(Stream Cancellation)指令;参见 第 4.4.2 节。该指令向编码器表明该流上对动态表的所有引用不再未决。具有最大动态表容量(第 3.2.3 节)为零的解码器 MAY 省略发送流取消,因为编码器不可能有任何动态表引用。编码器不能从该指令推断任何动态表更新已被接收。
节确认和流取消指令允许编码器移除对动态表条目的引用。当绝对索引小于 Known Received Count 且引用数为零的条目被视为可逐出;参见 第 2.1.1 节。
2.2.2.3. 新表条目
2.2.3. 无效引用
如果解码器在字段行表示中遇到对已被逐出的动态表条目的引用,或遇到对绝对索引大于或等于声明的 Required Insert Count(第 4.5.1 节)的引用,它 MUST 将其视为类型为 QPACK_DECOMPRESSION_FAILED 的连接错误。
如果解码器在编码器指令中遇到对已被逐出的动态表条目的引用,它 MUST 将其视为类型为 QPACK_ENCODER_STREAM_ERROR 的连接错误。
3. 引用表
与 HPACK 不同,QPACK 的静态表和动态表中的条目是分别寻址的。下面的各节描述了每个表中条目的寻址方式。
3.1. 静态表
静态表由一个预定义的字段行列表组成,每个条目的索引随时间保持不变。其条目在 附录 A 中定义。
静态表中的所有条目都有名称和值。然而,值可以为空(即长度为 0)。每个条目由唯一的索引标识。
注意 QPACK 静态表的索引从 0 开始,而 HPACK 静态表的索引从 1 开始。
当解码器在字段行表示中遇到无效的静态表索引时,必须将其视为类型为 QPACK_DECOMPRESSION_FAILED 的连接错误。如果该索引是在编码器流上接收的,则必须将其视为类型为 QPACK_ENCODER_STREAM_ERROR 的连接错误。
3.2. 动态表
动态表由按先进先出顺序维护的字段行列表组成。QPACK 编码器和解码器共享一个初始为空的动态表。编码器通过在编码器流上发送指令将条目添加到动态表;参见 第 4.3 节。
动态表可以包含重复条目(即具有相同名称和值的条目)。因此,解码器不得将重复条目视为错误。
动态表条目的值可以为空。
3.2.1. 动态表大小
动态表的大小为其条目大小之和。
一个条目的大小为其名称的字节长度、其值的字节长度以及额外 32 字节之和。条目的大小使用未经过霍夫曼编码的名称和值的长度来计算。
3.2.2. 动态表容量与逐出
编码器设置动态表的容量,该容量作为其大小的上限。动态表的初始容量为零。编码器通过发送带有非零容量的“设置动态表容量”指令(第 4.3.1 节)来开始使用动态表。
在将新条目添加到动态表之前,会从动态表末端逐出条目,直到动态表的大小小于或等于(表容量 - 新条目大小)。编码器不得导致逐出不可逐出的动态表条目;参见 第 2.1.1 节。然后将新条目添加到表中。如果编码器尝试添加一个大于动态表容量的条目,则为错误;解码器必须将此视为类型为 QPACK_ENCODER_STREAM_ERROR 的连接错误。
新条目可以引用在将该新条目添加到动态表时将被逐出的动态表中的某个条目。实现应谨慎,避免在插入新条目之前删除被引用的名称或值(如果被引用的条目在插入新条目之前已被逐出)。
每当编码器减小动态表容量(参见 第 4.3.1 节)时,会从动态表末端逐出条目,直到动态表的大小小于或等于新的表容量。通过将容量设置为 0(随后可恢复),可以使用此机制完全清空动态表中的条目。
3.2.3. 最大动态表容量
为限定解码器的内存需求,解码器限制编码器被允许为动态表容量设置的最大值。在 HTTP/3 中,该限制由解码器发送的 SETTINGS_QPACK_MAX_TABLE_CAPACITY 的值决定;见 第 5 节。编码器不得设置超过该最大值的动态表容量,但可以选择使用更低的动态表容量;参见 第 4.3.1 节。
对于在 HTTP/3 中使用 0-RTT 数据的客户端,服务器的最大表容量为记忆的设置值;如果先前未发送该值,则为零。当客户端的 0-RTT 设置值为零时,服务器可以在其 SETTINGS 帧中将其设置为非零值。如果记忆的值为非零,则服务器必须在其 SETTINGS 帧中发送相同的非零值。如果它指定任何其他值,或在 SETTINGS 中省略 SETTINGS_QPACK_MAX_TABLE_CAPACITY,则编码器必须将此视为类型为 QPACK_DECODER_STREAM_ERROR 的连接错误。
对于未使用 0-RTT 数据的客户端(无论是未尝试 0-RTT 还是被拒绝)以及所有 HTTP/3 服务器,在编码器处理包含非零 SETTINGS_QPACK_MAX_TABLE_CAPACITY 的 SETTINGS 帧之前,最大表容量为 0。
当最大表容量为零时,编码器不得向动态表插入条目,也不得在编码器流上发送任何编码器指令。
3.2.4. 绝对索引
每个条目具有一个在该条目生命周期内固定的绝对索引。第一个插入的条目的绝对索引为 0;每插入一个条目索引加一。
3.2.5. 相对索引
相对索引从 0 开始,并以与绝对索引相反的方向递增。哪个条目具有相对索引 0 取决于引用的上下文。
在编码器指令(第 4.3 节)中,相对索引 0 指向动态表中最近插入的值。注意,这意味着在解释编码器流上的指令时,给定相对索引所引用的条目会发生变化。
+-----+---------------+-------+
| n-1 | ... | d | Absolute Index
+ - - +---------------+ - - - +
| 0 | ... | n-d-1 | Relative Index
+-----+---------------+-------+
^ |
| V
Insertion Point Dropping Point
n = count of entries inserted
d = count of entries dropped
图 2:示例动态表索引 —— 编码器流
与编码器指令不同,字段行表示中的相对索引相对于编码字段节开始时的 Base;见 第 4.5.1 节。这确保即使编码字段节和动态表更新被乱序处理,引用仍然稳定。
在字段行表示中,相对索引 0 指向绝对索引等于 Base - 1 的条目。
Base
|
V
+-----+-----+-----+-----+-------+
| n-1 | n-2 | n-3 | ... | d | Absolute Index
+-----+-----+ - +-----+ - +
| 0 | ... | n-d-3 | Relative Index
+-----+-----+-------+
n = count of entries inserted
d = count of entries dropped
In this example, Base = n - 2
图 3:示例动态表索引 —— 表示中的相对索引
3.2.6. 后基索引
后基索引用于字段行表示中,针对绝对索引大于或等于 Base 的条目,从 0 开始表示绝对索引等于 Base 的条目,并以与绝对索引相同的方向递增。
后基索引允许编码器在单次遍历字段节时处理该字段节,并包含对在处理本字段节(或其他字段节)时添加的条目的引用。
Base
|
V
+-----+-----+-----+-----+-----+
| n-1 | n-2 | n-3 | ... | d | Absolute Index
+-----+-----+-----+-----+-----+
| 1 | 0 | Post-Base Index
+-----+-----+
n = count of entries inserted
d = count of entries dropped
In this example, Base = n - 2
图 4:示例动态表索引 —— 表示中的后基索引
4. 线 格式
4.1. 原语
4.1.1. 带前缀的整数
大量使用了来自 RFC 7541 第 5.1 节 的带前缀整数格式(参见 [RFC7541])。该格式未经修改直接使用。但请注意 QPACK 使用了一些在 HPACK 中未使用的前缀大小。
QPACK 实现必须能够解码长度最高为 62 位的整数。
4.1.2. 字符串字面量
也采用了来自 RFC 7541 第 5.2 节 的字符串字面量定义(参见 [RFC7541])。该字符串格式包含可选的霍夫曼编码。
HPACK 定义字符串字面量从字节边界开始。它们以一个位标志开始,在本文档中表示为 'H'(指示字符串是否为霍夫曼编码),随后是作为 7 位前缀整数编码的字符串长度,最后是指示的字节数的数据。当启用霍夫曼编码时,使用 RFC 7541 附录 B 中的霍夫曼表且长度指示为编码后字符串的大小。
本文档扩展了字符串字面量的定义,允许其不以字节边界开始。“N 位前缀字符串字面量”从字节中间开始,前 (8-N) 位分配给前面的字段。字符串使用 1 位作为霍夫曼标志,然后是作为 (N-1) 位前缀整数编码的编码后字符串长度。前缀大小 N 可以取 2 到 8 之间的值(包含两端)。字符串字面量的其余部分不作修改。
未标注前缀长度的字符串字面量为 8 位前缀字符串字面量,并遵循 [RFC7541] 中的定义且不作修改。
4.2. 编码器与解码器流
QPACK 定义了两种单向流类型:
- 编码器流是一种类型为 0x02 的单向流。它承载从编码器到解码器的不带帧的编码器指令序列。
- 解码器流是一种类型为 0x03 的单向流。它承载从解码器到编码器的不带帧的解码器指令序列。
HTTP/3 端点包含一个 QPACK 编码器和解码器。每个端点最多必须启动一个编码器流和最多一个解码器流。接收到第二个同类流实例必须被视为类型为 H3_STREAM_CREATION_ERROR 的连接错误。
发送方不得关闭这两种流中的任意一种,接收方不得请求发送方关闭这两种流中的任意一种。关闭任一单向流类型必须被视为类型为 H3_CLOSED_CRITICAL_STREAM 的连接错误。
如果端点不会使用编码器流(例如其编码器不希望使用动态表,或对端允许的动态表最大大小为零),则它可以选择不创建编码器流。
如果解码器将动态表的最大容量设置为零,则端点可以选择不创建解码器流。
即使连接的设置阻止其使用,端点也必须允许其对端创建编码器流和解码器流。
4.3. 编码器指令
编码器在编码器流上发送编码器指令以设置动态表的容量并添加动态表条目。添加表条目的指令可以使用现有条目以避免传输冗余信息。名称可以作为对静态表或动态表中现有条目的引用来传输,也可以作为字符串字面量传输。对于已存在于动态表中的条目,也可以通过引用完整条目来创建副本条目(duplicate)。
4.3.1. 设置动态表容量
编码器使用以 '001' 3 位模式开始的指令将动态表的新容量告知解码器。随后是用 5 位前缀表示的新动态表容量;参见 第 4.1.1 节。
0 1 2 3 4 5 6 7 +---+---+---+---+---+---+---+---+ | 0 | 0 | 1 | Capacity (5+) | +---+---+---+-------------------+
图 5:设置动态表容量
4.3.2. 以名称引用插入
当字段名称与静态表或动态表中某个条目的字段名称匹配时,编码器通过以 '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) |
+-------------------------------+
图 6:插入字段行 —— 索引名称
4.3.3. 以字面名称插入
当字段名称和值都以字符串字面量表示时,编码器通过以 '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) |
+-------------------------------+
图 7:插入字段行 —— 新名称
4.3.4. 复制(Duplicate)
编码器使用以 '000' 3 位模式开始的指令复制动态表中已存在的条目。随后是以 5 位前缀整数表示的现有条目的相对索引;参见 第 4.1.1 节。
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| 0 | 0 | 0 | Index (5+) |
+---+---+---+-------------------+
图 8:复制(Duplicate)
现有条目被重新插入到动态表中,而无需重新发送名称或值。这对于避免引用可能阻止插入新条目的较旧条目很有用。
4.4. 解码器指令
解码器在解码器流上发送解码器指令,以告知编码器有关字段节的处理和表更新,从而确保动态表的一致性。
4.4.1. 节确认 (Section Acknowledgment)
在处理一个声明的 Required Insert Count 非零的编码字段节之后,解码器发出一个 Section Acknowledgment 指令。该指令以 '1' 的 1 位模式开始,随后是与该字段节关联的流 ID,以 7 位前缀整数编码;参见 第 4.1.1 节。
0 1 2 3 4 5 6 7 +---+---+---+---+---+---+---+---+ | 1 | Stream ID (7+) | +---+---------------------------+
图 9:节确认 (Section Acknowledgment)
如果编码器收到对某个流的 Section Acknowledgment 指令,而该流上所有带有非零 Required Insert Count 的编码字段节已被确认,则编码器 MUST 将此视为类型为 QPACK_DECODER_STREAM_ERROR 的连接错误。
Section Acknowledgment 指令可能会增加 Known Received Count;参见 第 2.1.4 节。
4.4.2. 流取消 (Stream Cancellation)
当某个流被重置或放弃读取时,解码器发出一个 Stream Cancellation 指令。该指令以 '01' 的 2 位模式开始,随后是受影响流的 Stream ID,以 6 位前缀整数编码。
如 第 2.2.2 节 所述使用此指令。
0 1 2 3 4 5 6 7 +---+---+---+---+---+---+---+---+ | 0 | 1 | Stream ID (6+) | +---+---+-----------------------+
图 10:流取消 (Stream Cancellation)
4.4.3. 插入计数递增 (Insert Count Increment)
Insert Count Increment 指令以 '00' 的 2 位模式开始,随后是以 6 位前缀整数编码的 Increment。该指令将 Known Received Count(参见 第 2.1.4 节)增加 Increment 参数的值。解码器应发送一个将 Known Received Count 增加到到目前为止已处理的动态表插入与复制总数的 Increment 值。
0 1 2 3 4 5 6 7 +---+---+---+---+---+---+---+---+ | 0 | 0 | Increment (6+) | +---+---+-----------------------+
图 11:插入计数递增 (Insert Count Increment)
如果编码器收到的 Increment 字段等于零,或者将 Known Received Count 增加到超过编码器已发送的值,则编码器 MUST 将其视为类型为 QPACK_DECODER_STREAM_ERROR 的连接错误。
4.5. 字段行表示 (Field Line Representations)
一个编码字段节由一个前缀和本节定义的可能为空的表示序列构成。每个表示对应一个字段行。这些表示参考静态表或处于特定状态的动态表,但它们不会修改该状态。
编码的字段节通过封装协议定义的帧在流上承载。
4.5.1. 编码字段节前缀 (Encoded Field Section Prefix)
每个编码字段节以两个整数为前缀。Required Insert Count 使用 8 位前缀整数编码,编码方式见 第 4.5.1.1 节。Base 使用一个符号位 ('S') 和一个带 7 位前缀的 Delta Base 值进行编码;见 第 4.5.1.2 节。
0 1 2 3 4 5 6 7 +---+---+---+---+---+---+---+---+ | Required Insert Count (8+) | +---+---------------------------+ | S | Delta Base (7+) | +---+---------------------------+ | Encoded Field Lines ... +-------------------------------+
图 12:编码字段节 (Encoded Field Section)
4.5.1.1. Required Insert Count
Required Insert Count 标识处理该编码字段节所需的动态表状态。阻塞的解码器使用 Required Insert Count 来确定何时安全地处理字段节的其余部分。
编码器在编码前按如下方式转换 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 节。
该编码限制了长期连接上前缀的长度。
解码器可以使用例如下述算法重建 Required Insert Count。如果解码器遇到一个 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 字节,则 Required Insert Count 将对 6 取模。如果解码器已收到 10 次插入,则编码值 4 表示该字段节的 Required Insert Count 为 9。
4.5.1.2. Base
Base 用于按第 3.2.5 节 所述解析对动态表的引用。
为节省空间,Base 相对于 Required Insert Count 使用一位符号位(在 图 12 中表示为 'S')和 Delta Base 值进行编码。符号位为 0 表示 Base 大于或等于 Required Insert Count;解码器将 Delta Base 的值加到 Required Insert Count 上以确定 Base 的值。符号位为 1 表示 Base 小于 Required Insert Count;解码器将 Delta Base 从 Required Insert Count 中减去并再减一以确定 Base 的值。即:
if Sign == 0:
Base = ReqInsertCount + DeltaBase
else:
Base = ReqInsertCount - DeltaBase - 1
单次传递编码器在编码字段节之前确定 Base。如果编码器在编码字段节时向动态表插入了条目并引用了它们,则 Required Insert Count 将大于 Base,因此编码差为负,Sign 位设置为 1。如果字段节未使用引用到表中最新条目的表示进行编码且未插入任何新条目,则 Base 将大于 Required Insert Count,因此编码差为正,Sign 位设置为 0。
Base 的值 MUST NOT 为负。尽管协议在使用 post-Base 索引时可能在负 Base 下仍能正确运行,但这是不必要且低效的。若 Required Insert Count 小于或等于 Delta Base 的值而 Sign 位为 1,则端点 MUST 将该字段块视为无效。
在编码字段节之前产生表更新的编码器可能将 Base 设置为 Required Insert Count 的值。在这种情况下,Sign 位和 Delta Base 都将设置为零。
未使用对动态表的引用进行编码的字段节可以使用任意 Base 值;将 Delta Base 设置为零是最有效的编码方式之一。
例如,当 Required Insert Count 为 9 时,解码器接收到 Sign 位为 1 且 Delta Base 为 2。这将 Base 设为 6,并为三个条目启用 post-Base 索引。在此示例中,相对索引 1 指向第五个被添加到表中的条目;post-Base 索引 1 指向第八个条目。
4.5.2. 索引字段行 (Indexed Field Line)
索引字段行表示标识静态表中的条目或绝对索引小于 Base 值的动态表条目。
0 1 2 3 4 5 6 7 +---+---+---+---+---+---+---+---+ | 1 | T | Index (6+) | +---+---+-----------------------+
图 13:索引字段行 (Indexed Field Line)
此表示以 '1' 的 1 位模式开始,随后是 'T' 位,指示引用是静态表还是动态表。随后使用的 6 位前缀整数(参见 第 4.1.1 节)用于定位字段行对应的表条目。当 T=1 时,该数字表示静态表索引;当 T=0 时,该数字为动态表中该条目的相对索引。
4.5.3. 带后基索引的索引字段行
带后基索引的索引字段行表示标识动态表中绝对索引大于或等于 Base 值的条目。
0 1 2 3 4 5 6 7 +---+---+---+---+---+---+---+---+ | 0 | 0 | 0 | 1 | Index (4+) | +---+---+---+---+---------------+
图 14:带后基索引的索引字段行
4.5.4. 带名称引用的字面字段行 (Literal Field Line with Name Reference)
带名称引用的字面字段行表示用于编码字段行,其中字段名与静态表中的条目字段名匹配,或与绝对索引小于 Base 的动态表条目的字段名匹配。
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| 0 | 1 | N | T |Name Index (4+)|
+---+---+---+---+---------------+
| H | Value Length (7+) |
+---+---------------------------+
| Value String (Length bytes) |
+-------------------------------+
图 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. 带后基名称引用的字面字段行
带后基名称引用的字面字段行表示用于编码字段行,其中字段名与绝对索引大于或等于 Base 的动态表条目的字段名匹配。
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| 0 | 0 | 0 | 0 | N |NameIdx(3+)|
+---+---+---+---+---+-----------+
| H | Value Length (7+) |
+---+---------------------------+
| Value String (Length bytes) |
+-------------------------------+
图 16:带后基名称引用的字面字段行
此表示以 '0000' 的 4 位模式开始。第五位为在 第 4.5.4 节 中描述的 'N' 位。随后是动态表条目的 post-Base 索引(参见 第 3.2.6 节),该索引作为带 3 位前缀的整数编码;见 第 4.1.1 节。
仅从动态表条目中取字段名;字段值作为 8 位前缀字符串字面量进行编码;见 第 4.1.2 节。
4.5.6. 带字面名称的字面字段行
带字面名称的字面字段行表示将字段名和字段值都编码为字符串字面量。
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) |
+-------------------------------+
图 17:带字面名称的字面字段行
5. 配置
QPACK 为 HTTP/3 的 SETTINGS 帧定义了两个设置:
6. 错误处理
下列错误代码在 HTTP/3 中用于表示 QPACK 的故障,这些故障会阻止流或连接继续:
- QPACK_DECOMPRESSION_FAILED (0x0200):
-
解码器无法解释一个已编码的字段节,且无法继续解码该字段节。
- QPACK_ENCODER_STREAM_ERROR (0x0201):
-
解码器无法解释在编码器流上接收的编码器指令。
- QPACK_DECODER_STREAM_ERROR (0x0202):
-
编码器无法解释在解码器流上接收的解码器指令。
7. 安全性考虑
本节描述了 QPACK 可能涉及的安全关注点:
- 利用压缩作为基于长度的测试手段,以验证对被压缩进共享压缩上下文的机密猜测。
- 通过耗尽解码器的处理或内存容量导致的拒绝服务。
7.1. 探测动态表状态
QPACK 通过利用像 HTTP 这样的协议中固有的冗余来减小字段节的编码大小。其最终目标是减少发送 HTTP 请求或响应所需的数据量。
用于编码头部和尾部字段的压缩上下文可以被同时能够定义要编码并发送字段且能观测这些字段编码后长度的攻击者探测。当攻击者同时具备这两种能力时,他们可以自适应地修改请求以确认对动态表状态的猜测。如果猜测被压缩为更短的长度,攻击者可以通过观察编码长度推断猜测是正确的。
即使在传输层安全协议([TLS])和 QUIC 传输协议([QUIC-TRANSPORT])之上,这类攻击也是可能的,因为 TLS 与 QUIC 虽然对内容提供了机密性保护,但对内容长度只提供有限的保护。
像 CRIME ([CRIME]) 这样的攻击展示了这些通用攻击者能力的存在。该攻击利用了 DEFLATE([RFC1951])基于前缀匹配去除冗余的特性。这允许攻击者逐字符地确认猜测,将指数时间攻击降为线性时间攻击。
7.1.1. 对 QPACK 与 HTTP 的适用性
QPACK 通过要求猜测匹配整个字段行而不是单个字符来缓解但不能完全防止基于 CRIME 的攻击([CRIME])。攻击者只能得知猜测的正确与否,因此攻击者退化为对给定字段名的字段值进行暴力猜测。
因此,恢复特定字段值的可行性依赖于这些值的熵。高熵的值不太可能被成功恢复,而低熵的值则仍然脆弱。
只要两个互不信任的实体控制被放置到同一 HTTP/3 连接上的请求或响应,这类攻击就可能发生。如果共享的 QPACK 压缩器允许一个实体向动态表添加条目,而另一个实体在编码选择的字段行时引用这些条目,那么攻击者(第二实体)就可以通过观察编码输出的长度来学习表的状态。
例如,当中间节点发生以下情况时,来自互不信任实体的请求或响应可能会出现在同一连接上:
- 在向源服务器发送来自多个客户端的请求时复用到同一连接,或
- 在向客户端复用来自多个源服务器的响应时使用共享连接。
Web 浏览器还需要假定由不同 Web 源([RFC6454])在同一连接上发出的请求互不信任。其他涉及互不信任实体的场景也可能存在。
7.1.2. 缓解措施
需要对头部或尾部字段保密的 HTTP 使用者可以使用具有足够熵的值,使猜测变得不可行。然而这作为一种通用解决方案并不现实,因为它要求所有 HTTP 使用者采取措施来缓解攻击,这会对 HTTP 的使用施加新约束。
实现 QPACK 的一方可以通过限制压缩的应用方式来降低动态表探测的可能性,而不是对所有 HTTP 用户施加约束。
一个理想的方案是根据构造消息的实体对动态表的访问进行隔离。被添加到表的字段值应归属于某个实体,且只有创建该特定值的实体才能提取该值。
为提高该方案的压缩性能,某些条目可以被标记为公共。例如,浏览器可能会在所有请求中使 Accept-Encoding 头字段的值可用。
不具备字段来源信息的编码器可能会对具有相同字段名但不同值的许多字段行引入惩罚。该惩罚可能导致大量猜测尝试使该字段在未来消息中不再与动态表条目比较,从而有效地阻止进一步猜测。
此类响应可能与字段值长度成反比。对于较短的值,可能更快或更有可能禁用对动态表的访问,而对较长的值则不然。
该缓解在两个端点之间效果最好。如果消息被中间节点在不知道哪一实体构造该消息的情况下重新编码,中间节点可能会无意中合并原编码器特意分隔开的压缩上下文。
7.1.3. 永不索引的字面量
实现也可以选择通过不对敏感字段进行压缩,而是将其值编码为字面量来保护这些字段。
拒绝将字段行插入动态表只有在所有跳都避免这样做时才有效。永不索引的字面量位(见 第 4.5.4 节)可用于向中间节点表明某个值是有意作为字面量发送的。
中间节点 MUST NOT 将带有 'N' 位设置的字面表示的值重新编码为会将其索引的其它表示。如果使用 QPACK 进行重新编码,必须使用带 'N' 位设置的字面表示;如果使用 HPACK 进行重新编码,必须使用永不索引的字面表示(见 RFC 7541 第 6.2.3 节)。
是否将字段值标记为永不索引取决于多个因素。因为 QPACK 并不能防止对整个字段值的猜测,短的或低熵的值更容易被对手恢复。因此,编码器可能选择不对低熵的值进行索引。
编码器也可能选择不对被认为非常有价值或敏感的字段进行索引,例如 Cookie 或 Authorization 头字段。
相反,编码器可能倾向于为那些即使暴露也几乎没有价值的字段索引其值。例如,User-Agent 头字段在请求间通常没有太大变化并且会发送给任意服务器。在这种情况下,确认某个特定 User-Agent 值已被使用几乎没有价值。
注意,决定使用永不索引的字面表示的这些标准会随着新攻击被发现而发展。
7.2. 静态霍夫曼编码
目前尚未发现针对静态霍夫曼编码的攻击。一项研究表明,使用静态霍夫曼编码表会产生信息泄露;然而该研究也得出结论,攻击者无法利用这种信息泄露来恢复任何有意义的信息(参见 [PETAL])。
7.3. 内存消耗
攻击者可以试图使端点耗尽其内存。QPACK 的设计旨在限制端点分配的峰值和稳定内存量。
QPACK 使用动态表的最大大小定义和最大阻塞流数来限制编码器可以导致解码器消耗的内存量。在 HTTP/3 中,这些值由解码器通过设置参数 SETTINGS_QPACK_MAX_TABLE_CAPACITY 和 SETTINGS_QPACK_BLOCKED_STREAMS 控制(参见 第 3.2.3 节 和 第 2.1.2 节)。动态表大小的限制考虑了存储在动态表中的数据大小,加上用于开销的小幅补偿。阻塞流数的限制只是对解码器所需最大内存量的近似。实际最大内存量取决于解码器用于跟踪每个阻塞流所使用的内存量。
解码器可以通过为动态表设置适当的最大大小来限制用于动态表的状态内存量。在 HTTP/3 中,这通过设置适当的 SETTINGS_QPACK_MAX_TABLE_CAPACITY 参数值来实现。编码器可以通过选择小于解码器允许的动态表大小并向解码器示意来限制其使用的状态内存量(见 第 4.3.1 节)。
解码器可以通过设置适当的最大阻塞流数来限制用于阻塞流的状态内存量。在 HTTP/3 中,这通过设置适当的 SETTINGS_QPACK_BLOCKED_STREAMS 参数值来实现。可能成为阻塞的流不会在编码器端消耗额外的状态内存。
编码器为跟踪所有在未确认字段节中对动态表的引用而分配内存。实现可以通过仅使用其愿意跟踪的数量的动态表引用来直接限制状态内存量;这不需要向解码器发出任何信号。然而,限制对动态表的引用将降低压缩效果。
编码器或解码器消耗的临时内存量可以通过顺序处理字段行来限制。解码器实现不需要在解码字段节时保留完整的字段行列表。若编码器使用单次传递算法,则编码器实现也不需要在编码字段节时保留完整的字段行列表。注意,应用程序可能因其它原因需要保留完整的字段行列表;即使 QPACK 不强制这样做,应用约束也可能使其成为必要。
虽然协商的动态表大小限制涵盖了 QPACK 实现可能消耗的大部分内存,但由于流量控制导致无法立即发送的数据不受此限制影响。实现应限制未发送数据的大小,特别是在解码器流上,那里选择发送内容的灵活性受到限制。对过多未发送数据的可能响应包括限制对端打开新流的能力、只从编码器流读取,或关闭连接。
7.4. 实现限制
QPACK 的实现需要确保巨大的整数值、长整数编码或长字符串字面量不会引入安全弱点。
实现必须为其接受的整数值及其编码长度设定上限;参见 第 4.1.1 节。同样,必须为它接受的字符串字面量长度设定上限;参见 第 4.1.2 节。这些限制 SHOULD 足够大,以处理 HTTP 实现可配置为接受的最大单个字段。
如果实现遇到一个其无法解码的更大值,则对于请求流必须将其视为类型为 QPACK_DECOMPRESSION_FAILED 的流错误;若发生在编码器或解码器流上,则应视为适当类型的连接错误。
8. IANA 考虑事项
本文档在 [HTTP/3] 定义的注册表中进行了多项注册。本文档创建的分配均被赋予永久状态,并列出 IETF 作为变更控制者以及 HTTP 工作组(ietf-http-wg@w3.org)作为联系人。
8.1. 设置注册
本文档指定了两个设置。下表中的条目在 [HTTP/3] 建立的“HTTP/3 设置”注册表中注册。
| 设置名称 | 代码 | 规范 | 默认值 |
|---|---|---|---|
| QPACK_MAX_TABLE_CAPACITY | 0x01 | 第 5 节 | 0 |
| QPACK_BLOCKED_STREAMS | 0x07 | 第 5 节 | 0 |
出于格式化原因,此处的设置名称通过移除 'SETTINGS_' 前缀而被缩写。
8.2. 流类型注册
本文档指定了两种流类型。下表中的条目在 [HTTP/3] 建立的“HTTP/3 流类型”注册表中注册。
8.3. 错误代码注册
本文档指定了三种错误代码。下表中的条目在 [HTTP/3] 建立的“HTTP/3 错误代码”注册表中注册。
9. 参考文献
9.1. 规范性参考文献
- [HTTP]
- Fielding, R., Ed., Nottingham, M., Ed., and J. Reschke, Ed., “HTTP Semantics”, STD 97, RFC 9110, DOI 10.17487/RFC9110, June 2022, <https://www.rfc-editor.org/info/rfc9110>.
- [HTTP/3]
- Bishop, M., Ed., “HTTP/3”, RFC 9114, DOI 10.17487/RFC9114, June 2022, <https://www.rfc-editor.org/info/rfc9114>.
- [QUIC-TRANSPORT]
- Iyengar, J., Ed. and M. Thomson, Ed., “QUIC: A UDP-Based Multiplexed and Secure Transport”, RFC 9000, DOI 10.17487/RFC9000, May 2021, <https://www.rfc-editor.org/info/rfc9000>.
- [RFC2119]
- Bradner, S., “Key words for use in RFCs to Indicate Requirement Levels”, BCP 14, RFC 2119, DOI 10.17487/RFC2119, March 1997, <https://www.rfc-editor.org/info/rfc2119>.
- [RFC2360]
- Scott, G., “Guide for Internet Standards Writers”, BCP 22, RFC 2360, DOI 10.17487/RFC2360, June 1998, <https://www.rfc-editor.org/info/rfc2360>.
- [RFC7541]
- Peon, R. and H. Ruellan, “HPACK: Header Compression for HTTP/2”, RFC 7541, DOI 10.17487/RFC7541, May 2015, <https://www.rfc-editor.org/info/rfc7541>.
- [RFC8174]
- Leiba, B., “Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words”, BCP 14, RFC 8174, DOI 10.17487/RFC8174, May 2017, <https://www.rfc-editor.org/info/rfc8174>.
9.2. 说明性参考文献
- [CRIME]
- Wikipedia, “CRIME”, May 2015, <http://en.wikipedia.org/w/index.php?title=CRIME&oldid=660948120>.
- [HTTP/2]
- Thomson, M., Ed. and C. Benfield, Ed., “HTTP/2”, RFC 9113, DOI 10.17487/RFC9113, June 2022, <https://www.rfc-editor.org/info/rfc9113>.
- [PETAL]
- Tan, J. and J. Nahata, “PETAL: Preset Encoding Table Information Leakage”, April 2013, <http://www.pdl.cmu.edu/PDL-FTP/associated/CMU-PDL-13-106.pdf>.
- [RFC1951]
- Deutsch, P., “DEFLATE Compressed Data Format Specification version 1.3”, RFC 1951, DOI 10.17487/RFC1951, May 1996, <https://www.rfc-editor.org/info/rfc1951>.
- [RFC6454]
- Barth, A., “The Web Origin Concept”, RFC 6454, DOI 10.17487/RFC6454, December 2011, <https://www.rfc-editor.org/info/rfc6454>.
- [TLS]
- Rescorla, E., “The Transport Layer Security (TLS) Protocol Version 1.3”, RFC 8446, DOI 10.17487/RFC8446, August 2018, <https://www.rfc-editor.org/info/rfc8446>.
Appendix A. 静态表
该表通过分析 2018 年的实际互联网流量并包含最常见的头字段生成,同时过滤掉了一些不受支持和非标准的值。由于此方法论,某些条目可能不一致或以相似但不完全相同的值多次出现。条目的顺序经过优化,以尽量用最少的字节对最常见的头字段进行编码。
| 索引 | 名称 | 值 |
|---|---|---|
| 0 | :authority | |
| 1 | :path | / |
| 2 | age | 0 |
| 3 | content-disposition | |
| 4 | content-length | 0 |
| 5 | cookie | |
| 6 | date | |
| 7 | etag | |
| 8 | if-modified-since | |
| 9 | if-none-match | |
| 10 | last-modified | |
| 11 | link | |
| 12 | location | |
| 13 | referer | |
| 14 | set-cookie | |
| 15 | :method | CONNECT |
| 16 | :method | DELETE |
| 17 | :method | GET |
| 18 | :method | HEAD |
| 19 | :method | OPTIONS |
| 20 | :method | POST |
| 21 | :method | PUT |
| 22 | :scheme | http |
| 23 | :scheme | https |
| 24 | :status | 103 |
| 25 | :status | 200 |
| 26 | :status | 304 |
| 27 | :status | 404 |
| 28 | :status | 503 |
| 29 | accept | */* |
| 30 | accept | application/dns-message |
| 31 | accept-encoding | gzip, deflate, br |
| 32 | accept-ranges | bytes |
| 33 | access-control-allow-headers | cache-control |
| 34 | access-control-allow-headers | content-type |
| 35 | access-control-allow-origin | * |
| 36 | cache-control | max-age=0 |
| 37 | cache-control | max-age=2592000 |
| 38 | cache-control | max-age=604800 |
| 39 | cache-control | no-cache |
| 40 | cache-control | no-store |
| 41 | cache-control | public, max-age=31536000 |
| 42 | content-encoding | br |
| 43 | content-encoding | gzip |
| 44 | content-type | application/dns-message |
| 45 | content-type | application/javascript |
| 46 | content-type | application/json |
| 47 | content-type | application/x-www-form-urlencoded |
| 48 | content-type | image/gif |
| 49 | content-type | image/jpeg |
| 50 | content-type | image/png |
| 51 | content-type | text/css |
| 52 | content-type | text/html; charset=utf-8 |
| 53 | content-type | text/plain |
| 54 | content-type | text/plain;charset=utf-8 |
| 55 | range | bytes=0- |
| 56 | strict-transport-security | max-age=31536000 |
| 57 | strict-transport-security | max-age=31536000; includesubdomains |
| 58 | strict-transport-security | max-age=31536000; includesubdomains; preload |
| 59 | vary | accept-encoding |
| 60 | vary | origin |
| 61 | x-content-type-options | nosniff |
| 62 | x-xss-protection | 1; mode=block |
| 63 | :status | 100 |
| 64 | :status | 204 |
| 65 | :status | 206 |
| 66 | :status | 302 |
| 67 | :status | 400 |
| 68 | :status | 403 |
| 69 | :status | 421 |
| 70 | :status | 425 |
| 71 | :status | 500 |
| 72 | accept-language | |
| 73 | access-control-allow-credentials | FALSE |
| 74 | access-control-allow-credentials | TRUE |
| 75 | access-control-allow-headers | * |
| 76 | access-control-allow-methods | get |
| 77 | access-control-allow-methods | get, post, options |
| 78 | access-control-allow-methods | options |
| 79 | access-control-expose-headers | content-length |
| 80 | access-control-request-headers | content-type |
| 81 | access-control-request-method | get |
| 82 | access-control-request-method | post |
| 83 | alt-svc | clear |
| 84 | authorization | |
| 85 | content-security-policy | script-src 'none'; object-src 'none'; base-uri 'none' |
| 86 | early-data | 1 |
| 87 | expect-ct | |
| 88 | forwarded | |
| 89 | if-range | |
| 90 | origin | |
| 91 | purpose | prefetch |
| 92 | server | |
| 93 | timing-allow-origin | * |
| 94 | upgrade-insecure-requests | 1 |
| 95 | user-agent | |
| 96 | x-forwarded-for | |
| 97 | x-frame-options | deny |
| 98 | x-frame-options | sameorigin |
任何出现在字段名或值内的换行均为格式化造成。
Appendix B. 编码与解码示例
下面的示例表示编码器与解码器之间的一系列交互。这些交互旨在覆盖大多数 QPACK 指令并突出常见模式及其对动态表状态的影响。编码器发送三个每个包含一条字段行的编码字段节,以及两个未被引用的推测性插入。
显示了编码器动态表的状态及其当前大小。每个条目都显示条目的绝对索引 (Abs)、当前未完成的引用该条目的编码字段节数量 (Ref)、以及名称和值。位于“已确认”线之上的条目已被解码器确认。
B.1. 带名称引用的字面字段行
编码器发送一个包含使用静态名称引用的字段的字面表示的编码字段节。
Data | Interpretation
| Encoder's Dynamic Table
Stream: 0
0000 | Required Insert Count = 0, Base = 0
510b 2f69 6e64 6578 | Literal Field Line with Name Reference
2e68 746d 6c | Static Table, Index=1
| (:path=/index.html)
Abs Ref Name Value
^-- acknowledged --^
Size=0
B.2. 动态表
编码器设置动态表容量,插入带动态名称引用的头字段,然后发送一个可能导致阻塞的、引用该新条目的编码字段节。解码器对处理该编码字段节进行确认,这会隐含地确认到 Required Insert Count 为止的所有动态表插入。
Stream: Encoder
3fbd01 | Set Dynamic Table Capacity=220
c00f 7777 772e 6578 | Insert With Name Reference
616d 706c 652e 636f | Static Table, Index=0
6d | (:authority=www.example.com)
c10c 2f73 616d 706c | Insert With Name Reference
652f 7061 7468 | Static Table, Index=1
| (:path=/sample/path)
Abs Ref Name Value
^-- acknowledged --^
0 0 :authority www.example.com
1 0 :path /sample/path
Size=106
Stream: 4
0381 | Required Insert Count = 2, Base = 0
10 | Indexed Field Line With Post-Base Index
| Absolute Index = Base(0) + Index(0) = 0
| (:authority=www.example.com)
11 | Indexed Field Line With Post-Base Index
| Absolute Index = Base(0) + Index(1) = 1
| (:path=/sample/path)
Abs Ref Name Value
^-- acknowledged --^
0 1 :authority www.example.com
1 1 :path /sample/path
Size=106
Stream: Decoder
84 | Section Acknowledgment (stream=4)
Abs Ref Name Value
0 0 :authority www.example.com
1 0 :path /sample/path
^-- acknowledged --^
Size=106
B.3. 推测性插入
编码器使用字面名称将一个头字段插入动态表。解码器确认已收到该条目。编码器没有发送任何编码字段节。
Stream: Encoder
4a63 7573 746f 6d2d | Insert With Literal Name
6b65 790c 6375 7374 | (custom-key=custom-value)
6f6d 2d76 616c 7565 |
Abs Ref Name Value
0 0 :authority www.example.com
1 0 :path /sample/path
^-- acknowledged --^
2 0 custom-key custom-value
Size=160
Stream: Decoder
01 | Insert Count Increment (1)
Abs Ref Name Value
0 0 :authority www.example.com
1 0 :path /sample/path
2 0 custom-key custom-value
^-- acknowledged --^
Size=160
B.4. 复制指令,流取消
编码器复制动态表中已有的条目,然后发送一个引用动态表条目(包括复制条目)的编码字段节。包含编码器流数据的分组被延迟。在该分组到达之前,解码器取消了该流并通知编码器该编码字段节未被处理。
Stream: Encoder
02 | Duplicate (Relative Index = 2)
| Absolute Index =
| Insert Count(3) - Index(2) - 1 = 0
Abs Ref Name Value
0 0 :authority www.example.com
1 0 :path /sample/path
2 0 custom-key custom-value
^-- acknowledged --^
3 0 :authority www.example.com
Size=217
Stream: 8
0500 | Required Insert Count = 4, Base = 4
80 | Indexed Field Line, Dynamic Table
| Absolute Index = Base(4) - Index(0) - 1 = 3
| (:authority=www.example.com)
c1 | Indexed Field Line, Static Table Index = 1
| (:path=/)
81 | Indexed Field Line, Dynamic Table
| Absolute Index = Base(4) - Index(1) - 1 = 2
| (custom-key=custom-value)
Abs Ref Name Value
0 0 :authority www.example.com
1 0 :path /sample/path
2 1 custom-key custom-value
^-- acknowledged --^
3 1 :authority www.example.com
Size=217
Stream: Decoder
48 | Stream Cancellation (Stream=8)
Abs Ref Name Value
0 0 :authority www.example.com
1 0 :path /sample/path
2 0 custom-key custom-value
^-- acknowledged --^
3 0 :authority www.example.com
Size=217
B.5. 动态表插入与逐出
编码器向动态表插入另一个头字段,这会逐出最旧的条目。编码器未发送任何编码字段节。
Stream: Encoder
810d 6375 7374 6f6d | Insert With Name Reference
2d76 616c 7565 32 | Dynamic Table, Relative Index = 1
| Absolute Index =
| Insert Count(4) - Index(1) - 1 = 2
| (custom-key=custom-value2)
Abs Ref Name Value
1 0 :path /sample/path
2 0 custom-key custom-value
^-- acknowledged --^
3 0 :authority www.example.com
4 0 custom-key custom-value2
Size=215
Appendix C. 示例:单次传递编码算法
单次传递编码的伪代码(不包括复制处理、非阻塞模式、可用的编码器流流量控制和引用跟踪)。
# Helper functions:
# ====
# Encode an integer with the specified prefix and length
encodeInteger(buffer, prefix, value, prefixLength)
# Encode a dynamic table insert instruction with optional static
# or dynamic name index (but not both)
encodeInsert(buffer, staticNameIndex, dynamicNameIndex, fieldLine)
# Encode a static index reference
encodeStaticIndexReference(buffer, staticIndex)
# Encode a dynamic index reference relative to Base
encodeDynamicIndexReference(buffer, dynamicIndex, base)
# Encode a literal with an optional static name index
encodeLiteral(buffer, staticNameIndex, fieldLine)
# Encode a literal with a dynamic name index relative to Base
encodeDynamicLiteral(buffer, dynamicNameIndex, base, fieldLine)
# Encoding Algorithm
# ====
base = dynamicTable.getInsertCount()
requiredInsertCount = 0
for line in fieldLines:
staticIndex = staticTable.findIndex(line)
if staticIndex is not None:
encodeStaticIndexReference(streamBuffer, staticIndex)
continue
dynamicIndex = dynamicTable.findIndex(line)
if dynamicIndex is None:
# No matching entry. Either insert+index or encode literal
staticNameIndex = staticTable.findName(line.name)
if staticNameIndex is None:
dynamicNameIndex = dynamicTable.findName(line.name)
if shouldIndex(line) and dynamicTable.canIndex(line):
encodeInsert(encoderBuffer, staticNameIndex,
dynamicNameIndex, line)
dynamicIndex = dynamicTable.add(line)
if dynamicIndex is None:
# Could not index it, literal
if dynamicNameIndex is not None:
# Encode literal with dynamic name, possibly above Base
encodeDynamicLiteral(streamBuffer, dynamicNameIndex,
base, line)
requiredInsertCount = max(requiredInsertCount,
dynamicNameIndex)
else:
# Encodes a literal with a static name or literal name
encodeLiteral(streamBuffer, staticNameIndex, line)
else:
# Dynamic index reference
assert(dynamicIndex is not None)
requiredInsertCount = max(requiredInsertCount, dynamicIndex)
# Encode dynamicIndex, possibly above Base
encodeDynamicIndexReference(streamBuffer, dynamicIndex, base)
# encode the prefix
if requiredInsertCount == 0:
encodeInteger(prefixBuffer, 0x00, 0, 8)
encodeInteger(prefixBuffer, 0x00, 0, 7)
else:
wireRIC = (
requiredInsertCount
% (2 * getMaxEntries(maxTableCapacity))
) + 1;
encodeInteger(prefixBuffer, 0x00, wireRIC, 8)
if base >= requiredInsertCount:
encodeInteger(prefixBuffer, 0x00,
base - requiredInsertCount, 7)
else:
encodeInteger(prefixBuffer, 0x80,
requiredInsertCount - base - 1, 7)
return encoderBuffer, prefixBuffer + streamBuffer
致谢
IETF QUIC 工作组得到了许多人员的大力支持。
压缩设计团队在探索问题空间并影响本文件初始草案版本方面做了大量工作。特别感谢设计团队成员 Roberto Peon、Martin Thomson 和 Dmitri Tikhonov 的贡献。
以下人员也为本文件提供了大量贡献:
-
Bence Beky
-
Alessandro Ghedini
-
Ryan Hamilton
-
Robin Marx
-
Patrick McManus
-
奥 一穂 (Kazuho Oku)
-
Lucas Pardue
-
Biren Roy
-
Ian Swett
本文档大量借鉴了 [RFC7541] 的文本。对那些作者的间接贡献也在此致谢。
Buck Krasic 的贡献在其任职 Google 期间得到支持。
Mike Bishop 的部分贡献在其任职 Microsoft 期间得到支持。