第五章:Okhttp HTTP/2多路复用实现

221 阅读5分钟

5.1 HTTP/2核心架构

5.1.1 HTTP/2 vs HTTP/1.1 关键差异

deepseek_mermaid_20250717_5f5dc8.png

5.1.2 HTTP/2核心组件

// 源码路径: okhttp3/internal/http2/Http2Connection.kt
class Http2Connection(
    val client: Boolean,
    val listener: Listener,
    val pushObserver: PushObserver
) {
    // 流管理
    val streams = mutableMapOf<Int, Http2Stream>()
    
    // 帧读写器
    val reader: Http2Reader
    val writer: Http2Writer
    
    // 流量控制
    var initialWindowSize = DEFAULT_INITIAL_WINDOW_SIZE
    var peerSettings = Settings()
}

5.2 二进制分帧层

5.2.1 帧结构定义

// 源码路径: okhttp3/internal/http2/Frame.kt
class Frame {
    val streamId: Int     // 流标识符(31位无符号整数)
    val type: Byte        // 帧类型(DATA/HEADERS等)
    val flags: Byte       // 帧标志(END_STREAM等)
    val payload: Buffer   // 帧载荷数据
}

5.2.2 帧类型详解

类型值帧名称功能描述
0x0DATA传输应用数据
0x1HEADERS打开流并传输HTTP头部
0x2PRIORITY设置流优先级
0x3RST_STREAM立即终止流
0x4SETTINGS协商连接参数
0x5PUSH_PROMISE服务器推送资源
0x6PING测试连接活性
0x7GOAWAY关闭连接通知
0x8WINDOW_UPDATE更新流量控制窗口

5.2.3 帧编码过程

// 源码路径: okhttp3/internal/http2/Http2Writer.kt
fun frameHeader(
    streamId: Int,
    length: Int,
    type: Int,
    flags: Int
) {
    sink.writeInt(length and 0xffffff)  // 24位长度
    sink.writeByte(type.toByte())       // 8位类型
    sink.writeByte(flags.toByte())      // 8位标志
    sink.writeInt(streamId and 0x7fffffff) // 31位流ID
}

// 发送HEADERS帧
fun headers(
    streamId: Int,
    outHeaders: List<Header>,
    flushHeaders: Boolean
) {
    // 1. 压缩头部
    hpackWriter.writeHeaders(outHeaders)
    
    // 2. 写入帧头
    frameHeader(streamId, hpackBuffer.size, TYPE_HEADERS, FLAG_END_HEADERS)
    
    // 3. 写入压缩后的头部
    sink.write(hpackBuffer, hpackBuffer.size)
}

5.3 多路复用实现

5.3.1 流状态机

deepseek_mermaid_20250717_04e9af.png

5.3.2 流创建与复用

// 源码路径: okhttp3/internal/http2/Http2Connection.kt
public Http2Stream newStream(
    List<Header> requestHeaders, 
    boolean out, 
    boolean in
) : Http2Stream {
    val streamId: Int
    var stream: Http2Stream
    
    synchronized(writer) {
        synchronized(this) {
            // 分配流ID(奇数表示客户端发起的流)
            streamId = nextStreamId
            nextStreamId += 2
            
            // 创建新流
            stream = Http2Stream(streamId, this, out, in, requestHeaders)
            streams[streamId] = stream
        }
        
        // 发送HEADERS帧
        writer.headers(streamId, requestHeaders)
    }
    
    return stream
}

5.3.3 多路复用优势

// 伪代码:HTTP/1.1 vs HTTP/2请求对比
void http1Example() {
    // 串行请求(潜在队头阻塞)
    Response response1 = client.newCall(request1).execute();
    Response response2 = client.newCall(request2).execute();
}

void http2Example() {
    // 并行请求(多路复用)
    Call call1 = client.newCall(request1);
    Call call2 = client.newCall(request2);
    
    // 异步执行
    call1.enqueue(callback1);
    call2.enqueue(callback2);
}

