文章

HTTP/1.1与HTTP/2协议对比深入解析

HTTP/1.1与HTTP/2协议对比深入解析

一句话概括

HTTP/2 是 HTTP 协议自 1999 年 HTTP/1.1 以来的首次重大升级,其核心改进是多路复用(单一 TCP 连接并发传输多个请求/响应)、二进制分帧(取代文本协议)、HPACK 头部压缩(减少冗余头部开销)和服务器推送——解决了 HTTP/1.1 的队头阻塞(HOL Blocking)问题,使页面加载性能大幅提升。

背景与意义

HTTP/1.1 的”先天不足”

HTTP/1.1 在 1999 年标准化时,网页平均只有几十 KB,资源请求数不过个位数。而到了 2020+,一个典型网页平均发送 70-100 个请求,页面体积超过 2-3MB

HTTP/1.1 在这种场景下暴露了三个致命缺陷:

1. 队头阻塞(Head-of-Line Blocking)

1
2
3
4
5
连接 1: |---请求 1---|
                 |---等待 1 的响应---|
            超时             |---请求 2---|
                                    |---等待 2 的响应---|
时间线 → ████████████████████████████████████

每个连接同一时刻只能处理一个请求,后面的请求必须排队等待前一个完成。即使用 Connection: keep-alive,这也是”管道化”而非并行。

2. 头部冗余

1
2
3
4
5
6
7
8
9
10
11
12
13
# 一次请求的头部(约 500-800 字节)
GET /index.html HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)
Accept: text/html,application/xhtml+xml
Accept-Language: zh-CN,zh;q=0.9
Accept-Encoding: gzip, deflate
Connection: keep-alive
Cookie: session_id=abc123; user_id=456
Cache-Control: no-cache

# 70 个请求中,这些头部重复发送 70 次!
# 总头部开销 ≈ 50KB — 比传输数据本身还多

3. 域名分片(Domain Sharding)

为了绕开浏览器每个域名的连接数限制(通常 6 个),开发者被迫把资源分散到多个子域名:

1
2
3
4
<!-- 需要手动分片 -->
<script src="//cdn1.example.com/js/app.js"></script>
<script src="//cdn2.example.com/js/vendor.js"></script>
<script src="//cdn3.example.com/js/utils.js"></script>

面试高频信号

HTTP/2 是网络协议方向的明星考题:

  • “HTTP/2 解决了 HTTP/1.1 的哪些问题?”
  • “HTTP/2 的多路复用原理”(画图题)
  • “HTTP/2 的队头阻塞真的完全解决了吗?”(陷阱题)
  • “HTTP/2 和 HTTP/3 有什么区别?”

概念与定义

HTTP/2 的三大核心改进

1. 二进制分帧层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
HTTP/1.1(文本协议):
GET /index.html HTTP/1.1\r\n
Host: example.com\r\n
\r\n

HTTP/2(二进制协议):
┌─────────────────┐
│ Length (24 bit) │
├─────────────────┤
│ Type (8 bit)    │  ← HEADERS / DATA / SETTINGS 等
├─────────────────┤
│ Flags (8 bit)   │  ← END_HEADERS / END_STREAM
├─────────────────┤
│ R | Stream ID  │  ← 流标识符(31 bit)
├─────────────────┤
│ Frame Payload   │
└─────────────────┘

2. 多路复用(Multiplexing)

1
2
3
4
5
6
7
HTTP/2 单个 TCP 连接可以同时处理多个流(stream):
┌─── 流 1: 请求 index.html ───────────────────→
├─── 流 2: 请求 style.css ────────────────────→
├─── 流 3: 请求 app.js ──────────────────────→
├─── 流 1: ← 响应 index.html ─────────────────
├─── 流 2: ← 响应 style.css ─────────────────
├─── 流 3: ← 响应 app.js ────────────────────

3. HPACK 头部压缩

