请选择 进入手机版 | 继续访问电脑版

默认
打赏 发表评论 9
IM开发干货分享:如何优雅的实现大量离线消息的可靠投递
微信扫一扫关注!

本文由作者“fzully”授权发布,他的博客地址是:blog.csdn.net/fzrlly。即时通讯网收录时,有修订和改动。


1、点评


IM聊天消息的可靠投递,是每个线上产品都要考虑的IM热点技术问题。

IM聊天消息能保证可靠送达,对于用户来说,就好比把钱存在银行不怕被偷一样,是信任的问题。试想,如果用户能明显感知到聊天消息无法保证送达,谁还愿意来用你的APP?谁也不希望自已的话就像浮云一样随风飘逝。

必竟用IM聊天,虽然很多时候是费话,但总有关键时刻存在——比如向女神表白(哪怕明知被拒),作为合格的舔狗一定不希望女神错过这条消息。

11.jpg

所以,消息的可靠投递是每款IM产品和立足之本,也是IM开发者们孜孜不倦追求的技术目标。

本文作者将以自已IM开发过程中的真实总结,分享针对大量离线聊天消息,在确保用户端体验不降级的前提下,保证离线消息的可靠投递。

本文已同步发布于“即时通讯技术圈”公众号,欢迎关注:
52im_qr_即时通讯技术圈_400px.png
▲ 本文在公众号上的链接是:https://mp.weixin.qq.com/s/T2w9h_AN_T2UnqNdVikX0Q

2、本文作者


author.jpg
fzully(柳林勇):2005年数学系毕业,先后就职于福建新大陆、福建富士通、北京世纪奥通。长期从事服务端软件开发,涉及SIP服务器、内核RTP转送、电信级AAA认证系统、IM即时通讯系统等。在分布式高性能系统设计有多年经验积累。

本作者的另一篇:《IM群聊消息的已读未读功能在存储空间方面的实现思路探讨》也已被即时通讯网收录并整理发布,有兴趣可以前往阅读。

3、相关文章



4、正文引言


暗恋女神良久,终于鼓起勇气决定向女神写一封情书。但如何表达才能感动女神?自感才疏学浅,于是通读四书五经、熟背唐诗宋词、遍览四大名著,已然腹有诗书气自华。一周末冥思苦想整日才写就一首七言律诗,虽无惊天地泣鬼神之势,但诚挚的爱念在字里行间里流淌,亦歌亦诗,相信会感动到女神,手机欣然发出。

发出一秒后,手心冒汗,感觉脸颊发烫,心脏像受惊吓的野兔一样快速跳动,就像第一次看见女神那时的感觉。闭着眼睛,想象女神看到消息时的情形,她是否也期盼我的表白?看到消息时是否心跳加速、小脸绯红?

一分钟后,紧盯手机屏幕,等待、期盼女神回复。

22.jpg

时间一分一秒地逝去,等一分钟像等一年一样漫长。

一小时后,仍然杳无音讯,难道她没看到消息么?或许在忙什么而没留意手机吧!

33.jpg

一天过去了,坐立不安,等待是一种痛苦的煎熬,期待和煎熬在心中交织翻滚,有几个瞬间甚至希望女神赶快拒绝自己,好让自己解脱!茶饭无味,失眠多天,整日魂不守舍。

一个月过去了,死心。

44.jpg

半年后,女神出嫁,婚礼那天前去祝福。席间亦随众觥筹交错,略有醉意,向女神敬酒:祝福你,但愿以后能遇见像你这样的女人。女神先是愣住、收起笑容,低下头,目光无神地看着大红地毯,长叹一声,言:我等你表白,等了一年!空气凝滞了几秒,女神强作欢颜:从今往后,各自安好吧,干杯!

我转身踱回到座位,拿起手机,打开那个App,看着曾经发出的情书,一切仿佛还在昨日,但故事脚本已被别人书写,欲哭无泪,叹老天为何如此捉弄我?为何我发的消息女神没收到啊!