5.4 头部压缩(HPACK)

5.4.1 HPACK压缩原理

deepseek_mermaid_20250717_cee605.png

5.4.2 HPACK实现

// 源码路径: okhttp3/internal/http2/Hpack.kt
class Writer(
    private val out: BufferedSink,
    private val useCompression: Boolean
) {
    // 静态表(61个预定义头部)
    private val staticTable = StaticTable()
    
    // 动态表(FIFO队列)
    private val dynamicTable = DynamicTable()
    
    fun writeHeaders(headers: List<Header>) {
        for (header in headers) {
            // 1. 尝试查找完整匹配
            val index = staticTable.index(header) ?: dynamicTable.index(header)
            
            if (index != -1) {
                // 使用索引表示
                writeIndex(index)
            } else {
                // 2. 尝试查找名称匹配
                val nameIndex = staticTable.nameIndex(header.name) 
                    ?: dynamicTable.nameIndex(header.name)
                
                if (nameIndex != -1) {
                    // 增量索引表示
                    writeLiteral(header, nameIndex)
                } else {
                    // 3. 完整字面值表示
                    writeLiteral(header, 0)
                }
            }
        }
    }
}

5.5 流量控制

5.5.1 滑动窗口机制

// 源码路径: okhttp3/internal/http2/Http2Stream.kt
class Http2Stream(
    val id: Int,
    val connection: Http2Connection
) {
    // 接收窗口(客户端接收数据)
    var bytesLeftInReceiveWindow: Int = connection.initialWindowSize
    
    // 发送窗口(客户端发送数据)
    var bytesLeftInSendWindow: Int = connection.initialWindowSize
    
    // 更新接收窗口
    fun receiveData(length: Int) {
        bytesLeftInReceiveWindow -= length
        
        // 窗口不足时发送WINDOW_UPDATE
        if (bytesLeftInReceiveWindow < windowUpdateThreshold) {
            connection.writeWindowUpdateLater(id, bytesLeftInReceiveWindow)
            bytesLeftInReceiveWindow = initialWindowSize
        }
    }
}

5.5.2 窗口更新流程

// 源码路径: okhttp3/internal/http2/Http2Connection.kt
fun writeWindowUpdateLater(streamId: Int, unacknowledgedBytesRead: Long) {
    taskRunner.runLater {
        val windowUpdateIncrement = unacknowledgedBytesRead - initialWindowSize
        writer.windowUpdate(streamId, windowUpdateIncrement)
    }
}

5.6 优先级与依赖管理

5.6.1 优先级树结构

deepseek_mermaid_20250717_713cb2.png

5.6.2 优先级设置

// 源码路径: okhttp3/internal/http2/Http2Stream.kt
fun setPriority(
    streamDependency: Int,
    weight: Int,
    exclusive: Boolean
) {
    // 验证权重范围
    if (weight < 1 || weight > 256) {
        throw IllegalArgumentException("weight must be between 1 and 256")
    }
    
    // 发送PRIORITY帧
    writer.priority(id, streamDependency, weight, exclusive)
}

5.6.3 带宽分配算法

// 伪代码:基于权重的带宽分配
void allocateBandwidth(List<Http2Stream> streams) {
    int totalWeight = streams.sumOf { it.weight }
    for (stream in streams) {
        double ratio = stream.weight / totalWeight.toDouble()
        int bytesToSend = (connectionWindow * ratio).toInt()
        sendData(stream, bytesToSend)
    }
}

5.7 服务器推送

5.7.1 推送流程

deepseek_mermaid_20250717_fa5d6d.png

5.7.2 推送实现

// 源码路径: okhttp3/internal/http2/PushObserver.kt
interface PushObserver {
    // 服务器推送请求
    fun onRequest(streamId: Int, request: List<Header>)
    
    // 推送响应头
    fun onHeaders(streamId: Int, responseHeaders: List<Header>)
    
