跳到主要内容

5. 流和复用

"stream" 是在 HTTP/2 连接内客户端与服务器之间交换的独立双向帧序列. 流具有若干重要特性:

  • 单个 HTTP/2 连接可以包含多个并发打开的流, 任一端点都可以交错发送来自多个流的帧.

  • 流可以由客户端或服务器单方面建立和使用, 也可以被共享.

  • 流可以由任一端点关闭.

  • 帧在流上发送的顺序具有意义. 接收方按接收顺序处理帧. 特别是, HEADERS 和 DATA 帧的顺序具有语义意义.

  • 流由整数标识. 流标识符由发起该流的端点分配.

5.1. 流状态

流的生命周期见 Figure 2.

                             +--------+
send PP | | recv PP
,--------+ idle +--------.
/ | | \
v +--------+ v
+----------+ | +----------+
| | | send H / | |
,-----+ reserved | | recv H | reserved +-----.
| | (local) | | | (remote) | |
| +---+------+ v +------+---+ |
| | +--------+ | |
| | recv ES | | send ES | |
| send H | ,-------+ open +-------. | recv H |
| | / | | \ | |
| v v +---+----+ v v |
| +----------+ | +----------+ |
| | half- | | | half- | |
| | closed | | send R / | closed | |
| | (remote) | | recv R | (local) | |
| +----+-----+ | +-----+----+ |
| | | | |
| | send ES / | recv ES / | |
| | send R / v send R / | |
| | recv R +--------+ recv R | |
| send R / `----------->| |<-----------' send R / |
| recv R | closed | recv R |
`----------------------->| |<----------------------'
+--------+

send: endpoint sends this frame
recv: endpoint receives this frame

H: HEADERS frame (with implied CONTINUATION frames)
PP: PUSH_PROMISE frame (with implied CONTINUATION frames)
ES: END_STREAM flag
R: RST_STREAM frame

Figure 2: Stream States

请注意, 此图只显示流状态转换以及影响这些状态变化的帧和标志. 在这方面, CONTINUATION 帧不会导致状态转换, 实际上是其所跟随的 HEADERS 或 PUSH_PROMISE 的一部分. 就状态转换而言, END_STREAM 标志被作为独立于承载它的帧的事件处理; 设置了 END_STREAM 标志的 HEADERS 帧可能导致两次状态转换.

当帧处于传输途中时, 两个端点对流状态的主观看法可能不同. 端点不会协调流的创建; 流由任一端点单方面创建. 状态不匹配的负面后果仅限于发送 RST_STREAM 后的 "closed" 状态, 此时关闭后仍可能在一段时间内收到帧.

流具有以下状态:

idle: 所有流都从 "idle" 状态开始.

从该状态出发, 以下转换有效:

  • 发送或接收 HEADERS 帧会使流变为 "open". 流标识符按 Section 5.1.1 中描述的方式选择. 同一个 HEADERS 帧也可能使流立即变为 "half-closed".

  • 在另一个流上发送 PUSH_PROMISE 帧会保留被标识的空闲流以供稍后使用. 被保留流的流状态转换为 "reserved (local)".

  • 在另一个流上接收 PUSH_PROMISE 帧会保留被标识的空闲流以供稍后使用. 被保留流的流状态转换为 "reserved (remote)".

  • 请注意, PUSH_PROMISE 帧不是在空闲流上发送, 而是引用新保留的流的 Promised Stream ID 字段.

在此状态的流上收到 HEADERS 或 PRIORITY 以外的任何帧, MUST 被视为 PROTOCOL_ERROR 类型的连接错误 (Section 5.4.1).

reserved (local): 处于 "reserved (local)" 状态的流, 是通过发送 PUSH_PROMISE 帧而被承诺的流. PUSH_PROMISE 帧通过将空闲流与远端对等方发起的打开流关联来保留该空闲流 (见 Section 8.4).

在此状态下, 只可能发生以下转换:

  • 端点可以发送 HEADERS 帧. 这会使流以 "half-closed (remote)" 状态打开.

  • 任一端点可以发送 RST_STREAM 帧, 使流变为 "closed". 这会释放流保留.

