零基础入门 IM 系列——可靠的消息篇

前言

IM 本身其实是一个比较复杂常见的领域,每个公司都有自己的实现,如果细说可以讲出很多东西。本系列主要列出 IM 的常见问题,以及比较主流的方法论,如有错误之处还请指导。

IM 的核心——保证消息的可靠

说到 IM(即时聊天),可能大家下意识会觉得很简单,不就是把一条消息从 A 发给 B 嘛。其实不然,由于复杂的网络环境,分布式等问题。消息可能会出现丢失,时序错乱,消息重复等等问题。一个优秀的 IM 系统,必然要解决这些问题,消息的可靠则是一个 IM 系统的核心。

保证消息被送达

既然是聊天,那必然有消息的发送方与接收方。这里我们暂时先只讨论单聊的情况,不考虑群聊的情况,因此发送方与接收方都只有一个人。首先我们要确定如何才算消息被送达?根据接收方的在线与否我们可以分成两种场景:

  • 接收方在线时,需要保证消息被接收方实时拉取到

  • 接收方离线时,需要保证接收方下次上线时消息能被准确拉取到

接下来我们会对这两种场景分别讨论。

保证在线消息被送达

如何去保证一条消息能被稳定的送达呢?其实这个问题已经有很多程序猿给过我们答案了,我们可以参考 TCP 协议,接收方在收到发送方的每一条消息都返回一个 ACK 表示应答。 当然了对于 IM 系统而言,消息会更为复杂,因为一条消息的参与者实际上有三方:发送方,服务端,接收方。

基于此我们可以把消息种类分为三类:

  1. 请求报文(request):客户端主动发送给服务器的报文

  2. 应答报文(acknowledge):服务器被动应答客户端的报文

  3. 通知报文 (notify):服务器主动发送给客户端的报文

消息送达保证机制(QoS机制)

我们可以把整个消息投递流程分成两部分去看:一部分为发送方将消息投递给接收方,在这里我们称为 Msg 过程。另一部分则为接收方对收到的消息进行应答,在这里我们称为 Ack 过程。

接下来的我们以 ClientA 给 ClientB 发送一条消息举例:

发送方投递消息(Msg 过程)

  1. client-A 向 server 发送一个消息请求包,即 msg:request

  2. server 在成功处理后,回复 client-A 一个消息响应包,即 msg:acknowledge

  3. 如果此时 client-B 在线,则 server 主动向 client-B 发送一个消息通知包,即 notify(当然,如果 client-B 不在线,则消息会存储离线)

接收方对消息进行应答(Ack 过程)

  1. client-B 向 server 发送一个 ack 请求包,即 ack:request

  2. server 在成功处理后,回复 client-B 一个 ack 响应包,即 ack:acknowledge

  3. server 主动向 client-A 发送一个 ack 通知包,即 ack:notify

为什么需要 ACK 过程?

在前 3 步中,clientA 端收到 Msg(ACK) 只能表示 server 端收到了消息,并不能保证 clientB 接收到了消息。因此仍然需要 ClientB 进行确认才能保证消息的可靠性。

完整消息送达保证机制

一个应用层即时通讯消息的可靠投递,共涉及 6 个报文,这就是 im 系统中消息投递的最核心技术

消息送达保证机制会遇到的问题

Msg(Request),Msg(ACK) 报文的丢失

代表消息发送至服务端时出现问题,此时只需发送方提示发送失败或红点处理即可。

Msg(Notify),Ack(Request),Ack(Ack),Ack(Notify) 报文的丢失

此时发送方收不到期待的 ACK(Notify) 报文,无法确认 ClientB 是否收到消息。此时我们可以使用使用超时重试机制解决,即由发送方本地维护一个需要收到 ACK 的消息队列,来记录哪些消息没收到 ACK(Notify) 来定时重发。当然这样也引入了消息的重复性问题,下文也会介绍解决方案。

保证离线消息被送达

如果接受方不在线时,我们则需要保证在接收方下次上线时能再次收到该条消息,因此这些消息理所当然的会被持久化保存起来。注:由于医疗行业的特殊性,必须持久化所有的消息(无论接收方是否在线即消息漫游)。但是在大多数IM场景中(如微信,QQ),服务端是没有必要持久化所有的消息的,因为维护这么多的消息需要很大成本,离线保存的消息在接收方最终收到后也会被删除。

我们仍然以 ClientA 给 ClientB 发送消息举例:

离线消息持久化流程

  1. client-A 向 server 发送一个消息请求包,即 msg:request

  2. server 在成功处理后,回复 client-A 一个消息响应包,即 msg:acknowledge

  3. server 检查发现 client-B 已经离线

  4. server 将 client-A 发送的消息持久化至数据库中

  5. server 主动向 client-A 发送一个 ack 通知包,即 ack:notify

