首页 即时通迅 酷信即时通讯 即时通讯发展干货共享:我如何解决大量离线信息导致客户陷入困境的问题

即时通讯发展干货共享:我如何解决大量离线信息导致客户陷入困境的问题

2023-09-12 10:53:00 阅读量:115

 1.介绍


 


 我已经很久没有写技术文章了。今天的文章不是一篇原则性的文章,而是针对大量离线消息(包括消息漫游)带来的用户体验问题,与大家分享作者开发和实现的即时通讯即时通讯聊天系统升级改造的全过程。


 


 在本文中,我将从以下几个方面介绍它:


 


 1)该即时通讯产品的主要业务和特点;


 


 2)即时消息系统业务状态和难点;


 


 3)升级改造之路;


 


 4)消息确认逻辑的优化。


 


 以下内容都是基于作者个人开发即时消息的宝贵经验。干货满了,我期待你的赞扬。


 


 本文已在“即时通讯技术圈”公开发表。请注意:


 


 即时通讯发展干货共享:如何解决大量离线信息导致客户陷入困境的问题_52im_qr_即时通讯技术圈_ 400px.png。


 


 ▲本文关于公开号码的链接是:https://mp.weixin.qq.com/s/XHdt1IpCrdmaMDxL8WKn3w


 


 2.即时通讯开发干货系列文章


 


 本文是系列文章的第25篇,主要内容如下:


 


 即时消息传递保证机制的实现(一):保证在线实时消息的可靠传递


 


 即时消息传递保证机制的实现(二):保证离线消息的可靠传递


 


 如何确保即时消息的“时间”和“一致性”?》


 


 我应该使用“推”还是“拉”来同步即时消息单聊和群聊的在线状态?》


 


 即时消息群聊新闻如此复杂,如何确保它不丢失或沉重?》


 


 安卓即时通讯智能心跳算法的设计与实现探讨(含示例代码)


 


 “当即时消息登录移动终端时,如何通过提取数据来节省流量?》


 


 易于理解:基于集群共享移动终端即时通信接入层的负载均衡方案


 


 浅谈移动即时通信的多点登录和消息漫游原理


 


 构成即时通讯开发的基础知识(一):正确理解前端HTTP单点登录接口的原理


 


 构成即时消息开发的基础知识(2):如何为大量图像文件设计服务器端存储架构?》


 


 补充即时通讯开发的基础知识(3):快速了解服务器端数据库读写分离的原理和实用建议


 


 构成即时消息开发的基础知识(4):正确理解短连接中的Cookie、会话和令牌


 


 如何实现即时通讯群聊信息的阅读回执功能?》


 


 即时消息群聊消息是一份拷贝(即扩散阅读)还是多份拷贝(即扩散写作)?》


 


 构成即时消息开发的基础知识(5):易于理解、正确理解并充分利用MQ消息队列


 


 一种低成本保证即时消息定时的方法探讨


 


 补上一课关于即时消息开发的基础知识(6):您的数据库使用NoSQL还是SQL?读读这个!》


 


 在即时通讯中实现“身边人”功能的原则是什么?如何有效地实现它?》


 


 构成即时通讯开发的基础知识(七):主流移动账户登录方法的原理和设计思路


 


 弥补即时通讯发展的基础知识(8):历史上最流行的,彻底了解字符乱码问题的本质


 


 如何实现即时通讯的扫描码板功能?有一篇文章了解主流应用的扫描代码登陆技术的原理


 


 “即时通讯做手机扫描码登录?让我们先来看看微信扫描码登录功能的技术原理


 


 构成即时通讯发展的基础知识(9):想发展即时通讯集群吗?首先了解什么是RPC!》


 


 “即时消息开发实际的干货:我如何解决大量离线聊天信息导致客户陷入困境的问题”(本文)


 


 此外,如果你是即时通讯开发的初学者,强烈建议你先阅读“初学者:从零开始开发移动即时通讯”。


 


 3.该即时消息产品的主要业务和特点


 


 与传统的互联网行业不同,作者工作的公司(名字不会透露)是一家制作娱乐社交应用的公司,包括小游戏、聊天、朋友圈订阅等。


 


 每个人都应该知道,就技术和产品形式而言,游戏业务与电子商务和旅游业有着本质的不同。


 


 大多数做后端开发的朋友都在开发界面。客户端或浏览器h5通过HTTP向我们后端的控制器接口发出请求,并在后端查询数据库以将JSON返回给客户端。众所周知,HTTP协议具有短连接、无状态、三次握手四次挥动等特点。但是,游戏和实时通信等服务不适合使用HTTP协议。


 


 原因如下:


 


 1)HTTP不能达到实时通信的效果,所以可以被客户端轮询,但是浪费资源太多;


 


 2)三次握手挥了四次,出现了严重的性能问题;


 


 3)无国籍。


 


 例如,当两个用户通过应用聊天时,一方发送消息,另一方需要实时感知消息的到达。当两个或更多的人玩游戏时,玩家需要实时看到彼此的状态。这些场景是无法通过超文本传输协议实现的!因为HTTP只能拉,而聊天和游戏服务需要推。


 


 4.即时消息系统业务状态和难点


 


 4.1业务状况


 


 作者负责整个公司的实时聊天系统。与微信和QQ类似,它具有私人聊天、群聊、发送信息、语音图片、红包等功能。


 


 让我详细介绍一下整个聊天系统是如何工作的。


 


 首先,为了达到实时通信的效果,我们开发了一套基于Netty的长链路网关(扩展阅读:Netty干货共享:北京-东京-麦生产级TCP网关技术实践总结)。采用的协议是MQTT协议,当客户端登录时,应用通过MQTT协议连接到网关(网络服务器)。然后通过MQTT协议将聊天消息推送到网络服务器。网络服务器与网络客户端保持着长期的联系。NettyClient用于处理业务逻辑(如敏感词拦截、数据验证等)。)。最后,消息被推送到NettyServer,然后NettyServer通过mqtt推送到客户端。


 


 其次,如果客户端和服务器想要正常通信,我们需要制定一个统一的协议。以聊天为例,如果我们想和对方聊天,我们需要通过uid和其他信息定位对方的通道(通道(Netty相当于一个套接字连接),然后我们可以将消息发送到正确的客户端。同时,客户端必须通过协议中的数据(uid、groupId等)在私人聊天或群组聊天会话中显示消息。)。


 


 协议中的主要字段如下(我们将数据编码成protobuf格式进行传输):


 


 010203040506070809101112131415161718192021 { " cmd ":" chat "," time":1554964794220," uid":"69212694 "," ClientInfo ":{ " DeviceId ":" B3 b 1519 c-89ec "," deviceInfo":"MI 6X" }," body":{ "v


 


 补充说明:如果您不知道什么是Protobuf格式,请阅读《Protobuf通信协议详细说明:代码演示、详细原理介绍》等。


 


 如上json,协议的主要字段包括:


 


 即时消息开发干货共享:我如何解决大量离线消息导致客户陷入困境的问题_ 1.png。


 


 如果客户端不在线,我们的服务器需要将发送的消息存储在离线消息表中。当对方客户机下次联机时,服务器通过一个长链接将脱机消息推送给客户机。


 


 4.2业务难点


 


 随着业务的蓬勃发展,用户数量越来越多,用户创建的群组数量越来越多,他们加入的群组和朋友数量也越来越多,聊天活动也越来越多。当一些用户不在线时,会生成大量离线消息(特别是对于群聊,有许多离线消息)。


 


 当客户端下次上线时,服务器会将所有离线消息推送给客户端,导致客户端在登录后停留在主页上。此外,产品要求团队成员的数量应该增加(从以前的100人增加到1000人、10000人等)。)。


 


 因此,一些客户端在登录后会因为大量的离线消息而被卡住,用户体验非常糟糕。


 


 我和客户的同事分析了原因:


 


 1)用户登录时,服务器通过循环批量发送所有离线消息,数据量大;


 


 2)当客户端登录并进入主页时,要加载的数据不仅包括离线消息,还包括其他初始化数据;


 


 3)不同价位的客户处理数据的能力有限。在处理聊天消息时,他们需要将消息存储在本地数据库中,刷新用户界面并向服务器回复确认消息,这将消耗大量性能。


 


 (幸运的是,目前在线消息没有性能问题。)。


 


 因此,鉴于上述问题,结合对即时消息系统的雄心勃勃的规划,我们的服务器决定优化离线消息(一点点,客户端的处理能力不够,为什么服务器要优化?服务器的性能远远不是瓶颈。。。).


 


 5.升级改造之路


 


 幸运的是,作者100%参与了系统优化的整个过程,包括技术选择、方案制定和最终代码编写。在此期间,作者想出了各种方案,然后与服务器端和客户端的同事进行了讨论,最后决定了一个稳定的方案。


 


 5.1方案1(已过时的方案)


 


 [问题症状]:


 


 客户端登录的主要原因是服务器会向客户端推送大量的离线消息,客户端收到离线消息后会回复服务器确认,然后将消息存储在本地数据库中并刷新用户界面。客户端反馈,即使客户端采用异步模式,也会有严重的性能问题。


 


 [所以我想]:


 


 为什么客户端在收到消息后没有将数据存储在数据库中就回复服务器确认?存储很可能会失败,这本身就是不合理的。这是其中之一。第二,由于服务器推得很紧,客户端被卡住是不合理的,它不关心客户端的处理能力。


 


 [伪代码如下]:


 


 01020304050607080910 int max = 100;//从新库中读取时(最大值> 0){ listoofflinemsglistnew = shardchatofflinemsgdao . getbytouid(uid,20 );if(CollectionUnitils . ISempty(OfflineMSglistNew)){ break;} handleOfflineMsg(uid,offlineMsgListNew,CheckOnLineWhenSendingOfflinemsg);马克斯。}


 


 [初步计划]:


 


 既然推动是不合理的,我们可以改变方式。根据不同型号客户端的不同处理能力,服务器会以不同的速度发布。


 


 我们可以将整个过程视为生产者-消费者模型,其中服务器是消息生产者,客户端是消息消费者。收到消息后,客户端将消息存储在本地数据库中,刷新用户界面,然后向服务器发送确认消息。从客户端收到确认消息后,服务器推送下一批消息。


 


 这样,消息发布速度完全根据客户端的处理能力分批分配。但这种方式仍然属于推动方式。


 


 [悲惨的结果]:


 


 然而,理想是充实的,而现实是单薄的。


 


 鉴于这一方案,客户提出了一些问题:


 


 1)虽然客户端不会陷入这种方案,但是如果当前用户有很多离线消息,接收所有离线消息需要很长时间;


 


 2)每次客户端收到消息时,都会刷新界面,客户端很可能会上下跳动。


 


 所以,这个计划被否决了。。。


 


 5.2方案二


 


 [我的想法]:


 


 由于推送的数据量太大,我们能否按需加载?当客户端需要读取离线消息时,服务器会将消息发送给客户端,当不需要时,服务器不会发送消息。


 


 【技术方案】:对于离线消息,我们优化了以下方案


 


 1)我们增加了离线消息计数器的概念:为每个用户的每个会话保存未读消息的元数据(包括未读消息的数量、最新的未读消息、时间戳等数据),该计数器用于客户端显示未读消息的红色气泡。该数据属于增量数据,仅保留离线期间接收的消息元数据。


 


 消息格式如下:


 


 010203040506070809101112 { " session id 1 ":{ " count ":20," lastmsg": ["last n messages"]," tim estamp": 1234567890}," sessionid2": {


 


 即时消息开发干货共享:我如何解决大量离线消息导致客户陷入困境的问题_ 2.png。


 


 2)每次客户端登录时,服务器不推送全部离线消息,只推送离线消息计数器(这部分数据存储在redis中,数据量很小),这个用户数显示在客户端消息列表中未读消息的小红点上。


 


 3)客户端获取这些离线消息计数器数据,遍历会话列表,依次累积未读消息(注意:不是覆盖,服务器在离线后保存客户端的增量数据),然后通知服务器清空离线消息计数器的增量数据。


 


 4)当客户机进入一个会话并启动和加载时,它通过消息的msgId和其他信息向服务器发送一个HTTP请求,然后服务器转到页面查询脱机消息并将其返回给客户机。


 


 5)在接收到消息并将其保存在本地数据库中之后,客户端向服务器发送确认消息,然后服务器在离线消息表中删除离线消息。


 


 [预期结果]:


 


 客户端和服务器端的技术人员认可该方案。我们通过推和拉的方式解决了客户端无法加载离线消息的问题。(在转换之前,它被强制推,在转换之后,它采用推和拉的组合)


 


 流程图如下:


 


 即时消息开发干货共享:我如何解决大量离线消息导致客户陷入困境的问题_ 4.png。


 


 [新问题]:


 


 虽然该方案已经通过,但它带来了一个新的问题:客户端消息聚合的问题。


 


 问题描述如下:登录后,客户端进入会话页面,因为客户端本身存储历史消息,所以当客户端下载并加载新消息时,如何判断是否加载本地历史消息?还是要请求服务器加载脱机邮件?


 


 经过一番思考,服务器和客户端最终达成了一致的计划:


 


 1)在未读消息计数器的小红点逻辑中,服务器将向客户端发送每个会话的最新n条消息;


 


 2)当客户端进入会话时,将根据未读消息计数器的最新N条消息显示主页数据;


 


 3)当客户端每次拉下并加载时,请求服务器,服务器根据时间反转离线消息,返回当前会话的最新离线消息,直到离线消息库中的所有数据都返回给客户端;


 


 4)当离线消息库中没有离线消息时,向客户端返回一个标识符。根据该标识符,当会话页面被下拉并在下一次被加载时,客户端直接请求本地数据库,而不是服务器的离线消息。


 


 6.消息确认逻辑的优化


 


 最后,我们还优化了消息确认的逻辑。


 


 优化前:服务器使用推送模型向客户端推送消息,无论是在线消息还是离线消息,ack的逻辑都是一样的,其中使用了kafka和redis等中间件,过程非常复杂(这里我不详细介绍ack的具体过程,这是不合理的)。


 


 离线消息和在线消息的区别在于我们不存储在线消息,而离线消息将存储在单独的库中。绝对没有必要使用在线消息的ack逻辑来处理离线消息,但是这是不合理的,不仅在处理过程中存在问题,而且浪费了诸如kafka和redis等中间件的性能。


 


 优化后:我们和客户端决定在每次下载和加载离线消息时,将最后一批离线消息的msgId或消息偏移量发送到服务器。服务器根据msgId直接删除发送到离线库中客户端的离线消息,然后将下一批离线消息返回给客户端。


 


 此外,我们还增加了短信漫游功能,用户在切换手机登录后仍然可以找到历史短信,所以我就不详细介绍了。


 


 7.设计优化方案时的文档截图(仅供参考)

x
咨询留言
请填写以下信息,方便与您取得联系,已开启隐私保护

提交
x
企业认领

电话:

提交
x
图形验证码
填写图形验证码发送短信

发送短信