端点在此状态下 MUST NOT 发送 HEADERS、RST_STREAM 或 PRIORITY 以外的任何类型的帧.

在此状态下 MAY 收到 PRIORITY 或 WINDOW_UPDATE 帧. 在此状态的流上收到 RST_STREAM、PRIORITY 或 WINDOW_UPDATE 以外的任何类型的帧, MUST 被视为 PROTOCOL_ERROR 类型的连接错误 (Section 5.4.1).

reserved (remote): 处于 "reserved (remote)" 状态的流已被远端对等方保留.

在此状态下, 只可能发生以下转换:

  • 收到 HEADERS 帧会使流转换为 "half-closed (local)".

  • 任一端点可以发送 RST_STREAM 帧, 使流变为 "closed". 这会释放流保留.

端点 MAY 在此状态下发送 PRIORITY 帧, 以重新设置被保留流的优先级. 端点在此状态下 MUST NOT 发送 RST_STREAM、WINDOW_UPDATE 或 PRIORITY 以外的任何类型的帧.

在此状态的流上收到 HEADERS、RST_STREAM 或 PRIORITY 以外的任何类型的帧, MUST 被视为 PROTOCOL_ERROR 类型的连接错误 (Section 5.4.1).

open: 处于 "open" 状态的流可由两个对等方用于发送任何类型的帧. 在此状态下, 发送方遵守已通告的流级流量控制限制 (Section 5.2).

从此状态出发, 任一端点都可以发送设置了 END_STREAM 标志的帧, 这会使流转换为某个 "half-closed" 状态. 端点发送 END_STREAM 标志会使流状态变为 "half-closed (local)"; 端点接收 END_STREAM 标志会使流状态变为 "half-closed (remote)".

任一端点都可以从此状态发送 RST_STREAM 帧, 使其立即转换为 "closed".

half-closed (local): 处于 "half-closed (local)" 状态的流, 除 WINDOW_UPDATE、PRIORITY 和 RST_STREAM 外, 不能用于发送其他帧.

当收到设置了 END_STREAM 标志的帧, 或任一对等方发送 RST_STREAM 帧时, 流从此状态转换为 "closed".

端点可以在此状态下接收任何类型的帧. 使用 WINDOW_UPDATE 帧提供流量控制额度, 是继续接收受流量控制帧所必需的. 在此状态下, 接收方可以忽略 WINDOW_UPDATE 帧; 这些帧可能在发送设置了 END_STREAM 标志的帧后短时间内到达.

在此状态下收到的 PRIORITY 帧用于重新设置依赖于所标识流的流的优先级.

half-closed (remote): 处于 "half-closed (remote)" 的流不再被对等方用于发送帧. 在此状态下, 端点不再有义务维护接收方流量控制窗口.

如果端点在此状态的流上收到除 WINDOW_UPDATE、PRIORITY 或 RST_STREAM 以外的其他帧, 它 MUST 以 STREAM_CLOSED 类型的流错误 (Section 5.4.2) 响应.

处于 "half-closed (remote)" 的流可以由端点用于发送任何类型的帧. 在此状态下, 端点继续遵守已通告的流级流量控制限制 (Section 5.2).

流可以通过发送设置了 END_STREAM 标志的帧, 或在任一对等方发送 RST_STREAM 帧时, 从此状态转换为 "closed".

closed: "closed" 状态是终止状态.

端点 MUST NOT 在已关闭流上发送 PRIORITY 以外的帧. 端点在收到 RST_STREAM 后又收到 PRIORITY 以外的任何帧, MUST 将其视为 STREAM_CLOSED 类型的流错误 (Section 5.4.2). 类似地, 端点在收到设置了 END_STREAM 标志的帧后又收到任何帧, MUST 将其视为 STREAM_CLOSED 类型的连接错误 (Section 5.4.1), 除非该帧按下文描述被允许.

在发送包含 END_STREAM 标志的 DATA 或 HEADERS 帧后, WINDOW_UPDATE 或 RST_STREAM 帧可能会在短时间内于此状态下被接收. 在远端对等方收到并处理 RST_STREAM 或承载 END_STREAM 标志的帧之前, 它可能发送这些类型的帧. 端点 MUST 忽略在此状态下收到的 WINDOW_UPDATE 或 RST_STREAM 帧, 但端点 MAY 选择将发送 END_STREAM 后经过相当长时间才到达的帧视为 PROTOCOL_ERROR 类型的连接错误 (Section 5.4.1).

