HTTP/2 解析:一场协议层的并行革命
HTTP/2 解析:一场协议层的并行革命
HTTP/2 没有改变 HTTP 的语义,却彻底改变了数据在网络上流动的方式。本文梳理它的前世今生,适合想深入理解 Web 协议演进逻辑的工程师。
HTTP/1.1 统治了 Web 十五年以上。在此期间,Web 从纯文本页面演变为富媒体的动态应用。2008 年后智能手机全面普及,移动端访问量爆发。Google Maps、YouTube、Facebook 这类应用对页面加载速度的要求远非当年可比——一个典型页面的请求数从最初的两三个增长到几十甚至上百个。
2009 年,Google 内部发布了 SPDY 协议。核心思路与后来的 HTTP/2 如出一辙:多路复用、头部压缩、服务端推送。Google 在 Chrome 和自家服务器上部署,在生产环境中验证了这些方向——数据显示页面加载时间缩短了 27% 至 60%。
SPDY 的成功引起了 IETF 的关注。2012 年,IETF 成立 HTTP/2 工作组,2015 年 5 月 RFC 7540 正式发布,HTTP/2 成为官方 Internet 标准。同年 Google 宣布弃用 SPDY,全面转向 HTTP/2。长达二十年的 HTTP/1.x 时代由此开始落幕。
二、HTTP/1.x 的三大痛点
HTTP/1.1 诞生于 1999 年,用二十年前的设计来支撑今天的 Web,付出了三笔代价。
2.1 队头阻塞:串行请求的代价
HTTP/1.1 引入了持久连接(Connection: keep-alive),一个 TCP 连接可以复用,不用每次请求都重建。这解决了部分问题,但没有解决根本——HTTP 的请求-响应模型始终是串行的。
持久连接上,客户端必须等上一个请求的响应回来,才能发下一个。如果第一个请求卡住,后面全部排队等待。这就是 队头阻塞(Head-of-Line Blocking)。
场景很常见:一个页面包含 HTML + CSS + JS + 多张图片,CSS 请求被后端阻塞,图片和 HTML 本无依赖,但也得等 CSS 响应返回后才能发出。
2.2 头部冗余:每次请求都在重复劳动
HTTP 是无状态协议,每个请求必须携带完整头部:Host、User-Agent、Accept、Accept-Language、Accept-Encoding、Cookie……其中 Cookie 在登录态下往往很大,而 Host、User-Agent、Accept 这类字段在同一次会话中几乎从不变化。
50 个请求的页面,每个请求头部约 500 字节,光头部就浪费约 25KB。在移动网络下,这是可感知的延迟。
2.3 并发上限:域名分片的无奈
浏览器厂商通过多 TCP 连接来对抗单连接的串行限制。早期每个域名限制 2 个并发连接,后来逐步放宽到 6-8 个,但远不够用——一个中等复杂度页面有 30-80 个资源请求是常态。
于是工程师发明了域名分片(Domain Sharding):将静态资源分布到 img1.example.com、img2.example.com 等多个子域名,绕过并发连接数限制。代价是额外的 DNS 解析开销、TCP 连接无法复用、服务器负载增加。这是在用副作用打补丁。
小结
| 问题 | 根因 | 影响 |
|---|---|---|
| 队头阻塞 | HTTP 串行请求模型 | 延迟累加,高延迟 |
| 头部冗余 | 无状态协议每请求重复发送 | 浪费带宽 |
| 并发上限 | 单连接有限,浏览器限制 | 域名分片,副作用多 |
这三个问题的根因都在协议层,是设计时没有预料到的需求。HTTP/2 的任务,就是在协议层面对症下药。
三、HTTP/2 的核心改进
HTTP/2 的设计目标很明确:保持 HTTP 语义不变,重新定义数据在网络上的传输方式。 请求方法、状态码、URI 结构一切如旧,但数据流动的方式彻底变了。三项改进,对应三个历史痛点。
3.1 二进制分帧:使能层
HTTP/1.x 是文本协议,请求和响应都是人类可读的纯文本。这在调试时方便,但解析起来麻烦:需要处理空格差异、行尾差异(Unix \n、Windows \r\n),还容易产生安全漏洞(如 HTTP 响应走私)。
HTTP/2 引入二进制分帧层(Binary Framing Layer):所有消息在传输前被拆成更小的帧(Frame),每帧固定 9 字节头部(Length、Type、Flags、Stream Identifier),后面跟载荷。二进制格式带来两个关键改变:
- • 解析确定:逐字节读取,O(n) 复杂度,不会因输入格式不同而出现歧义
- • 为多路复用铺路:帧可以打散、交叉、组装,突破了文本协议无法并行的结构限制
二进制分帧本身不直接提升性能,但它是一切改进的底层基础——没有它,多路复用无从实现。
3.2 多路复用:一个连接,承载一切
HTTP/2 在一条 TCP 连接上引入 Stream(流) 的概念,实现了真正的多路复用。
机制如下:
- • 客户端和服务端可以同时打开多个 Stream,各自拥有唯一 ID(客户端发起的为奇数,服务端发起的为偶数)
- • 每个 Stream 内的请求-响应逻辑与 HTTP/1.1 一致,但被拆成多个 Frame
- • 不同 Stream 的 Frame 可以交叉传输,互不阻塞
这直接解决了队头阻塞:服务端在处理 Stream 1 的大文件时,随时可以插入 Stream 3 和 Stream 5 的小帧发出去。
假设三个资源耗时分别为 T1、T2、T3:
HTTP/1.1 串行:第二个请求必须等第一个响应完全返回后才能发出。

