如何处理 WebSocket 连接中的心跳包(Heartbeat)以维持连接活跃?

解读

国内面试场景下,这道题考察的是“保活”与“容错”两大维度。

  1. 保活:运营商 NAT 超时普遍 30 s~5 min,弱网、电梯、地铁场景频繁,心跳必须能在 30 s 内完成一次双向探测。
  2. 容错:国产 ROM 后台限制、息屏省电、无锁 CPU 休眠、应用被清理、用户强杀、系统 Doze/Standby 等,都会导致心跳线程被挂起或 Socket 被系统回收。
    因此,答案不能只说“定时发 ping”,必须给出“Android 语境下可落地的完整闭环方案”,包括线程模型、前后台策略、电量优化、断线重连、异常分级与监控埋点。面试官想听到的是“你怎么保证微信/钉钉级别的在线率”,而不是“我调了一下 OkHttp 的 pingInterval”。

知识点

  1. WebSocket 协议层:控制帧 opcode 0x9(Ping)/0xA(Pong),RFC6455 要求对方必须回 Pong,不回即可认为链路死。
  2. Android 8.0 后台限制:不允许后台应用创建后台服务;前台服务必须挂常驻通知;AlarmManager 不再精确触发。
  3. Doze 模式:Idle 阶段网络被挂起,心跳包根本发不出去;App Standby Bucket 决定 Job/Alarm 最小间隔。
  4. 长连接保活三件套:
    a. 心跳间隔 adaptive:根据网络类型(Wi-Fi/4G/5G)、历史 RTT、NAT 超时、运营商实验)动态调整 15 s~120 s。
    b. 心跳超时重试指数退避:连续 3 次无 Pong 立即触发重连,重连间隔 1 s→2 s→4 s…上限 5 min。
    c. 应用层心跳与协议层心跳分离:应用层发最小业务空包(如 {"type":"ping"}),防止某些国产网关丢弃 ICMP-like 帧。
  5. 线程与电源管理:
    • 必须放在前台 Service + 独立进程 :push,降低主进程被杀概率。
    • 使用 WakeLock(PARTIAL_WAKE_LOCK)+ WifiLock 包裹一次完整心跳收发,结束后立即释放;Android 14 需申请 FOREGROUND_SERVICE_CONNECTED_DEVICE 权限。
    • 心跳线程使用 HandlerThread/Executor,避免频繁新建线程;与重连线程隔离,防止阻塞。
  6. 断线感知分级:
    • 0 级:收到 onClosing/onClosed,立即重连。
    • 1 级:写队列满、onFailure,立即重连。
    • 2 级:连续 3 次心跳无回包,标记“疑似断链”,尝试发 FIN 探测,再失败则重连。
  7. 电量优化:
    • 前台 10 s 心跳,后台按运营商实验值 45 s~90 s;进入 Doze 后改用 WorkManager+NetworkType.CONNECTED 约束做“懒心跳”,牺牲 5 min 延迟换电量。
    • 使用 Android 12 引入的“Battery Manager 预估算”接口,电量低于 15 % 自动拉长间隔。
  8. 监控与回捞:
    • 每次 ping/pong 记录 RTT、是否成功、网络类型、CPU 休眠时长,写入 mmap 日志,次日上报。
    • 利用 Firebase/火山引擎做连接可用率大盘,低于 99.5 % 自动报警。
  9. 安全:心跳包必须带轻量级签名(HMAC-SHA256 时间窗 60 s),防止运营商中间人伪造 pong。
  10. 合规:独立进程推送服务需在隐私政策中明示“自启动/关联启动”场景,否则华为/小米应用商店审核会被打回。

答案

“我在生产环境把心跳拆成四阶段闭环:
阶段一:参数自适应。首次建连后,服务端在握手扩展字段返回建议心跳间隔(典型 30 s),客户端结合本地历史 RTT 做 EWMA 平滑,得到初始值。后续每次收到 pong 都更新 RTT,若连续 5 次 RTT < 1 s 且网络类型不变,间隔可翻倍,上限 120 s;一旦有一次超时,间隔立即减半,下限 15 s。
阶段二:双通道保活。协议层用 OkHttp 的 WebSocket 设置 pingInterval=0,关闭其自动 ping,完全由业务层掌控;业务层在独立 HandlerThread 每间隔发送一个最小空包 {"t":ping,"ts":timestamp,"sign":HMAC},服务端回 {"t":pong,"ts":echo},保证即使中间网关丢弃 ICMP-like 帧也能存活。
阶段三:前后台差异化。应用切到后台时,启动前台 Service 并弹出低优先级通知(IMPORTANCE_LOW),在通知栏显示‘正在运行’;心跳间隔从 30 s 放宽到 45 s。进入 Doze 后,WorkManager 每 15 min 检查一次网络,若有网络则一次性补发 3 次心跳,成功即继续保持长连接,否则放弃等待下次 Job。
阶段四:重连与监控。心跳连续 3 次无回包即触发重连,重连线程使用指数退避,最大 5 min;重连时携带上次会话 ID,服务端支持 0-RTT 恢复,降低推送延迟。所有 ping/pong 日志实时写入 mmap 文件,包含字段:uid、网络类型、是否 CPU 休眠、RTT、成功标志,次日随业务日志一起上报,可用率低于 99.5 % 自动报警。
通过这套方案,我们在国内 2000 万日活设备上实测后台存活 12 h 以上占比 96.4 %,平均心跳耗电 < 0.3 %/h,满足微信级要求。”

拓展思考

  1. 如果业务需要“秒级”推送,但用户又把 App 划到后台限制桶(RESTRICTED Bucket),WorkManager 最小间隔 24 h,此时如何保活?
    提示:可引导用户把应用加入厂商自启动白名单;或者利用系统账号同步机制 SyncAdapter,利用系统 15 min 周期唤醒;再不行就接受“拉活”方案,依赖其他应用互拉,但需评估合规风险。
  2. 折叠屏/多窗口场景下,前后台生命周期回调异常,可能误判后台,导致心跳间隔错误放大,如何校准?
    提示:监听 Activity 数量 + ProcessLifecycleOwner,结合 UsageStatsManager 判断真实可见性;或者使用 WindowVisibility 变化做二次确认。
  3. 海外版本无法使用 GMS Doze 白名单,如何兼顾电量与在线?
    提示:使用 Android 13 新增“用户可撤销的电池优化”权限,引导用户手动关闭 Battery Optimization;同时采用 FCM High Priority 消息做心跳外包,把心跳包外包给谷歌通道,自己不再主动发 ping。