PRIORITY 帧可以在已关闭流上发送, 以设置依赖于该已关闭流的流的优先级. 端点 SHOULD 处理 PRIORITY 帧, 但如果该流已从依赖树中移除, 可以忽略它们 (见 [RFC7540] Section 5.3.3).

如果此状态是由于发送 RST_STREAM 帧而到达, 收到 RST_STREAM 的对等方可能已经在该流上发送了帧, 或将帧排队等待发送, 且这些帧无法撤回. 端点在发送 RST_STREAM 帧后, MUST 忽略其在已关闭流上收到的帧. 端点 MAY 选择限制忽略帧的时间范围, 并将在此时间后到达的帧视为错误.

发送 RST_STREAM 后收到的受流量控制帧 (即 DATA) 会计入连接流量控制窗口. 即使这些帧可能被忽略, 由于它们是在发送方收到 RST_STREAM 之前发送的, 发送方也会认为这些帧计入流量控制窗口.

端点可能在发送 RST_STREAM 后收到 PUSH_PROMISE 帧. 即使关联流已被重置, PUSH_PROMISE 也会使一个流变为 "reserved". 因此, 需要使用 RST_STREAM 来关闭不需要的承诺流.

在没有更具体规则的情况下, 实现 SHOULD 将收到状态描述中未明确允许的帧视为 PROTOCOL_ERROR 类型的连接错误 (Section 5.4.1). 请注意, PRIORITY 可以在任何流状态中发送和接收.

本节规则仅适用于本文档定义的帧. 对于语义未知的帧, 不能将其接收视为错误, 因为发送和接收这些帧的条件也未知; 见 Section 5.5.

HTTP 请求/响应交换的状态转换示例见 Section 8.8. 服务器推送的状态转换示例见 Sections 8.4.1 和 8.4.2.

5.1.1. 流标识符

流由无符号 31 位整数标识. 客户端发起的流 MUST 使用奇数流标识符; 服务器发起的流 MUST 使用偶数流标识符. 零 (0x00) 流标识符用于连接控制消息; 零流标识符不能用于建立新流.

新建立流的标识符 MUST 在数值上大于发起端点已打开或已保留的所有流. 这适用于使用 HEADERS 帧打开的流, 也适用于使用 PUSH_PROMISE 保留的流. 收到意外流标识符的端点 MUST 以 PROTOCOL_ERROR 类型的连接错误 (Section 5.4.1) 响应.

HEADERS 帧会将帧头部中流标识符所标识的、由客户端发起的流从 "idle" 转换为 "open". PUSH_PROMISE 帧会将帧载荷中 Promised Stream ID 字段所标识的、由服务器发起的流从 "idle" 转换为 "reserved (local)" 或 "reserved (remote)". 当流离开 "idle" 状态时, 所有仍处于 "idle" 状态且可能由对等方以较小流标识符打开的流会立即转换为 "closed". 也就是说, 端点可以跳过某个流标识符, 其效果是被跳过的流立即关闭.

流标识符不能被重用. 长生命周期连接可能导致端点耗尽可用流标识符范围. 无法建立新流标识符的客户端可以为新流建立新连接. 无法建立新流标识符的服务器可以发送 GOAWAY 帧, 以迫使客户端为新流打开新连接.

5.1.2. 流并发

对等方可以使用 SETTINGS 帧中的 SETTINGS_MAX_CONCURRENT_STREAMS 参数 (见 Section 6.5.2) 限制并发活跃流的数量. 最大并发流设置特定于每个端点, 并且只适用于接收该设置的对等方. 也就是说, 客户端指定服务器可以发起的最大并发流数, 服务器指定客户端可以发起的最大并发流数.

处于 "open" 状态或任一 "half-closed" 状态的流, 会计入端点被允许打开的最大流数. 处于这三种状态中任意一种的流都会计入 SETTINGS_MAX_CONCURRENT_STREAMS 设置通告的限制. 处于任一 "reserved" 状态的流不计入流限制.

