如何在 Android 中实现 WebSocket 的自动重连机制?

解读

面试官问“自动重连”时,真正想考察的是:

  1. 对 WebSocket 全生命周期(OPEN → MESSAGE → ERROR/CLOSE → 重连)的闭环掌控;
  2. 对 Android 平台特性的适配:进程存活、网络变化、电量优化、前后台切换;
  3. 对高可用代码的落地能力:线程模型、指数退避、重连风暴抑制、状态机可视化调试。

国内场景下,还要兼顾厂商 ROM 对后台网络限制、国内推送通道共存、合规收集用户网络行为等“隐形需求”。回答时切忌只贴一段“while(true) reconnect()”的伪代码,必须给出“可灰度、可监控、可止损”的工程级方案。

知识点

  • WebSocket 协议关闭码 10001015、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 描述,不依赖第三方库黑盒。

  1. 状态机层
    使用 sealed class 定义状态:Connecting、Connected、Disconnected、Reconnecting。
    通过 StateFlow<ConnectionState> 暴露给 UI 层,避免回调地狱。

  2. 重连策略层
    指数退避:base=1s,max=30s,factor=2,带 ±20% 随机抖动。
    计数器:连续失败 5 次或累计 15 分钟窗口内失败 7 次,升級为“冷启动”——等待应用回到前台或网络可用广播再重试,防止电量耗尽。

  3. 网络感知层
    注册 NetworkCallback 监听 VALIDATED 网络,一旦变为可用立即取消当前退避计时器,从 0 开始重连;无网络时直接置为 Suspended 状态,不再空转线程。

  4. 通道层
    基于 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 启动后批量上报;
  • 连接成功时记录耗时,失败记录错误码与堆栈,方便后台按版本聚合。

拓展思考

  1. 如果业务要求“断网期间消息不丢失”,需引入本地队列 + 消息序号 ACK 机制,重连成功后按序号补发,此时 WebSocket 已升级为“可靠通道”,要考虑幂等性与顺序性。
  2. 折叠屏或车载场景下,进程可能被系统回收但保留 Task,建议在 Service 的 onStartCommand 返回 START_REDELIVER_INTENT,并在 onRebind 中恢复 WebSocket,防止用户切换屏幕后看到“假在线”。
  3. 国内厂商后台限制愈发严格,可降级为 Mqtt-over-WebSocket 并接入厂商 Push(如华为 Push+ 自建通道双写),确保 Kill 后仍能触达,面试时可提及“通道融合”思路,体现你对国内生态的深刻理解。