1
2
3
4
5
``` 使用静态字典 + 动态字典 + 哈夫曼编码

第一次请求:完整头部(但编码后更小)
后续请求:只发送变化的字段(通常只有 :path 不同)
压缩率:头部从 600-800 字节压缩到 30-50 字节

最小示例

1
2
3
4
5
6
7
8
9
10
11
12
13
# 查看网站是否支持 HTTP/2(测试环境)
curl -v --http2 https://www.example.com/ 2>&1 | grep -i "alpn\|http2"

# 观察 HTTP/1.1 下的多连接
curl -v --http1.1 https://www.example.com/ 2>&1

# nginx 启用 HTTP/2
# server {
#     listen 443 ssl http2;
#     server_name example.com;
#     ssl_certificate /path/to/cert.pem;
#     ssl_certificate_key /path/to/key.pem;
# }
1
2
3
// 前端检测协议
const protocol = performance.getEntriesByType('navigation')[0]?.nextHopProtocol;
console.log(protocol);  // "h2" = HTTP/2, "http/1.1" = HTTP/1.1

核心知识点拆解

知识点 1:HTTP/1.1 的优化手段与局限性

HTTP/1.1 时代的”病态优化”

1
2
3
4
5
6
7
8
9
10
11
12
<!-- 1. CSS Sprites:把所有图标拼成一张图 -->
.icon-home { background: url(sprite.png) -50px 0; }

<!-- 2. 域名分片:绕开 6 连接限制 -->
<link rel="stylesheet" href="//cdn1.example.com/a.css">
<link rel="stylesheet" href="//cdn2.example.com/b.css">

<!-- 3. 资源内联:减少请求数 -->
<img src="data:image/png;base64,iVBOR...">

<!-- 4. 文件合并:把所有 JS 合并成一个 -->
<script src="all.js"></script>  <!-- 即使只改了一行也要全量缓存失效 -->

💡 关键洞察:HTTP/2 让这些”优化”变得不必要甚至有害——多路复用消除了请求数焦虑,连接复用消除了域名分片。

知识点 2:HTTP/2 多路复用的工作原理

1
2
3
4
5
6
7
8
9
10
11
客户端                                    服务器
  │                                          │
  │──── 流 1 (HEADERS: GET /index.html) ────→│
  │──── 流 2 (HEADERS: GET /style.css) ────→│
  │──── 流 3 (HEADERS: GET /app.js) ───────→│
  │                                          │
  │←─ 流 2 (DATA: style.css 第1部分) ───────│
  │←─ 流 1 (DATA: index.html 第1部分) ──────│
  │←─ 流 2 (DATA: style.css 第2部分) ───────│
  │←─ 流 1 (DATA: index.html 第2部分) ──────│
  │←─ 流 3 (DATA: app.js 第1部分) ─────────│

关键机制

  • 流(Stream):每个请求/响应是一个独立的流
  • 帧(Frame):流的组成部分,多个流的帧可以交错发送
  • 每个流有唯一的 ID,接收方按 ID 重组

知识点 3:HTTP/2 的”残余队头阻塞”(RTT 级)

🎯 面试陷阱题:HTTP/2 真的完全解决了队头阻塞吗?

答案没有完全解决。HTTP/2 解决了应用层的队头阻塞(请求级别的排队),但 TCP 层仍然有队头阻塞

1
2
3
4
5
6
7
8
9
TCP 连接的队头阻塞:
                          💥 丢包!
包 1: ┌─────┐  ┌─────┐  ┌╳╳╳┐
流 1: │ A1  │  │ A2  │  │ A3 │  ← 需要重传
包 2: ┌─────┐  ┌─────┐  ┌─────┐
流 2: │ B1  │  │ B2  │  │ B3  │  ← 虽然收到了,但 TCP 要等 A3 重传完
包 3: ┌─────┐  ┌─────┐  ┌─────┐
流 3: │ C1  │  │ C2  │  │ C3  │  ← 也卡住了
                                   时间 →

这就引出了 HTTP/3(QUIC) 的诞生动机——用 UDP 代替 TCP,实现”单流级队头阻塞隔离”。

知识点 4:服务器推送(Server Push)

1
2
3
4
5
6
# HTTP/2 服务器可以"主动推送"客户端可能需要的资源
# 客户端发送:GET /index.html
# 服务器响应:
HTTP/2 200 OK
Content-Type: text/html
<link rel="stylesheet" href="/style.css">

同时推送 style.css

1
2
3
4
5
流 1: HEADERS → :path: /index.html
流 2: PUSH_PROMISE → :path: /style.css  ← 先告诉客户端"我要推了"
流 1: DATA → <html>...</html>
流 2: HEADERS → :path: /style.css       ← 推送内容
流 2: DATA → .class { color: red; }

实际效果:节省了一次 RTT(不用等 HTML 解析后再去请求 CSS)。

⚠️ 实践中的问题:服务器推送在某些场景下可能有副作用(已经缓存了的资源又被推一遍),目前很多站点已不再使用,而是改用 103 Early Hints 或 Preload hints。

实战案例

案例一:HTTP/2 下的构建策略调整

1
2
3
4
5
6
7
8
9
10
11
12
13
// Rollup/Vite 配置:不需要过度分包
export default {
  input: 'src/main.js',
  output: {
    // HTTP/1.1 时代:尽量合并
    // HTTP/2 时代:按需分包,便于缓存
    manualChunks(id) {
      if (id.includes('node_modules')) {
        return 'vendor';
      }
    }
  }
}

案例二:使用 Preload hints 替代 Server Push

1
2
3
4
5
6
7
<!-- 推荐的做法:使用 Preload -->
<head>
  <link rel="preload" href="/critical.css" as="style">
  <link rel="preload" href="/hero-image.webp" as="image">
  <!-- Preconnect:提前建立连接 -->
  <link rel="preconnect" href="https://api.example.com">
</head>

底层原理

HTTP/2 的二进制分帧层详解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
HTTP/2 处理一个请求/响应的完整流程:

1. 客户端创建流(Stream ID = 1)
2. 发送 HEADERS 帧(包含压缩后的 HTTP 头部)
3. 发送 DATA 帧(可能分多帧发送,每帧最大 16KB)
4. 发送 END_STREAM 标志(表示请求结束)
5. 服务端收到后处理请求
6. 服务端回复 HEADERS 帧(响应头部)
7. 服务端回复 DATA 帧(响应体)
8. 服务端发送 END_STREAM 标志(响应结束)

关键帧类型:
- HEADERS (0x01): 携带 HTTP 头部
- DATA (0x00): 携带实际数据
- SETTINGS (0x04): 连接参数协商
- PUSH_PROMISE (0x05): 服务器推送承诺
- GOAWAY (0x07): 优雅关闭连接

流优先级与依赖

1
2
3
4
5
6
7
8
9
10
11
/* HTTP/2 允许设置流的优先级 */
/*               A (主 html)
 *              / \
 *             B   C (CSS)
 *            /     \
 *           D       E (JS)
 *           |
 *           F (图片)
 *
 * 数字越小优先级越高:A=16, B=24, C=32, D=40, E=48, F=56
 */

高频面试题解析

Q1:HTTP/2 相比 HTTP/1.1 有哪些核心改进?

参考答案:

  1. 二进制分帧:文本协议 → 二进制协议,解析更高效
  2. 多路复用:单连接并发多请求,消除应用层队头阻塞
  3. HPACK 头部压缩:压缩率约 85-95%,减少带宽浪费
  4. 服务器推送:服务端主动推送资源
  5. 流优先级:设置请求优先级

Q2:HTTP/2 的多路复用原理是什么?

参考答案: HTTP/2 将请求和响应拆分为更小的,多个流的帧可以在同一个 TCP 连接中交错传输。接收方根据流 ID 重组出完整的请求/响应。这实现了真正的并行传输,不需要像 HTTP/1.1 那样开多个连接或做域名分片。

Q3:HTTP/2 有队头阻塞吗?

参考答案: 应用层的队头阻塞已经解决——多个请求可以在一个连接中并行发送。但 TCP 层的队头阻塞仍然存在:如果某个 TCP 包丢失,后续所有已经到达的包(属于不同流)都要等待重传。这个”残余队头阻塞”是 HTTP/3(基于 QUIC/UDP)要解决的核心问题。

Q4:HTTP/2 的服务器推送和 Preload 有什么区别?

参考答案:

  • Server Push:服务器主动发送资源,不受浏览器控制,可能推送已缓存资源造成浪费
  • Preload:浏览器解析到 <link rel="preload">优先级更高地发起请求,但不代替浏览器的缓存决策
  • 实践中 Preload 更推荐,Server Push 在 CDN 场景中常有兼容问题

Q5:部署 HTTP/2 需要什么条件?

参考答案:

  • 必须使用 TLS/SSL(虽然规范不强制,但主流浏览器只支持基于 TLS 的 h2)
  • 服务器软件升级(Nginx 1.9.5+ / Apache 2.4.17+)
  • CDN 一般默认支持(Cloudflare、Akamai 等)

总结与扩展

核心要点

  1. HTTP/2 = 二进制分帧 + 多路复用 + HPACK + 服务器推送
  2. 多路复用让域名分片、CSS Sprites 成为过时优化
  3. TCP 层队头阻塞仍然存在 → HTTP/3 解决
  4. 部署前提:强制 TLS

延伸学习方向

  • gRPC:基于 HTTP/2 的 RPC 框架,利用多路复用和流式传输
  • HTTP/3 (QUIC):基于 UDP,彻底解决队头阻塞
  • WebTransport:基于 QUIC 的下一代 Web 通信 API

相关主题

本文由作者按照 CC BY 4.0 进行授权

© 独行的风. 保留部分权利。

本站采用 Jekyll 主题 Chirpy

本站总访问量 本站访客数 本文阅读量