HTTP/1.1 在一条连接上同一时刻只能承载一个请求—响应对。为了缓解这一点,客户端会并行打开多条连接,但连接数量受限于域名并发上限,且每条连接都要独立完成握手与拥塞控制状态的维护。HTTP/2 的做法是在一条连接上引入二进制帧层,把若干请求与响应交错放在同一条连接里传输。
这套设计包含两层含义:协议从文本转为二进制分帧,而方法、状态码、头部等语义层保持不变;连接成为一条复用的通道,其上的并发由「流」这一抽象来管理。
帧与流
HTTP/2 的数据单位是帧,每个帧都带有一个头部,标明它属于哪条流、属于哪种类型以及标志位。所有帧在连接上按顺序传输,接收端按流号把帧重新组装成消息。
HEADERS stream=1 :method=GET :path=/index.html
DATA stream=1 <html 前若干字节>
HEADERS stream=3 :method=GET :path=/app.css
DATA stream=3 <样式表字节>
DATA stream=1 <html 后续字节>
常用的帧类型包括:
| 帧类型 | 作用 |
|---|---|
HEADERS | 携带头部字段,过长时后接 CONTINUATION |
DATA | 携带消息体字节 |
SETTINGS | 协商连接参数,如初始窗口、帧长上限 |
WINDOW_UPDATE | 通告可接收的字节数,用于流量控制 |
RST_STREAM | 终止某条流 |
PING | 探测连接存活并测量往返时延 |
GOAWAY | 关闭连接并告知已处理到的流号 |
流是连接内的逻辑通道,具有状态机:空闲、打开、半关闭、关闭。流号由发起方分配,客户端使用奇数号,服务端使用偶数号,且新流的编号必须递增。递增规则让接收端可以判定:编号小于某个值却从未见过的流,就是被跳过的流,从而支持流的复用与连接的优雅关闭。
HPACK 头部压缩
HTTP/1.1 的头部是重复的纯文本,同一连接上的多个请求往往携带大量相同字段。HTTP/2 用 HPACK 压缩头部,思路是把头部字段编码为「索引加值」,并维护两张表:
- 静态表:协议预定义的常见字段与取值组合,如
:method: GET、:status: 200,条目固定且双方均已知晓。 - 动态表:连接双方在通信过程中共同维护的表,新出现的字段被追加到表中,后续用索引引用。
:method: GET
:path: /assets/app.js
user-agent: <较长字符串,首次出现时按字面量传输>
首次出现的长字段以字面量编码传输,并可选择是否加入动态表;再次出现时只需发送一个索引,占用字节数明显下降。字段值还可以用霍夫曼编码进一步压缩,静态霍夫曼表按字符出现频率设计,常见字符的编码更短。
动态表由连接双方同步更新,其大小通过 SETTINGS 中的参数协商,超出上限的条目会被逐出。因为表的更新顺序必须一致,头部的编码与解码对连接状态存在依赖。
流量控制
HTTP/2 提供两级流量控制:
- 连接级窗口:约束整条连接上尚未被确认的
DATA字节总量。 - 流级窗口:约束单条流上尚未被确认的
DATA字节总量。
发送端每发出一个 DATA 帧就扣减对应窗口,接收端处理完成后以 WINDOW_UPDATE 归还。只有 DATA 帧受流量控制,HEADERS 等其它帧不受影响,因此即使某条流被窗口阻塞,其它流的控制帧仍可通行。
需要留意的是,流量控制解决的是接收端的缓冲压力,而不是传输路径上的拥塞。TCP 层的拥塞窗口仍独立生效:同一条连接上所有流共享同一个 TCP 窗口,因此丢包导致的拥塞窗口收缩会同时影响全部流。这就是「应用层已无队头阻塞,但传输层仍存在队头阻塞」的含义。
服务端推送
服务端推送允许服务器在收到请求后,主动向客户端发送它预判会需要的资源。实现方式是通过 PUSH_PROMISE 帧告知客户端「接下来会有一条流承载某个资源」,随后在该流上发送响应。
推送的流数由客户端通过 SETTINGS 限制。由于服务器无法确知客户端缓存中已有哪些内容,推送可能产生重复传输,因此实际部署中需要配合缓存摘要等手段,或者直接停用该特性。就缓存复用而言,它与 HTTP/1.1 时代的 HTTP 缓存 规则并无冲突。
连接建立与 ALPN
HTTP/2 的协商在 TLS 握手中完成。客户端在 ClientHello 的 ALPN 扩展里列出支持的协议名,服务端在 ServerHello 中选定一个:
openssl s_client -connect example.com:443 -alpn h2,http/1.1 </dev/null
若协商结果为 h2,则后续的 TLS 记录承载 HTTP/2 帧;若为 http/1.1,则回退到文本协议。明文场景下也可以通过 Upgrade 头部字段从 HTTP/1.1 升级,但常规部署以 ALPN 为准。这一协商细节与 TLS 1.3 握手流程 的扩展处理方式相关。
与 HTTP/1.1 管道化的差异
两者的共同点是都在一条连接上承载多个请求,差别在于处理方式。管道化要求响应按请求顺序返回,前一个响应未完成时,后续响应只能排队等待,因此一个慢响应会阻塞整条连接。HTTP/2 的响应可以按流交错返回,慢响应不再挡住其它流的响应。
代价是复杂度上升:需要帧解析、流状态机、头部压缩表与窗口维护。这些状态在连接两端必须保持一致,任何一方出错都可能导致连接被 GOAWAY 关闭。
小结
HTTP/2 在 TCP 之上叠加了一层二进制帧,用流把并发请求组织到同一条连接中,用 HPACK 压缩重复头部,用两级窗口管理缓冲压力。它消除了应用层的队头阻塞,但传输层的拥塞控制与丢包恢复仍以连接为单位进行,这一点在后续基于 UDP 的传输方案中被继续处理。