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

7.4 KiB
Raw Blame History

tags, create time
tags create time
计算机网络
HTTP/2
HPACK
Multiplexing
Server Push
2026-05-18 02:40

HTTP/2 多路复用

概述

HTTP/2 (RFC 7540) 是 HTTP 协议自 1999 年 HTTP/1.1 以来的第一次重大修订。核心改进:二进制分帧层,让一个 TCP 连接上能并发多个请求,彻底消除 Head-of-Line blocking(应用层层面)。

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!
# 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):

# 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 主要用于微服务间通信。

关联笔记