1、点评
IM聊天消息的可靠投递,是每个线上产品都要考虑的IM热点技术问题。
IM聊天消息能保证可靠送达,对于用户来说,就好比把钱存在银行不怕被偷一样,是信任的问题。试想,如果用户能明显感知到聊天消息无法保证送达,谁还愿意来用你的APP?谁也不希望自已的话就像浮云一样随风飘逝。
必竟用IM聊天,虽然很多时候是费话,但总有关键时刻存在——比如向女神表白(哪怕明知被拒),作为合格的舔狗一定不希望女神错过这条消息。
所以,消息的可靠投递是每款IM产品和立足之本,也是IM开发者们孜孜不倦追求的技术目标。
本文作者将以自已IM开发过程中的真实总结,分享针对大量离线聊天消息,在确保用户端体验不降级的前提下,保证离线消息的可靠投递。
学习交流:
- 即时通讯/推送技术开发交流5群: 215477170[推荐]
- 移动端IM开发入门文章:《 新手入门一篇就够:从零开发移动端IM》
本文已同步发布于“即时通讯技术圈”公众号,欢迎关注:
▲ 本文在公众号上的链接是:https://mp.weixin.qq.com/s/T2w9h_AN_T2UnqNdVikX0Q,原文链接是:http://www.52im.net/thread-3069-1-1.html
2、本文作者
fzully(柳林勇):2005年数学系毕业,先后就职于福建新大陆、福建富士通、北京世纪奥通。长期从事服务端软件开发,涉及SIP服务器、内核RTP转送、电信级AAA认证系统、IM即时通讯系统等。在分布式高性能系统设计有多年经验积累。
本作者的另一篇:《IM群聊消息的已读未读功能在存储空间方面的实现思路探讨》也已被即时通讯网收录并整理发布,有兴趣可以前往阅读。
3、相关文章
《 从客户端的角度来谈谈移动端IM的消息可靠性和送达机制》
《 移动端IM中大规模群消息的推送如何保证效率、实时性?》
《 IM消息送达保证机制实现(一):保证在线实时消息的可靠投递》
《 IM消息送达保证机制实现(二):保证离线消息的可靠投递》(* 强烈推荐)
《 如何保证IM实时消息的“时序性”与“一致性”?》
《 一个低成本确保IM消息时序的方法探讨》
《 IM群聊消息如此复杂,如何保证不丢不重?》(* 强烈推荐)
《 移动端IM登录时拉取数据如何作到省流量?》(* 强烈推荐)
《 完全自已开发的IM该如何设计“失败重试”机制?》
《 IM开发干货分享:我是如何解决大量离线消息导致客户端卡顿的》(* 强烈推荐)
4、正文引言
暗恋女神良久,终于鼓起勇气决定向女神写一封情书。但如何表达才能感动女神?自感才疏学浅,于是通读四书五经、熟背唐诗宋词、遍览四大名著,已然腹有诗书气自华。一周末冥思苦想整日才写就一首七言律诗,虽无惊天地泣鬼神之势,但诚挚的爱念在字里行间里流淌,亦歌亦诗,相信会感动到女神,手机欣然发出。
发出一秒后,手心冒汗,感觉脸颊发烫,心脏像受惊吓的野兔一样快速跳动,就像第一次看见女神那时的感觉。闭着眼睛,想象女神看到消息时的情形,她是否也期盼我的表白?看到消息时是否心跳加速、小脸绯红?
一分钟后,紧盯手机屏幕,等待、期盼女神回复。
时间一分一秒地逝去,等一分钟像等一年一样漫长。
一小时后,仍然杳无音讯,难道她没看到消息么?或许在忙什么而没留意手机吧!
一天过去了,坐立不安,等待是一种痛苦的煎熬,期待和煎熬在心中交织翻滚,有几个瞬间甚至希望女神赶快拒绝自己,好让自己解脱!茶饭无味,失眠多天,整日魂不守舍。
一个月过去了,死心。
半年后,女神出嫁,婚礼那天前去祝福。席间亦随众觥筹交错,略有醉意,向女神敬酒:祝福你,但愿以后能遇见像你这样的女人。女神先是愣住、收起笑容,低下头,目光无神地看着大红地毯,长叹一声,言:我等你表白,等了一年!空气凝滞了几秒,女神强作欢颜:从今往后,各自安好吧,干杯!
我转身踱回到座位,拿起手机,打开那个App,看着曾经发出的情书,一切仿佛还在昨日,但故事脚本已被别人书写,欲哭无泪,叹老天为何如此捉弄我?为何我发的消息女神没收到啊!
失魂落魄地回到家里,从冰箱里拿出几瓶罗斯福10号来麻醉自己,在酒精强烈的作用下,迷迷入睡。
第二天醒来,我明白了一个道理:对IM系统而言,消息必达永远摆在第一位!
以上是胡说八道,以下开始正文。。。
5、用全量离线消息实现消息必达
我们在重构IM系统时,需解决上一代设计的痛点之一就是确保消息必达。
5.1 离线消息实现消息必达的流程
自然而然地会想到这么做——即由服务端为每个人保存一个“离线消息列表”。
具体的思路是这样:
- 1)当用户在线时,由IMS主动确保消息下发且收到客户端的应答确认时,才认为消息送达客户端,相应地把消息从“离线消息列表”移除;
- 2)如果客户端没有发回应答确认,IM服务端会再发送。
以此来确保消息一定送到客户端,看起来是很符合逻辑。当时调查过市面上多款IM,行为基本如此。
5.2 海啸般的离线消息
5.2.1)和平时期:
重构后的IM上线,内部测试及在公网运行,离线消息的工作一直很正常。
5.2.2)被签到签死了:
后来,为某客户部署的私有环境,其用户量达几十万,其中的一个组织接近三万人,全员群也接近三万人;还有,底下的部门也有相应的群组,几百到几千人群不等。
“报到”、“签到”。。。大量的类似消息被发到几千、几万人的群内,然后如果有人一两天没上线,或者被加入到多个组织内,等到其上线时,几万条离线消息像海啸一般涌来,您想象一下:手机用户刚登陆的几分钟内,是什么场景?
用户真的很无辜:我不就是登陆了一下App,叮叮咚咚响了几分钟,还卡,还发热。。。
客户端承受不起大规模离线消息的轰炸,怎么办?
5.3 临时运用方案
- 1)对若干大组织的全员群,对非管理员禁言;
- 2)通知所有用户不要在大群签到。
我承认,这确实不算是个正经方案。。。
6、远离全量离线消息
我承认,一开始设计离线消息时,真没想到是这样的使用场景。对于大多数IM的开发者,或许不会碰到这种场景(但凡事住最坏的可能性想,总是没错的)。
6.1 放弃以离线消息的形式实现消息必达
我开始思考什么是消息必达,以前的想法是:把用户该收的消息都送到其客户端,是消息必达。
后来,给消息必达下了新的定义:
- 1)用户有新消息时,确保让用户知道;
- 2)当用户要查看这些消息时,确保其可一条不漏地看到。
打个比方:
- 1)客户要把钱给您,不必送到您家里才算送到;
- 2)而是转账到您的银行账户上,并告知您;
- 3)当您要用钱时,直接从银行账户上消费即可。
从此,不会在用户上线时向其发送大量离线消息(即全量推送)。
6.2 以会话列表为基础来实现消息必达
客户端在上线时,先从服务端更新会话列表,也就是你通常在每个IM客户端的首页看到的这个(如下图所示)。
(上图引用自《IM开发快速入门(一):什么是IM系统?》)
每一个会话列表项包含如下信息(此处简化了与本文无关的成员变量):
{
// 会话对象的角色类型,比如私聊、群聊、系统通知、业务通知。。。
uint32 session_role;
// 会话对象的ID
uint32 session_id;
// 会话时间戳,用于消息同步;
// 指会话的最后操作时间,比如清除角标的时间,与会话最后一条的消息时间未必一致
uint64 session_timestamp;
// true表示新增或更新,false表示被删除
boolis_add;
// 当is_add=false时,忽略以下信息
// 仅用于显示角标的未读数量,当用户查看该会话后清零,且客户端多端同步
uint32 new_msg_count;
// 会话的最后一条消息
MessageItem latest_msg;
// 跳转消息的时间戳,即new_msg_count的最旧1条消息的时间
uint64 goto_timestamp;
}
为方便讨论,假设以下前提:
- 1)周五傍晚18:00下班,我关闭App,我是9527;
- 2)有1小姐姐向我发了5条消息留言,约我周末去海边玩,她是杨幂3306;
- 3)然后,另1小姐姐也向我发了33条消息留言,内容我不便透露,她是景甜5672;
- 4)严正声明:我跟她们很清白,其实我喜欢的是6379。
对,既然是假设,假一点也无妨。
我下班回到家,看到手机有通知栏消息,打开App将会发生哪些事呢?
App和IM后端的交互:
1)登录后,App以18:00填充参数latest_session_time,向IMS获取会话列表(其实不是以下线时间18:00,但这样更易理解);
2)IM后端检查发现我从18:00开始,有2个会话更新了,于是向App发送应答,以增量形式携带2个会话项:杨幂3306,景甜5672。其中景甜5672的会话项信息如下:
{
uint32 session_role = Role_User; //表示私聊
uint32 session_id = 5672; //景甜的ID
uint64 session_timestamp = 1594464295335672; //最后一条消息的时间戳,微秒
boolis_add = true; // true表示是更新项
uint32 new_msg_count = 33; // 景甜向我发了33条消息
MessageItem latest_msg = "房号是0520"; //最后1条消息,结构体MessageItem简略不表
uint64 goto_timestamp = 1594463697556677; // 向我发的33条消息的最早1条的时间
}
3)App收到步骤2的应答,我在App的会话列表窗口里,能看到2项更新,景甜发来的未读消息数33条,杨幂的是5条,如下图所示:
4)点开景甜5672的会话,App将向IMS发起同步消息的请求,获取最新的10条聊天消息(为了显示一屏):
{
uint32 session_role = Role_User; //表示私聊
uint32 session_id = 5672; //景甜的ID
uint64 begin_time = 1594464295335672; //步骤2返回的session_timestamp
uint64 end_time = 1594434153444222; //景甜上午向我发的最后一条消息的时间
uint32 max_pieces = 10; //本次最多取10条,PC屏幕大则不妨取20条
}
5)IM后端收到步骤4请求,将返回33条新消息的最后10条给App,呈现聊天窗口内,且聊天窗口上方有一个tip:“↑ 33条新消息”,如下图所示:
6)我可以向上翻动聊天记录,那么App将持续向IMS获取第2批同步消息;或者也可以点击tip:“↑ 33条新消息”,直接跳转到33条消息的最旧一条,这样支持从最旧的消息向新的翻看。
相比于客户端简单地被动接收服务端的离线通知方式,这种设计使得客户端的处理逻辑更复杂。
主要体现在:
- 1)客户端向服务端取的同步消息是未必完整,这些存在客户端的消息,在时间区间上可能不连续的;
- 2)客户端需要知道不同消息之间是否有断代,如果有则需要向服务端查询同步消息来merge本地信息,使其连续,即客户端要实现消息融合。
我的建议:用C++实现一个统一的底层imsdk库,来负责这些共通的消息处理和存储。避免各客户端(Windows,iOS,Android等)各自实现这些逻辑,减少工作量,也降低各端不一致的风险。
6.3 以会话列表为基础与用全量离线消息的方案对比
6.3.1)用全量离线消息实现的方案优缺点:
实现原理:由IM服务端确保消息送达客户端,客户端存储后发回确认。
方案优点:逻辑简单。
在聊天消息不同数量级时的表现:
- a. 离线消息量不多(如几百条):没有效率问题,且消息全部达到客户端本地,方便进行查找等动作;
- b. 离线消息量巨大(如几万条):用户登录瞬间CS间瞬时流量大,客户端瞬时要存储、更新的数据量巨大,可能出现卡顿、假死等情况。
6.3.2)用会话列表为基础的方案优缺点:
实现原理:客户端先同步会话列表,由用户驱动不定次获取同步消息。
方案缺点:逻辑复杂,客户端增加不少工作。
在聊天消息不同数量级时的表现:
- a. 离线消息量不多(如几百条):没优势;
- b. 离线消息量巨大(如几万条):登录时交互数据小,对IM后端、客户端、用户体验,都比较友好。
7、多终端条件下,如何得到完整消息履历?
由于同一个用户的每个终端,其会话最后更新时间、每个会话的最后一条时间可能都不一样,参照上一节的实现思路,可以得到解决方案。
具体如下:
- 1)参照第6.2章节的“App和IM后端的交互”第1个步骤,可取到不同的增量变化的会话列表项;
- 2)参照第6.2章节的“App和IM后端的交互”第4个步骤,可取到任一区间的同步消息,得到完整消息。
8、离线消息是否就彻底废弃了?
有若干情况,仍然需要保留离线消息,以确保消息送达。
比如以下情形:
- 1)别人向我发送离线文件:这种情况下不能依赖同步消息来获取。因为不以离线消息通知的话,用户在没有拉取到对应的同步消息前,是不知道有离线文件的;
- 2)撤回消息:即使接收者不拉取同步,仍然要保证在上线后其数据在第一时间被撤回。注意:这里可能存在多端撤回问题;
- 3)用户在线时的消息下发:由于用户在线时,IM后端向客户端发送消息可能碰到网络抖动等情况,导致消息下发失败,这些消息先可以直接存在离线消息队列,IM后端可在收到客户端的心跳包时重发消息。相当于维护了一个在线消息的离线队列。
9、本文结语
曾经有一段真挚的爱情摆在我面前,如果时间倒流到半年前,我会选择一个靠谱的IM来发送消息,也许故事的脚本就由自己书写——是否要整一个时光倒流的版本,抱得美人归的那种?
不整了不整了,我得不到女神,你们才欢喜,我太了解你们了。。。各位爷欢喜就好。
附录:IM开发干货系列文章
本文是系列文章中的第26篇,总目录如下:
《 IM消息送达保证机制实现(一):保证在线实时消息的可靠投递》
《 IM消息送达保证机制实现(二):保证离线消息的可靠投递》
《 如何保证IM实时消息的“时序性”与“一致性”?》
《 IM单聊和群聊中的在线状态同步应该用“推”还是“拉”?》
《 IM群聊消息如此复杂,如何保证不丢不重?》
《 一种Android端IM智能心跳算法的设计与实现探讨(含样例代码)》
《 移动端IM登录时拉取数据如何作到省流量?》
《 通俗易懂:基于集群的移动端IM接入层负载均衡方案分享》
《 浅谈移动端IM的多点登陆和消息漫游原理》
《 IM开发基础知识补课(一):正确理解前置HTTP SSO单点登陆接口的原理》
《 IM开发基础知识补课(二):如何设计大量图片文件的服务端存储架构?》
《 IM开发基础知识补课(三):快速理解服务端数据库读写分离原理及实践建议》
《 IM开发基础知识补课(四):正确理解HTTP短连接中的Cookie、Session和Token》
《 IM群聊消息的已读回执功能该怎么实现?》
《 IM群聊消息究竟是存1份(即扩散读)还是存多份(即扩散写)?》
《 IM开发基础知识补课(五):通俗易懂,正确理解并用好MQ消息队列》
《 一个低成本确保IM消息时序的方法探讨》
《 IM开发基础知识补课(六):数据库用NoSQL还是SQL?读这篇就够了!》
《 IM里“附近的人”功能实现原理是什么?如何高效率地实现它?》
《 IM开发基础知识补课(七):主流移动端账号登录方式的原理及设计思路》
《 IM开发基础知识补课(八):史上最通俗,彻底搞懂字符乱码问题的本质》
《 IM的扫码登功能如何实现?一文搞懂主流应用的扫码登陆技术原理》
《 IM要做手机扫码登陆?先看看微信的扫码登录功能技术原理》
《 IM开发基础知识补课(九):想开发IM集群?先搞懂什么是RPC!》
《 IM开发实战干货:我是如何解决大量离线聊天消息导致客户端卡顿的》
《 IM开发干货分享:如何优雅的实现大量离线消息的可靠投递》(* 本文)
另外,如果您是IM开发初学者,强烈建议首先阅读《新手入门一篇就够:从零开发移动端IM》。
(本文同步发布于:http://www.52im.net/thread-3069-1-1.html)