失魂落魄地回到家里,从冰箱里拿出几瓶罗斯福10号来麻醉自己,在酒精强烈的作用下,迷迷入睡。

55.jpg

第二天醒来,我明白了一个道理:对IM系统而言,消息必达永远摆在第一位!

以上是胡说八道,以下开始正文。。。

5、用全量离线消息实现消息必达


我们在重构IM系统时,需解决上一代设计的痛点之一就是确保消息必达。

5.1离线消息实现消息必达的流程


自然而然地会想到这么做——即由服务端为每个人保存一个“离线消息列表”。

具体的思路是这样:

  • 1)当用户在线时,由IMS主动确保消息下发且收到客户端的应答确认时,才认为消息送达客户端,相应地把消息从“离线消息列表”移除;
  • 2)如果客户端没有发回应答确认,IM服务端会再发送。

以此来确保消息一定送到客户端,看起来是很符合逻辑。当时调查过市面上多款IM,行为基本如此。

5.2海啸般的离线消息


5.2.1)和平时期:

重构后的IM上线,内部测试及在公网运行,离线消息的工作一直很正常。

5.2.2)被签到签死了:

后来,为某客户部署的私有环境,其用户量达几十万,其中的一个组织接近三万人,全员群也接近三万人;还有,底下的部门也有相应的群组,几百到几千人群不等。

“报到”、“签到”。。。大量的类似消息被发到几千、几万人的群内,然后如果有人一两天没上线,或者被加入到多个组织内,等到其上线时,几万条离线消息像海啸一般涌来,您想象一下:手机用户刚登陆的几分钟内,是什么场景?

用户真的很无辜:我不就是登陆了一下App,叮叮咚咚响了几分钟,还卡,还发热。。。

66.jpg

客户端承受不起大规模离线消息的轰炸,怎么办?

5.3临时运用方案


  • 1)对若干大组织的全员群,对非管理员禁言;
  • 2)通知所有用户不要在大群签到。

我承认,这确实不算是个正经方案。。。

6、远离全量离线消息


我承认,一开始设计离线消息时,真没想到是这样的使用场景。对于大多数IM的开发者,或许不会碰到这种场景(但凡事住最坏的可能性想,总是没错的)。

6.1放弃以离线消息的形式实现消息必达


我开始思考什么是消息必达,以前的想法是:把用户该收的消息都送到其客户端,是消息必达。

后来,给消息必达下了新的定义:

  • 1)用户有新消息时,确保让用户知道;
  • 2)当用户要查看这些消息时,确保其可一条不漏地看到。

打个比方:

  • 1)客户要把钱给您,不必送到您家里才算送到;
  • 2)而是转账到您的银行账户上,并告知您;
  • 3)当您要用钱时,直接从银行账户上消费即可。

从此,不会在用户上线时向其发送大量离线消息(即全量推送)。

6.2以会话列表为基础来实现消息必达


客户端在上线时,先从服务端更新会话列表,也就是你通常在每个IM客户端的首页看到的这个(如下图所示)。

x3.png
上图引用自《IM开发快速入门(一):什么是IM系统?

