HTTP/2 多路复用到底比 HTTP/1.x 强在哪?

本文最后更新于:2026年2月12日 凌晨

HTTP/2 多路复用到底比 HTTP/1.x 强在哪?

先聊聊 HTTP/1.x 的那些坑

HTTP/1.0:一个请求一个连接,傻到家了

每个请求都得新建 TCP 连接,请求完立马断开:

1
2
3
4
5
6
7
8
9
10
11
浏览器 ─────TCP 握手─────► 服务器
◄────握手完成─────
─────请求 1──────►
◄────响应 1───────
─────TCP 断开─────►

浏览器 ─────TCP 握手─────► 服务器
◄────握手完成─────
─────请求 2──────►
◄────响应 2───────
─────TCP 断开─────►

这啥概念?TCP 三次握手已经够慢了,如果是 HTTPS 还得加 TLS 握手,这延迟叠在一起,页面加载能急死人。

HTTP/1.1:Keep-Alive 来了,但也没好到哪去

Connection: keep-alive 让 TCP 连接能复用了:

1
2
3
4
5
6
7
8
9
浏览器 ─────TCP 握手─────► 服务器
◄────握手完成─────
─────请求 1──────►
◄────响应 1───────
─────请求 2──────►
◄────响应 2───────
─────请求 3──────►
◄────响应 3───────
─────TCP 断开(超时后)─────►

省了握手开销是挺好,但有个要命的毛病:请求只能排队等着,一个没完下一个别想走 —— 这就是臭名昭著的队头阻塞

HTTP/1.1 管道化:试过,但失败了

想了个招,允许一口气发多个请求:

1
2
3
4
5
6
7
8
浏览器 ─────TCP 握手─────► 服务器
◄────握手完成─────
─────请求 1──────►
─────请求 2──────►
─────请求 3──────►
◄────响应 1───────
◄────响应 2───────
◄────响应 3───────

听着不错对吧?但响应必须按请求顺序返回。万一请求 1 的响应慢得像蜗牛,请求 2、3 就算早准备好了也得干等着。所以现实里基本没人用这功能。

HTTP/2 多路复用:这才叫并行

核心思路

把请求/响应拆成一个个独立的流(Stream),多个流在同一条 TCP 连接上穿插着走

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
HTTP/1.1 Keep-Alive:
┌─────────────────────────────────────────┐
│ 请求 1 ───────────────────────────────► │
│ ◄────────────────────────────── 响应 1 │
│ 请求 2 ───────────────────────────────► │
│ ◄────────────────────────────── 响应 2 │
│ 请求 3 ───────────────────────────────► │
│ ◄────────────────────────────── 响应 3 │
│ │
│ 特点:排队,前一个完事才能下一个 │
└─────────────────────────────────────────┘

HTTP/2 Multiplexing:
┌─────────────────────────────────────────┐
│ 流 1: ════HEADERS════════════════════► │
│ 流 3: ════HEADERS═════════DATA═══════► │
│ 流 1: ════════════════════DATA═══════► │
│ 流 5: ════HEADERS═════════DATA═══════► │
│ 流 3: ════════════════════DATA═══════► │
│ 流 1: ◄═══════════════════DATA════════ │
│ │
│ 特点:各走各的,谁也不堵谁 │
└─────────────────────────────────────────┘

关键区别一览

特性 HTTP/1.1 Keep-Alive HTTP/2 Multiplexing
连接数 同域名 6-8 个 同域名 1 个
请求方式 排队串行 真正并行
响应顺序 必须按顺序 想谁先回就谁先回
队头阻塞 HTTP 层解决
头部压缩 没有 HPACK 压缩
优先级控制 没有

多路复用是怎么实现的?

流(Stream)

HTTP/2 里每个请求/响应对应一个流,用流 ID 区分:

  • 客户端发起的流:奇数 ID(1, 3, 5…)
  • 服务器发起的流:偶数 ID(用于推送)
  • 流 ID 不能复用

帧(Frame)

数据被切成一个个帧,每个帧打上流 ID 的标签:

1
2
3
4
5
6
7
8
9
10
11
12
13
┌──────────────┬──────────┬──────────┬──────────────┐
长度(24bit) 类型(8bit)│ 标志(8bit)│ ID(31bit)
├──────────────┴──────────┴──────────┴──────────────┤
负载数据
└────────────────────────────────────────────────────┘

示例:
1: {stream_id: 1, type: HEADERS, data: ...}
2: {stream_id: 3, type: HEADERS, data: ...}
3: {stream_id: 1, type: DATA, data: ...}
4: {stream_id: 5, type: HEADERS, data: ...}
5: {stream_id: 3, type: DATA, data: ...}
6: {stream_id: 1, type: DATA, data: ...}

接收方按 stream_id 把帧重新拼成完整的请求/响应。

看图说话

HTTP/2 多路复用示意

这张图说明白了:

  • HTTP/1.x:请求 A 卡住了,请求 B 只能等着
  • HTTP/2:请求 A 卡了关我 B、C 什么事?照走不误

服务器推送:多路复用的骚操作

