IM - 消息四大特性与实现方案
1. 概述
本文介绍即时通讯软件中,消息
的几个特性如何保证
- 实时性
- 安全性
- 有序性
- 可靠性(不重不漏)
2. 实时性
消息的传输过程,分为三个阶段:
- 消息上行
- 消息处理
- 消息下行
其中,消息上下行,通过C/S全双工通信保证,并且有HeartBeat保证通道的可用性
消息处理由im-server实现,主要包含消息鉴权、消息审计、消息拆包,须保证稳定高效,可通过独立服务
实现,保证其可扩展性
消息鉴权
单聊情况
- 鉴权关系:sender与receiver是否好友(或同事)
群聊情况
- 鉴权关系:sender是否是群组成员
消息审计
- 审计内容:消息内容是否合法,不包含反党叛国、黄赌毒等语义
- 审计设备:移动设备不能接收文件
消息拆包
对于群组消息,需要拆包,1个发送包生成N个接收包,N是当前群组成员数
如果当前群组超过500人,例如10000人,可以考虑采用
消息广播
机制,提高消息下行效率。具体做法是,发送到MQ前,不拆分成10000个Package,而是X个Package,X等于SessionManager实例数。每个SessionManager实例收到Package后,自行拆包并发送到对应的TCP通道,Client即可实时接收到消息。
3. 安全性
- 传输过程中,采用https和wss,或者gRPC证书,保证消息安全
- 登录安全,采用JWT登录认证机制,防止身份信息被盗用
另外,
发送时加密
可进一步加强消息的安全性,但是不利于项目前期开发与调试,故优先级低
4. 有序性
4.1. 场景假设
场景1
在user1和user2的单聊会话中,user1发送3条消息,分别是aa、bb、cc。
其中aa和cc发送成功,bb由于网络或设备问题,在client自动重试3分钟之后,最终发送失败
,会展示一个是否重发的小button,用户可以自行决定是否重发。
如果用户点击按钮重发bb,user1和user2的消息列表会怎么展示?
user1
是aa、cc、bb
,其中bb
是从aa
和cc
中间删除,并append到最后
user2
是aa、cc、bb
场景2
在user1和user2的单聊会话中,user1发送3条消息,分别是aa、bb、cc。
其中aa发送成功,正在自动重发bb和cc时,user2发送了dd和ee,且user2发送成功。之后,bb和cc自动重发成功。
那么,user1和user2的消息列表会怎么展示?
user1
是aa、bb、cc、dd、ee
,切换一次session,再回到当前session,看到的消息是aa、dd、ee、bb、cc
user2
是aa、dd、ee、bb、cc
场景3
在user1、user2和user3的群聊会话中,user1和user2在同一时刻发送消息到im-server,甚至im-server生成的timestamp也一样。
那么,user1、user2和user3的消息列表如何展示?
尚未模拟,期望由MQ来决定,先到达客户端,则排在前面
从上下文语义来讲,两条timestamp一样的消息,前后顺序不重要
小结
与场景1相比,场景2有两个不同点
- 对于发送失败的消息,在
自动
发送成功后,原始顺序不变;如果是手动
重发,则删除后追加
到最后 - 对于
多条
发送失败的消息,在自动
发送成功后,receiver收到的顺序不应发生变化;如果是手动
重发,则依赖手动
触发重发的顺序
场景1和场景2的相同点
- 最终的消息顺序,依赖服务端的timestamp,即消息顺序的
最终一致性
4.2. 有序的定义
消息的有序性,更多是一种用户体验
- 允许同一session中,不同成员看到的消息顺序不一致
- 允许同一session中,某个成员的当前消息和历史消息顺序不一致
- 不允许同一session中,所有成员的历史消息顺序不一致
- 不允许单个user消息流乱序
- 不允许当前session消息列表存在
中间插入
的情况,允许首尾追加
和中间删除
的情况
4.3. 有序的实现
sequenceId
- 生成规则:在client发送消息给server时,由sender生成,全局唯一,可以是
{userId}-{sessionId}-{timestamp}-{autoIncrementNum}
- 作用1:sender基于
sequenceId
,接收send-ack
(包含server生成的messageId
) - 作用2:server基于
sequenceId
,完成消息去重(建议保存最近10条
且最长10分钟
即可) - 作用3:receiver基于
sequenceId
,完成消息去重 - 作用4:receiver基于
sequenceId
,解析得到userId
、timestamp
和autoIncrementNum
,校验单user消息流
顺序(非session的消息流)。如果消息顺序不一致,则报错,但是,仍然追加到消息列表末尾
messageId
- 生成规则:在server收到client的消息发送请求时,由server生成,全局唯一,统一标识某一消息,可以是
{timestamp}-{uuid}-{messageType}
- 作用1:client基于
messageId
,解析得到timestamp
,完成session消息流
排序 - 作用2:client基于
messageId
,完成消息的增值
功能,例如消息撤回
、消息编辑
、表情回复
等
单packge支持多message
- 消息收发的数据包,需要支持多个message,例如一次发送多条message,或者一次接收多条message
- 作用1:Client当前
正在发送
的消息数据包最多1个
,对于待发送
的消息,可以按顺序打包成一个消息数据包,一起发送 - 作用2:在
超大群组
聊天时,服务端可以实现消息合并
算法,即对receiver在x时间内的n条message进行打包发送
5. 可靠性(不重不漏)
如何实现消息的可靠性,保证消息不重不漏,需要从消息传输的三个环节着手
5.1. 消息上行
在c->s的过程中,采用send-ack的方式,如果client没有收到send-ack,则自动重发。
此过程,可保证消息不漏。但是,server收到的消息可能会重复。如果有重复sequenceId
的消息,server须去重。
注意:server须在投递RabbitMQ成功、且RabbitMQ持久化之后,再返回send-ack。否则,消息可能丢失。
5.2. 消息处理
Server处理消息的流程可能有多个服务参与,例如SessionManager、Auditor。
为了保证消息的可靠性,每次与RabbitMQ的交互,需要保证成功投递
、安全消费
。
- 成功投递,RabbitMQ完成持久化后才算成功投递
- 安全消费,消息处理成功,且有下一个服务
明确
接棒,才算安全消费
5.3. 消息下行
在s->c的过程中,采用receive-ack的方式,如果server没有收到receive-ack,则自动重发。
此过程,可保证消息不漏。但是,receiver收到的消息可能会重复。如果有重复sequenceId
的消息,receiver须去重。
如果receiver不在线,当receiver上线时,可通过API获取历史消息,并对历史消息列表进行二次校验(Server端须完成第一次校验),确保无重复sequenceId
的消息。
注意:server须在receiver返回receive-ack后,再告知RabbitMQ消费成功。否则,消息可能丢失。
5.4. 参考
- RabbitMQ如何保证消息的可靠性
https://segmentfault.com/a/1190000023501722
6. 总结
IM系统是不标准的,虽然XMPP协议试图解决这个问题,但事实证明根本不现实。
各家IM厂商几乎都是自已的私有协议,以及不同的实现逻辑,这也决定了即使同一个技术问题,对于IM来说很难有固定的实现套路和标准的解决方案。
不过,对于IM系统的几个硬性指标,是必须实现的。
实现过程中,需要考虑性价比,是追求更高的系统并发,还是更可靠的数据流;是追求更大的系统吞吐,还是更好的可扩展性和容灾。
最后,推荐一个IM专栏:
https://segmentfault.com/u/jackjiang