每一个会话列表项包含如下信息(此处简化了与本文无关的成员变量):
{
        // 会话对象的角色类型,比如私聊、群聊、系统通知、业务通知。。。
        uint32  session_role;
        // 会话对象的ID
        uint32        session_id;
        // 会话时间戳,用于消息同步;
        // 指会话的最后操作时间,比如清除角标的时间,与会话最后一条的消息时间未必一致
        uint64 session_timestamp;
        // true表示新增或更新,false表示被删除
        bool is_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; //最后一条消息的时间戳,微秒
        bool is_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条,如下图所示:
x1.jpg

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条新消息”,如下图所示:
x2.jpg

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来发送消息,也许故事的脚本就由自己书写——是否要整一个时光倒流的版本,抱得美人归的那种?

不整了不整了,我得不到女神,你们才欢喜,我太了解你们了。。。各位爷欢喜就好。

x4.jpg

附录:IM开发干货系列文章


本文是系列文章中的第26篇,总目录如下:


另外,如果您是IM开发初学者,强烈建议首先阅读《新手入门一篇就够:从零开发移动端IM》。

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

上一篇:IM开发快速入门(一):什么是IM系统?下一篇:请教有关IM中消息ID应该如何生成的疑惑

本帖已收录至以下技术专辑

想开发IM:买成品怕坑?租第3方怕贵?找开源自已撸?尽量别走弯路了... 找站长给点建议
推荐方案
评论 9
文章很好  会话维护模式 多端同步下很复杂 ,还是学微信的:消息只存指定的天数然后采用离线拉取模式 感觉还行。

如果不像作者一样不到万不得已我觉得还是采用全量拉消息好点。

因为如果是服务端维护会话:

  • 第一:消息搜索只能走服务端的搜索,
  • 第二:会话有很多状态比如有 [转账],有人@我 [红包] 等等这些状态就需要服务端维护了
  • 第三:用户进入会话刷消息体验相比第一种方式要差  
  • 第四:服务端和客户端实现第二种方式都比第一种方式复杂。
引用:tangtao 发表于 2020-07-14 11:08
文章很好  会话维护模式 多端同步下很复杂 ,还是学微信的:消息只存指定的天数然后采用离线拉取模式 感觉 ...

看的出来,是枚im开发老油条了
签名: 《如何让你的WebSocket断网重连更快速?》http://www.52im.net/thread-3098-1-1.html
引用:tangtao 发表于 2020-07-14 11:08
文章很好  会话维护模式 多端同步下很复杂 ,还是学微信的:消息只存指定的天数然后采用离线拉取模式 感觉 ...

大哥评价很在理!其中,针对第3点的体验,可在客户端取到会话列表时,对有未读条目的会话,App主动取一次同步(返回条目数 <= 一个屏幕显示的条数,因为客户端缺的消息可能只有几条)。
我在整理文字时有失误,确实不用让用户点击会话才触发首次同步,多谢指正!
引用:fzully 发表于 2020-07-14 12:01
大哥评价很在理!其中,针对第3点的体验,可在客户端取到会话列表时,对有未读条目的会话,App主动取一次 ...

活捉作者。感谢大佬的分享。
签名: 《如何让你的WebSocket断网重连更快速?》http://www.52im.net/thread-3098-1-1.html
引用:fzully 发表于 2020-07-14 12:01
大哥评价很在理!其中,针对第3点的体验,可在客户端取到会话列表时,对有未读条目的会话,App主动取一次 ...

感谢大牛的分享,读完了你的文章,基于会话列表这个方案,前提是你的服务端完整存储了所有聊天的记录内容对吧?

不然,多端同步、分段拉取都很难实现。
一直关注52im,大牛的这篇虽然没有高大上,但是很接地气,特地登陆回复。

另外,请问大牛,你的im里有没有用到集群?如果有,那么im集群中的socket长连接接入层跟业务逻辑层的通信,你是用RPC来实现的吗?
签名: 秋天到了,终于凉快了
引用:天黑请闭嘴 发表于 2020-07-14 14:07
感谢大牛的分享,读完了你的文章,基于会话列表这个方案,前提是你的服务端完整存储了所有聊天的记录内容 ...

服务端是得有完整记录,那些只需发在线的消息除外。游标在客户端手上,同一个用户的不同设备游标可能不一样,各自拿游标去同步即可。
引用:clark.li 发表于 2020-07-14 14:22
一直关注52im,大牛的这篇虽然没有高大上,但是很接地气,特地登陆回复。

另外,请问大牛,你的im里有没 ...

小弟我是用队列解耦,分布式设计普遍会这么玩的吧
666
打赏楼主 ×
使用微信打赏! 使用支付宝打赏!

返回顶部