OkHttp 超时机制总览

聊到 OkHttp 超时,很多人第一反应就是「设置个 connectTimeout 完事」。但我在线上踩过几次坑之后才发现——三个超时参数各管一摊,理解不清楚,启动阶段直接给你崩一个 SocketTimeoutException 或者 ConnectException,用户连首页都刷不出来。

这章我把 OkHttp 的三种超时——connectTimeout、readTimeout、writeTimeout——掰开揉碎讲清楚。说白了就是三个独立的计时器,分别在连接、读取、写入三个阶段生效。

OkHttp 超时机制总览图 connectTimeout 建立 TCP 连接 readTimeout 从 Socket 读取数据 writeTimeout 向 Socket 写入数据 阶段1:连接建立 阶段2:数据读取 阶段3:数据写入 ConnectException 超时抛异常 SocketTimeoutException 读取超时 SocketTimeoutException 写入超时 推荐启动阶段初始值 connect: 5s | read: 10s | write: 10s (根据网络状况可微调) 常见异常组合 连接超时 → 重试 读写超时 → 可能需要调整 Buffer

1. connectTimeout:连接建立的生死线

connectTimeout 负责的是 TCP 三次握手阶段。从你调用 OkHttpClient.newCall(request).execute() 开始,到与服务器建立 TCP 连接,这中间的时间不能超过这个值。

我自己的习惯是,移动端启动阶段的 connectTimeout 设到 5 秒。你想想看,用户刚打开 App,如果首页接口因为网络不好连了十几秒才报错,体验直接崩掉。而且启动阶段本身就有很多并发请求,一个卡死会拖慢整体时序。

🍎 典型场景: App 启动时请求配置接口,如果用户刚从地铁出来,信号弱但还没完全断开。connectTimeout 设得太长(比如 30 秒),用户就会看到大白屏;设得太短(比如 1 秒),正常网络下可能也会误报失败。我的经验是 5 秒是一个折中值。

2. readTimeout:等数据时别让我等太久

readTimeout 是 从 Socket 读取下一个字节的超时。注意,不是整个请求的总耗时,而是两次读取之间的间隔。如果服务器慢慢吞吞一次只发一点数据,readTimeout 能帮你及时断开连接。

我在项目中遇到过一个问题:某个后端接口偶尔会卡住,返回的数据头到了,但 Body 迟迟不发。结果 OkHttp 一直在等 readTimeout,默认 10 秒后再报错。用户反馈「点按钮后转圈 10 秒才提示失败」。后来我建议后端加心跳或者改短 readTimeout——但注意,太短(比如 1 秒)对直播、大文件下载这些场景不友好。

场景 推荐 readTimeout 备注
启动时小数据接口5~10s配置、、广告等
图片/文件下载30s 或更长配合断点续传
长轮询/SSE0(不超时)需要单独创建 OkHttpClient

3. writeTimeout:发数据时别磨蹭

writeTimeout 和 readTimeout 类似,区别在于它监控的是 写入 Socket 的动作。比如你要 POST 一个大 JSON body,如果网络拥堵导致数据迟迟发不出去,writeTimeout 就会触发。

我个人感觉,大部分 App 的写入数据量都不大(几百字节到几十 KB),所以 writeTimeout 用默认值 10 秒一般没问题。但如果你遇到上传大文件或者大量日志的场景——记得单独配。

⚠️ 我曾经踩过的坑: 项目里用 OkHttp 上传崩溃日志(文件可能到 5MB),结果写超时了。排查才发现连接没复用,每次都新建 TCP 连接,握手 + 写入耗时长。后来改成连接复用 + writeTimeout 提到 30 秒才解决。

4. 三个超时的协同与优先级

三者的关系不是「按顺序结束」,而是 同时独立计时。

  • 连接建立阶段:connectTimeout 先跑,一旦连接成功,它就失效了。
  • 正在发送请求体时:writeTimeout 开始计时,读取响应时:readTimeout 开始计时。
  • 如果连接还没建立,但 connectTimeout 到了,直接抛 ConnectException(消息 "Failed to connect to ...")。
  • 如果连接已建立,读写过程中超时了,抛 SocketTimeoutException(消息 "timeout")。

所以不要以为只配 connectTimeout 就安全了。我之前见过一个项目,connectTimeout 配了 10 秒,但 readTimeout 没改(默认 0 即无限)。结果网络波动时连接成功但读取卡死,用户杀掉 App 重试都没用。

5. 启动阶段的特殊考量

启动阶段跟普通页面请求不太一样。比如 App 冷启动时,DNS 缓存还没建立,SSL 握手可能也需要额外时间。我个人建议:

  1. 五个关键接口单独设置超时:别用统一全局配置。比如 config 接口设 5s,而用户信息接口可以放宽到 10s。
  2. 第一次连接失败后尽快重试:我习惯在超时回调里做一次「快速重试」,间隔 1 秒,最多 2 次。
  3. 监控超时异常的上报:建议用 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();

嗯,上面这段代码看着简单,但背后的原理要记牢。三个超时对应三个不同阶段,各自独立。调试时一看到超时崩溃,先定位是哪个阶段出了问题,再动手调参或修网络——方向对了,解决起来就快得多。