Skip to content

第 11 章 HTTP/2、gRPC 与多路复用 ​

返回目录 · 编写进度

HTTP/2保留HTTP方法、状态和大量字段语义,却不再用HTTP/1.1的文本行与chunked表示消息。它把连接中的通信组织成带流编号的二进制帧。gRPC再在这份服务上规定调用元信息、长度前缀消息和结束状态。两层的流、消息与帧仍不能互相等同。

11.1 连接前言与九字节帧头 ​

接收者必须先知道接下来要按HTTP/2解释。对https,TLS里的ALPN扩展用来协商应用协议,HTTP/2标识为 h2;这不是根据端口443猜出的结果。已知服务支持明文HTTP/2时,也可以在TCP建立后直接发送前言。RFC 9113弃用了旧HTTP/1.1 Upgrade到h2c的方式,并没有禁止所有明文先验连接。RFC 9113 §3。

客户端前言先有二十四字节固定序列:

text
PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n

50 52 49 20 2a 20 48 54 54 50 2f 32 2e 30 0d 0a
0d 0a 53 4d 0d 0a 0d 0a

文本与十六进制是同一序列的两种展示,空白排版不属于序列。它看似HTTP请求开头,却不是要求服务器执行一个普通PRI资源操作;之后必须紧跟SETTINGS帧,开始HTTP/2会话。服务器的前言则以SETTINGS帧开始,不发送客户端这二十四字节。

所有HTTP/2帧具有固定九字节头:

帧内范围字段宽度与含义
[0,3)Length24位无符号大端,计算后续帧负载,不含九字节头
[3,4)Type一字节帧类型
[4,5)Flags一字节,意义由Type决定
[5,9)R与Stream Identifier高一位保留,其余31位为流编号

流编号0保留给连接级控制,不能当普通请求流使用。R发送时置零,接收时忽略。帧类型决定其负载结构,未知类型按规范忽略并丢弃;未知标志位也不能脱离类型任意猜测语义。RFC 9113 §4.1。

例如空SETTINGS帧可为:

text
00 00 00 04 00 00 00 00 00

Length为零、Type为04、Flags为零、流编号零。对其确认的空SETTINGS ACK为 00 00 00 04 01 00 00 00 00,同样九字节。SETTINGS ACK不是TCP确认,也不是应用请求成功;它确认的是相应设置处理。ACK标志置位的SETTINGS不能携带设置负载。

再假设流1已按规则打开、有足够发送窗口,发送者用DATA结束自身发送方向并携带三字节abc:

text
00 00 03 00 01 00 00 00 01 61 62 63

Length=3,Type=00,Flags的END_STREAM位为1,流编号1,负载为 61 62 63。总长十二字节。它不能在尚未打开的流1上任意出现;帧头合法与流状态合法是两次检查,下一节将解释其状态转移。

默认接收帧负载上限是16384字节;只有接收方通过SETTINGS_MAX_FRAME_SIZE允许更大值,发送者才可使用更大帧。该设置范围为16384至16777215。它限制一帧负载,不限制一个HTTP内容总量,也不保证实现一次分配这么大缓存。某些帧类型还规定更小或固定的负载结构,不能拿通用最大值覆盖类型要求。

连接建立后,客户端可以按初始规则发送后续帧,不必无条件等服务器前言全部返回才开始任何请求;但收到设置后必须遵守它们。HTTP/2因此不能简单计为“TCP后永远再额外等一个SETTINGS往返”。实际握手依赖还要结合TLS、初始设置和应用请求路径分析。

11.2 流编号与状态:一条连接中有多次独立HTTP交换 ​

HTTP/2的流是同一连接内、带编号的双向帧序列。常规客户端请求使用1、3、5等奇数流编号;服务器发起的偶数编号涉及推送等专门机制,不等于服务器可任意倒过来发普通客户端请求。编号在同一连接内只能递增开启,不能关闭后重新使用。RFC 9113 §5.1。

一个普通请求用HEADERS开始,携带经HPACK编码的字段块。解码后的逻辑字段可能是:

逻辑字段值作用
:methodPOST请求操作
:schemehttpsURI方案
:authorityexample.com服务主机信息
:path/messages资源路径与查询
content-typetext/plain内容类型

