OkHttp 责任链模式与网络耗时监听分析

6 阅读3分钟

OkHttp 责任链模式与网络耗时监听分析

一、责任链模式在 OkHttp 中的体现

1.1 拦截器链(Interceptor Chain)

OkHttp 的拦截器链是典型的责任链模式:每个拦截器处理请求后,可选择将请求交给下一个拦截器,或直接返回响应。

Request ──→ Interceptor1 ──→ Interceptor2 ──→ ... ──→ CallServerInterceptor ──→ 真实网络
                │                   │
                └── chain.proceed() ──┘

RealInterceptorChain.proceed() 核心逻辑:

// RealInterceptorChain.java
@Override public Response proceed(Request request) throws IOException {
    RealInterceptorChain next = new RealInterceptorChain(interceptors, ..., index + 1, ...);
    Interceptor interceptor = interceptors.get(index);
    Response response = interceptor.intercept(next);  // 当前拦截器处理
    return response;
}

每个拦截器内部调用 chain.proceed(request) 时,会触发下一个拦截器,形成递归调用链。

1.2 责任链与 EventListener 的关系

机制职责调用时机
Interceptor修改请求/响应、缓存、重试等业务逻辑在拦截器链的 intercept()
EventListener观测连接生命周期、耗时、异常,不修改数据在 RealConnection、StreamAllocation 等底层组件中

EventListener 与拦截器链并行工作:拦截器处理业务,EventListener 只做观测,两者互不干扰。


二、EventListener 事件与网络耗时对应关系

2.1 事件时序(成功场景)

callStart
    │
    ├── dnsStart ──→ dnsEnd                    【DNS 解析耗时】
    │
    ├── connectStart ──→ connectEnd             【TCP 连接耗时】
    │       │
    │       └── (HTTPS) secureConnectStart ──→ secureConnectEnd   【TLS 握手 + 证书校验耗时】
    │
    ├── connectionAcquired
    │
    ├── requestHeadersStart ──→ requestHeadersEnd    【写请求头耗时】
    ├── requestBodyStart ──→ requestBodyEnd          【写请求体耗时】
    ├── responseHeadersStart ──→ responseHeadersEnd  【读响应头耗时】
    ├── responseBodyStart ──→ responseBodyEnd         【读响应体耗时】
    │
    ├── connectionReleased
    │
    └── callEnd

2.2 事件与耗时计算

阶段Start 事件End 事件说明
DNSdnsStartdnsEnd域名解析,连接池复用则可能不触发
TCP 连接connectStartconnectEnd建立 Socket,不含 TLS
TLS 握手secureConnectStartsecureConnectEnd含证书校验、密钥交换
请求头requestHeadersStartrequestHeadersEnd写入 HTTP 头
请求体requestBodyStartrequestBodyEnd写入 body(如有)
响应头responseHeadersStartresponseHeadersEnd读取 HTTP 头(TTFB 近似)
响应体responseBodyStartresponseBodyEnd读取 body

2.3 证书/握手异常时的回调

源码位置:RealConnection.connect()

} catch (IOException e) {
    // ... 清理资源 ...
    eventListener.connectFailed(call, route.socketAddress(), route.proxy(), null, e);
    // ...
}

establishProtocol() 中的 TLS 流程:

eventListener.secureConnectStart(call);
connectTls(connectionSpecSelector);   // 可能抛出 SSLHandshakeException、SSLPeerUnverifiedException
eventListener.secureConnectEnd(call, handshake);  // 仅成功时执行
异常类型触发时机EventListener 回调
SSLHandshakeException证书链校验失败、协议版本不匹配、密码套件协商失败等connectFailed(call, ..., ioe)
SSLPeerUnverifiedExceptionHostname 校验失败、CertificatePinner 校验失败connectFailed(call, ..., ioe)
SocketTimeoutException连接/读/写超时connectFailedcallFailed

注意:部分客户端证书过期场景下,connectFailed 可能不触发,仅 callFailed 被调用(OkHttp Issue #6115)。


三、connectTls 中的证书校验流程

// RealConnection.connectTls()
sslSocket.startHandshake();                    // 1. TLS 握手,可抛 SSLHandshakeException
Handshake unverifiedHandshake = Handshake.get(sslSocket.getSession());

if (!address.hostnameVerifier().verify(...)) {  // 2. 主机名校验
    throw new SSLPeerUnverifiedException("Hostname ... not verified");
}

address.certificatePinner().check(...);         // 3. 证书钉校验
步骤可能异常含义
startHandshake()SSLHandshakeException证书链无效、过期、根证书不受信任、协议/密码套件不匹配
hostnameVerifier.verify()SSLPeerUnverifiedException证书 CN/SAN 与请求域名不匹配
certificatePinner.check()SSLPeerUnverifiedException证书指纹与配置的 Pin 不一致

四、实现网络耗时监听器

通过继承 EventListener 并实现 EventListener.Factory(每个 Call 独立实例,避免并发问题),可统计各阶段耗时并捕获证书异常。可参考 EventListener 各回调方法自行实现。


五、使用注意事项

注意点说明
EventListener 必须轻量文档要求:不能做 IO、不能抛异常、不能阻塞,建议异步上报
Factory 与并发每个 Call 一个 Listener 实例,避免多 Call 共享状态导致数据错乱
connectFailed 的 ioe可据此区分 SSLHandshakeException、SSLPeerUnverifiedException 等证书问题
连接池复用复用连接时 dns/connect/secureConnect 可能不触发,只会有 request/response 事件