HTTP/2 并行:三个请求同时发出,响应各自独立返回,总耗时取决于最长的那个。

差异一目了然:HTTP/1.1 总耗时是各请求耗时的累加,HTTP/2 总耗时是各 Stream 耗时的最大值。在生产环境中,TLS 握手成本也被所有 Stream 共享,实际收益更显著。
3.3 头部压缩
HTTP/1.x 每个请求都带完整头部,是效率黑洞。HTTP/2 引入 HPACK(RFC 7541),压缩率通常达 60%-90%。
设计哲学很简洁:静态表 + 动态表 + 霍夫曼编码。
- • 静态表:61 个最常见的(头部名,值)组合在任何连接中都固定,编码只需传索引号(1 字节)
- • 动态表:会话中出现过的头部值会被记录,后续请求复用索引
- • 霍夫曼编码:对无法引用的字符串值,再做一层霍夫曼压缩
HPACK 还限制了动态表大小上限,防止恶意服务端撑大客户端内存。
三项改进对照
| 改进 | 解决的问题类型 | 解决方式 |
|---|---|---|
| 二进制分帧 | HTTP/1.x 文本协议局限 | 帧结构使能多路复用 |
| 多路复用 | HTTP/1.x 队头阻塞 + 并发上限 | 单一 TCP 连接多 Stream 并行 |
| HPACK 头部压缩 | HTTP/1.x 头部冗余 | 静态表 + 动态表 + 霍夫曼 |
四、核心概念:Stream、Frame、Message
理解 HTTP/2,只需要搞懂这三层:Message、Frame、Stream。它们各在其位,共同撑起了整个协议。
4.1 Message:应用层的 HTTP 语义
Message 是一次完整的请求或响应。请求 Message 包含请求行 + 头部 + 可选消息体;响应 Message 包含状态行 + 头部 + 可选消息体。
HTTP/2 没有改变这些语义,GET 还是 GET,200 还是 200。但 Message 传输时不再以文本流发出,而是被拆成 Frame。
4.2 Frame:传输层的最小单元
Frame 是 HTTP/2 在传输层的最小单元。定义了 10 种帧类型,所有帧共享固定 9 字节头部:
+-----------------------------------------------+
| Length (24 bits) | ← 载荷长度(0-16383字节)
+---------------+---------------+---------------+
| Type (8) | Flags (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31 bits) |
+-+---------------------------------------------------------------+
接收端读取 Length 字段即可精确划分帧边界,不存在粘包问题。
Message 映射到 Frame 的方式:
请求/响应 Message
│
├─ HEADERS 帧(1 个或多个,END_HEADERS flag 标记结束)
│ └─ 承载:请求行/状态行 + 全部头部
│
└─ DATA 帧(0 个或多个,END_STREAM flag 标记结束)
└─ 承载:消息体
GET 请求(无消息体)
Stream N → HEADERS 帧(END_HEADERS + END_STREAM)
POST 请求(有消息体)
Stream N → HEADERS 帧(END_HEADERS)
→ DATA 帧(END_STREAM)
一个 Message 跨多个帧,一个帧只属于一个 Stream。
常用帧类型:
| 帧类型 | 值 | 作用 |
|---|---|---|
| DATA | 0x0 | 传输消息载荷 |
| HEADERS | 0x1 | 传输头部,可带优先级 |
| SETTINGS | 0x4 | 连接参数协商 |
| WINDOW_UPDATE | 0x8 | 流控窗口更新 |
| RST_STREAM | 0x3 | 异常终止 Stream |
4.3 Stream:逻辑上的双向会话
Stream 是在单个 HTTP/2 连接内,客户端和服务端之间的独立双向消息序列。
关键属性:
- • Stream ID:全局唯一标识符,用于区分不同 Stream
- • 状态机:Idle → Open → Half-Closed → Closed
- • 优先级:权重 1-256,权重越大,服务端调度越优先。视频流配高权重(200),图片预加载配低权重(64),避免争抢主内容带宽。还支持声明 Stream 之间的依赖关系(如"Stream 5 依赖 Stream 3")
- • 双向性:客户端和服务端都可以发 DATA 帧,一方关闭后另一方仍可继续单向传输
Stream ID 奇偶约定是协议强制约束——客户端发起的为奇数(1, 3, 5…),服务端发起的为偶数(2, 4, 6…),ID=0 保留用于连接控制帧。违反奇偶约定会导致对端发送 GOAWAY 关闭连接。
4.4 三者关系