    // 推送数据
    fun onData(streamId: Int, source: BufferedSource, byteCount: Int)
    
    // 推送流终止
    fun onReset(streamId: Int, errorCode: ErrorCode)
}

// 默认实现:拒绝所有推送
object PushObserver.CANCEL : PushObserver {
    override fun onRequest(streamId: Int, request: List<Header>): Boolean {
        return false // 拒绝推送
    }
}

5.8 HTTP/2连接管理

5.8.1 连接健康检查

// 源码路径: okhttp3/internal/http2/Http2Connection.kt
public boolean isHealthy(long nowNs) {
    return !shutdown 
        && (lastGoodStreamId < Integer.MAX_VALUE) 
        && (connectionDegraded || bytesLeftInWriteWindow > 0);
}

5.8.2 连接保活

// 定时发送PING帧
connection.writer().ping(false, 0, 0, null)

// 接收PING帧处理
override fun ping(ack: Boolean, payload1: Int, payload2: Int) {
    if (!ack) {
        // 回复PING
        writer.ping(true, payload1, payload2)
    } else {
        // 更新最后活动时间
        lastPingTimeNs = System.nanoTime()
    }
}

5.9 性能优化实践

5.9.1 调整窗口大小

OkHttpClient client = new OkHttpClient.Builder()
    .connectionSpecs(Arrays.asList(
        new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
            .http2Settings(new Settings.Builder()
                .set(Settings.INITIAL_WINDOW_SIZE, 16777216) // 16MB窗口
                .set(Settings.MAX_FRAME_SIZE, 16777215)       // 最大帧大小
                .build())
            .build()))
    .build();

5.9.2 启用HTTP/2优化

// 强制使用HTTP/2(如果服务器支持)
OkHttpClient client = new OkHttpClient.Builder()
    .protocols(Arrays.asList(Protocol.H2_PRIOR_KNOWLEDGE, Protocol.HTTP_1_1))
    .build();

// 监听协议升级
EventListener listener = new EventListener() {
    @Override
    public void protocolSelected(Call call, Protocol protocol) {
        Log.d("HTTP Protocol", protocol.toString());
    }
};

5.10 故障排除

5.10.1 常见HTTP/2问题

  1. 协议协商失败

    • 原因:服务器不支持HTTP/2
    • 解决:回退到HTTP/1.1
  2. 流重置(RST_STREAM)

    • 原因:客户端取消请求或超时
    • 排查:检查日志中错误码
  3. GOAWAY帧接收

    • 原因:服务器关闭连接
    • 处理:创建新连接重试请求

5.10.2 诊断工具

# 使用Wireshark抓包分析
tshark -i eth0 -Y "http2" -O http2

# 关键帧过滤
http2.type == 0x7   # GOAWAY帧
http2.type == 0x3   # RST_STREAM帧

本章小结

  1. 二进制分帧

    • 所有通信通过帧传输
    • 帧包含长度、类型、标志和流ID
    • 支持多种帧类型满足不同需求
  2. 多路复用

    • 单个连接支持多个并发流
    • 避免HTTP/1.x队头阻塞问题
    • 流可设置优先级和依赖关系
  3. 头部压缩

    • HPACK算法减少头部大小
    • 静态表和动态表结合使用
    • Huffman编码进一步压缩字符串
  4. 流量控制

    • 基于窗口的流量控制
    • 初始窗口大小65535字节
    • 通过WINDOW_UPDATE帧更新窗口
  5. 服务器推送

    • 服务器可主动推送资源
    • 客户端可拒绝推送请求
    • 减少页面加载延迟
  6. 连接管理

    • PING帧检测连接活性
    • GOAWAY帧优雅关闭连接
    • 健康检查确保连接可用

在下一章中,我们将深入分析OkHttp的缓存系统设计,包括内存缓存和磁盘缓存的实现机制,以及缓存策略的决策过程