主题
HTTP/2 & HTTP/3
HTTP 协议演进
HTTP/1.0 (1996) HTTP/1.1 (1997) HTTP/2 (2015) HTTP/3 (2022)
───────────── ───────────── ────────── ──────────
每个请求一个连接 Keep-Alive 复用 多路复用 QUIC (UDP)
无 Host 头 管线化(pipelining) 二进制分帧 无队头阻塞
无缓存控制 chunked 传输 头部压缩(HPACK) 0-RTT 连接
Cache-Control Server Push 连接迁移
队头阻塞 流优先级 内建 TLS 1.3HTTP/1.1 的核心问题
队头阻塞(Head-of-Line Blocking)
HTTP/1.1 在同一个 TCP 连接上必须串行处理请求——前一个响应完成后才能发送下一个:
HTTP/1.1 单连接:
请求1 ──→ 响应1(200ms)
请求2 ──→ 响应2(50ms)
请求3 ──→ 响应3(100ms)
总时间:200 + 50 + 100 = 350ms
如果响应1很慢(500ms),所有后续请求都被阻塞:
请求1 ──→ ............响应1(500ms)
请求2 ──→ 响应2
请求3 ──→ 响应3浏览器的应对策略
Chrome 对同一域名最多开 6 个 TCP 连接:
连接1: 请求1 ──→ 响应1 请求7 ──→ 响应7
连接2: 请求2 ──→ 响应2 请求8 ──→ 响应8
连接3: 请求3 ──→ 响应3
连接4: 请求4 ──→ 响应4
连接5: 请求5 ──→ 响应5
连接6: 请求6 ──→ 响应6
问题:
- 6 个连接仍然不够(一个页面可能有 50+ 个资源)
- 每个连接都要 TCP + TLS 握手(延迟高)
- 占用服务器连接资源
HTTP/1.1 时代的优化手段(现在大多已过时):
- 域名分片(cdn1.example.com, cdn2.example.com)
- 资源合并(CSS Sprites, Bundle 所有 JS)
- 内联小资源(Base64 图片, 内联 CSS)头部冗余
HTTP/1.1 的 Header 是纯文本且不压缩,每次请求都重复发送:
GET /api/users HTTP/1.1
Host: api.example.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)...
Accept: application/json, text/plain, */*
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Accept-Encoding: gzip, deflate, br
Cookie: session_id=abc123; _ga=GA1.2.xxx; _gid=GA1.2.yyy; token=eyJhb...
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Connection: keep-alive
→ 每个请求 ~800 字节的 Header
→ 100 个请求 = ~80KB 纯 Header 流量(大部分是重复内容)HTTP/2 核心特性
1. 二进制分帧层(Binary Framing Layer)
HTTP/2 最根本的变化——在应用层(HTTP)和传输层(TCP)之间增加了二进制分帧层:
HTTP/1.1:纯文本协议
GET /index.html HTTP/1.1\r\n
Host: example.com\r\n
\r\n
HTTP/2:二进制帧(Frame)
┌───────────────────────────┐
│ Length (24 bit) │ 帧长度
│ Type (8 bit) │ 帧类型(HEADERS / DATA / ...)
│ Flags (8 bit) │ 标志位
│ Stream ID (31 bit) │ 流标识符
├───────────────────────────┤
│ Payload │ 帧负载
└───────────────────────────┘
帧类型:
HEADERS — 请求/响应头
DATA — 请求/响应体
PRIORITY — 流优先级
RST_STREAM — 取消流
SETTINGS — 连接设置
PUSH_PROMISE — 服务器推送
PING — 心跳
GOAWAY — 关闭连接
WINDOW_UPDATE — 流量控制2. 多路复用(Multiplexing)
单个 TCP 连接上并行传输多个请求和响应——HTTP/2 最重要的特性:
HTTP/1.1(6 个连接,串行):
连接1: [请求A ─────────→][请求D ──→]
连接2: [请求B ──→][请求E ────→]
连接3: [请求C ───────→][请求F ──→]
HTTP/2(1 个连接,并行):
单连接: [A帧][B帧][C帧][A帧][B帧][C帧][D帧][A帧][E帧]...
↑ ↑ ↑
流1-A 流2-B 流3-C 三个请求的数据帧交错传输核心概念:
连接(Connection):一个 TCP 连接
└─ 流(Stream):双向的帧序列,代表一个请求/响应对
└─ 帧(Frame):通信的最小单位
一个连接可以承载多个流(通常上限 100-256 个并发流)
每个流有唯一的 Stream ID
客户端发起的流用奇数 ID(1, 3, 5...)
服务器推送的流用偶数 ID(2, 4, 6...)
帧可以交错发送,接收方根据 Stream ID 重新组装示例:3 个资源并行传输
Stream 1 (index.html) Stream 3 (app.js) Stream 5 (style.css)
──────────────── ────────────── ────────────────
HEADERS 帧 HEADERS 帧 HEADERS 帧
DATA 帧 #1 DATA 帧 #1 DATA 帧 #1
DATA 帧 #2 DATA 帧 #2 DATA 帧 #2
DATA 帧 #3(END) DATA 帧 #3 DATA 帧 #3(END)
DATA 帧 #4(END)
TCP 连接上实际传输顺序:
[S1:H][S3:H][S5:H][S1:D1][S3:D1][S5:D1][S1:D2][S5:D2][S3:D2][S1:D3][S5:D3][S3:D3][S3:D4]3. 头部压缩(HPACK)
HTTP/2 使用 HPACK 算法压缩 Header,大幅减少重复头部的传输:
HPACK 三大策略:
1. 静态表(Static Table):61 个常见 Header 预定义
┌─────┬──────────────────┬──────────────┐
│ 索引 │ Header Name │ Header Value │
├─────┼──────────────────┼──────────────┤
│ 1 │ :authority │ │
│ 2 │ :method │ GET │
│ 3 │ :method │ POST │
│ 4 │ :path │ / │
│ 5 │ :path │ /index.html │
│ ... │ ... │ ... │
│ 61 │ www-authenticate │ │
└─────┴──────────────────┴──────────────┘
例:":method: GET" → 只需发送索引 2(1 字节)
2. 动态表(Dynamic Table):连接级别的 Header 缓存
首次发送:
Cookie: session_id=abc123 → 完整传输 + 加入动态表(索引 62)
后续发送:
Cookie: session_id=abc123 → 发送索引 62(1 字节)
3. Huffman 编码:对字面值进一步压缩
"application/json" → Huffman 编码后更短压缩效果示例:
第一个请求(~800 字节 → ~200 字节):
:method: GET → 索引 2
:path: /api/users → 索引 4 + 字面值 "/api/users"
host: api.example.com → 字面值(加入动态表)
cookie: session=abc → 字面值(加入动态表)
authorization: Bearer → 字面值(加入动态表)
第二个请求(~800 字节 → ~20 字节!):
:method: GET → 索引 2
:path: /api/posts → 索引 4 + 字面值 "/api/posts"
host: → 动态表索引(1 字节)
cookie: → 动态表索引(1 字节)
authorization: → 动态表索引(1 字节)4. 流优先级(Stream Prioritization)
客户端可以告诉服务器哪些资源更重要,让服务器优先发送:
优先级树(HTTP/2 原始方案):
Stream 1 (HTML) — 权重 256
/ \
Stream 3 (CSS) Stream 5 (JS)
权重 256 权重 256
|
Stream 7 (image)
权重 128
含义:
- HTML 最先传输
- CSS 和 JS 平分 HTML 之后的带宽
- 图片在 JS 之后传输,且只获得一半带宽
实际问题:
- 浏览器实现不一致
- 服务器可能忽略优先级
- HTTP/2 的优先级方案过于复杂
HTTP/3 改进(Extensible Priorities - RFC 9218):
使用更简单的 urgency(0-7)+ incremental(是否增量)
priority: u=0 → 最高优先级(HTML/CSS)
priority: u=3 → 中等优先级(JS)
priority: u=6 → 低优先级(图片)5. Server Push(服务器推送)
服务器可以主动推送客户端可能需要的资源:
传统流程:
客户端 → GET /index.html
服务器 → 200 OK (HTML)
客户端 → 解析 HTML,发现需要 style.css
客户端 → GET /style.css
服务器 → 200 OK (CSS)
Server Push 流程:
客户端 → GET /index.html
服务器 → PUSH_PROMISE (style.css) ← 提前推送
服务器 → 200 OK (HTML)
服务器 → 200 OK (style.css) ← 客户端不用再请求
客户端 → 解析 HTML,发现 style.css 已在缓存中 ✅实际现状(2025):
Server Push 已被大部分实践证明收益有限:
- 推送的资源可能客户端已有缓存(浪费带宽)
- 推送时机难以精确控制
- 与 CDN 配合困难
- Chrome 从 106 版本开始已移除 HTTP/2 Push 支持
替代方案:
- 103 Early Hints:服务器在最终响应前先发提示
HTTP/1.1 103 Early Hints
Link: </style.css>; rel=preload; as=style
Link: </app.js>; rel=preload; as=script
→ 浏览器提前开始下载,比 Push 更灵活
- <link rel="preload">:在 HTML 中声明预加载HTTP/2 仍存在的问题
TCP 层的队头阻塞
HTTP/2 解决了应用层的队头阻塞,但**传输层(TCP)**的队头阻塞仍然存在:
HTTP/2 多路复用:
Stream 1: [帧A1][帧A2][帧A3]
Stream 2: [帧B1][帧B2]
Stream 3: [帧C1][帧C2][帧C3]
TCP 传输的实际包序列:
[包1:A1][包2:B1][包3:C1][包4:A2][包5:B2][包6:C2][包7:A3][包8:C3]
如果包 2 丢失:
✅ 包1(A1) 正常
❌ 包2(B1) 丢失 → TCP 必须等待重传
⏸️ 包3(C1) 已收到但不能交付(TCP 保证顺序)
⏸️ 包4(A2) 已收到但不能交付
⏸️ 包5(B2) 已收到但不能交付
... 所有后续包都被阻塞!
TCP 不知道 HTTP/2 的"流"概念
→ 一个包丢失会阻塞所有流
→ 在高丢包率网络(如移动网络)下,HTTP/2 性能可能不如 HTTP/1.1TCP 连接建立延迟
TCP + TLS 1.2:3-RTT
TCP 三次握手(1 RTT)+ TLS 握手(2 RTT)
TCP + TLS 1.3:2-RTT
TCP 三次握手(1 RTT)+ TLS 握手(1 RTT)
在高延迟网络(如卫星、跨洋)下:
RTT = 200ms → 连接建立 = 400-600ms
用户等了半秒还没开始传数据连接迁移问题
TCP 连接由四元组标识:(源IP, 源端口, 目标IP, 目标端口)
场景:手机从 Wi-Fi 切换到 4G
Wi-Fi IP: 192.168.1.100 → 4G IP: 10.0.0.50
源 IP 变了 → TCP 连接断开 → 必须重新建连 + TLS 握手
用户感知:页面短暂无响应 → 重新加载HTTP/3 核心特性
HTTP/3 将传输层从 TCP 切换为 QUIC(Quick UDP Internet Connections):
协议栈对比:
HTTP/2: HTTP/3:
┌──────────┐ ┌──────────┐
│ HTTP/2 │ │ HTTP/3 │
├──────────┤ ├──────────┤
│ TLS │ │ QUIC │ ← QUIC 内建了 TLS 1.3
├──────────┤ │ │ + 可靠传输
│ TCP │ │ │ + 多路复用
├──────────┤ ├──────────┤
│ IP │ │ UDP │ ← 基于 UDP
└──────────┘ ├──────────┤
│ IP │
└──────────┘1. 无队头阻塞的多路复用
QUIC 在传输层原生支持**流(Stream)**的概念——每个流独立可靠传输:
HTTP/2 over TCP(一个包丢失阻塞所有流):
TCP 字节流:[A1][B1][C1][A2][B2][C2]
↑ 丢失
所有后续数据被阻塞
HTTP/3 over QUIC(一个流丢包只影响该流):
QUIC 流 A: [A1] [A2] → A 正常接收
QUIC 流 B: [B1] ❌ → B 等待重传,但不影响 A 和 C
QUIC 流 C: [C1] [C2] → C 正常接收
关键区别:QUIC 的每个流有独立的丢包恢复
→ 一个流丢包不影响其他流
→ 高丢包率网络下优势明显2. 更快的连接建立
TCP + TLS 1.3:2-RTT(TCP 1 RTT + TLS 1 RTT)
QUIC 首次连接:1-RTT
┌─────────────────────────────────────┐
│ Client Hello + TLS ClientHello │ ──→
│ ←── Server Hello + TLS + 握手完成 │ 1 个 RTT
│ 加密数据传输开始 │ ──→
└─────────────────────────────────────┘
QUIC 将传输握手和 TLS 握手合并为一步
QUIC 恢复连接:0-RTT
┌─────────────────────────────────────┐
│ Client Hello + 缓存的密钥 + 数据 │ ──→ 0 个 RTT!
│ ←── Server 响应 │ 首个包就携带数据
└─────────────────────────────────────┘
条件:客户端和服务器之前建立过连接,缓存了密钥
风险:0-RTT 数据可能被重放攻击(需要应用层防护)3. 连接迁移(Connection Migration)
TCP:四元组标识连接 → IP 变化 = 连接断开
QUIC:Connection ID 标识连接 → IP 变化不影响
场景:Wi-Fi → 4G
TCP: Wi-Fi 连接断开 → 重新三次握手 + TLS → 恢复传输
QUIC: Connection ID 不变 → 无缝切换 → 继续传输
┌──────────────────────────────────────┐
│ Wi-Fi (IP: 192.168.1.100) │
│ QUIC Connection ID: 0xABCD1234 │
│ 正在传输数据... │
│ │
│ ── 切换到 4G (IP: 10.0.0.50) ── │
│ │
│ QUIC Connection ID: 0xABCD1234 │ ← 同一个 Connection ID
│ 继续传输数据(无中断) │
└──────────────────────────────────────┘4. QPACK 头部压缩
HTTP/3 使用 QPACK 替代 HPACK,解决了 HPACK 在无序传输下的问题:
HPACK 的问题:
动态表的更新依赖帧的顺序
HTTP/2 over TCP 保证了顺序 → 没问题
但 QUIC 的流是独立传输的,可能乱序到达 → 动态表状态不一致
QPACK 的解决方案:
使用单独的单向流来同步动态表状态
┌─ Encoder Stream(客户端 → 服务器):发送动态表更新指令
├─ Decoder Stream(服务器 → 客户端):确认收到的指令
└─ 请求/响应流:引用静态表和已确认的动态表条目
效果:保持了 HPACK 的压缩率,同时支持无序传输三代协议对比
| 特性 | HTTP/1.1 | HTTP/2 | HTTP/3 |
|---|---|---|---|
| 传输层 | TCP | TCP | QUIC (UDP) |
| 连接复用 | Keep-Alive(串行) | 多路复用(并行) | 多路复用(并行) |
| 队头阻塞 | 应用层 ✅ / TCP 层 ✅ | 应用层 ❌ / TCP 层 ✅ | 应用层 ❌ / 传输层 ❌ |
| 头部格式 | 文本 | 二进制 | 二进制 |
| 头部压缩 | 无 | HPACK | QPACK |
| 加密 | 可选(HTTPS) | 实际强制 HTTPS | 内建 TLS 1.3 |
| 连接建立 | 1-3 RTT | 1-2 RTT | 1 RTT / 0-RTT |
| 连接迁移 | ❌ | ❌ | ✅ (Connection ID) |
| Server Push | ❌ | ✅(已弃用) | ✅(同样不推荐) |
| 流优先级 | ❌ | 复杂树模型 | 简化 urgency + incremental |
| 部署难度 | 低 | 中 | 高(需 UDP 支持) |
实际部署
HTTP/2 部署
nginx
# Nginx 配置
server {
listen 443 ssl http2;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
# HTTP/2 特有配置
http2_max_concurrent_streams 128;
http2_initial_window_size 65535;
}HTTP/3 部署
nginx
# Nginx(需要 1.25.0+)
server {
listen 443 ssl;
listen 443 quic; # 启用 QUIC/HTTP/3
http2 on;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
# 告诉浏览器支持 HTTP/3
add_header Alt-Svc 'h3=":443"; ma=86400';
}HTTP/3 协商流程:
1. 首次访问:浏览器用 HTTP/2 (TCP) 连接
2. 服务器响应头:Alt-Svc: h3=":443"; ma=86400
3. 浏览器后台尝试 QUIC 连接
4. 如果 QUIC 连接成功 → 后续请求使用 HTTP/3
5. 如果 QUIC 失败(UDP 被防火墙阻断)→ 继续用 HTTP/2
注意:
- 企业网络/某些运营商可能屏蔽 UDP 443 端口
- 所以 HTTP/3 总是需要 HTTP/2 作为 fallback
- 浏览器会缓存 Alt-Svc 信息,下次直接尝试 HTTP/3CDN 支持情况
主流 CDN 的 HTTP/3 支持(2025):
Cloudflare — ✅ 默认启用(最早支持者之一)
Fastly — ✅ 支持
AWS CloudFront — ✅ 支持
Google Cloud CDN — ✅ 支持
Akamai — ✅ 支持
阿里云 CDN — ✅ 支持
腾讯云 CDN — ✅ 支持
推荐:直接在 CDN 层启用 HTTP/3,比自己配置 Nginx 简单得多HTTP/2 时代的优化策略变化
HTTP/1.1 时代的优化 HTTP/2 时代是否仍需要
────────────────── ───────────────────
域名分片 ❌ 不需要(多路复用,一个连接够了)
CSS Sprites ❌ 不需要(并行请求无代价)
资源合并(bundle all) ⚠️ 视情况(代码分割更灵活,但有取舍)
内联小资源(Base64) ⚠️ 视情况(小资源并行请求也很快)
资源压缩(gzip/brotli) ✅ 仍然需要(减少传输体积)
CDN 加速 ✅ 仍然需要(减少 RTT)
缓存策略 ✅ 仍然需要(减少请求数)
代码分割(Code Splitting) ✅ 更重要了(细粒度分割 + 并行加载)
<link rel="preload"> ✅ 仍然重要(提前发现关键资源)Bundle 策略的变化
HTTP/1.1 时代:尽量合并,减少请求数
app.js (500KB) = 所有代码打在一起
缺点:改一行代码 → 整个 500KB 缓存失效
HTTP/2 时代:细粒度分割,利用并行加载 + 缓存
vendor.js (200KB) → 第三方库,很少变化
framework.js (80KB) → 框架代码,偶尔更新
app.js (50KB) → 业务公共代码
page-home.js (30KB) → 首页代码(按需加载)
page-about.js (20KB) → 关于页代码(按需加载)
优势:
- 改业务代码只失效 app.js(50KB),vendor 继续用缓存
- 页面间共享的 vendor/framework 只需加载一次
- 首屏只加载必要代码面试高频题
1. HTTP/2 的多路复用是如何解决 HTTP/1.1 的队头阻塞的?
HTTP/1.1 在同一个 TCP 连接上请求必须串行——前一个响应完成后才能发下一个,导致慢响应阻塞后续请求。HTTP/2 引入二进制分帧层,将请求/响应分割为帧(Frame),每个请求/响应对应一个流(Stream),多个流的帧可以在同一个 TCP 连接上交错传输。接收方根据帧的 Stream ID 将它们重新组装为完整的请求/响应。这样一个连接就能并行传输多个请求,不再需要排队等待。但 HTTP/2 只解决了应用层的队头阻塞,TCP 层的队头阻塞仍然存在——一个 TCP 包丢失会导致所有流的数据被阻塞。
2. HTTP/2 的 HPACK 头部压缩是怎么工作的?
HPACK 使用三种策略:①静态表——预定义 61 个常见 Header(如 :method: GET),只需发送 1 字节索引;②动态表——连接级别的 Header 缓存,首次发送完整值并加入动态表,后续只发送索引号;③Huffman 编码——对字面值做进一步压缩。效果:首次请求 Header 可从 ~800 字节压缩到 ~200 字节,后续同类请求可压缩到 ~20 字节(大部分 Header 都能用索引替代)。
3. HTTP/3 为什么使用 UDP 而不是 TCP?
本质原因是 TCP 的设计无法满足 HTTP/3 的需求:①TCP 的队头阻塞无法解决——TCP 保证字节流顺序,一个包丢失会阻塞所有后续数据,即使它们属于不同的 HTTP 流;②TCP 握手延迟无法减少——TCP 三次握手是协议规范,无法跳过;③TCP 不支持连接迁移——TCP 用四元组标识连接,IP 变化就断开。HTTP/3 选择在 UDP 之上构建 QUIC 协议,自己实现可靠传输、拥塞控制和多路复用,从而获得了流级别的独立丢包恢复、1-RTT/0-RTT 连接建立、和基于 Connection ID 的连接迁移能力。
4. QUIC 的 0-RTT 是怎么实现的?有什么安全风险?
0-RTT 适用于客户端与服务器之前已建立过连接的场景。首次连接时,双方通过 1-RTT 握手协商密钥,客户端缓存会话密钥和传输参数。下次连接时,客户端用缓存的密钥直接加密第一个数据包,与 ClientHello 一起发送——服务器如果能验证密钥有效,就直接解密处理。安全风险:0-RTT 数据可能被重放攻击(攻击者截获并重发)——因为在握手完成前无法确认客户端是否真的发起了请求。防御措施:①服务器端对 0-RTT 数据做幂等性检查;②只对安全的操作(GET 请求)使用 0-RTT;③使用 ticket 失效机制限制重放窗口。
5. HTTP/2 时代还需要做哪些前端优化?哪些传统优化不再必要?
不再需要:①域名分片(多路复用一个连接够了);②CSS Sprites/图标合并(并行请求无额外代价);③内联 Base64 小资源(直接请求也很快)。仍然需要:①代码分割更重要了——细粒度分割利用并行加载和独立缓存(vendor/framework/page 分开);②资源压缩(Brotli/Gzip)减少传输体积;③缓存策略(长期强缓存 + hash 文件名);④<link rel="preload"> 提前发现关键资源;⑤CDN 加速减少 RTT。核心思维转变:从"减少请求数"转向"合理分割 + 并行加载 + 精细化缓存"。
追问思考
- 为什么 HTTP/2 要求使用 HTTPS?纯 HTTP 不能用 HTTP/2 吗?规范和实际实现有什么区别?
- QUIC 基于 UDP,如果企业防火墙或运营商屏蔽了 UDP 443 端口怎么办?浏览器如何处理 HTTP/3 降级?
- HTTP/2 的 Server Push 为什么失败了?Chrome 为什么在 106 版本移除了它?103 Early Hints 有什么优势?
- 从抓包工具(如 Wireshark)的角度,HTTP/2 和 HTTP/3 的数据分别长什么样?为什么 HTTP/3 更难调试?
- 假设一个页面需要加载 50 个小资源(平均每个 2KB),分别在 HTTP/1.1、HTTP/2、HTTP/3 下的加载行为有什么区别?