Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

推送 / 长连接 / 保活

实时系统只能在网络, 功耗, 后台限制和厂商策略允许的范围内提高送达概率, 不能承诺后台永远在线或消息必达. 可靠性来自 sequence/ACK/去重/补拉和服务端状态, 不是单靠心跳或 “拉活”.

一, 通道定位

通道适用主要限制
轮询 / 长轮询低频状态, 兼容方案延迟, 连接和功耗成本
WebSocket/TCP前台低延迟双向通信网络切换, NAT, 心跳, 后台冻结
系统 / 厂商推送后台触达和唤醒用户不保证实时, 顺序, 完整或必达
补拉 API恢复完整状态需要游标, 分页, 幂等和限流

常见组合是 “前台长连接 + 后台系统推送 + 回前台按游标补拉”. 推送 payload 只作为提示, 业务真相仍从受鉴权的服务端同步.

二, 消息可靠性协议

建议每条消息至少包含:

conversation/stream id
sequence or cursor
message id / idempotency key
server timestamp
schema version
payload or payload reference

客户端处理:

  1. 持久化已确认 cursor 和去重集合.
  2. 收到消息先校验身份, 版本和顺序.
  3. 重复 messageId 幂等忽略; 发现 sequence 缺口立即补拉.
  4. 先落库再驱动 UI; ACK 的时机要区分 “已收到, 已持久化, 业务已读”.
  5. 重连后携带最后确认 cursor, 服务端重放缺失区间.
  6. 超出服务端保留窗口时获取状态快照, 不能无限依赖增量日志.

TCP/WebSocket 连接可靠不等于业务消息可靠. 进程死亡, 数据库失败, ACK 丢失和多设备并发都需要业务层协议处理.

三, 心跳与重连

心跳间隔应依据 NAT 超时, 前后台, 网络类型, 服务端成本和功耗实验决定. 重连采用有上限的指数退避和 jitter, 并在网络恢复, 回前台, 用户主动刷新等事件触发.

sealed interface SocketState {
    data object Disconnected : SocketState
    data object Connecting : SocketState
    data object Connected : SocketState
    data class Backoff(val retryAtElapsedRealtimeMs: Long) : SocketState
}

使用单调时钟计算本地退避, 避免用户修改系统墙上时钟时间 (wall-clock time). 连续鉴权失败, 协议版本不兼容或账号撤销应停止自动重连并进入明确状态.

四, 推送 Token 生命周期

推送 token 可能刷新, 失效或因卸载/清数据/恢复备份变化. 客户端应:

  • token 变化后与当前账号, 应用安装实例和环境重新绑定.
  • 登出或账号切换时解除旧绑定, 防止消息发给错误用户.
  • 服务端根据无效 token 回执清理注册关系.
  • 多设备分别维护 endpoint, 不把账号当成唯一 token.
  • payload 最小化, 锁屏敏感内容由本地策略决定是否展示.

FCM 与各厂商通道的优先级, 权限, 配额和回执能力不同, 应按目标市场和当前官方文档维护矩阵.

差异样例 (面试话术素材, 具体以各厂商最新文档为准): 通道: FCM 依赖 Google Play Services 系统级长连, 华为走 HMS Push 通道, 小米走 MiPush 通道, 三者相互独立不能互投; token 机制: FCM 由 FirebaseMessaging 回调下发并监听刷新, 华为 / 小米 token 由各自推送 SDK 注册获取, 都要上报服务端绑定并按刷新重绑; 厂商进程存活策略: 厂商通道由系统级常驻进程转发消息, 宿主无需双进程守护等拉活手段, 但各厂商对自启动 / 后台白名单策略不同, 通道送达不等于 App 进程存活.

五, Android 后台与 Doze 边界

Doze, App Standby, 后台执行限制和厂商电源策略会延迟网络, 任务和闹钟. WorkManager 适合可延迟的持久工作, 不是实时长连接保活器; 前台服务只用于用户可感知且符合类型 / 权限要求的持续任务.

具体边界: targetSdk 34+ 要求前台服务必须声明类型并运行时申请对应权限, 长连接场景常用 dataSync 或 remoteMessaging, 分别对应 FOREGROUND_SERVICE_DATA_SYNC / FOREGROUND_SERVICE_REMOTE_MESSAGING; 未声明类型启动会抛 MissingForegroundServiceTypeException. 但 Doze 下系统仍限制后台网络与消息实时性, 前台服务不豁免全部限制.

双进程守护, 1 像素 Activity, 滥用闹钟等历史 “黑科技” 不应作为现代主方案: 它们不可靠, 增加功耗和合规风险, 还可能违反商店或厂商政策.

六, 观测与排障

