OkHttp 重试机制基础
在OkHttp的设计哲学中,网络恢复能力是传输层的内建能力。默认情况下,OkHttp会在特定条件下自动重试失败的请求,减少因瞬时网络抖动导致的调用失败。理解该机制的默认行为、控制开关以及精确的触发条件,是后续在Retrofit层实现自定义重试策略的前提。
学习目标:掌握OkHttp默认重试行为、
retryOnConnectionFailure配置的作用范围、区分可重试与不可重试的失败场景,为后续覆盖Retrofit拦截器重试、合并延迟重试做好准备。4.1 默认重试行为
OkHttpClient在构建时不显式配置任何重试相关参数时,其默认行为如下:
- 启用状态:默认开启重试(
retryOnConnectionFailure=true)。 - 重试次数:最多重试 1 次(即总共执行2次请求,包括原始请求和一次重试)。这是一个硬编码上限,不可通过配置修改。
- 触发场景:仅当连接建立阶段(TCP握手、TLS握手、HTTP连接复用)发生失败时触发。
- 不触发场景:请求已发送并收到部分响应体、IO异常发生在读取应用层数据阶段、路由选择失败后无可选备用路由。
OkHttp的重试发生在 连接池 和 路由选择器 层面。当某个连接的建立失败时,OkHttp会检查是否存在其他备选路由(例如,域名解析出的多个IP、代理配置等)。若存在备选路由,OkHttp会使用新路由重新发起连接,这个过程对上层应用(如Retrofit)完全透明。
4.2 retryOnConnectionFailure 配置
该配置项提供全局开关来控制默认重试行为的启用与否。配置方式位于构建OkHttpClient时:
OkHttpClient client = new OkHttpClient.Builder()
.retryOnConnectionFailure(true) // 或 false
.build();
| 配置值 | 行为描述 | 适用场景 |
|---|---|---|
true |
启用默认重试:连接失败时,最多重试1次使用备选路由。 | 公网服务、不稳定WiFi、需要容忍TCP层瞬断的生产环境。 |
false |
禁用默认重试:任何连接失败都直接抛出IOException,不尝试备选路由。 |
内网高可靠通信、对幂等性有严格要求的场景、自定义拦截器层已经实现了更精细的重试策略时。 |
重要警告:即使在
retryOnConnectionFailure(false) 情况下,OkHttp仍然会执行 一次路由选择回退(比如从IPv6切换到IPv4),但这不归类为重试,而是路由层面的一次尝试。将 retryOnConnectionFailure 设为 false 并不会阻止OkHttp在每个路由上尝试连接,只是禁止在 所有已选路由都失败 后重新选择新路由并再次重试连接。4.3 重试触发条件与排除条件
重试触发条件(满足任一即触发):
- 连接失败(TCP连接建立阶段,如
ConnectException、SocketTimeoutException连接超时)。 - TLS/SSL握手失败(如证书错误、协议版本不匹配导致的
SSLHandshakeException)。 - 连接被重置(如收到RST包导致的
IOException)。 - HTTP/2 流错误或GoAway帧。
不触发重试的场景(直接向上层抛出异常):
- 请求体已经通过
RequestBody.writeTo()开始发送,且发送了部分或全部数据。(非幂等请求自动跳过重试) - 应用层读写IO异常(如
SocketException: Socket closed发生在读取响应体期间)。 - 超时异常中属于
SocketTimeoutException: Read timed out或SocketTimeoutException: Write timed out(仅连接超时connect timed out会触发重试)。 - 路由选择阶段,所有备选路由均已尝试过且均失败。(默认重试次数已耗尽)。
4.4 默认重试流程可视化
4.5 如何验证默认重试行为
通过本地启动一个仅监听端口的简易服务(如Python一行命令:python -m http.server 8080)。然后在客户端配置retryOnConnectionFailure(true),并在OkHttp的EventListener中重写connectStart、connectEnd、connectionFailure回调。日志中应观察到两次连接尝试,且第二次尝试使用不同的IP(若域名解析出多个IP)或不同的代理。
OkHttpClient client = new OkHttpClient.Builder()
.eventListener(new EventListener() {
@Override
public void connectStart(Call call, InetSocketAddress inetSocketAddress, Proxy proxy) {
System.out.println("[重试日志] 开始连接: " + inetSocketAddress);
}
@Override
public void connectFailed(Call call, InetSocketAddress inetSocketAddress, Proxy proxy, Protocol protocol, IOException ioe) {
System.out.println("[重试日志] 连接失败: " + inetSocketAddress + " 原因:" + ioe);
}
})
.retryOnConnectionFailure(true)
.build();