如何在 Android 中实现 WebSocket 的自动重连机制?
解读
面试官问“自动重连”时,真正想考察的是:
- 对 WebSocket 全生命周期(OPEN → MESSAGE → ERROR/CLOSE → 重连)的闭环掌控;
- 对 Android 平台特性的适配:进程存活、网络变化、电量优化、前后台切换;
- 对高可用代码的落地能力:线程模型、指数退避、重连风暴抑制、状态机可视化调试。
国内场景下,还要兼顾厂商 ROM 对后台网络限制、国内推送通道共存、合规收集用户网络行为等“隐形需求”。回答时切忌只贴一段“while(true) reconnect()”的伪代码,必须给出“可灰度、可监控、可止损”的工程级方案。
知识点
- WebSocket 协议关闭码 1000
1015、30004999 的语义与重连策略差异 - OkHttp 实现中的 pingInterval、closeTimeout、writeTimeout 三参数对重连的影响
- Android 9+ 对后台进程使用网络的限制(Background Network Restrictions)
- Netty、Ktor、Socket.IO 客户端的指数退避算法实现细节
- Kotlin Coroutines 的 SharedFlow/StateFlow 如何充当“连接状态机”
- WorkManager 带网络约束的周期性任务与“立即重连”的冲突解决
- 国内厂商“省电精灵”杀后台后,如何借助前台 Service + 通知保活 5 秒完成握手
- 重连风暴抑制:令牌桶 + 最大重试窗口(如 15 分钟内最多 5 次)
- 监控埋点:连接耗时、重连次数、错误码分布,上报到 Crash 平台或 APM
答案
给出一个可直接落地到中大型 App 的“四层架构”示例,全部用 Kotlin + Coroutines 描述,不依赖第三方库黑盒。
-
状态机层
使用 sealed class 定义状态:Connecting、Connected、Disconnected、Reconnecting。
通过 StateFlow<ConnectionState> 暴露给 UI 层,避免回调地狱。 -
重连策略层
指数退避:base=1s,max=30s,factor=2,带 ±20% 随机抖动。
计数器:连续失败 5 次或累计 15 分钟窗口内失败 7 次,升級为“冷启动”——等待应用回到前台或网络可用广播再重试,防止电量耗尽。 -
网络感知层
注册 NetworkCallback 监听 VALIDATED 网络,一旦变为可用立即取消当前退避计时器,从 0 开始重连;无网络时直接置为 Suspended 状态,不再空转线程。 -
通道层
基于 OkHttp 新实例(防止旧连接干扰),在 CoroutineScope(IO) 中启动,通过 sendChannel.offer() 发送业务消息,receiveChannel 作为热流收集服务端推送。
发生 onFailure 或 onClosed 时,先判断关闭码:- 1000(正常)或 1001(端点离开)→ 不重连;
- 1006(异常断开)或 1011(服务端异常)→ 进入 Reconnecting;
- 其它业务码 4000~4999 按产品策略决定是否重连。
核心代码(精简可运行):
class AutoReconnectionWebSocket(
private val url: String,
private val okHttpClient: OkHttpClient,
private val externalScope: CoroutineScope = CoroutineScope(Dispatchers.IO)
) {
private val _state = MutableStateFlow<ConnectionState>(Disconnected)
val state: StateFlow<ConnectionState> = _state.asStateFlow()
private var webSocket: WebSocket? = null
private var reconnectJob: Job? = null
fun start() {
externalScope.launch { connectInternal() }
}
fun stop() {
reconnectJob?.cancel()
webSocket?.close(1000, "App stop")
_state.value = Disconnected
}
private suspend fun connectInternal() {
if (_state.value is Reconnecting) return
_state.value = Connecting
val request = Request.Builder().url(url).build()
webSocket = okHttpClient.newWebSocket(request, WSListener())
}
private inner class WSListener : WebSocketListener() {
override fun onOpen(ws: WebSocket, response: Response) {
_state.value = Connected
reconnectJob?.cancel()
}
override fun onFailure(ws: WebSocket, t: Throwable, response: Response?) {
handleDisconnect()
}
override fun onClosed(ws: WebSocket, code: Int, reason: String) {
if (code != 1000 && code != 1001) handleDisconnect()
else _state.value = Disconnected
}
}
private fun handleDisconnect() {
_state.value = Reconnecting
reconnectJob = externalScope.launch {
val delay = computeBackoff()
delay(delay)
connectInternal()
}
}
private fun computeBackoff(): Long {
val base = 1000L
val max = 30000L
val attempt = (_state.value as? Reconnecting)?.attempt ?: 0
val jitter = (0.8 + 0.4 * Random.nextDouble())
return (min(base * 2.0.pow(attempt), max.toDouble()) * jitter).toLong()
}
}
在 Application 或单例 Repository 中持有该实例,配合 ProcessLifecycleOwner 感知前后台:
- 进入后台 30 秒无活跃则主动 close(1000),释放资源;
- 回到前台立即 start(),实现“按需保活”。
监控:
- 每次状态变更写入本地环形缓存,App 启动后批量上报;
- 连接成功时记录耗时,失败记录错误码与堆栈,方便后台按版本聚合。
拓展思考
- 如果业务要求“断网期间消息不丢失”,需引入本地队列 + 消息序号 ACK 机制,重连成功后按序号补发,此时 WebSocket 已升级为“可靠通道”,要考虑幂等性与顺序性。
- 折叠屏或车载场景下,进程可能被系统回收但保留 Task,建议在 Service 的 onStartCommand 返回 START_REDELIVER_INTENT,并在 onRebind 中恢复 WebSocket,防止用户切换屏幕后看到“假在线”。
- 国内厂商后台限制愈发严格,可降级为 Mqtt-over-WebSocket 并接入厂商 Push(如华为 Push+ 自建通道双写),确保 Kill 后仍能触达,面试时可提及“通道融合”思路,体现你对国内生态的深刻理解。