RFC 7540 - HTTP/2
文档信息
- RFC编号: 7540
- 标题: Hypertext Transfer Protocol Version 2 (HTTP/2)
- 标题(中文): 超文本传输协议第2版
- 发布日期: 2015年5月
- 作者: M. Belshe (BitGo), R. Peon (Google), M. Thomson (Mozilla), ed.
- 废弃文档: 无(HTTP/1.1并存)
- 后继版本: RFC 9113 (HTTP/2, 2022更新)
- 状态: 标准轨道 (Standards Track)
摘要
HTTP/2是HTTP协议的第二个主要版本,解决了HTTP/1.1的性能瓶颈。它通过多路复用、头部压缩、服务器推送等特性,显著提升了Web应用的加载速度和效率。HTTP/2保持了HTTP/1.1的语义,但改变了数据在客户端和服务器之间的传输方式。
核心问题
HTTP/1.1的局限
问题1: 队头阻塞 (Head-of-Line Blocking)
┌─────────────────────────────────┐
│ HTTP/1.1 - 顺序处理 │
├─────────────────────────────────┤
│ Request 1 → Response 1 (慢) │
│ Request 2 等待... │
│ Request 3 等待... │
└─────────────────────────────────┘
解决方案: 打开多个TCP连接
问题: 资源浪费、拥塞窗口小
问题2: 头部冗余
每个请求都携带大量重复的头部
Cookie等可能有几KB大小
问题3: 无服务器推送
服务器只能被动响应请求
HTTP/2的解决方案
解决方案1: 多路复用
┌─────────────────────────────────┐
│ HTTP/2 - 单连接并行处理 │
├─────────────────────────────────┤
│ Request 1 → Response 1 │
│ Request 2 → Response 2 │ 同时进行
│ Request 3 → Response 3 │
└─────────────────────────────────┘
解决方案2: HPACK头部压缩
减少90%以上的头部大小
解决方案3: 服务器推送
服务器主动推送所需资源
核心概念
1. 二进制分帧层 (Binary Framing Layer)
HTTP/2在应用层(HTTP)和传输层(TCP)之间添加了二进制分帧层:
HTTP/1.1文本协议:
GET /index.html HTTP/1.1\r\n
Host: example.com\r\n
\r\n
HTTP/2二进制帧:
┌─────────────────────────────┐
│ Frame Header (9 bytes) │
├─────────────────────────────┤
│ Frame Payload (variable) │
└─────────────────────────────┘
帧结构:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Length (24) |
+---------------+---------------+---------------+
| Type (8) | Flags (8) |
+-+-+-----------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+=+=============================================================+
| Frame Payload (0...) ...
+---------------------------------------------------------------+
字段说明:
- Length (24 bits): 帧载荷长度(不包括帧头的9字节)
- Type (8 bits): 帧类型
- Flags (8 bits): 帧特定标志
- R (1 bit): 保留位,必须为0
- Stream Identifier (31 bits): 流标识符
2. 流 (Stream)
流是HTTP/2连接中的独立、双向的帧序列:
单个TCP连接上的多个流:
TCP连接
│
├─ Stream 1 (GET /index)
│ ├─ HEADERS帧
│ ├─ DATA帧
│ └─ DATA帧 (END_STREAM)
│
├─ Stream 3 (GET /style.css)
│ ├─ HEADERS帧
│ └─ DATA帧 (END_STREAM)
│
└─ Stream 5 (POST /api/data)
├─ HEADERS帧
├─ DATA帧
└─ DATA帧 (END_STREAM)
流的特性:
- 流ID由客户端发起的流为奇数(1, 3, 5...)
- 流ID由服务器发起的流为偶数(2, 4, 6...)
- 流ID 0用于连接级控制
- 流可以并发传输
- 流可以被赋予优先级
3. 帧类型
HTTP/2定义了10种帧类型:
| 类型 | 编码 | 用途 |
|---|---|---|
| DATA | 0x0 | 传输应用数据 |
| HEADERS | 0x1 | 传输头部信息 |
| PRIORITY | 0x2 | 指定流优先级 |
| RST_STREAM | 0x3 | 终止流 |
| SETTINGS | 0x4 | 连接配置参数 |
| PUSH_PROMISE | 0x5 | 服务器推送通知 |
| PING | 0x6 | 心跳检测 |
| GOAWAY | 0x7 | 优雅关闭连接 |
| WINDOW_UPDATE | 0x8 | 流量控制 |
| CONTINUATION | 0x9 | 继续传输头部 |
HTTP/2连接建立
方法1: ALPN协商(HTTPS)
1. TLS握手(带ALPN扩展)
Client → Server: ClientHello
Extension: ALPN
Protocol: h2, http/1.1
Server → Client: ServerHello
Extension: ALPN
Selected: h2
2. 连接前言 (Connection Preface)
Client → Server: "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
Client → Server: SETTINGS帧
Server → Client: SETTINGS帧
3. 开始HTTP/2通信
方法2: HTTP Upgrade(HTTP明文)
Client → Server:
GET / HTTP/1.1
Host: example.com
Connection: Upgrade, HTTP2-Settings
Upgrade: h2c
HTTP2-Settings: <base64url编码的SETTINGS>
Server → Client:
HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: h2c
[切换到HTTP/2]
Server → Client: SETTINGS帧
连接前言示例
# HTTP/2连接前言(客户端必须发送)
CONNECTION_PREFACE = b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n'
# 发送
sock.sendall(CONNECTION_PREFACE)
# 发送SETTINGS帧
settings_frame = build_settings_frame({
SETTINGS_HEADER_TABLE_SIZE: 4096,
SETTINGS_ENABLE_PUSH: 1,
SETTINGS_MAX_CONCURRENT_STREAMS: 100,
SETTINGS_INITIAL_WINDOW_SIZE: 65535
})
sock.sendall(settings_frame)
多路复用详解
流的生命周期
+--------+
send | | recv
PP | idle | PP
+--------+
| |
send H/ | | recv H
recv H | |
v v
+----------+
send | | recv
ES | open | ES
+----------+
| |
send ES | | recv ES
| |
v v
+--------+ +--------+
send R/ | | | | recv R/
recv R | half | | half | send R
| closed | | closed |
| (local)| |(remote)|
+--------+ +--------+
| |
send R/ | | recv R
recv R | | send R
v v
+--------+
recv | | send
ES | closed | ES
+--------+
图例:
H = HEADERS帧
PP = PUSH_PROMISE帧
ES = END_STREAM标志
R = RST_STREAM帧
并发请求示例
// HTTP/1.1 - 需要6个连接
Promise.all([
fetch('/api/user'), // 连接1
fetch('/api/posts'), // 连接2
fetch('/api/comments'), // 连接3
fetch('/images/logo.png'), // 连接4
fetch('/css/style.css'), // 连接5
fetch('/js/app.js') // 连接6
]);
// HTTP/2 - 1个连接足够
Promise.all([
fetch('/api/user'), // Stream 1
fetch('/api/posts'), // Stream 3
fetch('/api/comments'), // Stream 5
fetch('/images/logo.png'), // Stream 7
fetch('/css/style.css'), // Stream 9
fetch('/js/app.js') // Stream 11
]);
// 全部在同一个TCP连接上并发传输!
HPACK头部压缩
压缩原理
HPACK使用三种压缩技术:
1. 静态表 (Static Table)
预定义的常用头部:
索引 | 头部名称 | 头部值
-----|-------------------|--------
1 | :authority |
2 | :method | GET
3 | :method | POST
4 | :path | /
5 | :path | /index.html
8 | :status | 200
9 | :status | 204
15 | accept-encoding | gzip, deflate
...
2. 动态表 (Dynamic Table)
连接期间构建的头部索引:
首次请求:
:method: GET
:path: /api/users
authorization: Bearer token123...
编码后加入动态表:
索引62 | authorization | Bearer token123...
后续请求:
只需发送: 索引62
节省数百字节!
3. 霍夫曼编码 (Huffman Coding)
原始: "www.example.com"
霍夫曼编码后: 减少30-40%
压缩示例
# 首次请求 - 完整头部
headers = {
':method': 'GET',
':path': '/index.html',
':scheme': 'https',
':authority': 'www.example.com',
'user-agent': 'Mozilla/5.0...',
'accept': 'text/html...'
}
# 编码大小: ~800字节
# 动态表更新后
# 第二次请求 - 引用索引
headers_encoded = [
2, # :method GET (静态表)
4, # :path / (静态表)
7, # :scheme https (静态表)
62, # :authority (动态表)
63, # user-agent (动态表)
64 # accept (动态表)
]
# 编码大小: ~50字节!
# 压缩比: 94%
服务器推送 (Server Push)
工作原理
传统HTTP/1.1流程:
1. Client: GET /index.html
2. Server: 返回HTML
3. Client: 解析HTML,发现需要style.css
4. Client: GET /style.css
5. Server: 返回CSS
总往返: 2次
HTTP/2服务器推送:
1. Client: GET /index.html
2. Server: PUSH_PROMISE (Stream 2: /style.css)
3. Server: 返回HTML (Stream 1)
4. Server: 推送CSS (Stream 2)
总往返: 1次
PUSH_PROMISE帧
客户端请求:
Stream 1: GET /index.html
服务器响应:
Stream 1: PUSH_PROMISE帧
:method: GET
:path: /style.css
:scheme: https
:authority: example.com
Promised Stream ID: 2
Stream 2: HEADERS帧 + DATA帧
(包含style.css内容)
Stream 1: HEADERS帧 + DATA帧
(包含index.html内容)
服务器推送示例(Node.js)
const http2 = require('http2');
const fs = require('fs');
const server = http2.createSecureServer({
key: fs.readFileSync('server-key.pem'),
cert: fs.readFileSync('server-cert.pem')
});
server.on('stream', (stream, headers) => {
const path = headers[':path'];
if (path === '/index.html') {
// 推送CSS文件
stream.pushStream(
{
':path': '/style.css',
':method': 'GET'
},
(err, pushStream) => {
if (err) throw err;
pushStream.respond({
':status': 200,
'content-type': 'text/css'
});
const css = fs.readFileSync('./style.css');
pushStream.end(css);
}
);
// 推送JavaScript文件
stream.pushStream(
{
':path': '/app.js',
':method': 'GET'
},
(err, pushStream) => {
if (err) throw err;
pushStream.respond({
':status': 200,
'content-type': 'application/javascript'
});
const js = fs.readFileSync('./app.js');
pushStream.end(js);
}
);
// 响应主请求
stream.respond({
':status': 200,
'content-type': 'text/html'
});
const html = fs.readFileSync('./index.html');
stream.end(html);
}
});
server.listen(8443);
客户端处理推送
// 浏览器自动处理推送的资源
// 资源会被缓存,后续请求直接使用
// 监控服务器推送
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.initiatorType === 'push') {
console.log('服务器推送:', entry.name);
}
}
});
observer.observe({ entryTypes: ['resource'] });
流优先级
优先级模型
HTTP/2使用依赖树模型:
示例优先级树:
Root
│
┌────┴────┐
│ │
Stream1 Stream3
(HTML) (CSS)
│ weight:16
│
Stream5
(Image)
weight:8
说明:
- HTML最高优先级(父级)
- CSS高优先级(权重16)
- Image中优先级(权重8,依赖HTML)
设置优先级
// PRIORITY帧
const priorityFrame = {
streamId: 5,
exclusive: false, // 非独占
dependency: 1, // 依赖Stream 1
weight: 8 // 权重 8
};
// 在HEADERS帧中设置
const headersFrame = {
streamId: 5,
flags: PRIORITY_FLAG,
exclusive: false,
dependency: 1,
weight: 16,
headers: {
':method': 'GET',
':path': '/image.png'
}
};
流量控制
HTTP/2实现了两级流量控制:
连接级流量控制
整个连接的流量窗口:
初始窗口: 65535字节
Client发送65KB后必须等待
Server发送WINDOW_UPDATE才能继续
流级流量控制
每个流独立的流量窗口:
Stream 1: 窗口32KB
Stream 3: 窗口64KB
Stream 5: 窗口16KB
各自独立管理,互不影响
WINDOW_UPDATE示例
def send_window_update(stream_id, increment):
"""发送WINDOW_UPDATE帧"""
frame = bytearray(13) # 9字节头 + 4字节载荷
# 载荷长度: 4
frame[0:3] = (4).to_bytes(3, 'big')
# 类型: WINDOW_UPDATE (0x8)
frame[3] = 0x08
# 标志: 无
frame[4] = 0x00
# 流ID
frame[5:9] = stream_id.to_bytes(4, 'big')
# 窗口增量
frame[9:13] = increment.to_bytes(4, 'big')
return bytes(frame)
# 增加Stream 1的窗口32KB
window_update = send_window_update(1, 32768)
sock.sendall(window_update)
完整的HTTP/2请求-响应示例
客户端请求
import socket
import ssl
import h2.connection
import h2.config
# 创建TLS连接
context = ssl.create_default_context()
context.set_alpn_protocols(['h2'])
sock = socket.create_connection(('www.example.com', 443))
sock = context.wrap_socket(sock, server_hostname='www.example.com')
# 验证ALPN
if sock.selected_alpn_protocol() != 'h2':
raise RuntimeError('HTTP/2不支持')
# 初始化HTTP/2连接
config = h2.config.H2Configuration(client_side=True)
conn = h2.connection.H2Connection(config=config)
conn.initiate_connection()
sock.sendall(conn.data_to_send())
# 发送GET请求
stream_id = conn.get_next_available_stream_id()
request_headers = [
(':method', 'GET'),
(':path', '/'),
(':scheme', 'https'),
(':authority', 'www.example.com'),
('user-agent', 'Python HTTP/2 Client'),
]
conn.send_headers(stream_id, request_headers, end_stream=True)
sock.sendall(conn.data_to_send())
# 接收响应
while True:
data = sock.recv(65536)
if not data:
break
events = conn.receive_data(data)
for event in events:
if isinstance(event, h2.events.ResponseReceived):
print('响应头部:')
for name, value in event.headers:
print(f' {name}: {value}')
elif isinstance(event, h2.events.DataReceived):
print('响应数据:', event.data.decode('utf-8'))
conn.acknowledge_received_data(event.flow_controlled_length, stream_id)
elif isinstance(event, h2.events.StreamEnded):
print('流结束')
sock.sendall(conn.data_to_send())
break
sock.sendall(conn.data_to_send())
服务器响应(Nginx配置)
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
# HTTP/2配置
http2_max_concurrent_streams 128;
http2_max_field_size 4k;
http2_max_header_size 16k;
# 服务器推送
location = /index.html {
http2_push /style.css;
http2_push /script.js;
http2_push /logo.png;
}
# 优先级提示
location ~* \.(css|js)$ {
add_header Link "</style.css>; rel=preload; as=style" always;
}
}
性能优化最佳实践
1. 域名分片不再必要
HTTP/1.1: 需要域名分片
- static1.example.com
- static2.example.com
- static3.example.com
目的: 突破浏览器6连接限制
HTTP/2: 单域名即可
- example.com
多路复用已解决并发问题
2. 资源合并策略调整
HTTP/1.1: 合并所有CSS/JS
bundle.css (500KB)
HTTP/2: 按页面分割
home.css (50KB)
about.css (30KB)
contact.css (20KB)
优势:
- 更好的缓存利用率
- 按需加载
- 更快的首屏渲染
3. 内联资源重新考虑
HTTP/1.1: 内联小图片
<img src="data:image/png;base64,iVBOR...">
HTTP/2: 使用独立文件
<img src="/icons/logo.png">
原因:
- 可以被缓存
- 并行加载不阻塞
- 服务器推送优化
4. 合理使用服务器推送
推荐推送:
✅ 关键CSS
✅ 关键JavaScript
✅ 网页字体
不推荐推送:
❌ 大文件
❌ 可能已缓存的资源
❌ 不是所有页面都需要的资源
检查缓存:
使用Cookie或LocalStorage标记
避免推送已缓存资源
调试和监控
Chrome DevTools
1. 打开DevTools
2. Network标签
3. 查看Protocol列
4. 筛选"h2"查看HTTP/2请求
查看推送资源:
Initiator列显示"Push / Other"
Wireshark抓包
# 过滤HTTP/2流量
tls.handshake.extensions_alpn_str == "h2"
# 查看帧
http2.frame
# 查看SETTINGS帧
http2.frame.type == 4
curl测试
# HTTP/2请求
curl --http2 -I https://www.example.com
# 查看详细信息
curl --http2 -v https://www.example.com
# 强制HTTP/2先验知识(明文)
curl --http2-prior-knowledge http://localhost:8080
常见问题
1. HTTP/2是否总是更快?
不一定!
更快的场景:
✅ 多个小资源
✅ 高延迟网络
✅ 复杂页面
可能更慢的场景:
❌ 单个大文件下载
❌ 服务器配置不当
❌ 过度使用服务器推送
2. 是否需要改代码?
前端代码: 无需修改
API调用: 无需修改
语义保持: 完全兼容HTTP/1.1
需要调整:
- 资源打包策略
- 域名分片移除
- CDN配置
3. 如何回退到HTTP/1.1?
浏览器自动处理:
1. 尝试ALPN协商HTTP/2
2. 如果失败,使用HTTP/1.1
3. 对用户透明
强制HTTP/1.1:
curl --http1.1 https://example.com
总结
HTTP/2是Web性能的重大飞跃:
- 信: 准确实现多路复用
- 达: 清晰的二进制协议
- 雅: 优雅的向后兼容
核心优势:
- 🚀 多路复用 - 单连接并发
- 📦 头部压缩 - 减少开销
- 🔄 服务器推送 - 主动优化
- ⚖️ 流优先级 - 智能调度
适用场景:
- 现代Web应用
- API服务
- 高延迟网络
- 移动应用
相关RFC:
- RFC 7540: HTTP/2(本文档)
- RFC 7541: HPACK头部压缩
- RFC 9113: HTTP/2(2022更新)
- RFC 9114: HTTP/3