Flutter 断线重连后的消息补偿

Jul 10·6 min
AI 生成的摘要
商业 IM 软件 App 的离线消息处理记录,包括历史接口、房间同步锁,以及退出账号后怎样拦住迟到的请求结果。

之前写过 Flutter 的渲染机制,这次记一下聊天数据的处理。

商业 IM 软件的 App 用 XMPP 收发消息。手机切网,或者从后台回到前台,除了重新连接,还要把离线期间漏掉的消息拉回来。连接到了 ready,后面这部分工作才刚开始。

先把连接和消息补偿分开

项目里有两层职责。

XMPP 协议层处理连接状态和协议能力;App 里的 XmppClientStore、房间消息逻辑处理账号、生命周期以及业务恢复。

离线补偿会调用 HTTP 历史消息接口,把返回的数据转换为客户端消息,再交给后续存储和状态更新流程。

这样可以分别回答两个问题:

  • XMPP 现在能不能继续收发?
  • 本地缺少的消息有没有补回来?

比如,XMPP 已经恢复,但历史接口请求失败。这时继续把连接标成断开,并不能准确表达问题;反过来,只显示在线也不能代表同步完成。

从排障角度看,至少要能区分连接恢复失败和补偿失败。否则日志里一句“重连成功”,很容易让人忽略后面的数据缺口。

补偿需要一个起点

如果每次回到前台都拉全部历史,逻辑虽然直接,但历史越多,重复工作也越多。

项目的离线同步入口会接收房间、最后消息 ID、最后消息时间,以及可选的 sourceGroupJid。HTTP 请求使用 afterOriginId 表达增量边界。

其中,来源群也属于同步范围。相同聊天对象在不同群业务下的私聊,不能共用一个模糊的“最后一条消息”。

房间层还会计算补偿起点,某些分支会往前回退一段时间。这里反映了一种取舍:允许同步区间有重叠,再按消息身份合并,给边界附近的消息留出补偿空间。

但回退多久属于项目策略,不能照搬成所有聊天系统的固定值。判断策略是否够用,需要结合服务端历史查询的排序和边界语义。

同一个房间,不要同时跑几套补偿

恢复连接、进入会话等入口,都可能触发同步。如果不协调,它们可能同时读取同一个旧边界,再请求同一批消息。

当前 App 的 syncOfflineMessage 按房间和来源群取锁。下面是源码中的关键片段:

snippet
dart
final lock = _roomSyncLocks.putIfAbsent(
    (roomId, sourceGroupJid?.isEmpty == true ? null : sourceGroupJid),
    Lock.new);

后续同步逻辑放在 lock.synchronized 里面执行。同一范围的调用会串行进入这段逻辑。

这里有个容易混淆的地方:串行执行,并不等于多个调用自动共用同一次请求结果。

如果两个调用都已经拿到了旧边界,排队以后仍然可能各请求一次。是否需要进一步合并任务,或者执行时重新读取边界,是另一层优化,不能仅凭加了一把锁就认为重复请求已经全部消失。

请求返回时,账号可能已经变了

这个场景更值得单独说一下:

snippet
text
账号 A 发起历史消息请求
  → 用户退出账号 A
  → 用户登录账号 B
  → 账号 A 的请求现在才返回

HTTP 请求成功了,数据也能正常解析,但它已经不属于当前账号。

所以异步处理除了检查“有没有报错”,还要检查“结果现在还有没有资格继续生效”。

App 在同步开始时记录任务代次 generation 和账号 accountJid,并在多个异步边界后检查。下面是项目中的校验方法:

snippet
dart
void _checkOfflineSyncGeneration(int generation, String? accountJid) {
  if (generation != _offlineSyncGeneration ||
      accountJid != _offlineSyncAccountJid) {
    throw StateError('离线消息请求所属账号已失效');
  }
}

当同步失效时,_offlineSyncGeneration 会递增,并清理房间同步锁的映射。旧任务恢复执行后,就能发现自己的代次已经过期。

同时比较账号和代次也有意义。账号标识相同,不一定还是同一轮会话,例如退出后又登录同一个账号。代次可以表达这种生命周期变化。

这种检查不会让已经发出的网络请求凭空消失。它控制的是后续处理:在检查点发现任务失效,就不再继续把旧结果当作当前结果使用。

类似问题也会出现在搜索联想、切换详情页和附件处理里,只是聊天系统的账号与会话边界,让这个问题更明显。

请求失败,不能伪装成没有新消息

历史接口返回空数组,可以表示这一轮没有更多数据。网络请求失败,表达的却是这一轮没有拿到可靠结果。

如果两者最后都变成 [],上层就无法知道该结束同步还是等待重试。

当前 syncOfflineMessageFromApi 使用严格请求模式,失败会继续抛出。房间层负责安排有限重试,而不是让失败批次以空数组或部分结果的形式冒充成功。

这里处理的是请求错误,没有把整个同步过程放进数据库事务。

请求失败就让上层重试;账号失效则停止处理,不能再往当前账号写数据。

排查同步问题

我会沿着下面几个节点检查一次恢复:连接进入可用状态、确定会话范围和起点、获取历史、校验任务有效性、转换并保存消息、更新窗口与未读状态。

尤其是退出账号后旧请求才返回的情况,光看请求有没有成功很容易漏掉。账号和 generation 的检查需要跟着异步处理一起看。

最后修改时间: Sep 15
cd ..