聊天项目里的消息发送确认

Jul 3·6 min
AI 生成的摘要
记录商业 IM 软件 Web 端的消息发送流程:先显示本地消息,再等 ACK,收不到确认时通过 HTTP 查询,最后处理超时和重发。

这几年一直在做一款商业 IM 软件,App 用 Flutter,Web 端是工作台。趁着整理代码,把里面一些平时容易忽略的细节记下来。

先写消息发送。点击发送以后,输入框清空、消息出现在列表里,这时请求可能都还没到服务器。页面上的发送状态到底应该在哪一步改变?

消息出现在列表里,只是第一步

在 Web 端,文本发送会先创建一个本地消息,把状态设为 SENDING,然后交给本地消息状态处理,再通过 XMPP 发送。

这样用户不用等远端确认,就能在聊天窗口看到自己的消息。

这里有几个不同的状态:

时刻能说明什么
本地消息创建完成客户端已经记录这次发送意图
调用 XMPP 发送方法客户端尝试把消息交给连接
收到发送确认,或查询到服务端记录可以按项目的确认规则更新发送状态
收到已读信息阅读状态发生了变化

尤其是最后两项,需要分开处理。消息发送成功,不意味着对方已经阅读;对方阅读了,也不应该反过来成为发送流程唯一的成功条件。

项目里 messageStatusisReadisViewMarker 等字段承担不同职责。如果把它们都压进一个 success,UI 看起来简单了,后面的逻辑反而很难解释。

ACK 没回来,怎么办?

项目的 Web 端有一个 progressiveMessageCheckWithAck,负责发送后的渐进式确认。

它会先等待一段时间,检查 ACK 缓存;如果没有命中,再调用消息存在性接口。如果仍然没有确认,就继续下一轮。

当前源码里的等待间隔是:

snippet
typescript
const checkIntervals = [300, 300, 500, 1000, 2000, 4000, 6000, 10000]

单位是毫秒,每一项都是这一轮额外等待的时间。它们不是从发送开始计算的绝对时间点,整个确认过程还要加上 HTTP 请求本身的耗时。

这个细节在排查“为什么发送状态这么久才结束”时很有用。只看最后一个 10000,会误以为整个流程最多十秒。

实际的确认顺序可以写成:

snippet
text
等待本轮间隔
  → 检查本地 ACK 缓存
  → 未命中时查询服务端是否存在该消息
  → 已确认则结束,否则进入下一轮
  → 所有轮次结束后,再做一次最终查询

HTTP 查询确认存在后,也会把消息 ID 加入 ACK 缓存。这个缓存最多保留 100 个 ID,用来保存近期确认结果,不能把它当成完整的消息历史。

ACK 能及时回来最好;收不到时,还有 HTTP 可以主动查一次。

超时最麻烦的地方,是结果可能还不确定

假设发生下面这个过程:

  1. 客户端把消息发了出去。
  2. 服务端已经收到消息。
  3. 客户端切换网络,没拿到确认。
  4. 后续查询也因为网络问题失败。

从客户端看到的结果来说,确认流程失败了。但服务端可能已经保存了这条消息。

这款商业 IM 软件当前的 Web 实现会在确认耗尽后把发送状态设为 FAILED。这个字段可以支持界面的失败提示,却不能推导出“服务端一定没有这条消息”。

同样,查询接口返回存在,可以支持客户端把消息标为已发送;它不能证明所有接收设备都收到了,更不能证明对方读过。

如果继续细化这套状态,我会考虑把“明确拒绝”和“暂时无法确认”分开。目前代码还没有这样细分。

重发之前,先想清楚消息 ID

一旦允许用户重发,问题就从“再调用一次发送方法”,变成“这次调用和上次是什么关系”。

同一段文本有可能是用户主动发送了两次,也可能是同一次发送的重试。不能靠内容相同来去重。

项目在普通文本发送时会生成消息 ID,并把它放进 stanza 的 idorigin-id。这样本地消息和协议消息有了可以关联的标识。

但有 origin-id 字段,不代表整个系统就自动具备完整的重试幂等能力。

设计重试时,需要把几件事一起对齐:本地保存的是哪个 ID、重发携带哪个 ID、确认接口查哪个 ID,以及服务端按什么规则判重。客户端复用标识,也仍然需要服务端配合。

例如,如果希望“同一次发送操作重试后仍然对应一条业务消息”,就应该明确一个稳定的业务标识,并让相关链路使用同一套语义。客户端有 ID 还不够,服务端也要按同一个 ID 判重。

几个需要补测的情况

单独检查正常发送,覆盖不到最有讨论价值的部分。我更关心下面几种情况:

  • ACK 到达前,HTTP 查询先确认成功,之后 ACK 再到达。
  • ACK 始终没有到达,但 HTTP 能查到消息。
  • HTTP 暂时失败,后续一轮恢复成功。
  • 确认耗尽后,原消息又通过历史同步回到客户端。
  • 用户点击重发,本地消息与新的确认结果能否正确关联。

这些情况适合补进故障测试,尤其要看迟到的 ACK 会不会改错消息。

排查发送状态时,我会先拿消息 ID 对一下 ACK 缓存和历史接口。先确认服务器上有没有这条消息,再决定要不要重发。

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