HTTP/1.1的问题
队头阻塞(Head-of-Line Blocking)
HTTP/1.1管道化(Pipelining):
客户端:GET /1.js → GET /2.js → GET /3.js
↓ ↓ ↓
服务器:200 OK 等待... 等待...
(1.js)
问题:第一个响应慢,阻塞后续响应
解决方案的局限
方案1:并发多个TCP连接
浏览器同时开6-8个TCP连接:
连接1: GET /1.js
连接2: GET /2.js
连接3: GET /3.js
...
问题:
- TCP连接数有限
- 每个连接都要握手(延迟)
- 拥塞控制独立(带宽利用率低)
方案2:域名分片
static1.example.com
static2.example.com
static3.example.com
每个域名6个连接 × 3个域名 = 18个连接
问题:
- DNS解析开销
- TLS握手开销
- 服务器资源浪费
HTTP/2核心特性
1. 二进制分帧
HTTP/1.x:文本协议
GET /api/users HTTP/1.1\r\n
Host: api.example.com\r\n
\r\n
HTTP/2:二进制协议
[帧头部(9字节)]
+-----------------------------------------------+
| Length (24) | Type (8) | Flags (8) | Stream ID (31) |
+-----------------------------------------------+
[帧负载]
帧类型:
- HEADERS:头部帧
- DATA:数据帧
- SETTINGS:设置帧
- PING:心跳帧
- GOAWAY:关闭连接帧
2. 多路复用(Multiplexing)
核心思想:一个TCP连接,多个Stream并行
客户端 服务器
| |
| Stream 1: GET /1.js (HEADERS) |
|------------------------------>|
| |
| Stream 3: GET /2.js (HEADERS) |
|------------------------------>|
| |
| Stream 5: GET /3.js (HEADERS) |
|------------------------------>|
| |
| Stream 1: 200 OK (DATA) |
|<------------------------------|
| |
| Stream 3: 200 OK (DATA) |
|<------------------------------|
| |
| Stream 5: 200 OK (DATA) |
|<------------------------------|
一个TCP连接,3个请求并行!
无队头阻塞!
Stream ID:
- 奇数:客户端发起
- 偶数:服务器发起
- 0:连接控制帧
3. 头部压缩(HPACK)
HTTP/1.1问题:头部冗余
每个请求都携带相同的头部:
GET /api/users
Host: api.example.com
User-Agent: Mozilla/5.0...
Accept: application/json
Cookie: sessionId=abc123...
GET /api/orders
Host: api.example.com # 重复
User-Agent: Mozilla/5.0... # 重复
Accept: application/json # 重复
Cookie: sessionId=abc123... # 重复
HTTP/2 HPACK:
[静态表]:常见头部预定义索引
:method GET → 索引2
:path / → 索引4
host → 索引38
[动态表]:会话期间构建
首次请求:发送完整头部,加入动态表
后续请求:只发送索引号
示例:
首次:Host: api.example.com(加入动态表索引62)
后续:62(引用动态表)
压缩率:70-90%!
4. 服务器推送(Server Push)
客户端请求:GET /index.html
服务器推送:
PUSH_PROMISE: /style.css
PUSH_PROMISE: /script.js
服务器主动发送:
200 OK /style.css
200 OK /script.js
200 OK /index.html
客户端解析HTML时,CSS和JS已经在缓存中!
HTTP/2 vs HTTP/1.1
| 特性 | HTTP/1.1 | HTTP/2 |
|---|---|---|
| 协议格式 | 文本 | 二进制 |
| 多路复用 | ❌(需要多个连接) | ✅(一个连接) |
| 队头阻塞 | ✅(应用层) | ❌(应用层解决) |
| 头部压缩 | ❌ | ✅(HPACK) |
| 服务器推送 | ❌ | ✅ |
| 优先级 | ❌ | ✅ |
| 流量控制 | TCP层 | HTTP/2层 + TCP层 |
性能对比:
场景:加载100个小文件
HTTP/1.1:
- 6个并发连接
- 每个连接顺序加载
- 总时间:约10秒
HTTP/2:
- 1个连接
- 100个Stream并行
- 总时间:约2秒
提升5倍!
Spring Boot HTTP/2配置
Tomcat HTTP/2
# application.yml
server:
port: 8443
ssl:
enabled: true
key-store: classpath:keystore.jks
key-store-password: 123456
http2:
enabled: true # 启用HTTP/2
注意:HTTP/2通常需要HTTPS(浏览器限制)
Nginx HTTP/2
server {
listen 443 ssl http2; # 启用HTTP/2
server_name api.example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_http_version 1.1; # 后端仍使用HTTP/1.1
}
}
验证HTTP/2
# curl验证
curl -I --http2 https://api.example.com
# 输出
HTTP/2 200
content-type: application/json
# Chrome开发者工具
Network → Protocol列显示 "h2"
微服务HTTP/2实战
案例1:gRPC使用HTTP/2
gRPC默认基于HTTP/2:
// gRPC服务端
@GrpcService
public class UserServiceImpl extends UserServiceGrpc.UserServiceImplBase {
@Override
public void getUser(GetUserRequest request, StreamObserver<User> responseObserver) {
User user = userService.findById(request.getId());
responseObserver.onNext(user);
responseObserver.onCompleted();
}
// HTTP/2流式响应
@Override
public void listUsers(Empty request, StreamObserver<User> responseObserver) {
List<User> users = userService.findAll();
// 服务器流:多个响应通过一个Stream发送
users.forEach(user -> {
responseObserver.onNext(user);
});
responseObserver.onCompleted();
}
}
// gRPC客户端
ManagedChannel channel = ManagedChannelBuilder
.forAddress("user-service", 9090)
.usePlaintext() // 或 useTransportSecurity() 使用HTTPS
.build();
UserServiceGrpc.UserServiceBlockingStub stub =
UserServiceGrpc.newBlockingStub(channel);
// HTTP/2多路复用:一个连接,多个RPC并行
User user1 = stub.getUser(GetUserRequest.newBuilder().setId(1).build());
User user2 = stub.getUser(GetUserRequest.newBuilder().setId(2).build());
案例2:Feign HTTP/2(实验性)
# application.yml
feign:
okhttp:
enabled: true # 使用OkHttp支持HTTP/2
client:
config:
user-service:
url: https://user-service:8443
@FeignClient(name = "user-service")
public interface UserClient {
@GetMapping("/api/users/{id}")
User getUser(@PathVariable Long id);
}
// Feign配置
@Configuration
public class FeignConfig {
@Bean
public OkHttpClient okHttpClient() {
return new OkHttpClient.Builder()
.protocols(Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1)) # 支持HTTP/2
.build();
}
}
案例3:服务器推送(SSE替代)
HTTP/2服务器推送:推送静态资源
服务器推送(Server-Sent Events):推送动态数据
// Spring Boot SSE
@GetMapping(value = "/api/notifications", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<Notification>> streamNotifications() {
return Flux.interval(Duration.ofSeconds(1))
.map(seq -> ServerSentEvent.<Notification>builder()
.id(String.valueOf(seq))
.event("notification")
.data(new Notification("消息" + seq))
.build());
}
// 客户端(JavaScript)
const eventSource = new EventSource('/api/notifications');
eventSource.onmessage = (event) => {
console.log('收到通知:', event.data);
};
HTTP/2优化建议
1. 不需要域名分片
HTTP/1.1:
static1.example.com
static2.example.com
static3.example.com
HTTP/2:
static.example.com # 一个域名足够
2. 减少资源合并
HTTP/1.1:需要合并
bundle.js = a.js + b.js + c.js + d.js
bundle.css = a.css + b.css + c.css
HTTP/2:可以单独加载
a.js, b.js, c.js, d.js(并行加载,缓存更好)
3. 启用服务器推送
# Nginx服务器推送
server {
location = /index.html {
http2_push /style.css;
http2_push /script.js;
}
}
TCP层队头阻塞
HTTP/2仍然存在TCP层队头阻塞:
HTTP/2多路复用(应用层无阻塞):
Stream 1: [帧1] [帧2] [帧3]
Stream 2: [帧1] [帧2] [帧3]
↓
TCP传输(TCP层可能阻塞):
[帧1][帧1][帧2]【丢失】[帧2][帧3][帧3]
↑
TCP丢包重传,阻塞所有Stream!
解决方案:HTTP/3(基于UDP的QUIC协议)
总结
核心要点
- 二进制分帧:HTTP/2使用二进制协议,效率更高
- 多路复用:一个TCP连接,多个Stream并行,解决队头阻塞
- 头部压缩:HPACK算法,压缩率70-90%
- 服务器推送:主动推送资源,减少请求
- 微服务应用:gRPC默认使用HTTP/2
下一篇预告
《HTTP/3与QUIC:基于UDP的下一代HTTP》