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 事件 | 说明 |
|---|---|---|---|
| DNS | dnsStart | dnsEnd | 域名解析,连接池复用则可能不触发 |
| TCP 连接 | connectStart | connectEnd | 建立 Socket,不含 TLS |
| TLS 握手 | secureConnectStart | secureConnectEnd | 含证书校验、密钥交换 |
| 请求头 | requestHeadersStart | requestHeadersEnd | 写入 HTTP 头 |
| 请求体 | requestBodyStart | requestBodyEnd | 写入 body(如有) |
| 响应头 | responseHeadersStart | responseHeadersEnd | 读取 HTTP 头(TTFB 近似) |
| 响应体 | responseBodyStart | responseBodyEnd | 读取 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) |
| SSLPeerUnverifiedException | Hostname 校验失败、CertificatePinner 校验失败 | connectFailed(call, ..., ioe) |
| SocketTimeoutException | 连接/读/写超时 | connectFailed 或 callFailed |
注意:部分客户端证书过期场景下,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 事件 |