Message 保持 HTTP 语义不变,Frame 是传输层的最小单元,TCP 连接负责可靠传输并承载多个 Stream 的帧交错。多路复用的本质就是:帧是传输层最小单元,多个帧组成消息,消息在 Stream 上有序传输。
五、HPACK 头部压缩详解
HPACK(RFC 7541)把每个请求数百字节的头部开销,压缩到几十字节。
5.1 为什么不用通用压缩算法
gzip、DEFLATE 这类通用压缩算法有一个致命缺陷:CRIME 攻击。攻击者通过观察压缩后密文的大小变化,可以逐步推断出原始内容中是否包含特定字符串,进而猜出 Cookie 等敏感信息。SPDY 早期用 DEFLATE 就受过这个攻击。
HPACK 的设计在原理上绕开了这个问题:它只压缩头部名称和值的字面量,不动态发现上下文模式。攻击者无法通过压缩输出的大小变化来推断其他请求的内容。
5.2 静态表:常见头部的索引
静态表定义了 61 个预定义的(名称,值)组合,编码时只需传索引号(1 字节)。
常见条目:
| 索引 | 名称 | 值 |
|---|---|---|
| 1 | :method | GET |
| 2 | :method | POST |
| 3 | :scheme | http |
| 4 | :scheme | https |
| 5 | :host | (空) |
| 6-7 | :path | /index.html, / |
| 8 | :status | 200 |
| 12 | :status | 404 |
| 33 | content-type | application/json |
| 44 | user-agent | (空) |
完全匹配静态表的头部,编码只需 1 字节:0b1xxxxxxx(最高位为 1 表示引用静态表索引)。
5.3 动态表:会话内的历史复用
动态表在会话进行中逐步构建。同一次会话中反复出现的头部(如 Cookie),第一次完整发送后加入动态表,后续请求直接用索引引用。
动态表容量由 SETTINGS_HEADER_TABLE_SIZE 控制,默认上限 4KB(RFC 7541 规定最大 64KB)。双方可协商调整,接收方也可以随时把 size 设为 0 来清空动态表,作为防御动态表耗尽攻击的手段。
5.4 霍夫曼编码:字符串级别的再压缩
无法通过索引表达的值,再用霍夫曼编码压一道。HTTP/2 定义的霍夫曼编码表针对头部字符分布优化,字母和符号出现频率高,编码更短。
最终效果:500 字节的典型请求头,压缩后通常只有 50-80 字节,压缩率 80%-90%。
5.5 编码示例
请求头部:
:method: GET
:path: /index.html
:scheme: https
user-agent: Mozilla/5.0
cookie: session=abc123
HPACK 编码:
:method 静态表索引 1 → 0x81(1 字节,二进制 1000 0001)
:path 静态表索引 6 → 0x86(1 字节,二进制 1000 0110)
:scheme 静态表索引 4 → 0x84(1 字节,二进制 1000 0100)
user-agent 霍夫曼编码 → ~15 字节(明文约 50+ 字节)
cookie 霍夫曼编码 + 动态表引用 → 动态变化
5.6 安全性
HPACK 的攻击面是动态表耗尽攻击:恶意服务端发送大量唯一的头部值,撑大客户端动态表来消耗内存。防御手段是接收方随时通过 SETTINGS 帧缩小动态表,甚至清零。
六、实战:启用 HTTP/2
6.1 服务端配置
现代 Web 服务器均已原生支持 HTTP/2,无需额外模块。
Nginx(1.25.x+)
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
root /var/www/html;
}
注意:http2 必须和 ssl 同时使用,Nginx 标准构建不支持非加密的 h2c。
Apache(2.4.17+)
<VirtualHost *:443>
ServerName example.com
SSLEngine on
SSLCertificateFile /path/to/cert.pem
SSLCertificateKeyFile /path/to/key.pem
Protocols h2 http/1.1
</VirtualHost>
6.2 验证
Chrome DevTools → Network 面板 → 右键列头 → 勾选"Protocol",显示 h2 即为 HTTP/2,http/1.1 则未命中。也可使用在线工具 HTTP/2 Test。
6.3 服务端推送
Nginx 从 1.13.9 开始支持服务端推送,通过 http2_push_preload 指令,在响应头中声明要推送的资源:
add_header Link "</style.css>; rel=preload; as=style" always;
add_header Link "</app.js>; rel=preload; as=script" always;
Nginx 自动推送,客户端无需额外操作。
6.4 降级与 Alt-Svc
服务器通常同时启用 HTTP/1.1 和 HTTP/2,由 TLS 握手阶段的 ALPN 协商决定用哪个版本,老客户端自动回退到 HTTP/1.1。
如想让客户端下次直接使用 HTTP/2,可在响应头加入 Alt-Svc(RFC 7838):
add_header Alt-Svc 'h2=":443"' always;
Header set Alt-Svc "h2=\":443\""
h2 是协议名,":443" 是端口号。客户端收到后,下次访问直接建 HTTP/2 连接,跳过协商。
6.5 常见问题
启用后性能反而下降? 通常是代理层(如 Nginx 前置的 Varnish 或 HAProxy)不支持 HTTP/2,导致中间节点做协议转换,把收益吃掉了。确保链路全程都支持 HTTP/2。
如何确认是否命中了 HTTP/2? Chrome DevTools Network 面板右键 → Protocol 列,HTTP/2 显示 h2,HTTP/1.1 显示 http/1.1。
七、HTTP/2 的局限
HTTP/2 解决了 HTTP/1.x 的核心痛点,但它不是银弹,自身也带来了新的问题。
7.1 TCP 层面的线头阻塞依然存在
这是 HTTP/2 最大的遗憾。HTTP/2 在应用层解决了 HTTP 的队头阻塞,但 TCP 层面的线头阻塞(HOL Blocking) 还在。
TCP 要求数据按序交付——如果 Stream 1 的某个包丢了,TCP 会暂停所有后续数据交付,直到丢包重传成功。哪怕 Stream 3 和 Stream 5 的数据已全部就绪,也得等 Stream 1 的包补上。这与 HTTP 无关,HTTP/2 无力绕过。
7.2 TCP + TLS 握手延迟依然存在
HTTP/2 减少了连接数(多路复用),但 TCP + TLS 握手的那一次 RTT 不可忽略。跨国长距离网络下,这是几十到上百毫秒的固定开销。
7.3 多路复用带来的新问题
单一 TCP 连接承载所有请求,在某些场景下反而不如多个 HTTP/1.1 连接:
- • 丢包影响范围更大:一个连接的丢包影响所有 Stream;多连接时,一条连接受损不影响其他
- • 拥塞窗口竞争:所有 Stream 共享同一个拥塞窗口,一个大文件 Stream 会抢占其他小 Stream 的带宽
- • 中间设备兼容性问题:部分老旧代理、负载均衡器、CDN 对 HTTP/2 多路复用支持不完善
7.4 无法依赖 HTTP/2 做性能规划
链路上任何一个节点不理解 HTTP/2,连接就回退到 HTTP/1.1。在公网环境下,不能假设 HTTP/2 一定生效。
八、HTTP/3:彻底解决线头阻塞
HTTP/2 的局限催生了下一代协议。2018 年 IETF 成立 QUIC 工作组,2022 年 HTTP/3(RFC 9114)正式发布。
8.1 QUIC:UDP 之上的可靠传输
HTTP/3 的核心变化是将传输层从 TCP 切换到 QUIC。QUIC 由 Google 在 2013 年提出,基于 UDP,被视为 TCP 的现代替代品。
HTTP/1.1: TCP + TLS + HTTP/1.1
HTTP/2: TCP + TLS + HTTP/2(多路复用)
HTTP/3: UDP + QUIC + HTTP/3(多路复用)
QUIC 的核心设计:
- • UDP 传输:避免 TCP 三次握手和 TLS 握手分离,0-RTT 或 1-RTT 建立连接
- • 独立 Stream:每个 HTTP/3 Stream 有独立的丢包检测和重传,不受其他 Stream 影响——彻底消灭了 TCP 层面的线头阻塞
- • 连接迁移:连接通过 Connection ID 标识,WiFi 切到 4G 时无需重建连接,用户无感知
8.2 实际收益
- • 高丢包率网络(移动网络、跨国网络)下,HTTP/3 相比 HTTP/2 延迟降低 30%-50%
- • 恢复会话时 0-RTT 握手,完全零延迟
- • 网络切换时连接不断开
8.3 落地情况
Chrome、Firefox、Edge、Safari 均已支持 HTTP/3。Cloudflare、Fastly 等主流 CDN 已提供服务。nginx 从 1.25.0 开始支持 QUIC(listen 443 ssl quic)。截至 2025 年,Google 超过 30% 的流量已通过 HTTP/3 传输。
8.4 挑战
- • UDP 防火墙:部分企业网络、防火墙阻止 UDP 443 端口流量,HTTP/3 必须有 HTTP/2 回退机制
- • CPU 开销:QUIC 在用户态实现,高于内核实现的 TCP
- • 调试复杂:UDP 抓包比 TCP 复杂,Wireshark 对 QUIC 的分析支持还不够完善
8.5 演进路线
HTTP/0.9 (1991) → HTTP/1.0 (1996) → HTTP/1.1 (1999) → HTTP/2 (2015) → HTTP/3 (2022)
每一次升级都是对前一代核心问题的直接回应。理解每一代的"为什么",才能理解整个 Web 基础设施的演进逻辑。
九、总结
HTTP/2 没有改变 HTTP 的语义,却在传输机制上做了根本性重新设计:从文本到二进制,从串行到并行,从冗余头部到智能压缩。
核心要点:
- • 三大痛点:队头阻塞(串行请求)、头部冗余(每请求重复)、并发上限(浏览器限制),根因都在协议层
- • 三项改进:二进制分帧(使能层)+ 多路复用(核心)+ HPACK(效率)
- • 三层模型:Message 保持语义,Frame 是传输最小单元,Stream 是逻辑双向会话
- • HPACK:静态表 + 动态表 + 霍夫曼,三者缺一不可
- • HTTP/2 局限:TCP 层面线头阻塞依然存在,这是它被 HTTP/3 取代的根本原因
- • HTTP/3:QUIC 替换 TCP,独立 Stream 彻底解决线头阻塞,连接迁移是意外惊喜
HTTP/2 不是终点,而是演进链上的一环。站在 HTTP/3 的视角回望,才能更清楚地看到整个协议演进的内在逻辑。
文章首发与小小寰宇

浙公网安备 33010602011771号