至少按应用版本, OS, 厂商, 网络和通道拆分:

  • 注册 / token 更新成功率与无效 token 比例.
  • 连接成功率, 握手时延, 在线时长, 重连原因和退避轮次.
  • 服务端发送, 通道接收, 设备到达, 落库, 展示, 点击各阶段漏斗.
  • sequence 缺口, 重复率, 补拉成功率和最大恢复时间.
  • 后台 / 前台, Doze, 网络切换和进程死亡后的恢复率.
  • 心跳和重连带来的电量, 流量和服务端连接成本.

“消息到达率” 必须定义分母, 窗口, 回执含义和超时, 不同通道的回执不能直接等同于用户已看到.

ACK 时序与断线恢复状态机

服务端 -> 客户端: message(seq=42, id=m42)
客户端: seq=42 若连续,事务写消息,去重记录与连续 cursor=42
客户端 -> 服务端: ACK(kind=PERSISTED, cursor=42)
服务端: 记录该设备已持久化至 42,可回收其重放窗口
客户端: 用户阅读后,可选发 READ(cursor=42);READ 不替代 PERSISTED ACK

Disconnected -> Connecting -> Authenticating -> Syncing(cursor) -> Online
      ^              |              |             |               |
      |              +-> Backoff <--+             +-> Backoff <----+
      +------------------ terminal auth/revoked -> LoggedOut

cursor 只能推进到最高连续已持久化 sequence. 若先收到 44 而连续 cursor 为 42, 事务落库消息和去重记录但 cursor 仍为 42, 同时把 44 放入乱序缓存并补拉 43; 43 到达后同一事务将 cursor 推到 44. 可选 SACK 表示已持久化的非连续范围, 用于减少重发, 但不能替代连续 cursor. ACK 只能在上述事务提交后发送; 鉴权失败, 账号被踢或协议不兼容是终止条件.

NAT 与到达率排障实验

心跳参数不能凭经验固定.实验方案, 不声称已有测量结果: 使用受控测试账号和静默连接, 分别制造 NAT 空闲回收, Doze, Wi-Fi/蜂窝切换及服务端主动断开; 服务端记录连接 ID, 最后收发, 主动 close 原因和探测超时, 客户端记录单调时间, 网络回调, 前后台与 pong. 仅 “长时间无流量后服务端探测失败” 才支持 NAT 回收假设; Doze 和网络切换要按各自证据归因. 按运营商/网络/前后台分桶比较恢复时延, 掉线率, 耗电流量和连接成本, 再灰度调整与回退.

到达率排障从端到端漏斗开始: 服务端入队成功 -> 推送通道接受 -> 设备回执 / 长连持久化 ACK -> 本地展示. 先固定事件 ID 与时间窗口, 再按 app 版本, 厂商, 权限, Doze, 网络和 token 状态分组. 没有设备 ACK 的 “渠道接受成功” 只能说明上游接受, 不能称为用户到达.

到达率下降排障叙事模板 (面试可套用): 症状: 某版本 / 某厂商机型到达率明显下滑; 证据: 端到端漏斗各段 (服务端入队成功 -> 通道接受 -> 设备回执 / 长连 ACK -> 本地展示) 看衰减发生在哪段; 定位: 无 ACK 且服务端探测失败指向 NAT 回收或心跳参数失效, 单段下降常是厂商通道或 token 失效, 后台段下降则归因 Doze / 后台限制; 修复: 调整心跳与重连参数, 补厂商通道降级或补拉, 灰度放量; 验证: 按 app 版本 / 厂商 / 网络分桶对比恢复后的到达率与在线时长.

学完能做什么与练习

你应能实现连续 cursor 而不是 “最大已见 seq”.练习: 按 42, 44, 43 的顺序注入消息并模拟 43 补拉失败后重启; 预期证据是首次 ACK 不超过 42, 消息/去重/cursor 事务一致, 恢复后重放 43, 44 不产生重复 UI.

高频面试题

Q1: IM 为什么不能只靠推送?
推送用于后台触达, 不保证实时, 顺序和完整. 前台通常用长连接降低延迟, 但最终仍靠 sequence, ACK, 去重, 持久化和补拉保证业务一致性.

Q2: 重连怎么避免雪崩?
指数退避 + jitter + 最大次数 / 时限, 按网络恢复分批唤醒, 服务端下发 retry-after, 鉴权和协议错误不盲目重试.

Q3: 如何回答保活?
先声明系统与厂商限制, 再给 “系统推送 + 合规前台能力 + 可延迟任务 + 状态补拉” 的组合, 并用功耗和送达指标验证; 不承诺永久在线.

版本与参考资料

  • 最后核验: 2026-08-10.
  • Android 后台限制, 前台服务类型和推送产品能力会持续变化; 实施时按目标 OS, targetSdk, 地区和厂商官方文档核验.