默认
发表评论 4
想开发IM:买成品怕坑?租第3方怕贵?找开源自已撸?尽量别走弯路了... 找站长给点建议
一种简单保障IM消息可靠性和有序性的实现方案
阅读(19249) | 评论(4 收藏1 淘帖 1

定义


消息可靠性:不丢失、不重复
消息有序性:任意时刻消息保证与发送端顺序一致
总结:不丢、不重、有序

典型的 IM 架构


一种简单保障IM消息可靠性和有序性的实现方案_1681025692120-760483f2-a3f0-40cc-b2f0-0b23b311665b.png

典型的服务端中转型IM架构:一条消息从 clientA 发出后,需要先经过 IM 服务器来进行中转,然后再由 IM 服务器推送给 clientB。

所以准确来说:

  • 消息的可靠性 = 上行消息可靠 + 服务端消息可靠 + 下行消息可靠
  • 消息的有序性 = 上行消息有序 + 服务端消息有序 + 下行消息有序

TCP 并不能保证消息的可靠性和有序性


TCP 是网络层的协议,只能保证网络层间的可靠传输和数据有序,并不能保障应用层的可靠性和有序性。

  • clientA 发送的数据可靠抵达 Server 网络层后,还需要应用层进行处理,此时 Server 进程崩溃后重启,clientA 认为已经送达,但是 Server 业务层无感知,因此消息丢失
  • clientA 发送 msg1 和 msg2 达到应用层,解析后交给两个线程处理,msg2 先落库,造成消息乱序

如何保障消息的可靠性


TCP 虽然不能直接帮我们,但是我们可以借鉴 TCP 的可靠传输:超时重传 + 消息确认(ACK) + 消息去重,我们可以实现应用层的消息确认机制。

通过在应用层加入超时重传 + 消息确认机制,保障了消息不会丢失,但是带来了新问题:消息重复,TCP 其实也告诉我们答案了,消息id,幂等去重。

如何保证消息的有序性


保证消息有序性的难点在于:没有全局时钟
缩小思路:其实不需要全局序列,在会话范围(单聊、群聊)内有序即可
解决思路:仿微信的序列号生成思路,将标识消息唯一性的 id 和标识消息有序性的 id 单独拆开

现在我们只需要考虑该 id:

  • 会话内唯一
  • 单调递增

简单实现:对于每个用户,使用 Redis  incr 命令得到递增的序号;Redis 可能挂掉,重启换另一个节点可能导致该 id 回退,可以用 lua 脚本保证,当 Redis 节点重启,实现序列号跳变。

优化:递增 id 使用微信消息序列号生成的思路,使用双 buffer 优化。

项目实现


1上行消息的可靠性


clientA -> Server:使用 clientId 保证:

  • clientA 创建连接后,初始化 clientId = 0,发送消息时携带 clientId,并且 clientId++
  • clientA 发送消息后创建消息计时器,存入以 clienId 为 key,以 context.WithCancel 返回的 cancel 函数为 value 的 map,有限次数内指定间隔后 (利用 time.Ticker)重发消息,或者收到该 clientId 的 ACK 回复
  • Server 收到消息,解析后得到消息中的 clientId,Server 中维护当前连接收到的最大 max_clientId,当且仅当 max_clientId+1 == clientId,才接收该消息,否则拒绝
  • 仅当 Server 收到消息后,经过处理,回复 clientA 携带 clientId 的 ACK 消息
  • clientA 收到 ACK 消息后,根据 clientId 获取 cancel 函数执行

缺点:依靠 clientId 只能保证发送方的消息顺序,无法保证整个会话中消息的有序性。会话消息的有序性需要服务端的 id 来保证。

2服务端消息的可靠性


消息在 Server 中处理时的可靠性:使用 MQ + seqId 保证。

使用 userid 作为 key,使用 Redis incr 递增生成 seqId:

  • 消息到达 Server,Server 根据 max_clientId + 1 == clientId 校验是否接收消息,如果接收消息,更新 max_clientId 为 clientId,然后继续往下执行
  • Server 请求 Redis 获取发送者 userid incr 得到新的 seqId,并落库消息
  • Server 将消息写入 MQ,交给 MQ 的消费者异步处理,MQ 保证服务端消息可靠性
  • Server 回复 clientA  ACK 消息,携带接收消息中的 clientId 和前面发送者得到的最新 seqId
  • Server 中的 MQ 处理消息前,通过 Redis 获取收件人 userId 的 seqId,落库消息,并进行下行消息推送
  • Server 发送消息后创建消息计时器,丢入时间轮等待超时重发,或者收到 clientB ACK 后取消超时重发

群聊消息通过 seqId 保证:

  • 单个客户端群聊中,看到的任一(任何一个)客户端消息顺序和其发送消息顺序一致(群聊中存在 ABC,A 看到 B 的消息肯定和 B 的发送顺序一致,A 看到 C 的消息肯定和 C 的发送顺序一致)
  • 在多个客户端参与同一个群聊时,每个客户端所看到的来自任何一个客户端发送的消息以及消息发送的顺序都是一致的。但是,不同客户端所看到的消息顺序可能不同(群聊中存在 ABC,A 看到 B 的消息肯定和 B 的发送顺序一致,C 看到 B 的消息也肯定和 B 的发送顺序一致,但是 A 看到的整体消息可能和 C 看到的整体消息顺序不一致)

优化点:

  • 使用会话层面的 id,能保证群聊绝对有序,但是需要再构建会话层,维护更多状态,不确定是否值得
  • 换一种 id 生成的方式

3下行消息的可靠性


Server -> clientB:使用 seqId 保证:

  • Server 携带该用户最新 seqId 发送消息给 clientB
  • clientB 检查消息中的 seqId 是否等于自己本地存的 seqId+1,如果是则直接显示,回复 Server ACK 消息并更新本地 seqId
  • 如果不是(seqId 不等于本地存的 seqId +1),则携带最新消息中的 seqId 进行离线消息同步

离线消息同步:

  • 客户端登录时,携带本地存储的 seqId 拉取离线消息
  • 服务端分页返回所有当前用户消息表中大于 seqId 的消息(WHERE userId = x AND seqId > x LIMIT n ORDER BY seq ASC)
  • 客户端收到离线数据,并根据返回参数检查是否还需要继续拉取数据

代码实现:https://github.com/callmePicacho/GoChat

参考资料


即时通讯网 - 即时通讯开发者社区! 来源: - 即时通讯开发者社区!

标签:IM开发
上一篇:求教websocket 能实现 Reactor 模型吗?下一篇:一些简单优化IM消息推送延迟的思路
推荐方案
评论 4
楼主你好,你这贴的是markdown语法,我这里不支持MD语法,你可以直接在文本编辑模式下,用编辑器里自带的效果去排版
引用:JackJiang 发表于 2023-04-10 17:32
楼主你好,你这贴的是markdown语法,我这里不支持MD语法,你可以直接在文本编辑模式下,用编辑器里自带的效 ...

已修改

简单帮你重新排了个版,你看看是不是阅读体验要好多了。其实还可以优化地更好
引用:JackJiang 发表于 2023-04-10 18:24
简单帮你重新排了个版,你看看是不是阅读体验要好多了。其实还可以优化地更好

感谢站长大大!
打赏楼主 ×
使用微信打赏! 使用支付宝打赏!

返回顶部