IM (即时通讯应用) 的特点是实时、可靠性,即可以快速、正确的收 / 发消息,在本篇我们着重讨论可靠性。

# IM 中消息的可靠性

消息的可靠性指的是消息的不丢失和不重复,这点在 IM 中是非常重要的,特别是不丢失,你不希望编辑了一段很重要的话看似发送成功,但其实没有被对方接收到。

# 报文类型

我们可以简单地把客户端和服务器之间传输的报文看作 3 个类型。

  1. 请求报文 (Request:R):客户端主动发送给服务器的报文。
  2. 应答报文 (Ack:A):服务器被动回应给客户端的报文,一个请求报文对应一个应答报文。
  3. 通知报文 (Notice:N):服务器主动发送给客户端的报文。

# 普通消息的投递流程

在一次 IM 的聊天过程中,A 给 B 发送了一条消息,比较容易想到的流程是这样的:

image.png

  1. client-A 给 im-server 发送了 msg:R 请求报文。
  2. im-server 收到 msg:R 后,给 client-A 回应一个 msg:A 应答报文。
  3. 然后 im-server 需要主动给 client-B 发送一个 msg:N 通知报文。

# 上述投递流程存在的问题

首先在第 2 步中,client-A 收到应答报文只能代表 im-server 真的接收到了消息,而不能说明 clinet-B 就收到了,例如下面的情况:

  1. 服务器崩溃,正常发送了 msg:R 但未能正常发送 msg:N。
  2. 客户端崩溃,没能正常处理 msg:N 报文。
  3. 网络抖动,msg:N 在传输过程中丢包。

因此这个流程是存在风险的,client-A 无法准确地知晓 client-B 是否真的收到了消息。

# 更可靠的投递流程

首先我们需要知道一点,为什么上述流程不可靠,TCP 不是通过超时、重传、确认机制来实现了消息在传输层的可靠性吗?

因此 TCP 是基于 2 方通信的,它只能确保单条连接的可靠性。但是它没有一个消息送达保证 (QoS) 机制,即主动地让 client-A 知道 client-B 是否收到了信息,这点我们就需要在应用层进行实现,流程如下:

  1. client-B 在收到 msg:N 后主动向 im-server 发送一个 ack:R。
  2. im-server 在成功处理后,回复给 client-B 一个 ack 响应包,即 ack:A。
  3. im-server 同时主动向 client-A 发送一个 ack:N 通知报文,告诉 client-A 说 client-B 成功收到了消息。

所以一条消息的发送如果要保持可靠,总共需要 6 个报文。分别是上半场的 msg:R/A/N 和下半场的 ack:R/A/N。这就是 im 系统中消息投递的最核心技术。

# 可靠的投递流程需要的错误处理

上面说到了 TCP 在传输层可以保证 2 方通信的可靠性,但在一个 im 系统中,我们需要对其中产生的错误做应用层的处理,如下:

  1. msg:R 或 msg:A 丢失:此时 client-A 未能确认 im-server 是否收到了消息,在应用层提示” 发送失败”,然后发起重传。
  2. 剩下的 4 个报文丢失:此时 client-A 未能确认 clinet-B 是否收到了消息,同样也需要重传。

# 消息的超时和重传

client-A 在发送消息后,如果在一个期望的时间内没有收到 msg:A、ack:N 报文,client-A 需要将 msg:R 重发。所以 client-A 需要在本地维护一个等待 ack 队列,来记录哪些消息没有收到 msg:A、ack:N 报文,需要等待重发。

# 消息的去重

既然是重传,就有可能会存在重复发送消息的问题。解决方法也比较简,client-A 需要生成一个消息去重的 msgid,保存在 ack 等待队列中,同一条消息使用相同的 msgid 来进行重传,供 client-B 去重。

# 去重中的 msgid 是什么

首先你可能有一个概念,im 系统中的数据库会为每个消息分配一个 msgid,以供数据的同步。比如每次的分页拉取,那么上面指的 msgid 就是它吗?

答案是否定的。首先需要明白这个数据库的 msgid 是服务器去生成的,也就是说需要在 msg:A 中传递给 client-A,那么就会存在一个问题:

如果服务器已经为这条消息生成了 msgid,但是 msg:A 在传输中丢失,client-A 此时仍然需要进行重传,但此时 client-A 还没有收到发来的 msgid,于是只能发送一条重复内容的消息,就可能造成数据的重复。

# 我们需要两种 msgid

为了解决上面的问题,我们就需要在 client-A 发送消息时,也维护一个客户端生成的唯一 msgid,它是会话级的,作用是用于 A-B 设备间的消息去重和 ACK 确认。

对比维度前端临时 msgid后端持久化 msgid
生成时机消息发送前即时生成服务端收到消息后生成
冲突风险仅需保证单设备内唯一必须保证全局唯一
存储位置客户端内存服务端数据库
典型用途重传去重、ACK 确认消息检索、多端同步

参考文章:

http://www.52im.net/thread-294-1-1.html