这些行不是线上HTTP/1.1文本。冒号开头的伪字段传递请求控制信息,必须位于普通字段之前;响应则用 :status 表达状态。普通字段名在HTTP/2中必须小写。HPACK只决定怎样编码这些信息,不改变POST或200的基本语义。

请求内容放在同一流的DATA中,响应头与响应内容也使用这个流编号。请求与响应不需要另分两个编号,因为方向已经由发送端区分。服务器可以在请求发送方向尚未结束时开始响应,这使同一流能够承载双向进行的交换。

状态,以本端视角命名意义
idle尚未打开或保留
reserved (local)本端为推送保留
reserved (remote)对端为推送保留
open双方可按规则发送该流帧
half-closed (local)本端已结束发送方向,仍可接收
half-closed (remote)对端已结束发送方向,本端仍可发送
closed已结束或被重置,编号不能复用

普通请求的HEADERS把空闲流打开;在HEADERS或DATA上发送END_STREAM,结束的是发送者这个方向。请求头已经携带END_STREAM时,客户端没有后续请求体,但服务器仍可返回响应;双方方向都结束才正常关闭整个流。RST_STREAM可以让某流异常结束,而不必把其它流的业务一并判定失败。

END_HEADERS与END_STREAM是两个独立标志。前者说字段块已经传完,后者说这个方向已经结束;只有END_HEADERS的请求仍可以随后发送DATA。字段块过大时,HEADERS之后用同流CONTINUATION继续,最后一帧以END_HEADERS结束。在这个字段块序列中,不得插入其它类型或其它流的帧;HTTP/2的多路复用并非任何位置都允许交错。

HTTP内容可以跨多个DATA帧,DATA帧也不对应应用写入边界。HTTP/2不用chunked作为消息定界,发送方向由END_STREAM等状态结束;若带content-length,它仍须与实际内容长度一致,不能因帧自己有长度就任意伪造这一字段。DATA中的填充等协议字节不应误算为应用内容。

连接相关的Connection、Keep-Alive、Proxy-Connection、Transfer-Encoding、Upgrade等字段不能按HTTP/1.1方式生成到HTTP/2消息;te只有值trailers的特定例外。网关从HTTP/1.1转成HTTP/2时,必须处理这些差异,而不是只把字段名改成小写。RFC 9113 §8。

流状态有自己的并发上限和错误规则,连接还维护设置、压缩上下文及总窗口。某流结束,并不把这些连接状态清空。这种“独立业务流+共享连接状态”的组合,是多路复用效率与共享故障范围同时存在的原因。

11.3 HPACK:用索引表达重复字段,用状态换取字节节省 ​

同一连接中的请求常反复出现相同方法、主机和字段。每次重新发送完整文本既占字节,也重复处理。HPACK把一组HTTP字段编码为字段块:常见字段可以用索引表示,新的字段用字面值表示,还可以把部分结果放入动态表供后续复用。它只编码字段,不压缩DATA中的内容。RFC 7541 §2。

索引空间由两部分组成。静态表固定有61项,包含常见名字或完整名字值组合,例如2为:method: GET、4为:path: /、7为:scheme: https。动态表初始为空,新增条目放在最前面;最新动态条目位于联合索引62,随后是更旧条目。插入新项后旧动态索引会移动,所以62不是某个主机永远不变的编号。

不同表示由首字节前缀区分:

起始位表示类别是否更新动态表
1已索引的完整名字值否,直接引用
01字面字段,增量索引是,插入新项
0000字面字段,不索引否
0001字面字段,永不索引否,并传达相应处理约束
001动态表大小更新改变容量,不是普通字段

字面字段也可以只引用已有名称,另附新值;只有名称未用索引表示时才完整发送名称。字符串具有Huffman标志与长度:H=0时是原始字节,H=1时按标准固定码表压缩。长度计算的是编码后字符串所占字节,不是字符个数或解码后的长度。

先看一个完全不用Huffman、动态表为空的构造。要表达GET、https、路径/和主机example.com,字段块可以是:

text
82 87 84 41 0b 65 78 61 6d 70 6c 65 2e 63 6f 6d
字节解释
82索引表示,索引2,GET
87索引7,https
84索引4,路径/
4101增量索引表示,名称索引1,即:authority
0bH=0,值长十一字节
后十一字节example.com的原始ASCII