端点 MUST NOT 超过其对等方设置的限制. 如果端点收到导致其通告的并发流限制被超过的 HEADERS 帧, MUST 将其视为 PROTOCOL_ERROR 或 REFUSED_STREAM 类型的流错误 (Section 5.4.2). 错误码的选择决定端点是否希望启用自动重试 (详见 Section 8.7).

希望将 SETTINGS_MAX_CONCURRENT_STREAMS 的值降低到低于当前打开流数量的端点, 可以关闭超过新值的流, 或允许这些流完成.

5.2. 流量控制

使用流进行复用会引入对 TCP 连接使用的争用, 导致流被阻塞. 流量控制方案确保同一连接上的流不会彼此造成破坏性干扰. 流量控制既用于单个流, 也用于整个连接.

HTTP/2 通过使用 WINDOW_UPDATE 帧 (Section 6.9) 提供流量控制.

5.2.1. 流量控制原则

HTTP/2 流的流量控制旨在允许使用多种流量控制算法, 而无需协议变更. HTTP/2 中的流量控制具有以下特性:

  1. 流量控制特定于连接. HTTP/2 流量控制运行在单跳的两个端点之间, 而不是整个端到端路径上.

  2. 流量控制基于 WINDOW_UPDATE 帧. 接收方通告它们准备在某个流以及整个连接上接收多少八位组. 这是一种基于信用额度的方案.

  3. 流量控制具有方向性, 总体控制由接收方提供. 接收方 MAY 为每个流和整个连接选择任意期望的窗口大小. 发送方 MUST 遵守接收方施加的流量控制限制. 客户端、服务器和中介都独立地作为接收方通告其流量控制窗口, 并在发送时遵守其对等方设置的流量控制限制.

  4. 对于新流和整个连接, 流量控制窗口的初始值均为 65,535 八位组.

  5. 帧类型决定流量控制是否适用于某个帧. 在本文档指定的帧中, 只有 DATA 帧受流量控制约束; 所有其他帧类型都不消耗已通告流量控制窗口中的空间. 这确保重要控制帧不会被流量控制阻塞.

  6. 端点可以选择禁用自己的流量控制, 但端点不能忽略来自其对等方的流量控制信号.

  7. HTTP/2 只定义 WINDOW_UPDATE 帧的格式和语义 (Section 6.9). 本文档不规定接收方如何决定何时发送该帧或发送什么值, 也不规定发送方如何选择发送分组. 实现可以选择任何满足其需要的算法.

实现还负责对请求和响应的发送进行优先级排序, 选择如何避免请求的队头阻塞, 并管理新流的创建. 这些算法选择可能与任何流量控制算法相互作用.

5.2.2. 流量控制的适当使用

定义流量控制是为了保护在资源受限条件下运行的端点. 例如, 代理需要在许多连接之间共享内存, 并且还可能具有较慢的上游连接和较快的下游连接. 流量控制处理这样一种情况: 接收方无法处理某个流上的数据, 但仍希望继续处理同一连接中的其他流.

不需要此能力的部署可以通告最大大小 (2^31-1) 的流量控制窗口, 并在收到任何数据时发送 WINDOW_UPDATE 帧来维持此窗口. 这实际上为该接收方禁用了流量控制. 相反, 发送方始终受接收方通告的流量控制窗口约束.

资源受限 (例如内存受限) 的部署可以使用流量控制来限制对等方可消耗的内存量. 但请注意, 如果在不了解带宽 * 延迟乘积 (见 [RFC7323]) 的情况下启用流量控制, 可能导致可用网络资源使用不佳.

即使完全了解当前带宽 * 延迟乘积, 实现流量控制也可能很困难. 端点 MUST 在数据可用后尽快从 TCP 接收缓冲区读取并处理 HTTP/2 帧. 若未及时读取, 当 WINDOW_UPDATE 等关键帧未被读取和执行时, 可能导致死锁. 及时读取帧不会使端点暴露于资源耗尽攻击, 因为 HTTP/2 流量控制会限制资源承诺.

5.2.3. 流量控制性能

