Files
cs-note/hhs/NETWORK/05-应用层协议/02-HTTP-2多路复用.md
T
2026-05-24 11:42:38 +08:00

215 lines
7.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [计算机网络, HTTP/2, HPACK, Multiplexing, Server Push]
create time: 2026-05-18 02:40
---
# HTTP/2 多路复用
## 概述
HTTP/2 (RFC 7540) 是 HTTP 协议自 1999 年 HTTP/1.1 以来的第一次重大修订。核心改进:**二进制分帧层**,让一个 TCP 连接上能并发多个请求,彻底消除 Head-of-Line blocking(应用层层面)。
```mermaid
flowchart TD
A["HTTP/1.1<br/>问题重重"] -->|"串行请求"| B["每个请求等响应"]
B --> C["慢 ❌"]
D["HTTP/2<br/>二进制分帧"] -->|"多路复用"| E["并发请求/响应"]
E --> F["快 ✅"]
style A fill:#FF6B6B,color:#fff
style D fill:#98FB98,color:#000
```
## HTTP/2 的三层层级
```
┌─────────────────────────────────────┐
│ Application Layer │
│ HTTP Frames / Streams │ ← 你操作的对象
├─────────────────────────────────────┤
│ Frame Layer │ ← HPACK + Flow Control
│ Binary Framing + Headers │
├─────────────────────────────────────┤
│ Transport Layer │ ← TCP (or QUIC for H3)
│ Connection Management │
└─────────────────────────────────────┘
```
## 二进制帧类型(Frame Types)
| Frame | 说明 |
|-------|------|
| **HEADERS** | 请求/响应头部 |
| **DATA** | 实际数据载荷 |
| RST_STREAM | 异常关闭流 |
| SETTINGS | 通信参数协商 |
| PUSH_PROMISE | 服务器推送 |
| PING | 延迟测量 |
| GOAWAY | 优雅关闭连接 |
| WINDOW_UPDATE | 流量控制 |
| CONTINUATION | HEADERS 太长时的延续帧 |
### HEADERS 帧结构
```
+---------------+
|Pad Length? |(0-7 bits)
|Padding (8-bit)|
|E |
|+---------------+----------------------------------------------+
| |Header Block Fragment (*) |
|+-----------------------------------------------------+
|... |
+-----------------------------------------------------+
```
## Stream — 多路复用的核心
HTTP/2 将每个请求/响应绑定到一个唯一的 **Stream ID**:
```
TCP Connection: client:45678 ↔ server:443
Stream 1: GET /index.html → 200 OK
Stream 3: GET /style.css → 200 OK
Stream 5: POST /api/login → 200 OK
Stream 7: GET /app.js → 200 OK
Client Server
│ │
│ ───HEADERS(stream=1)──→ │ GET /index.html
│ ───DATA(stream=1, 5KB)──→ │
│ │
│ ←──HEADERS(stream=1, 200 OK)── │
│ ←──DATA(stream=1, 15KB)── │
│ │
│ ───HEADERS(stream=3)──→ │ GET /style.css
│ ←──HEADERS(stream=3, 200 OK)── │
│ ←──DATA(stream=3, 3KB)── │
│ │
│ ───HEADERS(stream=5)──→ │ POST /api/login
│ ───DATA(stream=5, 50B)──→ │ body: {"user":"alice"}
│ ←──HEADERS(stream=5, 200 OK)── │
│ ←──DATA(stream=5, 80B)── │ body: {"token":"abc"}
```
### Stream 状态机
```
idle → reserved(local) ———→ half closed(local) —→ closed
↑ ↓ ↑
│ half closed(remote) │
│ ↓ │
└──── open ←───────── half closed(remote) ◄─────┘
↑ ↑
│ half closed(local)
│ ↓
send HEADERS closed
recv HEADERS
```
### 优先级分组(Stream Dependency)
```
Stream 1 (weight=16) [root]
├── Stream 3 (weight=8): /index.html
│ ├── Stream 7 (weight=4): style.css
│ └── Stream 9 (weight=2): logo.png
├── Stream 5 (weight=8): API data
└── Stream 11 (weight=1): tracking pixel
```
## HPACK — Header 压缩
HTTP/1.1 每次请求都重复发送 `User-Agent`、`Cookie`、`Accept` 等头部,浪费带宽。HPACK 用两种技术解决:
| 技术 | 说明 |
|------|------|
| **Header Table** (动态表) | 服务端缓存常用头,后续请求用索引代替 |
| **Huffman Coding** | 对字符串值做霍夫曼编码压缩 |
### HPACK 编码示例
原始 HTTP/1.1 请求头部(重复发送):
```
GET /page HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 ...
Accept: text/html
Cookie: session=abc123
Accept-Encoding: gzip
```
重复 10 次后,HTTP/2 只发一次完整头部,后续使用动态表索引:
```
索引 1: Host: example.com (查表! 不重传)
索引 4: User-Agent: ... (查表!)
索引 5: Accept: text/html (查表!)
索引 6: Cookie: session=abc123 (查表!)
索引 7: Accept-Encoding: gzip (查表!)
```
**节省效果:** header 体积减少 70%~90%。
## Server Push
服务器可以主动向客户端推送资源,无需客户端请求:
```
Client: GET /index.html
Server: HEADERS(stream=2, 200 OK) — HTML content
Server: PUSH_PROMISE(stream=2, promised_stream=4)
→ style.css
Server: HEADERS(stream=4, 200 OK)
Server: DATA(stream=4, CSS content)
← 客户端在还没解析到 <link> 之前就已经收到了 CSS!
```
```bash
# Go 中启用 HTTP/2 Server Push
pusher, ok := w.(http.Pusher)
if ok {
pusher.Push("/static/style.css", nil)
}
```
> [!warning] Push 的实际争议
> Push 被广泛认为弊大于利:浏览器可能已经缓存了资源(导致冗余传输),且无法像 Client-initiated 那样利用浏览器已有的缓存决策。现代实践中更多使用 Preload `<link rel="preload">` 替代 Push。
## HTTP/2 vs HTTP/1.1 对比
| 特性 | HTTP/1.1 | HTTP/2 |
|------|---------|--------|
| 格式 | 文本 | **二进制** |
| 并发 | 需要 N 个连接 | **单连接多路复用** |
| Header | 明文全量发送 | **HPACK 压缩** |
| 服务端推送 | ❌ | ✅ (可选) |
| 头部压缩 | ❌ (需 HPACK extension) | ✅ 原生支持 |
| 头部顺序 | 按发送顺序 | 可重新排序 |
| HoL Blocking | 严重 (TCP 层) | **消除** (Stream 独立) |
| TLS 要求 | 不需要 | **推荐** (nginx/apache 默认 h2c) |
## h2c — HTTP/2 Clear Text (未加密)
HTTP/2 可以通过 ALPN 升级(TLS 下的 h2)或 HTTP Upgrade 机制(纯 TCP 的 h2c):
```bash
# curl 测试 h2c
curl --http2 -H "Upgrade-H2C: true" http://localhost:8080/api
# Go net/http 中 h2c 支持需 import golang.org/x/net/http2
import "golang.org/x/net/http2"
import "golang.org/x/net/http2/h2c"
```
> [!tip] 生产环境始终用 HTTPS + h2
> 浏览器仅支持 TLS 下的 HTTP/2 (ALPN = "h2")。纯 TCP 的 h2c 主要用于微服务间通信。
## 关联笔记
- [[hhs/NETWORK/HTTP/1.1完全指南]] — HTTP/2 的基础
- [[hhs/NETWORK/HTTP/3与QUIC]] — HTTP/2 的 UDP 替代品
- [[hhs/NETWORK/HTTPS与TLS握手]] — HTTP/2 通常配合 HTTPS