OkHttpClient需配置超时与心跳:设pingInterval≤45秒,readTimeout≥pingInterval,启用retryOnConnectionFailure仅对初始失败有效;WebSocket回调须切主线程,send()需校验isOpen,重连前必须cancel旧连接。

OkHttpClient必须配置超时与心跳,否则在移动网络切换或后台休眠时会静默断连,且无法自动恢复。
如何正确初始化 OkHttpClient 并启用心跳
Android 端 WebSocket 断连的头号原因是 OkHttp 默认不发心跳,而系统或中间网关会在 30–90 秒无流量后主动 kill 连接。
必须显式设置 pingInterval,且建议设为 ≤ 45 秒(避开多数运营商和 Android 省电策略的阈值):
val client = OkHttpClient.Builder()
.pingInterval(45, TimeUnit.SECONDS) // 关键:触发 OkHttp 自动发 Ping 帧
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS) // readTimeout 要 ≥ pingInterval,否则可能因等待 pong 超时关闭
.writeTimeout(10, TimeUnit.SECONDS)
.retryOnConnectionFailure(true) // 配合后续重连逻辑才生效,单设无效
.build()
-
readTimeout设太短(如 10 秒)会导致未收到服务端 Pong 就直接触发onFailure - 不要依赖
retryOnConnectionFailure(true)实现重连——它只对初始连接失败有效,对已建立连接后的断开无作用 - 如果服务端不回 Pong(比如某些 Nginx 配置或老旧网关),心跳会失效,需配合应用层心跳 + ACK 机制
WebSocketListener 中哪些回调必须做主线程切换
onOpen、onMessage、onFailure、onClosed 全部在 OkHttp 的 IO 线程回调,**不能直接更新 UI 或调用主线程敏感 API(如 LiveData.postValue()、View.setText())**。
最轻量安全的做法是用 Handler(Looper.getMainLooper()) 或 Kotlin 的 lifecycleScope.launch { ... } 切回主线程:
override fun onMessage(webSocket: WebSocket, text: String) {
mainHandler.post {
onMessageCallback(text) // 安全分发到业务层
}
}
-
onClosing和onClosed不一定成对出现;onClosing是服务端发起关闭,onClosed才代表本地连接真正释放 -
onFailure可能携带null response(如 DNS 失败、TLS 握手失败),需判空再取response.code() - 不要在
onFailure里直接client.newWebSocket(...)—— 会创建新 client 实例,导致资源泄漏
为什么 webSocket.send() 有时会静默失败
OkHttp 的 send() 是异步非阻塞的,返回 true 仅表示“已入队”,不代表已发出或对方已收到。常见静默失败场景:
- 连接实际已断开但本地状态未更新(
webSocket对象仍非 null) - 发送时连接正处于
CLOSING状态(onClosing已触发但onClosed还没来) - 消息过大(默认限制约 1MB),触发
IOException: frame length exceeds maximum
安全发送写法:
fun safeSend(msg: String): Boolean {
return if (webSocket != null && webSocket.isOpen) {
webSocket.send(msg)
} else {
false
}
}
注意:webSocket.isOpen 是 OkHttp 提供的瞬时状态检查,但它不保证下一毫秒仍有效——仅作快速过滤,不能替代重连兜底。
重连不能只靠“断了就重连”,得区分失败原因
盲目重试会让问题更糟:DNS 永久失效时每秒重连会打爆日志;TLS 证书过期时重连一百次也没用。
应在 onFailure 中根据 Throwable 类型和 response 状态码分级处理:
-
UnknownHostException/SocketTimeoutException→ 可重试,指数退避(1s → 2s → 4s) -
SSLHandshakeException→ 记录并停止重连(大概率证书或系统时间错误) -
response?.code() == 401→ 先刷新 token,再重连(需提前注入 token 刷新能力) - 无
response且t is IOException→ 网络层中断,可立即重试一次,再退避
最关键一点:重连前必须 webSocket?.cancel() 或 close(1000, "reconnect"),否则旧连接残留可能占用 fd 导致新连接失败。
重连逻辑本身不难写,难的是判断“现在到底该不该重”。很多崩溃和卡顿,都源于把网络不可达当成临时抖动来反复重试。