块共十六字节,并把:authority: example.com放入动态表。只要期间没有其它插入、容量变化或淘汰,下一条相同请求的字段块可以简化为 82 87 84 be:be的高位为1,剩余七位62,引用刚才的动态项。四字节便恢复相同四个逻辑字段。这是有状态复用的确定性例子,不代表所有实际请求都只需四字节。

把第一个字段块放进流1的无内容请求,HEADERS头可以是 00 00 10 01 05 00 00 00 01:负载十六,类型HEADERS,END_HEADERS与END_STREAM同时置位,流1。头与块合计二十五字节。第二个块若用于流3,同样标志的头为 00 00 04 01 05 00 00 00 03,合计十三字节。客户端前言与SETTINGS等连接成本未计入。

HPACK索引与长度使用带前缀的变长整数,而不全部采用固定大端整数。N位前缀能直接容纳小值;值达到 2^N−1 时,先写满前缀,再把剩余值分成七位组,后续字节高位标记是否继续。以N=5、值1337为例,前缀31,剩余1306=26+10×128,后续为9a 0a,整体数值表示为 1f 9a 0a。这些七位组的权重顺序不能误当普通网络大端序。

Huffman则用标准规定的不同长度比特码表示字节,各码可按前缀规则逐位区分;它不需要通信双方临时交换一棵码树。编码末尾可能补齐到字节边界,补位必须满足EOS前缀要求且不超过七位;EOS本身不能作为普通字符串数据出现。字符串长度、Huffman解码输出及整数宽度都需要实现上限,不能只限制线上字段块大小。

动态表容量还有独立计量。每项大小按“未作Huffman的名称字节+值字节+32”计算,例中是 10+11+32=53。这是一份协议容量预算,不等于某个语言对象真实只占53字节。默认HTTP/2表上限为4096,可通过设置和HPACK表更新约束;总解码字段列表还需要另一项上限,不能因表很小就认为所有字段输出也很小。

双向状态相互独立:客户端请求编码器与服务器对应解码器共享请求侧历史,服务器响应编码器与客户端解码器共享另一份历史。不同流却可能引用同一方向的连接级动态表。因此丢弃一个不想处理的业务流,也不能随意跳过会改变HPACK历史的字段块,否则后面流的索引解释会失同步。HTTP/2要求按规则处理压缩状态;字段解码错误属于连接级问题。RFC 9113 §4.3。

永不索引有助于限制敏感字段被放入索引复用路径,但它不加密字段、不验证发送者,也不隐藏所有长度。HPACK带来的是压缩效率与连接状态约束;真正的通道保密和身份验证仍由其它机制提供。

11.4 多路复用解决了什么,又把什么留给TCP ​

HTTP/1.1流水线要求响应按请求顺序完整发送。HTTP/2给帧标上流编号,使接收者能够把来自不同请求的数据分别交给各自状态机。在字段块已经完整、相关状态和窗口允许时,一条连接可以先发送流1的一部分DATA,再发送流3的DATA,再继续流1;不必等流1整个内容结束。

text
同一方向的HTTP/2字节顺序:
[流1 HEADERS完整][流3 HEADERS完整]
[流1 DATA一部分][流3 DATA并结束][流1 DATA继续]

接收者不把流3的DATA当作流1正文,因为九字节帧头给出了边界与编号。这消除了HTTP/1.1那种“前一个响应尚未结束,后一个响应就不能在同一连接上完成”的应用承载限制。它没有允许在一个帧负载内部任意插入另一帧,也没有取消HEADERS到CONTINUATION序列的连续性规则。

然而这些帧编码后,仍是一条TCP字节流。假设流1一个DATA帧中的某段TCP字节丢失,随后承载流3帧的网络数据先到达。TCP接收端会保存乱序数据,但在缺口补齐之前,不能把后面的字节越过缺口交给HTTP/2。即使流3在逻辑上独立,它的帧解析也可能被这个传输缺口阻挡。

层次产生等待的原因流编号能否直接消除
HTTP/1.1响应序列前一响应未完整,后一响应不能插入HTTP/2映射能改变这项约束
HTTP/2同一流本流先前数据或应用结果尚未就绪不能,本流仍有顺序
TCP交付连接字节序列有缺口不能,依赖TCP恢复
连接共享资源发送预算、接收窗口或调度受限需要相应机制,不由编号自动解决