如果端点不能确保其对等方始终具有大于该连接上对等方带宽 * 延迟乘积的可用流量控制窗口空间, 则其接收吞吐量将受 HTTP/2 流量控制限制. 这会导致性能下降.

及时发送 WINDOW_UPDATE 帧可以提升性能. 端点需要在提升接收吞吐量的需求与管理资源耗尽风险的需求之间取得平衡, 并且在定义其窗口大小管理策略时应仔细注意 Section 10.5.

5.3. 优先级排序

在 HTTP/2 这样的复用协议中, 对带宽和计算资源向流的分配进行优先级排序, 对获得良好性能可能至关重要. 糟糕的优先级方案可能导致 HTTP/2 性能不佳. 在 TCP 层没有并行性的情况下, 性能可能显著差于 HTTP/1.1.

良好的优先级方案受益于上下文知识的应用, 例如资源内容、资源之间的关联方式, 以及这些资源将如何被对等方使用. 特别是, 客户端可能拥有与服务器优先级排序相关的请求优先级知识. 在这些情况下, 让客户端提供优先级信息可以提升性能.

5.3.1. RFC 7540 中优先级的背景

RFC 7540 定义了用于请求优先级信令的丰富系统. 然而, 该系统被证明很复杂, 且没有被统一实现.

灵活的方案意味着客户端可能以非常不同的方式表达优先级, 采用的方法几乎没有一致性. 对服务器而言, 为该方案实现通用支持很复杂. 客户端和服务器对优先级的实现都不均衡. 许多服务器部署在对请求处理进行优先级排序时忽略客户端信号.

简而言之, RFC 7540 [RFC7540] 中的优先级信令并不成功.

5.3.2. 本文档中的优先级信令

本次 HTTP/2 更新废弃 RFC 7540 [RFC7540] 中定义的优先级信令. 与优先级信号相关的大部分文本未包含在本文档中. 保留帧字段说明和部分强制处理要求, 是为了确保本文档的实现仍能与使用 RFC 7540 所述优先级信令的实现互操作.

RFC 7540 优先级方案的完整说明仍见 [RFC7540] Section 5.3.

在许多情况下, 传递优先级信息对于获得良好性能是必要的. 在传递优先级信息很重要的场景中, 鼓励端点使用替代方案, 例如 [HTTP-PRIORITY] 中描述的方案.

尽管 RFC 7540 的优先级信令未被广泛采用, 在缺少更好信息时, 它提供的信息仍可能有用. 接收 HEADERS 或 PRIORITY 帧中优先级信号的端点可以从应用这些信息中受益. 特别是, 消费这些信号的实现不会因在缺少替代方案时丢弃这些优先级信号而受益.

在没有任何优先级信号时, 服务器 SHOULD 使用其他上下文信息来确定请求优先级. 服务器 MAY 将完全没有信号解释为客户端未实现该特性的指示. 已知 [RFC7540] Section 5.3.5 中描述的默认值在大多数条件下性能较差, 因而使用这些默认值不太可能是有意为之.

5.4. 错误处理

HTTP/2 分帧允许两类错误:

  • 使整个连接不可用的错误条件是连接错误.

  • 单个流中的错误是流错误.

错误码列表见 Section 7.

端点可能遇到会导致多个错误的帧. 实现 MAY 在处理期间发现多个错误, 但 SHOULD 最多报告一个流错误和一个连接错误.

给定流上报告的第一个流错误会阻止该流上报告任何其他错误. 相比之下, 协议允许多个 GOAWAY 帧, 但除非在优雅关闭期间遇到错误, 否则端点 SHOULD 只报告一种连接错误. 如果发生这种情况, 除任何先前包含 NO_ERROR 的 GOAWAY 外, 端点 MAY 发送带有新错误码的额外 GOAWAY 帧.

如果端点检测到多个不同错误, MAY 选择报告其中任一错误. 如果某个帧导致连接错误, 则该错误 MUST 被报告. 此外, 当端点检测到错误条件时, MAY 使用任何适用的错误码; 通用错误码 (例如 PROTOCOL_ERROR 或 INTERNAL_ERROR) 始终可以代替更具体的错误码使用.

