OkHttp 超时机制总览
聊到 OkHttp 超时,很多人第一反应就是「设置个 connectTimeout 完事」。但我在线上踩过几次坑之后才发现——三个超时参数各管一摊,理解不清楚,启动阶段直接给你崩一个 SocketTimeoutException 或者 ConnectException,用户连首页都刷不出来。
这章我把 OkHttp 的三种超时——connectTimeout、readTimeout、writeTimeout——掰开揉碎讲清楚。说白了就是三个独立的计时器,分别在连接、读取、写入三个阶段生效。
1. connectTimeout:连接建立的生死线
connectTimeout 负责的是 TCP 三次握手阶段。从你调用 OkHttpClient.newCall(request).execute() 开始,到与服务器建立 TCP 连接,这中间的时间不能超过这个值。
我自己的习惯是,移动端启动阶段的 connectTimeout 设到 5 秒。你想想看,用户刚打开 App,如果首页接口因为网络不好连了十几秒才报错,体验直接崩掉。而且启动阶段本身就有很多并发请求,一个卡死会拖慢整体时序。
2. readTimeout:等数据时别让我等太久
readTimeout 是 从 Socket 读取下一个字节的超时。注意,不是整个请求的总耗时,而是两次读取之间的间隔。如果服务器慢慢吞吞一次只发一点数据,readTimeout 能帮你及时断开连接。
我在项目中遇到过一个问题:某个后端接口偶尔会卡住,返回的数据头到了,但 Body 迟迟不发。结果 OkHttp 一直在等 readTimeout,默认 10 秒后再报错。用户反馈「点按钮后转圈 10 秒才提示失败」。后来我建议后端加心跳或者改短 readTimeout——但注意,太短(比如 1 秒)对直播、大文件下载这些场景不友好。
| 场景 | 推荐 readTimeout | 备注 |
|---|---|---|
| 启动时小数据接口 | 5~10s | 配置、、广告等 |
| 图片/文件下载 | 30s 或更长 | 配合断点续传 |
| 长轮询/SSE | 0(不超时) | 需要单独创建 OkHttpClient |
3. writeTimeout:发数据时别磨蹭
writeTimeout 和 readTimeout 类似,区别在于它监控的是 写入 Socket 的动作。比如你要 POST 一个大 JSON body,如果网络拥堵导致数据迟迟发不出去,writeTimeout 就会触发。
我个人感觉,大部分 App 的写入数据量都不大(几百字节到几十 KB),所以 writeTimeout 用默认值 10 秒一般没问题。但如果你遇到上传大文件或者大量日志的场景——记得单独配。
4. 三个超时的协同与优先级
三者的关系不是「按顺序结束」,而是 同时独立计时。
- 连接建立阶段:connectTimeout 先跑,一旦连接成功,它就失效了。
- 正在发送请求体时:writeTimeout 开始计时,读取响应时:readTimeout 开始计时。
- 如果连接还没建立,但 connectTimeout 到了,直接抛
ConnectException(消息 "Failed to connect to ...")。 - 如果连接已建立,读写过程中超时了,抛
SocketTimeoutException(消息 "timeout")。
所以不要以为只配 connectTimeout 就安全了。我之前见过一个项目,connectTimeout 配了 10 秒,但 readTimeout 没改(默认 0 即无限)。结果网络波动时连接成功但读取卡死,用户杀掉 App 重试都没用。
5. 启动阶段的特殊考量
启动阶段跟普通页面请求不太一样。比如 App 冷启动时,DNS 缓存还没建立,SSL 握手可能也需要额外时间。我个人建议:
- 五个关键接口单独设置超时:别用统一全局配置。比如 config 接口设 5s,而用户信息接口可以放宽到 10s。
- 第一次连接失败后尽快重试:我习惯在超时回调里做一次「快速重试」,间隔 1 秒,最多 2 次。
- 监控超时异常的上报:建议用
EventListener抓超时事件,方便分析哪类接口容易出问题。
SocketTimeoutException 是「Read timed out」还是「Write timed out」。前者多半是后端慢,后者多半是客户端发送数据问题,查对方向能省很多时间。
// 启动阶段推荐的 OkHttpClient 配置示例
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
.build();
嗯,上面这段代码看着简单,但背后的原理要记牢。三个超时对应三个不同阶段,各自独立。调试时一看到超时崩溃,先定位是哪个阶段出了问题,再动手调参或修网络——方向对了,解决起来就快得多。