这解释了为什么“HTTP/2有多路复用”与“HTTP/2仍有TCP队头阻塞”可以同时成立。前者说应用帧可以标识和交错不同流,后者说承载它们的TCP仍然有序交付。两个结论讨论不同层次,不能以其中一个否定另一个。

共享连接还共享底层发送节奏。TCP根据路径反馈控制这条连接向网络注入数据,某一流不能单凭自己的编号另开一份独立拥塞状态。HTTP/2的接收流量控制则进一步管理对端缓存,下一节会讲。网络拥塞、端点处理慢和应用不读取,都可能表现为发送等待,却由不同预算造成。

调度决定下一次先编码哪个流。即使两个流都有数据和窗口,某个实现也可能偏向大流、低延迟流或特定优先规则;标准帧格式本身不给出所有场景下的公平性和延迟保证。很大的DATA帧会占用一段连续发送范围,较小帧可以提供更频繁的调度机会,但增加帧头和处理次数。

将许多代理连接放入一条HTTP/2外层,可以摊薄建连和安全握手;该连接断开或传输缺口出现时,多个业务也会共同受影响。使用多条外层连接可能分散这种共享影响,同时增加握手、状态和网络竞争。选择不是单纯的“复用越多越好”,而是复用粒度与实际负载的取舍。

gRPC通常把不同调用映射为不同HTTP/2流,一次双向流式调用内部又可以有多条gRPC消息。流编号隔离的是调用承载,不会让同一调用内部的所有消息自动独立交付,更不会让应用处理本身并行完成。到QUIC时,本书将进一步比较“流在传输层本身独立”与“流只在TCP上方独立”的差别。

这里的等待分析来自TCP服务契约与 RFC 9113 §5 的流模型,不给出未经测量的丢包吞吐排名。帧交错、应用调度、网络恢复与端点缓存共同决定性能,需要分别归因。

11.5 双层流量控制:允许发送不等于确认处理完成 ​

多路复用连接可以同时产生很多数据。只依赖TCP接收缓冲,接收者很难表达“这一条业务流暂时不读取,但其它流仍然可读”。HTTP/2因此在TCP上方再提供接收流量控制:每个发送方向同时维护连接窗口和各流窗口,发送DATA前必须取得两份额度。

窗口是剩余可发送字节数。若某流窗口为Ws、连接窗口为Wc、对端最大帧负载为F、当前待发数据为Q,在不考虑填充与其它发送限制时,一帧DATA可放的数据量不能超过:

N≤min(Ws,Wc,F,Q).

底层TCP是否可写、当前网络发送预算与调度也可能进一步限制N。HTTP/2窗口约束接收方缓存,TCP拥塞控制则约束对网络路径的发送;两者作用不同,不能因为都有“窗口”就合并计算成一份状态。

初始流窗口与连接窗口各为65535字节,且两个通信方向各自独立。发送一个有N字节帧负载的DATA后,两个适用窗口都扣除N。计数包括DATA填充及其长度字段,不包括九字节HTTP/2帧头;不能只统计最终交给应用的内容。HEADERS、SETTINGS、PING等不消耗DATA窗口,但仍需要独立的资源与频率约束。RFC 9113 §5.2、§6.9。

例如当前连接窗口12000,流1窗口5000,待发8000、帧上限16384且没有其它阻塞。可先发5000,之后连接窗口剩7000、流1窗口为0。其它有窗口的流可以使用连接剩余额度;流1则等待自己的额度增加。若整个连接窗口归零,即使每个流都还有余额,所有非零DATA也要等待连接额度。

WINDOW_UPDATE提供增量额度。Stream Identifier为0时增加连接窗口,非零时增加对应流窗口;负载四字节中高一位保留,低31位是非零增量。对流1补4000,只改变它的窗口,不会顺便恢复连接余额。接收者通常在释放缓存、允许继续接收时发更新,更新不证明HTTP操作执行成功,更不是应用层确认收到一条完整业务消息。

假设向流1增加4000,对应帧可以是:

text
00 00 04 08 00 00 00 00 01 00 00 0f a0

