小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
1. 如何保证消息顺序性?
私聊
假设消息通过 TCP 传输,假设发送两条消息的话,就有可能出现先发后至的情况。
一种解决方案是:
首先保证发送方发送的消息顺序是一致的、是正确的。比如说发送方维护一个消息 ID,这样可以按照发送方的消息 ID 顺序排列。
然后发送方和接收方的顺序,可以按照服务器时间作为消息的顺序。这样保证消息的顺序性。
群聊
- 群聊不再利用发送方的序列号来保证时序,因为发送方不单点,时间也不一致(个人发送的时序,本地存储可以实现,发送的内容本来就是有序的)
- 可以利用服务器的单点做序列化;
这个方法能实现,所有群友消息展示的时序相同。缺点是,这个生成全局递增序列号的服务很容易成为系统瓶颈。
进一步的优化策略是:群消息不用保证全局消息序列有序,而只要保证一个群内的消息有序即可。
在这个方案中,Service 层不再需要去一个统一的后端去拿全局序列号。在 Service 层来序列化同一个群的消息,保证所有群友看到消息的时序是相同的。
2. 如何保证在线消息的可靠性?
消息可靠性需要有确认送达机制,否则就无法谈消息的可靠性。
首先,发送消息是一个三方通信,即 A - Server - B
可靠消息传递需要 6 个报文,即:
- 客户端 A 向服务端 Server 发送消息请求包 msg:Request
- 服务端 Server 向客户端 A 发送一个回应报文 msg:Acknowledge
- 服务端 Server 向客户端 B 发送通知报文 msg:Notify
- 客户端 B 向服务端 Server 发送一个 ACK 请求包 ack:Rquest
- 服务端 Server 回复客户端 B 一个 ACK 响应包 ack:Acknowledge
- 服务端 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. 离线消息的可靠传递是如何做的?
离线消息的发送步骤一般如下:
- 用户 A 发送一条消息给用户 B;
- 服务器查看用户 B 的状态,发现 B 的状态为“offline”(即 B 当前不在线);
- 服务器将此条消息以离线消息的形式持久化存储到 DB 中;
- 服务器返回用户 A 发送成功 ACK 确认包(对于消息发送方而言,消息一旦落地存储至 DB 就认为是发送成功了)
消息拉取的方案可以是 按需拉取。
6. 实际业务实现方案
通过心跳来保证 websocket 连接,如果断开的话,重试四次,每 20 秒 1 次,共 80 秒。
消息重传共 2 次,每 15 秒 1 次。