传统模式:等浏览器要我才给

1
2
3
4
5
6
7
浏览器: GET /index.html
服务器: 返回 index.html
浏览器: [解析 HTML]...
浏览器: GET /style.css
服务器: 返回 style.css
浏览器: GET /app.js
服务器: 返回 app.js

时间线:

1
2
3
4
5
6
index.html: ████████░░░░░░░░░░░░
parse HTML: ████░░░░░░░░░░
style.css: ████████░░░░
app.js: ████████

总时间

服务器推送模式:我猜你要啥,提前塞给你

1
2
3
4
5
6
7
8
9
浏览器: GET /index.html
服务器: 返回 index.html
PUSH_PROMISE /style.css ← 预测推送
PUSH_PROMISE /app.js ← 预测推送
[推送 style.css 数据]
[推送 app.js 数据]
浏览器: [解析 HTML]...
[style.css 已经在了]
[app.js 已经在了]

时间线:

1
2
3
4
5
6
index.html:  ████████░░░░░░░░░░░░
parse HTML: ████░░░░░░░░░░
style/css: ████████████░░░░░░░░░░ ← 并行
app.js: ██████████████████░░░░ ← 并行

总时间(快多了)

服务器推送对比

推送流程示意

推送别乱用

适合推的

  • 首页关键 CSS/JS
  • 用户大概率会访问的下一页资源

别瞎推的

  • 浏览器可能已经缓存的 —— 浪费带宽
  • 根本用不到的资源 —— 浪费流量

配合缓存策略来,只推那些没缓存的。

头部压缩:省下的都是钱

HTTP/1.x 的 Header 有多啰嗦:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 请求 1
GET /page1 HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)...
Accept: text/html,application/xhtml+xml...
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Cookie: session=abc123; user=xyz789

# 请求 2(大部分都一样,还得重发一遍)
GET /page2 HTTP/1.1
Host: example.com ← 重复
User-Agent: Mozilla/5.0... ← 重复
Accept: text/html... ← 重复
Accept-Language: zh-CN... ← 重复
Accept-Encoding: gzip, deflate, br ← 重复
Connection: keep-alive ← 重复
Cookie: session=abc123; user=xyz789 ← 重复

HTTP/2 的 HPACK 压缩:

1
2
请求 1: 完整发送(约 400 字节)
请求 2: 只发不一样的(约 10 字节)

100 个请求能省约 40KB 的 Header 传输。

性能实测

典型网页(1 个 HTML + 3 个 CSS + 5 个 JS + 10 个图片):

指标 HTTP/1.1 HTTP/2 提升
TCP 连接数 6-8 个 1 个 -85%
首屏时间 2.5s 1.2s -52%
完全加载时间 4.0s 2.1s -47%
总传输大小 850KB 780KB -8%

切到 HTTP/2 后,这些优化可以扔了

1. 域名分片

1
2
3
4
5
6
7
// HTTP/1.1 时代的做法(HTTP/2 不需要了)
<script src="//static1.example.com/app.js"></script>
<script src="//static2.example.com/lib.js"></script>

// HTTP/2 一条连接走天下
// 直接用一个域名
<script src="//cdn.example.com/app.js"></script>

2. 文件合并

1
2
3
4
5
6
7
// HTTP/1.1:为了减少请求数,硬把一堆小文件塞成一个 bundle
// 影响缓存命中率

// HTTP/2:小文件该分就分,缓存粒度更细,命中更高
// 按需加载
import { Button } from './components/Button';
import { Chart } from './components/Chart';

3. 雪碧图

1
2
3
4
5
6
7
/* HTTP/1.1:把一堆小图拼成一张大图 */
background: url('sprites.png');
background-position: -10px -20px;

/* HTTP/2:小图直接单独请求 */
/* 简单直接 */
background: url('icon-search.png');

4. 内联资源

1
2
3
4
5
6
7
8
<!-- HTTP/1.1:为了减少请求,把 CSS/JS 直接写 HTML 里 -->
<!-- 没法缓存 -->
<style>/* 内联 CSS */</style>
<script>/* 内联 JS */</script>

<!-- HTTP/2:该外链就外链 -->
<!-- 能缓存,复用率高 -->
<link rel="stylesheet" href="/critical.css">

一句话总结

维度 HTTP/1.x Keep-Alive HTTP/2 Multiplexing
连接复用 ✅ 能复用 TCP ✅ 也能复用
请求并行 ❌ 串行排队 ✅ 真·并行
队头阻塞 ❌ 存在 ✅ 解决
头部开销 ❌ 大 ✅ 压缩后小
推送能力 ❌ 没有 ✅ 服务器主动推
优先级 ❌ 没有 ✅ 有

多路复用是 HTTP/2 最硬核的改进,它从根本上改变了 Web 资源的传输方式。搞懂它和 HTTP/1.x 的本质区别,写代码的时候心里才有底。


HTTP/2 多路复用到底比 HTTP/1.x 强在哪?
http://bestkele.com/2020/06/03/concept/http2-multip/
作者
kele
发布于
2020年6月3日
许可协议