Type=08,负载四字节,流编号1,增量 00 00 0f a0=4000,总长十三字节。对连接补同样额度,只把流编号改成零;这两个更新相互独立。不能用增量零表达查询或保活,也不能让窗口增长超过规定最大值。

SETTINGS_INITIAL_WINDOW_SIZE则修改流初始窗口,并对已有流按新旧差值调整。它不修改连接窗口,连接额度要用WINDOW_UPDATE处理。若设置让某活跃流剩余额度变成负数,发送者要保留这个负数,停止新的受控发送直到额度恢复;把负数无条件截成零,会丢掉协议所需的差值状态。

窗口配置影响缓存与延迟。过早无限补额度,可能让本来需要背压的慢应用在用户态堆积大量内容;补得过晚,可能让发送者虽然网络空闲却等不到许可。若希望在一段反馈时间内持续发送,窗口与补充策略要容纳对应在途数据;这是一项缓冲与流量需求关系,不是把窗口数值调大就增加物理带宽。

并发流数上限又约束另一种资源。允许一万个逻辑流,不意味着每流都能无限占用窗口和缓存;限制总窗口也不能完全限制每流头部、状态对象和未完成消息。可靠实现需要把流数、字段解码、DATA缓存和应用处理能力一起纳入预算。

流量控制还需要避免协议自锁。接收者必须继续处理能使连接前进的控制帧,而不是因为某流暂不读数据,就停止解析整个连接中的所有帧。若应用队列与协议读取线程互相等待,即使网络正常,也可能出现没有任何一方能补窗口的局面。多流承载只有与有界队列和正确调度结合,才能把独立窗口转化为实际隔离能力。

11.6 gRPC:调用语义、五字节前缀与最终状态 ​

gRPC把一次远程过程调用映射到HTTP/2流。客户端选择服务和方法,提交零个或多个消息,服务器返回零个或多个消息及最终调用状态。普通单次调用、客户端流式、服务器流式和双向流式,区别在双方消息数量与发送时机,不是四种不同HTTP/2帧格式。

本节依据gRPC官方HTTP/2协议文档,固定提交为 0bce44dbb4063c31af3b159b6b2c1719ffdbf074(2026-10-15)。它描述gRPC线上约定,不作为某一运行库所有默认行为的实测报告。固定文档。

请求逻辑字段包含POST、方案、服务方法路径、相应authority、content-type与 te: trailers。路径通常为 /服务名/方法名,例如本书虚构教学服务的 /book.Example/Send;这不是声称互联网上存在该服务。content-type: application/grpc+proto表示采用相应gRPC内容约定与Protobuf消息表示。线上字段仍需HPACK编码,不是直接发送这些可读行。

每条gRPC消息在DATA承载内有五字节包络:

范围字段含义
[0,1)Compressed-Flag00未压缩,01使用约定消息压缩
[1,5)Message-Length四字节无符号大端,只计后续消息字节
[5,5+L)Message长度L的序列化消息

压缩标志不是算法编号。为1时,算法由grpc-encoding等协商信息说明,长度数的是压缩后本条消息的表示;缺少相应消息编码字段时标志必须为0。压缩上下文不在消息之间接管,每条消息按约定独立处理,不能套用前章WebSocket的跨消息历史。

消息本身怎样序列化是下一层规则。Protobuf是常见选择,不等于gRPC五字节包络本身。为把内外区别展示清楚,定义一条教学消息:字段号1、长度限定类型,值为字符串Hello。Protobuf标签把字段号与线类型组合,1×8+2=10=0a;后面一字节05声明字符串长五,再跟Hello,所以完整序列化消息为:

text
0a 05 48 65 6c 6c 6f

它有七字节。放进未压缩gRPC包络后为:

text
00 00 00 00 07 0a 05 48 65 6c 6c 6f

共十二字节。前面的四字节长度属于gRPC,Protobuf里的长度属于字段,两者字节序与计数对象不能混淆:本例Protobuf的小长度用变长整数表达,外层gRPC固定四字节大端。Protobuf格式依据 官方编码指南,不把虚构字段模式冒充真实代理服务定义。

这十二字节可以装在一个DATA帧,也可以跨帧。例如前两字节先在一个DATA里到达,余下十字节在另一个DATA里到达,消息解码器必须保留尚未完整的四字节长度状态。反过来,一个DATA也可含多条gRPC消息。HTTP/2帧长、gRPC消息长和Protobuf字段长分别定界不同对象。

