HTTP协议用过这么多年,但HTTP/1.1、HTTP/2、HTTP/3到底有什么区别?各自解决了什么问题,又各自留下了什么遗憾?这篇把三个版本放在同一张桌子上,逐项对比。
原文地址
先说结论
| 维度 | HTTP/1.1 | HTTP/2 | HTTP/3 (QUIC) |
|---|---|---|---|
| 传输层协议 | TCP | TCP | UDP |
| 多路复用 | ❌ 不支持 | ✅ 支持(流级别) | ✅ 支持(流级别) |
| 头部压缩 | ❌ 纯文本重复发送 | ✅ HPACK(静态字典+动态字典+Huffman) | ✅ QPACK(改进版HPACK) |
| 服务端推送 | ❌ 不支持 | ✅ 支持 | ❌ 已移除(几乎没人用) |
| 队头阻塞 | TCP层级阻塞 + 请求级阻塞 | TCP层级仍阻塞 | 彻底解决 |
| 连接建立 | 1-RTT(TCP握手) | 1-RTT(TCP握手) + TLS | 0-RTT(恢复连接时) |
| 连接迁移 | ❌ IP变了就断 | ❌ IP变了就断 | ✅ 网络切换不断连 |
一句话总结:HTTP/2解决了应用层的队头阻塞,HTTP/3解决了传输层的队头阻塞。
HTTP/1.1到底慢在哪?
问题一:连接级队头阻塞(Connection-level Head-of-Line Blocking)
HTTP/1.1的队头阻塞发生在TCP连接层面 ------浏览器对同一域名有连接数限制,Chrome限制6个TCP连接。这意味着同时最多只能发6个请求,第7个必须等前面某个完成才能发。
md
浏览器想加载一个页面,需要请求10个资源:
CSS、JS、图片1、图片2、图片3、字体、API数据...
但只有6个通道:
[通道1: CSS ████████████ 完成]
[通道2: JS ███████████████████████ 还在传...]
[通道3: 图片1 ██████ 完成]
[通道4: 图片2 ████████████████ 还在传...]
[通道5: 字体 ██████████████████████████████████ 还在传...]
[通道6: API ████████████ 完成]
图片3、图片4... 全在排队等!
因为通道2的JS文件特别大,堵住了后面的所有请求。
这就是连接级队头阻塞:一个慢请求占满通道,导致后面的所有请求排队。
问题二:头部冗余
HTTP头部是纯文本,每次请求都完整携带:
http
GET /api/user HTTP/1.1
Host: api.example.com
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
User-Agent: Mozilla/5.0 ...
Accept-Language: zh-CN,zh;q=0.9
Cookie: session=abc123; theme=dark; ...
--- 第二次请求,同样的头部再发一遍 ---
GET /api/order HTTP/1.1
Host: api.example.com ← 一样
Accept: application/json ← 一样
Content-Type: application/json ← 一样
Authorization: Bearer eyJhbGciOiJIUzI1NiIs... ← 一样
User-Agent: Mozilla/5.0 ... ← 一样
Accept-Language: zh-CN,zh;q=0.9 ← 一样
Cookie: session=abc123; ... ← 差不多
一次页面加载可能发出几十个请求,每个请求重复携带500-2000字节的头部。大部分字段完全相同,纯浪费带宽。
问题三:无法主动推送
用户请求了HTML,服务器知道HTML里引用了CSS和JS。但按照HTTP/1.1的规则,服务器必须等浏览器解析完HTML、发现需要CSS和JS、再发起请求------白白多等了一个往返时间(RTT)。
HTTP/2怎么解决这些问题?
核心变化:二进制分帧层(Binary Framing)
这是HTTP/2最根本的改变。HTTP/1.1是基于文本的,HTTP/2把所有数据拆成更小的单元------帧(Frame)。
md
HTTP/1.1:一条完整的请求/响应 = 一大块文本
HTTP/2:一条消息 → 拆成多个帧
┌─────────────────────────────┐
│ 帧类型 │ 帧标识 │ 负载 │
├─────────┼─────────┼─────────┤
│ HEADERS │ Stream3 │ 请求头 │
│ DATA │ Stream3 │ 请求体 │
│ HEADERS │ Stream5 │ 请求头 │
│ DATA │ Stream5 │ 请求体 │
│ HEADERS │ Stream7 │ 请求头 │
│ DATA │ Stream7 │ 请求体 │
└─────────┴─────────┴─────────┘
↓
在同一个TCP连接上交错发送
帧的类型:
| 帧类型 | 作用 |
|---|---|
| HEADERS | 携带HTTP头部信息 |
| DATA | 携带HTTP body |
| SETTINGS | 协商连接参数 |
| PING | 心跳检测 |
| WINDOW_UPDATE | 流量控制 |
| RST_STREAM | 终止某个流(不影响其他) |
| PUSH_PROMISE | 服务端推送承诺 |
多路复用:一个连接搞定一切
有了二进制分帧,HTTP/2可以在同一个TCP连接上同时发送多个请求/响应,互不干扰:
yaml
HTTP/1.1(6个连接):
TCP连接1 ──→ 请求A ──→ 响应A
TCP连接2 ──→ 请求B ──→ 响应B
TCP连接3 ──→ 请求C ──→ 响应C
...
HTTP/2(1个连接):
TCP连接1 ─┬─→ Stream1: 请求A ←→ 响应A(交错传输)
├─→ Stream3: 请求B ←→ 响应B(交错传输)
├─→ Stream5: 请求C ←→ 响应C(交错传输)
└─→ Stream7: 请求D ←→ 响应D(交错传输)
好处很明显:
- 浏览器不再受6连接数限制
- 一个慢请求不会阻塞其他请求
- 减少TCP连接建立的开销(每次握手都要1-RTT)
HPACK头部压缩
HTTP/2用HPACK算法压缩头部,三层机制配合:
第一层:静态字典
常用头部组合预定义好,编号0-61,直接用编号代替:
md
编号2 : method: GET
编号4 : path: /
编号6 : scheme: http
编号20: content-type: application/json
...
第二层:动态字典
连接期间出现过的头部存下来,后续直接用编号引用。假设第1个请求完整发送了:
md
:method: GET
:path: /api/user
:scheme: https
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
这些被加入动态字典,编号依次为 61、62、63、64...
第2个请求要发完全相同的 authorization,只需要发一个字节 0x3E(二进制 00111110,对应编号62)------连"authorization"这个词都不用发,解码器查字典就知道是哪个头部。实际传输量从几十字节压缩到1-2字节。
第三层:Huffman编码
对还没被字典收录的字段,用Huffman算法进一步压缩。HTTP头部中大量ASCII字符,Huffman编码后体积能减少20-30%。
实际效果: 头部大小从平均800字节降到约200字节,压缩率约75%。
服务端推送(Server Push)
服务器可以在客户端请求之前主动推送资源:
md
浏览器请求:GET /index.html
服务器返回HTML的同时,顺便推:
PUSH_PROMISE: /style.css
PUSH_PROMISE: /main.js
浏览器收到HTML时,CSS和JS已经在路上了
省了一次RTT
但这个功能在实际中几乎没人用。 原因不少:
- 推送的资源浏览器可能已经有了(缓存命中),白推了
- 推送占用带宽,可能挤掉真正需要的资源
- 很难精确判断该推什么、什么时候推
- 开发调试困难
所以HTTP/3直接把这个特性移除了。Google的数据显示,Server Push的实际使用率不到0.1%。
HTTP/2还有问题吗?
有。而且是个致命问题。
TCP级队头阻塞(Transport-layer Head-of-Line Blocking)
HTTP/2用多路复用解决了HTTP层面的队头阻塞------多个请求可以交错发送互不干扰。但它跑在TCP之上,TCP有自己的问题:
md
TCP保证有序交付:包1、包2、包3 必须按顺序到达
如果包2丢了:
包1 到达 ✓ → 交给应用层
包2 丢失 ✗ → 等待重传...
包3 到达 ✗ → 不能交给应用层!必须等包2先到!
结果:包3明明已经到了,却因为包2丢了而被阻塞
这就是 **TCP级队头阻塞**:传输层丢一个包,应用层所有流都被卡住。
在网络状况不好的场景下(移动网络、跨运营商),丢包率升高,TCP重传频繁,整个连接的所有流都会受影响------哪怕只有其中一个流的包丢了。
具体影响有多大? Google测试数据显示:在1%丢包率的网络环境下,HTTP/2的吞吐量下降比HTTP/1.1还严重。因为HTTP/1.1可以开多个TCP连接,一个连接丢包不影响其他;而HTTP/2只有一个连接,一损俱损。
HTTP/3(QUIC):彻底换底座
既然问题出在TCP上,那就不走TCP了。HTTP/3底层换成QUIC协议 ,而QUIC跑在UDP之上。
为什么选UDP而不是继续改TCP?
TCP的问题不是小修小补能解决的------它的有序交付机制是协议层面写死的,改不了。要彻底解决队头阻塞,只能换协议。
但重新设计一个传输层协议推广难度极大(操作系统内核、中间设备全要支持)。所以QUIC选择在用户空间实现,跑在UDP之上------UDP足够简单,几乎所有网络设备都能放行。
QUIC的核心改进
1. 无队头阻塞
QUIC的每个Stream独立有序,但Stream之间无序:
yaml
TCP(HTTP/2):
Connection ──→ [Stream1][Stream2][Stream3] 包2丢了 → 全阻塞
QUIC(HTTP/3):
Connection ──┬→ Stream1: [包1][包2][包3] 独立有序
├→ Stream2: [包1][包2][包3] 独立有序,互不影响
└→ Stream3: [包1][包2][包3] Stream2丢包只阻塞Stream2
Stream2的包丢了,只影响Stream2本身。Stream1和Stream3照常交付。
2. 0-RTT连接建立
传统TLS握手需要2-RTT(ECDHE密钥交换场景):
yaml
首次连接(HTTP/1.1 或 HTTP/2):
Client Server
│ │
│──ClientHello──>│ RTT 1: 发起握手
│<─ServerHello───│ RTT 2: 服务器响应 + 证书
│──KeyExchange──>│ RTT 3: 密钥交换
│<─Finished──────│ RTT 4: 握手完成
│ │
│════ 数据传输 (已加密) ════│ 终于可以发数据了
│ │
首次连接(QUIC/HTTP/3):
Client Server
│ │
│──ClientHello──>│ RTT 1: 握手 + 第一个请求一起发
│<─ServerHello───│ RTT 2: 响应 + 确认
│ │
│════ 数据传输 (已加密) ════│ 少了一个RTT
恢复连接(QUIC/HTTP/3):
Client Server
│ │
│──请求数据 + 恢复凭证 ─>│ RTT 0: 直接带数据
│<──────── 数据 ─────────│ 第一次往返就能拿到响应
对于移动端用户来说,0-RTT 意味着打开 App 的首屏时间显著缩短。
代价也要说清楚: 0-RTT 存在"重放攻击"风险------攻击者截获历史请求和密钥材料,重新发送相同请求到服务器。解决方案是请求加上"anti-replay"机制,服务端对相同请求只执行一次。所以0-RTT适合读请求,不适合写请求(下单、转账等)。
3. 连接迁移
换个WiFi或者从4G切到5G,IP地址会变。TCP用四元组(源IP、源端口、目的IP、目的端口)标识连接,IP一变连接就断了,得重新握手。
QUIC用Connection ID标识连接,跟IP无关:
yaml
手机从WiFi切到4G:
TCP:IP变了 → 连接断开 → 重新三次握手 → 重新TLS握手 → 恢复数据传输(卡顿明显)
QUIC:IP变了 → Connection ID没变 → 连接保持 → 继续传输(用户无感知)
这对移动端体验提升很大------地铁里进出站、电梯上下,网络频繁切换,QUIC不断连。
4. 内置TLS 1.3
QUIC强制使用TLS 1.3加密,不像TCP+TLS是两层拼起来的。加密是协议原生的一部分,不存在"明文握手阶段"这种漏洞窗口。
QPACK:改进的头部压缩
HTTP/3用 QPACK 替代 HPACK,解决的是HPACK在多路复用场景下的队头阻塞问题。
HPACK为什么会阻塞?
HPACK的动态字典是"按顺序积累"的------第1个请求的头部加入字典,第2个请求复用第1个的字典状态。如果第1个请求的HEADERS帧丢了,第2个请求的帧就解不出来了(字典状态对不上)。
这跟TCP的队头阻塞是一个道理,只是堵的是头部解码器,不是传输层。
QPACK怎么解决的?
QPACK引入单向流(Uni-directional Stream)机制------字典同步和数据传输彻底分开:
md
QPACK 两条独立流:
编码器 ──单向流──> 解码器
(只发送字典变化,不携带数据)
编码器 ──单向流──> 解码器
(携带数据帧,但引用字典编号,不包含完整头部)
字典同步走专门的流,跟数据流完全隔离。即使数据流丢包,字典同步流不受影响,解码器仍然可以处理其他帧。
效果:即使某个流的HEADERS帧丢失,不会影响其他流的头部解码,真正实现了"一Stream堵了,其他Stream照常工作"。
三代HTTP对比全景图
应用层:
| 特性 | HTTP/1.1 | HTTP/2 | HTTP/3 |
|---|---|---|---|
| 协议格式 | 文本协议 | 二进制分帧 | 二进制分帧 |
| 头部压缩 | 无,每次重复发送 | HPACK(静态+动态字典+Huffman) | QPACK(单向流隔离) |
| 多路复用 | 无(连接数限制6个) | 支持(流级别) | 支持(流级别) |
| 服务端推送 | 不支持 | 支持 | 已废弃 |
安全层:
| 特性 | HTTP/1.1 | HTTP/2 | HTTP/3 |
|---|---|---|---|
| 加密 | TLS可选 | TLS必须 | TLS 1.3内置(原生) |
| 握手 | 独立层 | 独立层 | 集成在QUIC内部 |
传输层:
| 特性 | HTTP/1.1 | HTTP/2 | HTTP/3 |
|---|---|---|---|
| 传输协议 | TCP | TCP | UDP |
| 可靠性 | TCP保证有序可靠 | TCP保证有序可靠 | QUIC在用户空间实现可靠+有序 |
| 队头阻塞 | TCP级阻塞(丢包堵所有连接) | TCP级阻塞(丢包堵所有流) | 无队头阻塞(Stream独立) |
实际性能差多少?
理论 vs 现实
理论上HTTP/3应该全面碾压HTTP/2,但实际效果取决于场景:
| 场景 | HTTP/2表现 | HTTP/3表现 | 说明 |
|---|---|---|---|
| 理想网络(低延迟低丢包) | 已经很快 | 略快 | 差距不大,瓶颈不在协议 |
| 高延迟网络(跨国) | 较慢 | 明显更快 | 0-RTT和减少握手轮次优势大 |
| 高丢包网络(移动弱网) | 性能下降严重 | 表现稳定 | 无队头阻塞的优势体现 |
| 大文件下载 | 好 | 差不多 | 单流场景差异不大 |
| 小文件密集请求 | 一般 | 更好 | 多路复用+头部压缩叠加 |
什么时候该升级?
- 网站主要面向国内用户:HTTP/2已经够用,国内网络质量好,HTTP/3提升有限
- 有海外用户或移动端为主:HTTP/3值得上,弱网和跨境场景收益明显
- 实时性要求高(直播、在线协作):HTTP/3的低延迟优势重要
- 还在用HTTP/1.1:优先升HTTP/2,改动小收益大;然后再考虑HTTP/3
浏览器和服务端支持情况
浏览器支持:
| 浏览器 | HTTP/2 | HTTP/3 |
|---|---|---|
| Chrome | ✅ 默认启用 | ✅ 默认启用(2019年起) |
| Firefox | ✅ 默认启用 | ✅ 默认启用 |
| Safari | ✅ 默认启用 | ✅ 默认启用(14.1+) |
| Edge | ✅ 默认启用 | ✅ 默认启用 |
主流浏览器全部支持HTTP/3,不需要做兼容处理。
服务端支持:
| 服务/软件 | HTTP/2 | HTTP/3 |
|---|---|---|
| Nginx | ✅ | ⚠️ 需要编译QUIC模块(官方尚未默认内置) |
| Apache | ✅ | ⚠️ 需要mod_quic |
| Caddy | ✅ | ✅ 默认支持(最简单的HTTP/3开启方式) |
| Cloudflare | ✅ | ✅ 全网支持(CDN场景免配置) |
| 阿里云CDN | ✅ | ✅ 支持开启 |
如果你用的是Nginx,目前最省事的方式是在前面套一层Cloudflare CDN,自动获得HTTP/3能力。自建的话Caddy是最友好的选择------一行配置搞定。
总结
三代HTTP的演进主线很清晰:
HTTP/1.1 --- 能用,但问题一堆:队头阻塞、头部冗余、连接数受限。就像一条单车道公路,车多了就堵。
HTTP/2 --- 解决了应用层的大部分问题:二进制分帧实现多路复用、HPACK压缩头部、单连接搞定所有请求。但底层还是TCP,丢包时全军覆没。像拓宽到了多车道高速,但只要有一辆车抛锚,整条路都得停。
HTTP/3 --- 彻底换了运输工具:放弃TCP改用QUIC(基于UDP),从根子上消灭了队头阻塞,加上0-RTT连接和连接迁移,移动端弱网体验大幅提升。像升级成了高铁加飞机,不仅快,还不怕堵。
不过技术选型永远要看场景。如果你的用户在国内、网络条件好,HTTP/2已经能给到90分的体验,HTTP/3那额外的10分不一定值得投入。但如果你的产品面向全球或有大量移动端用户,HTTP/3就是那个值得投入的升级点。