关于 IM 设计实现的一些思路

1,265 阅读4分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

1. 如何保证消息顺序性?

私聊

假设消息通过 TCP 传输,假设发送两条消息的话,就有可能出现先发后至的情况。

一种解决方案是:

首先保证发送方发送的消息顺序是一致的、是正确的。比如说发送方维护一个消息 ID,这样可以按照发送方的消息 ID 顺序排列。

然后发送方和接收方的顺序,可以按照服务器时间作为消息的顺序。这样保证消息的顺序性。

群聊

  • 群聊不再利用发送方的序列号来保证时序,因为发送方不单点,时间也不一致(个人发送的时序,本地存储可以实现,发送的内容本来就是有序的)
  • 可以利用服务器的单点做序列化;

这个方法能实现,所有群友消息展示的时序相同。缺点是,这个生成全局递增序列号的服务很容易成为系统瓶颈。

进一步的优化策略是:群消息不用保证全局消息序列有序,而只要保证一个群内的消息有序即可

在这个方案中,Service 层不再需要去一个统一的后端去拿全局序列号。在 Service 层来序列化同一个群的消息,保证所有群友看到消息的时序是相同的。

2. 如何保证在线消息的可靠性?

消息可靠性需要有确认送达机制,否则就无法谈消息的可靠性。

首先,发送消息是一个三方通信,即 A - Server - B

可靠消息传递需要 6 个报文,即:

  1. 客户端 A 向服务端 Server 发送消息请求包 msg:Request
  2. 服务端 Server 向客户端 A 发送一个回应报文 msg:Acknowledge
  3. 服务端 Server 向客户端 B 发送通知报文 msg:Notify
  4. 客户端 B 向服务端 Server 发送一个 ACK 请求包 ack:Rquest
  5. 服务端 Server 回复客户端 B 一个 ACK 响应包 ack:Acknowledge
  6. 服务端 Server 回复客户端 A 一个 ACK 响应包 ack:Notify

所以,一条消息的发送,分别包含上、下两个半场,即 Msg 的 R/A/N 三个报文,ack 的 R/A/N 三个报文。一个应用层即时通讯消息的可靠通地,共涉及 6 个报文,这就是 IM 系统中消息投递的最核心技术。

3. 消息丢失怎么办?(超时重传)

我们还是基于上述 6 个报文的设计方案。

  • msg:Request、msg:Acknowledge 报文丢失:
    • 直接提示消息“发送失败”,问题不大。
  • msg:Notify、ack:Rquest、ack:Acknowledge、ack:Notify 报文丢失:
    • 超时重传
      • 客户端 A 发出 msg:Request、收到 msg:Acknowledge 之后,在一个期待的时间内,如果没有收到 ack:Notify,客户端 A 会尝试将 msg:Request 重发,可能客户端 A 同时发出了很多消息,故客户端 A 需要在本地维护一个等待 ACK 队列,并配合 timer 超时机制,来记录哪些消息没有收到 ACK-N,以定时重发。
      • 一旦收到了 ack:Notify,则将消息从“等待 ACK 队列”中移除。

4. 消息由于重传导致消息重复怎么办?

重传是有可能导致消息重复的。

msg:Notify 消息丢失导致的重传是有效地,此时消息不会重复。

ack:Notify 消息丢失导致的重传,会导致消息重复,因为客户端 B 已经收到消息了,只是对 A 的 ACK 消息没有收到。

那么消息如何去重呢?

其实也很简单,由客户端生成一个去重 ID,保存在等待 ACK 队列里,同一条消息使用相同的 去重 ID 来传,这样当客户端 B 收到消息之后,可以根据此 ID 去重,而且不影响用户体验。

5. 离线消息的可靠传递是如何做的?

离线消息的发送步骤一般如下:

  1. 用户 A 发送一条消息给用户 B;
  2. 服务器查看用户 B 的状态,发现 B 的状态为“offline”(即 B 当前不在线);
  3. 服务器将此条消息以离线消息的形式持久化存储到 DB 中;
  4. 服务器返回用户 A 发送成功 ACK 确认包(对于消息发送方而言,消息一旦落地存储至 DB 就认为是发送成功了)

消息拉取的方案可以是 按需拉取

6. 实际业务实现方案

通过心跳来保证 websocket 连接,如果断开的话,重试四次,每 20 秒 1 次,共 80 秒。

消息重传共 2 次,每 15 秒 1 次。

参考文档