响应还需要最终状态。HTTP层通常返回 :status: 200 和gRPC内容类型,再发送消息;最后用携带 grpc-status 的尾部HEADERS结束流。grpc-status: 0表示调用成功,非零表示gRPC层失败。立即失败时允许Trailers-Only,把所需响应信息和最终状态放在结束字段块中。即使成功也必须有最终状态,不能把HTTP200或收到某个DATA当作完整RPC成功。

text
请求方向:HEADERS → DATA内的若干长度前缀消息 → END_STREAM
响应方向:HEADERS → DATA内的若干长度前缀消息 → 尾部HEADERS及END_STREAM

这些是逻辑顺序,字段块需要时仍可按HTTP/2规则延续;消息数和各方向发送时机取决于调用类型。如果HTTP连接中途断开、缺少正常尾部,客户端必须按运行库约定向应用报告异常,不能因为已收到一部分消息就擅自补成功。代理或网关若错误地丢弃尾部,也会破坏调用完成语义。

gRPC元数据中的 -bin 字段值需要Base64表示,但DATA里的序列化消息并不因此也要整体Base64。区分字段编码与消息编码,可以避免把二进制数据重复转为文本,造成不必要开销。

用于代理时,双向流式调用可以交换一串携带字节的应用消息;消息模式、服务路径与流适配由具体代理实现定义。gRPC本身不认识VLESS请求,也不凭借POST、HTTP/2或Protobuf自动增加代理身份验证。其价值与成本应沿“调用→消息→DATA→连接”逐层解释。

11.7 把相同业务字节放进不同承载 ​

比较承载开销时,应该先选定相同的业务内容,再说明哪些控制与封装已计入。以下把三字节 abc分别放入几种已经讲过的结构,假设连接和所需会话已经建立、没有压缩、填充或其它可选字段,不计TLS、TCP/IP和链路开销。

承载模型业务之外的持续封装本例总长
直接字节流本次无额外记录头3
客户端WebSocket,单个二进制帧基础头2+键49
服务器WebSocket,单帧基础头25
HTTP/2,放入一个DATA帧头912
gRPC,用字段1的Protobuf bytes包装,再放入一个DATAProtobuf标签与小长度2+gRPC前缀5+帧头919

最后一项的Protobuf消息为 0a 03 61 62 63,gRPC包络为 00 00 00 00 05 0a 03 61 62 63,共十字节,DATA再加九字节头。这个模式是教学定义,不代表每个gRPC服务都采用相同字段包装。若使用另一序列化,消息长度和开销会变化。

表格也没有把“每个应用消息一个DATA”写成协议要求。若连续两条同样大小的gRPC消息合在一个DATA,DATA负载二十字节,总长二十九,而不是两个独立DATA的三十八。若一条消息跨两个DATA,则多一个九字节帧头。外层帧的分批策略会改变持续开销,内层消息边界仍由五字节前缀定义。

HTTP/2的HEADERS、HPACK状态、SETTINGS、窗口更新与gRPC尾部字段,都没有出现在这一条DATA的数字里;WebSocket的GET升级和关闭控制也未计入。所以十九与九的比较只能说明本次构造的负载编码,不能据此断言完整gRPC调用总共十九字节,或WebSocket在任何连接寿命下都更省资源。

服务能力同样需要对齐。WebSocket提供文本或二进制消息与控制帧,HTTP/2提供HTTP流和双层窗口,gRPC提供调用消息及最终状态。直接TCP没有自动的应用消息定界。对一个需要多调用、多路元数据和明确RPC状态的应用,多出的结构可能就是所需能力,而非可以无损删掉的冗余。

这些能力也不自动成为代理权限。HTTP/2服务器理解POST,不等于它会把DATA当作可连接任意目标的请求;gRPC服务器需要存在相应服务和消息定义;WebSocket端点需要有相应流适配。网络入口是否接受某个承载,与它是否理解内层代理,是两次独立匹配。

对长期大流,帧头通常能按较大负载摊薄,但每层仍有缓冲、状态与处理路径。HTTP/2可以把多个调用放在共享连接中,WebSocket也可以由上层另行设计多路复用;若后者增加流编号、窗口和调度,就要把新增协议一起计算,而不是只拿基础WebSocket头长作比较。

