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.1HTTP/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协议)


总结

核心要点

  1. 二进制分帧:HTTP/2使用二进制协议,效率更高
  2. 多路复用:一个TCP连接,多个Stream并行,解决队头阻塞
  3. 头部压缩:HPACK算法,压缩率70-90%
  4. 服务器推送:主动推送资源,减少请求
  5. 微服务应用:gRPC默认使用HTTP/2

下一篇预告

《HTTP/3与QUIC:基于UDP的下一代HTTP》