5. Streams and Multiplexing (流与多路复用)
流 (Stream) 是在HTTP/2连接内,客户端和服务器之间交换的独立、双向的帧序列。流具有以下几个重要特征:
-
单个HTTP/2连接可以包含多个并发打开的流,任一端点都可以交错来自多个流的帧。
-
流可以由任一端点单方面建立和使用或共享。
-
流可以由任一端点关闭。
-
帧发送的顺序是有意义的。接收方按接收顺序处理帧。特别是,HEADERS和DATA帧的顺序在语义上是重要的。
-
流由整数标识。流标识符由发起流的端点分配。
5.1. Stream States (流状态)
流的生命周期如图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 |
`----------------------->| |<-----------------------'
+--------+
图2: 流状态转换
图例说明:
- send: 端点发送此帧
- recv: 端点接收此帧
- H: HEADERS帧 (包含隐含的CONTINUATION帧)
- ES: END_STREAM标志
- R: RST_STREAM帧
- PP: PUSH_PROMISE帧 (包含隐含的CONTINUATION帧);状态转换针对承诺的流
注意: 此图仅显示流状态转换以及影响这些转换的帧和标志。CONTINUATION帧不会导致状态转换;它们实际上是它们所跟随的HEADERS或PUSH_PROMISE的一部分。为了状态转换的目的,END_STREAM标志被作为与承载它的帧分开的事件处理;设置了END_STREAM标志的HEADERS帧可能导致两次状态转换。
两个端点对流状态的主观视图可能不同,因为帧在传输中。端点不协调流的创建;它们由任一端点单方面创建。状态不匹配的负面后果仅限于在发送RST_STREAM后的"closed"状态,在关闭后的一段时间内可能会收到帧。
流状态详解
idle (空闲)
所有流都从"idle"状态开始。
允许的转换:
-
作为客户端发送HEADERS帧,或作为服务器接收HEADERS帧,会使流变为"open"状态。流标识符按第5.1.1节所述选择。同一HEADERS帧也可能立即使流变为"half-closed"。
-
在另一个流上发送PUSH_PROMISE帧会保留所标识的空闲流以供以后使用。流状态转换为"reserved (local)"。只有服务器可以发送PUSH_PROMISE帧。
-
在另一个流上接收PUSH_PROMISE帧会保留所标识的空闲流以供以后使用。流状态转换为"reserved (remote)"。只有客户端可以接收PUSH_PROMISE帧。
-
注意,PUSH_PROMISE帧不是在空闲流上发送的,而是在Promised Stream ID字段中引用新保留的流。
-
打开具有更高数值流标识符的流会导致流立即转换到"closed"状态;注意此转换未在图中显示。
在此状态下,在流上接收除HEADERS或PRIORITY之外的任何帧必须 (MUST) 被视为类型为PROTOCOL_ERROR的连接错误 (第5.4.1节)。如果此流由服务器发起 (如第5.1.1节所述),则接收HEADERS帧也必须 (MUST) 被视为类型为PROTOCOL_ERROR的连接错误 (第5.4.1节)。
reserved (local) - 保留(本地)
处于"reserved (local)"状态的流是通过发送PUSH_PROMISE帧承诺的流。PUSH_PROMISE帧通过将流与远程对等方发起的打开流关联来保留空闲流 (参见第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的连接错误 (第5.4.1节)。
reserved (remote) - 保留(远程)
处于"reserved (remote)"状态的流已被远程对等方保留。
在此状态下,只允许以下转换:
-
接收HEADERS帧会使流转换到"half-closed (local)"。
-
任一端点都可以发送RST_STREAM帧以使流变为"closed"。这会释放流保留。
端点禁止 (MUST NOT) 在此状态下发送除RST_STREAM、WINDOW_UPDATE或PRIORITY之外的任何类型的帧。
在此状态下的流上接收除HEADERS、RST_STREAM或PRIORITY之外的任何类型的帧必须 (MUST) 被视为类型为PROTOCOL_ERROR的连接错误 (第5.4.1节)。
open (打开)
处于"open"状态的流可以被双方用来发送任何类型的帧。在此状态下,发送对等方遵守公告的流级流控制限制 (第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的流错误 (第5.4.2节) 响应。
处于"half-closed (remote)"的流可以被端点用来发送任何类型的帧。在此状态下,端点继续遵守公告的流级流控制限制 (第5.2节)。
流可以通过发送设置了END_STREAM标志的帧或任一对等方发送RST_STREAM帧从此状态转换到"closed"。
closed (关闭)
"closed"状态是终止状态。
在端点既发送又接收设置了END_STREAM标志的帧后,流进入"closed"状态。在端点发送或接收RST_STREAM帧后,流也进入"closed"状态。
端点禁止 (MUST NOT) 在关闭的流上发送除PRIORITY之外的帧。端点可以 (MAY) 将在关闭的流上接收任何其他类型的帧视为类型为STREAM_CLOSED的连接错误 (第5.4.1节),但以下说明的情况除外。
在发送设置了END_STREAM标志的帧或RST_STREAM帧的端点可能会在对等方接收和处理关闭流的帧之前,从其对等方接收WINDOW_UPDATE或RST_STREAM帧。
在"open"或"half-closed (local)"状态的流上发送RST_STREAM帧的端点可能会接收任何类型的帧。对等方可能在处理RST_STREAM帧之前已发送或排队发送这些帧。端点必须 (MUST) 最低限度地处理然后丢弃在此状态下接收的任何帧。这意味着为HEADERS和PUSH_PROMISE帧更新头部压缩状态。接收PUSH_PROMISE帧也会使承诺的流变为"reserved (remote)",即使在关闭的流上接收PUSH_PROMISE帧。此外,DATA帧的内容计入连接流控制窗口。
端点可以对处于"closed"状态的所有流执行此最小处理。端点可以 (MAY) 使用其他信号来检测对等方是否已接收导致流进入"closed"状态的帧,并将除PRIORITY之外的任何帧的接收视为类型为PROTOCOL_ERROR的连接错误 (第5.4.1节)。端点可以使用指示对等方已接收关闭信号的帧来驱动此操作。端点不应 (SHOULD NOT) 为此目的使用定时器。例如,在关闭流后发送SETTINGS帧的端点在接收到设置确认后,可以安全地将该流上的DATA帧接收视为错误。可能使用的其他信号包括PING帧、在关闭流后创建的流上接收数据,或在关闭流后创建的请求的响应。
在没有更具体规则的情况下,实现应该 (SHOULD) 将接收在状态描述中未明确允许的帧视为类型为PROTOCOL_ERROR的连接错误 (第5.4.1节)。注意,PRIORITY可以在任何流状态下发送和接收。
本节中的规则仅适用于本文档中定义的帧。接收语义未知的帧不能被视为错误,因为发送和接收这些帧的条件也是未知的;参见第5.5节。
HTTP请求/响应交换的状态转换示例可以在第8.8节中找到。服务器推送的状态转换示例可以在第8.4.1节和第8.4.2节中找到。
5.1.1. Stream Identifiers (流标识符)
流由无符号31位整数标识。客户端发起的流必须 (MUST) 使用奇数流标识符;服务器发起的流必须 (MUST) 使用偶数流标识符。流标识符零 (0x00) 用于连接控制消息;流标识符零不能用于建立新流。
新建立的流的标识符必须 (MUST) 在数值上大于发起端点已打开或保留的所有流。这适用于使用HEADERS帧打开的流和使用PUSH_PROMISE保留的流。接收到意外流标识符的端点必须 (MUST) 以类型为PROTOCOL_ERROR的连接错误 (第5.4.1节) 响应。
HEADERS帧将使帧头部中流标识符标识的客户端发起的流从"idle"转换到"open"。PUSH_PROMISE帧将使帧载荷中Promised Stream ID字段标识的服务器发起的流从"idle"转换到"reserved (local)"或"reserved (remote)"。当流从"idle"状态转换出来时,所有"idle"状态中可能由对等方使用较低数值的流标识符打开的流立即转换到"closed"。也就是说,端点可以跳过流标识符,其效果是跳过的流立即关闭。
流标识符不能重用。长期连接可能导致端点耗尽可用的流标识符范围。无法建立新流标识符的客户端可以为新流建立新连接。无法建立新流标识符的服务器可以发送GOAWAY帧,以便客户端被迫为新流打开新连接。
5.1.2. Stream Concurrency (流并发)
对等方可以使用SETTINGS帧中的SETTINGS_MAX_CONCURRENT_STREAMS参数 (参见第6.5.2节) 限制并发活动流的数量。最大并发流设置特定于每个端点,仅适用于接收设置的对等方。也就是说,客户端指定服务器可以发起的最大并发流数,服务器指定客户端可以发起的最大并发流数。
处于"open"状态或处于任一"half-closed"状态的流计入端点被允许打开的最大流数。处于这三种状态中任何一种的流计入SETTINGS_MAX_CONCURRENT_STREAMS设置中公告的限制。处于任一"reserved"状态的流不计入流限制。
端点禁止 (MUST NOT) 超过其对等方设置的限制。接收导致其公告的并发流限制被超过的HEADERS帧的端点必须 (MUST) 将其视为类型为PROTOCOL_ERROR或REFUSED_STREAM的流错误 (第5.4.2节)。错误码的选择决定端点是否希望启用自动重试 (详见第8.7节)。
希望将SETTINGS_MAX_CONCURRENT_STREAMS的值减少到低于当前打开流数的端点可以关闭超过新值的流或允许流完成。
5.2. Flow Control (流控制)
使用流进行多路复用会引入对TCP连接使用的争用,导致流被阻塞。流控制方案确保同一连接上的流不会相互破坏性干扰。流控制既用于单个流,也用于整个连接。
HTTP/2通过使用WINDOW_UPDATE帧 (第6.9节) 提供流控制。
5.2.1. Flow-Control Principles (流控制原则)
HTTP/2流控制旨在允许使用各种流控制算法而无需更改协议。HTTP/2中的流控制具有以下特征:
-
特定于连接的流控制
HTTP/2流控制在单跳的端点之间运行,而不是整个端到端路径。 -
基于WINDOW_UPDATE帧
接收方公告它们准备在流上和整个连接上接收多少个八位字节。这是基于信用的方案。 -
方向性的,由接收方整体控制
接收方可以 (MAY) 为每个流和整个连接选择设置任何所需的窗口大小。发送方必须 (MUST) 遵守接收方施加的流控制限制。客户端、服务器和中介作为接收方都独立公告其流控制窗口,并在发送时遵守其对等方设置的流控制限制。 -
初始值为65,535个八位字节
新流和整个连接的流控制窗口的初始值为65,535个八位字节。 -
帧类型决定是否应用流控制
在本文档中指定的帧中,只有DATA帧受流控制约束;所有其他帧类型不消耗公告的流控制窗口中的空间。这确保重要的控制帧不会被流控制阻塞。 -
端点可以选择禁用自己的流控制
但端点不能忽略来自其对等方的流控制信号。 -
HTTP/2仅定义WINDOW_UPDATE帧的格式和语义
本文档不规定接收方何时决定发送此帧或发送什么值,也不规定发送方如何选择发送数据包。实现能够选择适合其需求的任何算法。
实现还负责优先处理请求和响应的发送、选择如何避免请求的队头阻塞以及管理新流的创建。这些算法选择可能与任何流控制算法交互。
5.2.2. Appropriate Use of Flow Control (流控制的适当使用)
流控制的定义是为了保护在资源约束下运行的端点。例如,代理需要在许多连接之间共享内存,也可能有慢速的上游连接和快速的下游连接。流控制解决了接收方无法处理一个流上的数据但希望继续处理同一连接中其他流的情况。
不需要此功能的部署可以公告最大大小 (2^31-1) 的流控制窗口,并可以通过在接收到任何数据时发送WINDOW_UPDATE帧来维护此窗口。这有效地为该接收方禁用了流控制。相反,发送方始终受接收方公告的流控制窗口的约束。
具有受限资源 (例如内存) 的部署可以使用流控制来限制对等方可以消耗的内存量。但是请注意,如果在不了解带宽延迟乘积的情况下启用流控制,这可能导致可用网络资源的次优使用 (参见 [RFC7323])。
即使完全了解当前带宽延迟乘积,流控制的实现也可能很困难。端点必须 (MUST) 在数据可用时立即从TCP接收缓冲区读取和处理HTTP/2帧。未能及时读取可能导致死锁,因为关键帧 (如WINDOW_UPDATE) 未被读取和处理。及时读取帧不会使端点暴露于资源耗尽攻击,因为HTTP/2流控制限制了资源承诺。
5.2.3. Flow-Control Performance (流控制性能)
如果端点无法确保其对等方始终具有大于此连接上对等方带宽延迟乘积的可用流控制窗口空间,其接收吞吐量将受到HTTP/2流控制的限制。这将导致性能下降。
及时发送WINDOW_UPDATE帧可以提高性能。端点将希望平衡提高接收吞吐量的需求与管理资源耗尽风险的需求,并应仔细注意第10.5节在定义其管理窗口大小的策略时的内容。
5.3. Prioritization (优先级)
在像HTTP/2这样的多路复用协议中,优先分配带宽和计算资源给流对于获得良好性能至关重要。糟糕的优先级方案可能导致HTTP/2提供糟糕的性能。在TCP层没有并行性的情况下,性能可能明显差于HTTP/1.1。
良好的优先级方案受益于上下文知识的应用,例如资源的内容、资源如何相互关联以及对等方将如何使用这些资源。特别是,客户端可以拥有与服务器优先级相关的请求优先级知识。在这些情况下,让客户端提供优先级信息可以提高性能。
5.3.1. Background on Priority in RFC 7540 (RFC 7540中优先级的背景)
RFC 7540定义了一个丰富的请求优先级信号系统。然而,这个系统被证明是复杂的,并且没有得到统一实现。
灵活的方案意味着客户端可以以非常不同的方式表达优先级,采用的方法几乎没有一致性。对于服务器,为方案实现通用支持很复杂。客户端和服务器中优先级的实现都不均衡。许多服务器部署在优先处理请求时忽略了客户端信号。
简而言之,RFC 7540 [RFC7540] 中的优先级信号不成功。
5.3.2. Priority Signaling in This Document (本文档中的优先级信号)
此HTTP/2更新弃用了RFC 7540 [RFC7540] 中定义的优先级信号。与优先级信号相关的大部分文本不包括在本文档中。保留了帧字段的描述和一些强制处理,以确保本文档的实现与使用RFC 7540中描述的优先级信号的实现保持互操作。
RFC 7540优先级方案的完整描述保留在 [RFC7540] 的第5.3节中。
在许多情况下,信号优先级信息对于获得良好性能是必要的。在信号优先级信息很重要的情况下,鼓励端点使用替代方案,例如 [HTTP-PRIORITY] 中描述的方案。
尽管RFC 7540的优先级信号没有被广泛采用,但在缺乏更好信息的情况下,它提供的信息仍然有用。接收HEADERS或PRIORITY帧中优先级信号的端点可以从应用该信息中受益。特别是,消费这些信号的实现在没有替代方案的情况下不会从丢弃这些优先级信号中受益。
服务器应该 (SHOULD) 在缺少任何优先级信号的情况下使用其他上下文信息来确定请求的优先级。服务器可以 (MAY) 将信号的完全缺失解释为客户端未实现该功能的指示。[RFC7540] 第5.3.5节中描述的默认值已知在大多数条件下性能不佳,并且它们的使用不太可能是故意的。
5.4. Error Handling (错误处理)
HTTP/2帧允许两类错误:
-
使整个连接不可用的错误条件是连接错误 (Connection Error)。
-
单个流中的错误是流错误 (Stream Error)。
错误码列表包含在第7节中。
端点可能会遇到导致多个错误的帧。实现可以 (MAY) 在处理期间发现多个错误,但应该 (SHOULD) 最多报告一个流错误和一个连接错误。
给定流报告的第一个流错误会阻止报告该流上的任何其他错误。相比之下,协议允许多个GOAWAY帧,尽管端点应该 (SHOULD) 仅报告一种类型的连接错误,除非在优雅关闭期间遇到错误。如果发生这种情况,端点可以 (MAY) 发送带有新错误码的额外GOAWAY帧,以及任何先前包含NO_ERROR的GOAWAY。
如果端点检测到多个不同的错误,它可以 (MAY) 选择报告其中任何一个错误。如果帧导致连接错误,则必须 (MUST) 报告该错误。此外,端点可以 (MAY) 在检测到错误条件时使用任何适用的错误码;通用错误码 (如PROTOCOL_ERROR或INTERNAL_ERROR) 始终可以代替更具体的错误码使用。
5.4.1. Connection Error Handling (连接错误处理)
连接错误是阻止进一步处理帧层或损坏任何连接状态的任何错误。
遇到连接错误的端点应该 (SHOULD) 首先发送GOAWAY帧 (第6.8节),其中包含它从对等方成功接收的最后一个流的流标识符。GOAWAY帧包括指示连接终止原因的错误码 (第7节)。在为错误条件发送GOAWAY帧后,端点必须 (MUST) 关闭TCP连接。
GOAWAY可能不会被接收端点可靠接收。在连接错误的情况下,GOAWAY仅提供尽力尝试与对等方通信连接终止原因。
端点可以随时结束连接。特别是,端点可以 (MAY) 选择将流错误视为连接错误。端点应该 (SHOULD) 在结束连接时发送GOAWAY帧,前提是情况允许。
5.4.2. Stream Error Handling (流错误处理)
流错误是与特定流相关的错误,不影响其他流的处理。
检测到流错误的端点发送RST_STREAM帧 (第6.4节),其中包含发生错误的流的流标识符。RST_STREAM帧包括指示错误类型的错误码。
RST_STREAM是端点可以在流上发送的最后一个帧。发送RST_STREAM帧的对等方必须 (MUST) 准备好接收远程对等方发送或排队发送的任何帧。这些帧可以被忽略,除非它们修改连接状态 (例如为字段段压缩 (第4.3节) 或流控制维护的状态)。
通常,端点不应 (SHOULD NOT) 为任何流发送多个RST_STREAM帧。但是,如果端点在超过一个往返时间后在关闭的流上接收帧,它可以 (MAY) 发送额外的RST_STREAM帧。允许此行为以处理行为不当的实现。
为避免循环,端点禁止 (MUST NOT) 发送RST_STREAM以响应RST_STREAM帧。
5.4.3. Connection Termination (连接终止)
如果TCP连接在流仍处于"open"或"half-closed"状态时被关闭或重置,则受影响的流无法自动重试 (详见第8.7节)。
5.5. Extending HTTP/2 (扩展HTTP/2)
HTTP/2允许扩展协议。在本节描述的限制内,协议扩展可用于提供额外服务或更改协议的任何方面。扩展仅在单个HTTP/2连接的范围内有效。
这适用于本文档中定义的协议元素。这不影响扩展HTTP的现有选项,例如定义新方法、状态码或字段 (参见 [HTTP] 第16节)。
扩展允许使用新的帧类型 (第4.1节)、新的设置 (第6.5节) 或新的错误码 (第7节)。用于管理这些扩展点的注册表在 [RFC7540] 的第11节中定义。
实现必须 (MUST) 忽略所有可扩展协议元素中的未知或不支持的值。实现必须 (MUST) 丢弃具有未知或不支持类型的帧。这意味着任何这些扩展点都可以安全地由扩展使用,而无需事先安排或协商。但是,出现在字段块 (第4.3节) 中间的扩展帧是不允许的;这些必须 (MUST) 被视为类型为PROTOCOL_ERROR的连接错误 (第5.4.1节)。
扩展应该 (SHOULD) 避免更改本文档中定义的协议元素或没有定义扩展机制的元素。这包括对帧布局的更改、对帧组成HTTP消息方式的添加或更改 (第8.1节)、伪头部字段的定义或对任何协议元素的更改,这些更改可能被兼容端点视为连接错误 (第5.4.1节)。
更改现有协议元素或状态的扩展必须 (MUST) 在使用前进行协商。例如,更改HEADERS帧布局的扩展在对等方给出此可接受的肯定信号之前不能使用。在这种情况下,还可能需要协调修订布局何时生效。例如,将DATA帧以外的帧视为流控制的需要双方端点理解的语义更改,因此这只能通过协商来完成。
本文档不强制要求协商扩展使用的特定方法,但注意到设置 (第6.5.2节) 可用于此目的。如果双方都设置了表示愿意使用扩展的值,则可以使用扩展。如果设置用于扩展协商,则必须 (MUST) 以使扩展最初被禁用的方式定义初始值。
🔑 关键要点总结
流状态机
6个状态: idle → reserved (local/remote) → open → half-closed (local/remote) → closed
关键转换:
- HEADERS: idle → open
- PUSH_PROMISE: idle → reserved
- END_STREAM: open → half-closed
- RST_STREAM: 任何状态 → closed
流标识符规则
- 客户端: 奇数 (1, 3, 5, ...)
- 服务器: 偶数 (2, 4, 6, ...)
- 连接控制: 0
- 单调递增: 不可重用
流控制特性
- 基于信用: WINDOW_UPDATE帧
- 初始窗口: 65,535字节
- 仅DATA帧: 受流控制
- 双向独立: 发送/接收分开
- 性能关键: 避免队头阻塞
优先级演进
- RFC 7540: 复杂但失败
- 本文档: 已弃用
- 推荐: HTTP-PRIORITY扩展
错误分类
- 连接错误: GOAWAY → 关闭TCP
- 流错误: RST_STREAM → 关闭流