因此合理的选型分析分两步:先确定所需服务与中间设备约束,再在同等工作负载、连接复用和安全层条件下比较成本。协议名提供线索,逐层布局和状态才给出可验证的差异。

11.8 流级隧道与错误作用域:连接共享不意味着命运完全相同 ​

HTTP/2的CONNECT保持“建立到某端点的隧道”这一服务语义,却把隧道映射到一个流,而非接管整条HTTP/2连接。传统CONNECT请求的逻辑伪字段只有方法CONNECT与目标authority等适用信息,不带普通请求的 :scheme 和 :path。代理返回该流的2xx响应以后,隧道字节由双方的DATA承载;其它流仍可继续普通HTTP或其它隧道。RFC 9113 §8.5。

因此一条连接可以有流1通向网站A,流3通向网站B。网站A的数据不会因流1尚未结束,就成为网站B的正文;流编号维持归属。每个隧道方向仍受对应流窗口与共享连接窗口约束,END_STREAM可以表达该方向结束,错误则按流级或连接级规则传播。代理要先转送已接受数据,再按相应结束语义处理下游,不能把单流结束误写成整条TCP关闭。

WebSocket over HTTP/2还需要RFC 8441的扩展CONNECT,不是把HTTP/1.1的GET与101复制到一个字段块。服务器先通过SETTINGS_ENABLE_CONNECT_PROTOCOL=1表明支持;客户端才可在新流中使用 :protocol: websocket,并提供适用的scheme、path和authority。这个authority定位WebSocket服务,不能像传统CONNECT那样无条件解释为要拨出的任意TCP目标。

扩展路径不使用Connection、Upgrade或101,也不执行Sec-WebSocket-Key/Accept的旧握手计算;适用的Origin、版本、子协议与扩展等信息仍按规范处理。成功后,WebSocket帧在这个HTTP/2流提供的通道中交流,角色掩码等帧规则并未取消。中间节点只有支持相应映射,才可正确连接两边会话。RFC 8441 §3—5。

独立流还提供局部取消。RST_STREAM类型03携带四字节错误码,作用于非零流编号。例如在合法状态下取消流3,可有:

text
00 00 04 03 00 00 00 00 03 00 00 00 08

负载 00 00 00 08 是CANCEL,总长十三字节。它说明流3被取消,不证明应用尚未执行任何业务,也不必关闭健康的其它流。传输取消、RPC错误和业务撤销并非一回事:服务端可能已经提交结果,只是客户端不再等待。

有些状态却属于整个连接。HPACK错误可能使后续所有字段索引失去共同解释;连接级协议错误、底层TCP失效和共享安全会话失败也会影响多个流。此时只重置最先看见异常的一条流,不能修复全局状态。错误作用域应由规范和实际损坏对象确定,而不是总想尽量保持连接。

GOAWAY用于告知对方连接不再接受相应的新工作,并报告Last-Stream-ID和错误信息。这个编号表示发送者可能已经处理的最后一条由对端发起的流范围,不是“最后成功完成的请求”。高于边界的相应流未被该端处理,可以据规范考虑重试;低于或等于边界的工作可能已执行,不能从GOAWAY推断它们全都未生效。

有序停机可以让既有较早流继续完成,同时把新请求转到另一条连接;错误关闭也可能无法等它们完成。gRPC或代理连接池因此需要同时管理连接是否接收新流、现有流是否仍有结果、以及哪些调用具备安全重试条件。简单地“换一个连接重发所有DATA”既破坏流状态,也可能重复业务操作。

HTTP/2的共享收益来自减少连接、复用压缩状态与调度多个流;它的共享成本来自TCP交付、连接窗口、压缩上下文和连接故障。准确讨论多路复用,需要同时保留局部流状态与全局连接状态,既不把所有流视为完全独立TCP,也不把一次流取消视为全部工作都应失败。

本章的二进制承载以 RFC 9113 为准,字段压缩以 RFC 7541 为准,WebSocket映射另据RFC 8441,gRPC据11.6固定的官方文档。下一章将把流带到QUIC传输层,进一步分析丢包恢复、流量控制、路径变化与HTTP/3映射。

从字节与状态机出发,理解每一层的职责。