抛弃TCP改用UDP,HTTP3疯了吗?

HTTP协议用过这么多年,但HTTP/1.1、HTTP/2、HTTP/3到底有什么区别?各自解决了什么问题,又各自留下了什么遗憾?这篇把三个版本放在同一张桌子上,逐项对比。


原文地址

墨渊书肆/抛弃TCP改用UDP,HTTP3疯了吗?


先说结论

维度 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就是那个值得投入的升级点。

相关推荐
flash俊杰1 分钟前
Electron 桌面应用的进程模型:为什么 fork Next.js standalone
前端
沙蒿同学11 分钟前
我把架构约定编译成了会变红的测试:Wails v2 + Go + Vue3 桌面脚手架实战
前端·后端·github
cpolar技术支持14 分钟前
本地 Playwright 测试报告怎么远程复盘?Trace Viewer 跑起来后,用 cpolar 分享失败现场
前端·自动化测试·测试工具·cpolar·playwright
wordbaby14 分钟前
企业级后台管理系统路由设计与最佳实践指南
前端
胡志辉的博客23 分钟前
【完全开源】IP 纯净度检测 可一键部署到自己的CF
前端·javascript·chrome·ip·chromium
Hilaku1 小时前
作为面试官,我最怕遇到什么样的候选人?
前端·javascript·程序员
酣大智1 小时前
华为 VRRP的抢占延迟时间
网络·tcp/ip·vrrp
TiDi1 小时前
Pinia优化重复请求
前端
web3d5202 小时前
01-用 Leafletjs 10 分钟搭一张水利一张图(Vue3 + Vite 实战)
前端·javascript
晚安日记wanna2 小时前
Vue2 的 defineProperty 差在哪四层追问筛掉九成候选人
前端·vue.js·面试