IM开发干货分享:如何优雅的实现大量离线消息的可靠投递

Wesley13
• 阅读 560

1、点评

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

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

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

IM开发干货分享:如何优雅的实现大量离线消息的可靠投递

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

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

本文已同步发布于“即时通讯技术圈”公众号。

2、本文作者 

IM开发干货分享:如何优雅的实现大量离线消息的可靠投递

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

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

3、正文引言

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

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

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

IM开发干货分享:如何优雅的实现大量离线消息的可靠投递

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

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

IM开发干货分享:如何优雅的实现大量离线消息的可靠投递

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

一个月过去了,死心。

IM开发干货分享:如何优雅的实现大量离线消息的可靠投递

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

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

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

IM开发干货分享:如何优雅的实现大量离线消息的可靠投递

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

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

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

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

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

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

具体的思路是这样:

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

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

4.2 海啸般的离线消息

4.2.1)和平时期:

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

4.2.2)被签到签死了:

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

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

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

IM开发干货分享:如何优雅的实现大量离线消息的可靠投递

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

4.3 临时运用方案

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

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

5、远离全量离线消息

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

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

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

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

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

打个比方:

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

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

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

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

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条,如下图所示:

IM开发干货分享:如何优雅的实现大量离线消息的可靠投递

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

IM开发干货分享:如何优雅的实现大量离线消息的可靠投递  

**6)**我可以向上翻动聊天记录,那么App将持续向IMS获取第2批同步消息;或者也可以点击tip:“↑ 33条新消息”,直接跳转到33条消息的最旧一条,这样支持从最旧的消息向新的翻看。

相比于客户端简单地被动接收服务端的离线通知方式,这种设计使得客户端的处理逻辑更复杂。

主要体现在:

  • 1)客户端向服务端取的同步消息是未必完整,这些存在客户端的消息,在时间区间上可能不连续的;
  • 2)客户端需要知道不同消息之间是否有断代,如果有则需要向服务端查询同步消息来merge本地信息,使其连续,即客户端要实现消息融合。

我的建议:用C++实现一个统一的底层imsdk库,来负责这些共通的消息处理和存储。避免各客户端(Windows,iOS,Android等)各自实现这些逻辑,减少工作量,也降低各端不一致的风险。

5.3 以会话列表为基础与用全量离线消息的方案对比

5.3.1)用全量离线消息实现的方案优缺点:

实现原理:由IM服务端确保消息送达客户端,客户端存储后发回确认。

方案优点:逻辑简单。

在聊天消息不同数量级时的表现:

  • a. 离线消息量不多(如几百条):没有效率问题,且消息全部达到客户端本地,方便进行查找等动作;
  • b. 离线消息量巨大(如几万条):用户登录瞬间CS间瞬时流量大,客户端瞬时要存储、更新的数据量巨大,可能出现卡顿、假死等情况。

5.3.2)用会话列表为基础的方案优缺点:

实现原理:客户端先同步会话列表,由用户驱动不定次获取同步消息。

方案缺点:逻辑复杂,客户端增加不少工作。

在聊天消息不同数量级时的表现:

  • a. 离线消息量不多(如几百条):没优势;
  • b. 离线消息量巨大(如几万条):登录时交互数据小,对IM后端、客户端、用户体验,都比较友好。

6、多终端条件下,如何得到完整消息履历?

由于同一个用户的每个终端,其会话最后更新时间、每个会话的最后一条时间可能都不一样,参照上一节的实现思路,可以得到解决方案。

具体如下:

  • 1)参照第6.2章节的“App和IM后端的交互”第1个步骤,可取到不同的增量变化的会话列表项;
  • 2)参照第6.2章节的“App和IM后端的交互”第4个步骤,可取到任一区间的同步消息,得到完整消息。

7、离线消息是否就彻底废弃了?

有若干情况,仍然需要保留离线消息,以确保消息送达。

比如以下情形:

  • 1)别人向我发送离线文件:这种情况下不能依赖同步消息来获取。因为不以离线消息通知的话,用户在没有拉取到对应的同步消息前,是不知道有离线文件的;
  • 2)撤回消息:即使接收者不拉取同步,仍然要保证在上线后其数据在第一时间被撤回。注意:这里可能存在多端撤回问题;
  • 3)用户在线时的消息下发:由于用户在线时,IM后端向客户端发送消息可能碰到网络抖动等情况,导致消息下发失败,这些消息先可以直接存在离线消息队列,IM后端可在收到客户端的心跳包时重发消息。相当于维护了一个在线消息的离线队列。

8、本文结语

曾经有一段真挚的爱情摆在我面前,如果时间倒流到半年前,我会选择一个靠谱的IM来发送消息,也许故事的脚本就由自己书写——是否要整一个时光倒流的版本,抱得美人归的那种?

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

IM开发干货分享:如何优雅的实现大量离线消息的可靠投递

9、参考资料

点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Wesley13 Wesley13
2年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Stella981 Stella981
2年前
IM消息ID技术专题(六):深度解密滴滴的高性能ID生成器(Tinyid)
1、引言在中大型IM系统中,聊天消息的唯一ID生成策略是个很重要的技术点。不夸张的说,聊天消息ID贯穿了整个聊天生命周期的几乎每一个算法、逻辑和过程,ID生成策略的好坏有可能直接决定系统在某些技术点上的设计难易度。有中小型IM场景下,消息ID可以简单处理,反正只要唯一就行,而中大型场景下,因为要考虑到分布式的性能、一致性等,所以要考虑的问题
Wesley13 Wesley13
2年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
2年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
为什么mysql不推荐使用雪花ID作为主键
作者:毛辰飞背景在mysql中设计表的时候,mysql官方推荐不要使用uuid或者不连续不重复的雪花id(long形且唯一),而是推荐连续自增的主键id,官方的推荐是auto_increment,那么为什么不建议采用uuid,使用uuid究
Python进阶者 Python进阶者
5个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这