5.4.1. 连接错误处理

连接错误是阻止进一步处理帧层或破坏任何连接状态的任何错误.

遇到连接错误的端点 SHOULD 首先发送 GOAWAY 帧 (Section 6.8), 其中包含它从对等方成功接收的最后一个流的流标识符. GOAWAY 帧包含一个错误码 (Section 7), 指示连接为何终止. 在因错误条件发送 GOAWAY 帧后, 端点 MUST 关闭 TCP 连接.

GOAWAY 可能无法被接收端点可靠接收. 发生连接错误时, GOAWAY 只能尽力向对等方传达连接终止的原因.

端点可以随时结束连接. 特别是, 端点 MAY 选择将流错误视为连接错误. 在情况允许时, 端点 SHOULD 在结束连接时发送 GOAWAY 帧.

5.4.2. 流错误处理

流错误是与特定流相关、但不影响其他流处理的错误.

检测到流错误的端点发送 RST_STREAM 帧 (Section 6.4), 其中包含发生错误的流的流标识符. RST_STREAM 帧包含一个指示错误类型的错误码.

RST_STREAM 是端点可以在流上发送的最后一个帧. 发送 RST_STREAM 帧的对等方 MUST 准备好接收远端对等方已经发送或排队等待发送的任何帧. 这些帧可以被忽略, 除非它们修改连接状态 (例如为字段段压缩 (Section 4.3) 或流量控制维护的状态).

通常, 端点 SHOULD NOT 为任何流发送多个 RST_STREAM 帧. 但是, 如果端点在超过一个往返时间后仍在已关闭流上收到帧, MAY 发送额外的 RST_STREAM 帧. 允许这种行为是为了处理行为异常的实现.

为避免循环, 端点 MUST NOT 响应 RST_STREAM 帧而发送 RST_STREAM.

5.4.3. 连接终止

如果 TCP 连接在仍有流处于 "open" 或 "half-closed" 状态时被关闭或重置, 则受影响的流不能被自动重试 (详见 Section 8.7).

5.5. 扩展 HTTP/2

HTTP/2 允许扩展协议. 在本节描述的限制内, 协议扩展可用于提供附加服务或改变协议的任何方面. 扩展仅在单个 HTTP/2 连接的范围内有效.

这适用于本文档定义的协议元素. 这不影响扩展 HTTP 的现有选项, 例如定义新的方法、状态码或字段 (见 [HTTP] Section 16).

扩展允许使用新的帧类型 (Section 4.1)、新的设置 (Section 6.5) 或新的错误码 (Section 7). 用于管理这些扩展点的注册表在 [RFC7540] Section 11 中定义.

实现 MUST 忽略所有可扩展协议元素中的未知或不支持的值. 实现 MUST 丢弃未知或不支持类型的帧. 这意味着扩展可以安全地使用这些扩展点中的任何一个, 而无需预先安排或协商. 但是, 不允许扩展帧出现在字段块 (Section 4.3) 中间; 这些帧 MUST 被视为 PROTOCOL_ERROR 类型的连接错误 (Section 5.4.1).

扩展 SHOULD 避免改变本文档定义的协议元素, 或未定义扩展机制的元素. 这包括改变帧布局, 增加或改变帧组成 HTTP 消息 (Section 8.1) 的方式, 改变伪头字段定义, 或改变合规端点可能视为连接错误 (Section 5.4.1) 的任何协议元素.

改变现有协议元素或状态的扩展 MUST 在使用前经过协商. 例如, 改变 HEADERS 帧布局的扩展, 在对等方给出明确肯定信号表示可接受之前不能使用. 在这种情况下, 还可能需要协调修订后的布局何时生效. 例如, 将 DATA 帧以外的帧视为受流量控制, 需要双方端点都理解语义变化, 因此只能通过协商完成.

本文档不强制规定协商使用扩展的具体方法, 但指出可以使用设置 (Section 6.5.2) 来实现此目的. 如果两个对等方都设置了表示愿意使用扩展的值, 则可以使用该扩展. 如果设置用于扩展协商, 初始值 MUST 以使扩展最初处于禁用状态的方式定义.


第 5 章完成!