离线消息拉取流程

  1. client-B 上线后开始想 server 端请求拉取离线消息

  2. server 端从数据库拉取离线消息

  3. server 端返回离线消息给 client-B

  4. client-B 收到离线消息后,成功渲染页面无误后向服务端返回 ACK,表示离线消息已收到

  5. server 端将刚才拉取的离线消息从数据库中删除

为什么收到 ACK 后才能删除离线消息?

为了保证消息的可靠性,如果在第 3 步时因为网络问题数据丢失了,那么离线消息也就直接丢失了。因此一定要确保接收方已经收到了离线消息再删除。

拉取离线消息优化

上文只提到了拉取离线消息,但是究竟是拉取哪些离线消息呢?一般有两种方案:

一次只拉取一个好友的离线消息

用户上线后先拉取出所有好友的离线消息数量,当用户想点进去看某一个好友消息时再拉取该好友的离线消息。这样的好处是按需拉取,可以节省部分流量电量(如果是移动端的话)。

一次拉取所有好友的离线消息

用户上线后直接拉取出所有好友的离线消息,存储至本地客户端,当用户想看好友消息时直接客户端计算出所需要展示的消息即可。这样的好处是减少前后端交互次数。目前主流的 IM(微信,QQ)都采用这种方案,毕竟用户体验会更好。当然也可能会出现拉取数据量过多的问题,不过我们可以使用优秀的编码协议(如 Protocol Buffer )去缓解。

保证消息的不重复

一个健壮完善的的 IM 系统一定能保证消息是不重复的,试想一下如果微信是这样的你还会用吗?

小明:在吗?

小明:明天晚上有空吗?

小明:在吗?

小明:在吗?

小明:明天晚上有空吗?

小明:在吗?

小明:明天晚上有空吗?

小明:明天晚上有空吗?

女神:你有病呀!滚!!

小明:???

上文我们有提到过,为了保证消息的送达,发送方一般会对某些没有收到 ACK 的消息进行定时重发。此时也会区分成两种场景

  1. 接收方没有收到发送的消息,此时超时重试机制就非常有效

  2. 接收方收到了消息,但是 ACK 因为网络问题丢失了,此时超时重试可能会导致接受方重复收到消息。

解决方案也非常简单,每一条消息由发送方去生成唯一的 UUID,重试时也使用该 UUID,由 Client-B 对消息进行去重即可。

保证消息的时序性

什么是消息的时序性?

要保证消息的时序性,我们先得弄懂什么才是消息的时序性,简单来说就是发送方发送顺序与接收方展现顺序一致。 举个栗子: 小明给女神发的信息是:

小明:我的兴趣爱好有:

小明:写代码

小明:打篮球

小明:你的兴趣爱好是什么?

女神收到的则是:

小明:打篮球

小明:你的兴趣爱好是什么?

小明:写代码

小明:我的兴趣爱好有:

女神:你是个好人。

如何保证消息的时序性

一般来说我们可以为消息生成单调递增的序列号 (seq) 来设置顺序,在消息展示时根据序列号对消息进行排序即可。至于如何生成该序列号,也有很多种方案,如:基于 db 的自增,分布式 id 框架生成等等,感兴趣的可以自己去查阅,这里就不再展开了。

以客户端(发送方)还是服务端的时序为准?

由于网络延迟,服务端接收消息的顺序与客户端发送消息的顺序可能是不一致的(当然一般误差不会太大),因此保证消息时序性时,我们需要先界定下究竟是以客户端发送顺序为准还是服务端接收消息的顺序为准。此时我们可以根据自己的业务去判断。

针对于单聊的场景

对于单聊来说我们既可以以客户端的时序为准,也可以以服务端的时序为准。当然我更倾向于按照客户端的时序为准是更为贴切的。

针对于群聊的场景

对于群聊来说,只能以服务端的时序为准。因为发送方客户端并不是单点的,时间可能也是不一致的,因此此时应该以服务端的时序为准。

结语

现在你知道了你给女神发出的一条消息,它可能会丢,可能会重复,甚至可能会错乱,不过你已经学会了怎样去解决这些问题。你以为这就结束了?当然远没有,传输使用 TCP 还是 UDP?使用什么编码格式?如何实现多端同步?如何去支持群聊?想做好一个 IM 系统还有许多路要走,后续将会一一带来。

参考资料

  • http://www.52im.net(如果对 IM 感兴趣的话,墙裂推荐该网站)

全文完


以下文章您可能也会感兴趣:

我们正在招聘 Java 工程师,欢迎有兴趣的同学投递简历到 rd-hr@xingren.com 。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值