本文由得物技术王卫强分享,为了更好的浏览体验,有较多的内容订正和排版优化。

一、引言

客服IM的外围业务其实就是在线沟通,客服IM的益处是使得客服与用户通过实时沟通的形式能够在最短的工夫内帮忙用户解决问题。为了疾速撑持公司业务倒退需要,咱们客服IM在倒退初期是基于第三方的云IM SDK进行二次开发而来。尽管晋升了我的项目停顿,但同时也埋下了问题定位艰难、非凡性能实现老本低等隐患。随着公司业务的高速倒退,客服对IM聊天的性能和体验都有了更高的要求,在第三方云IM SDK音讯通信上逐步遇到了技术瓶颈。为解决租用第三方云IM SDK接入带来的潜在隐患、晋升IM的稳定性和高扩展性,自研一套可控、稳固、灵便的IM零碎已是火烧眉毛了。本篇文章将基于工程实际,分享咱们从0到1自研一套客服IM零碎时在各种关键技术点上的设计思路和实际办法。

注:为了简化内容,本文分享的技术栈次要是以Web客服端为主。
相干文章:1)从零到卓越:京东客服即时通讯零碎的技术架构演进历程;2)瓜子IM智能客服零碎的数据架构设计(整顿自现场演讲,有配套PPT)。
学习交换:

  • 挪动端IM开发入门文章:《新手入门一篇就够:从零开发挪动端IM》
  • 开源IM框架源码:https://github.com/JackJiang2011/MobileIMSDK(备用地址点此)
    (本文已同步公布于:http://www.52im.net/thread-4153-1-1.html)

二、业务场景

客服与用户在聊天的过程中,直观上就是客服在输出文案,而后通过网络发送给用户。然而IM聊天SDK该如何设计能力使客服在发送音讯过程中感知不到卡顿?这一点是十分要害的,要防止卡顿就要设计正当的发送策略以及防止大量JS脚本执行。举个客服与用户聊天的例子:1)客服发送了“客服小冰为您服务”这个文案,通过业务侧调用SDK的接口,传入到SDK;2)再将该数据存储到数据池中,序列化后把这个数据对象data传递给socket接口,通过网络通道发送到网关;3)网关侧接管到音讯后,再反序列化,传递到数据池中进行解决,组装成业务可辨认的model,推送到业务侧应用。针对第1)点,SDK会先创立音讯体,即把这个字符串封装成一个自定义的构造体model。其聊天流程如下图所示:

从上图中能够清晰的看出一条音讯发送和接管的残缺流程链路。如果IM的SDK设计不合理,发送音讯和接管音讯流程呈现了卡顿,将间接影响用户的体验。

三、自研框架架构图概览

下图是咱们的自研IM零碎架构原理图:

咱们整体的技术改造次要是两个方面:
1)对音讯链路的形象革新:次要是音讯数据存储和音讯排序的重构;
2)业务接入侧的形象革新:次要是将业务逻辑和SDK源码进行解耦,做到代码分层更加的清晰。
上面咱们将针对次要的技术点进行具体地总结和分享。

四、音讯链路公布/订阅实现

在IM SDK自研开发过程中,如何解耦框架代码和业务代码,做到灵便的音讯监听,后期调研之后应用了RxJS。
这里简略介绍几个RxJS的外围概念:
1)Observable(可察看对象):示意一个可调用的将来值或事件的汇合;
2)Observer(观察者):监听由Observable提供的值;
3)Subscription (订阅):示意 Observable 的执行。
注:Subscription 有一个重要的办法,即 unsubscribe,它不须要任何参数,只是用来清理由 Subscription 占用的资源次要用于勾销 Observable 的执行。

SDK底层在接管到数据后须要同步到业务侧,之前的做法是通过监听形式实现,这种形式不具备勾销订阅的能力,保护老本绝对较高。而应用RxJS能够清晰的梳理出数据流向,通过公布订阅的形式实现数据的通信。
RxJS在公布订阅的实现流程如下:

从上图能够看到音讯解决的整个流向十分清晰,框架底层接管音讯,订阅者生产音讯。

五、音讯框架的分层构造概览

在咱们整个自研的IM音讯通信框架中,次要构造分成三层:
1)网络层;
2)数据链路层;
3)应用层。
具体如下图所示:

接下来我将具体分享各层的设计和实现思路。

六、音讯框架的分层实现:网络层

网络层作为音讯发送的最底层,负责TCP的连贯、音讯发送和接管。网络协议咱们抉择的是TCP协定。咱们为什么没有抉择UDP呢?因为UDP是无连贯的、不够平安、无奈提供牢靠传输的服务,通过TCP连贯传送的数据能够无差别、不失落、不反复且按序达到。
PS:尺有所短、寸有所长,TCP和UDP的优劣应该主观对待,感兴趣能够深刻学习上面的文章:
《疾速了解TCP和UDP的差别》
《一泡尿的工夫,疾速搞懂TCP和UDP的区别》
《疾速了解为什么说UDP有时比TCP更有劣势》
《深刻地了解UDP协定并用好它》
《如何让不牢靠的UDP变的牢靠?》
咱们整个IM SDK的通信形式采纳的是 WebSocket + JSON、grpc + protobuf。(如果你对WebSocket和Protobuf不相熟,能够具体学习《WebSocket从入门到精通,半小时就够!》、《Protobuf从入门到精通,一篇就够!》)
首先咱们要做的就是建设Websocket连贯:代码层面咱们会先创立一个Connection的抽象类,次要解决网络连接相干配置、超时后从新连贯的弥补实现,和一些继承类须要实现的形象办法。

如上述代码所示:外围在解决超时重连,传统的重试策略是每隔一段时间重试一次,因为是固定的工夫距离重试,重试时又会有大量的申请在同一时刻涌入,会一直地造成限流。(这里应用了指数退却的形式,指数退却是一种通过反馈,成倍地升高某个过程的速率,以逐步找到适合速率的算法,可依据时隙和重试尝试次数来决定提早重试。)其实现算法大抵如下:

Websocket连贯咱们是通过继承Connect类实现的:

至此:网络层连贯就已实现了,绝对比较简单,都是一些socket api的封装,外围的点在用指数退却算法实现音讯发送失败重连贯。

七、音讯框架的分层实现:数据链路层

数据链路层是IM SDK的核心层,次要波及到用户信息、聊天音讯、数据池等等,咱们来一步步对每个模块进行剖析。首先梳理一下客服在登录到用户进线发送音讯和接管音讯的全过程。过程有如下几个阶段:

7.1、协定类型

音讯协定类型十分重要,是音讯发送的基石。初始化协定数据体,能够用于后续各种音讯、事件的发送。在IM自研的SDK通信协议类型次要有如下几种:

  

具体解释一下:
1)Hi:发送客户端根底信息,通知server以后client的版本、设施类型、语言等信息;
2)Login: 登录,token验证,获取或创立以后用户topic信息;
3)Sub: 订阅topic或更新topic数据;
4)Leave: 勾销订阅,解绑之前的订阅关系;
5)Pub: 发送数据音讯给指定topic的订阅者;
6)Get: 获取topic的metadata信息,例如:获取订阅者列表、历史数据等;
7)Set: 更新topic的metadata信息,例如:删除音讯或删除topic;
8)Del: 用于删除操作,包含删除音讯、删除订阅关系、删除topic等;
9)Note: client发送告诉给topic的订阅者,例如音讯已收到,音讯已读,以后正在输出等;10)Action: 触发的事件,例如:切换客服状态、获取机器人问题等;
11)Datares: ack机制,通知网关已收到该音讯。

7.2、创立连贯

对网络层音讯链接实例化,实现音讯的失常发送和接管。其实现如下:

7.3、音讯定义

客服要发送一条音讯,必定有对应的音讯构造体model,即须要对音讯体进行设计,这里会设计一下message类,每次创立新的音讯体都会new一个实例,通过对实例的操作能够更新音讯状态等。如下所示:

针对单个音讯,咱们也要定义好消息状态,用于聊天过程中音讯状态的更新。如下:

7.4、数据池

音讯类创立好之后,就须要有音讯数据池来存储。音讯池构造定义如下:

这里还波及到音讯体的一些基本操作办法对数据池中的数据进行操作,就不做过多的论述。

7.5、用户维度

下面都是在剖析公共模块,然而客服和用户是一对多的关系存在,还须要设计一个用户维度模块,后续在业务侧的操作根本都是以用户维度来操作,须要从单个用户维度设计对应的订阅关系、音讯发送、删除等等。其实现大抵如下:

7.5.1发送音讯链路剖析

针对客服发送音讯,咱们首先要站在客服角度思考音讯是否已收回去,优先展现的聊天页面,而不是等网关给了回复后在展现到聊天页面。依据已往教训来看,只有回车音讯就要立刻展现到聊天页面,否则客服会认为呈现了卡顿,体验成果不佳,鉴于这种场景的需要,在设计发送音讯链路的时候就要充分考虑到这一点。所以设计流程如下:

如上图所示:先在SDK内进行解决对应的音讯,解决实现后返回到业务侧实现渲染后再进行音讯发送到网关,失常状况下都在一帧之内,客服是感知不到有提早的。这里要关注音讯体序列化、反序列化的机会,防止无谓的性能节约。上述图中有个虚构seq:次要是为了在未收到IM网关响应之前进行排序用的,比方图片、视频、断网发送音讯、音讯发送失败,或收到IM网关回复短少seq(场景:敏感词)等状况都须要通过虚构seq进行精确排序。

7.5.2接管音讯链路剖析

接管音讯过程绝对比较简单,收到音讯进行反序列化后更新相干数据,而后在数据池中实现去重(重试机制)、排序后更新到业务侧渲染即可(如下图所示)。

7.5.3音讯的牢靠传递

IM音讯的牢靠投递次要是指:音讯在发送接管过程中,可能做到不丢音讯、音讯不反复、音讯程序不错乱。咱们先来剖析以下2种状况。
第一种状况:如果客服A在把音讯发送到IM网关的过程中:
1)因为网络不通等起因失败了;
2)或者IM网关接管到音讯进行存储时失败了;
3)或者IM网关始终没有返回后果,导致超时。
以上这些状况客服A都会被提醒音讯发送失败。
第二种状况:音讯在IM网关存储完后,客服A被告知音讯发送胜利了,而后IM网关把音讯推送给用户A的在线设施:
1)在推送的筹备阶段或者把音讯写入到内存后,如果服务端呈现掉电,也会导致音讯不能胜利推送给用户A;
2)如果用户A的设施在接管到音讯,在后续处理过程中呈现问题,也会导致音讯失落。
针对第2)点,具体场景比方:用户A的设施在把音讯写入本地DB时,出现异常导致落库失败,这种状况下,因为网络层面实际上曾经胜利传输,但用户A却看不到音讯。咱们客服IM对于音讯失落的解决计划次要是参考TCP协定的ACK机制,实现了一套基于业务层的ACK协定。增加ACK之前音讯发送的时序图如下:

7.5.3.1)ACK 机制:
在TCP协定中,默认提供了ACK机制,通过一个协定自带的规范的ACK数据包,来对通信方接管的数据进行确认,告知通信发送方已确认胜利接管了数据。ACK机制也是相似,须要解决的是:IM网关推送后如何确认音讯是否胜利送达接管方并明确被接管方所接管。具体实现的时序图如下:

客服或用户在发送音讯的过程中都会携带一个msgid(32位的uuid,相似TCP的sequenceId),IM网关在接管到音讯后,会依据msgid到数据库中查问是否存在该条音讯,如果存在就不落库,如果不存在就落库。而后再推送到接管方,接管方在收到音讯后会回复ACK,ACK包中会携带上以后最新的seqid,IM网关收到ACK回复后会对最大的seqid进行更新。这里为什么要更新最大seqid呢?这么设计必定有肯定情理的,IM网关在收到发送方发送的音讯后除了到数据库中检测该音讯是否存在外,还会比照以后接管到音讯的seq和最大seqid两者之间的差值,会把 [seq, seqid) 之间的数据全副推到接管方,失常状况下都是 [n, n-1),如果IM网关没有收到接管方ACK,n-1就不会更新,推送的音讯个数就大于1了。如果seq和seqid相等那就是发送方反复推送的音讯,这个时候就不会向接管方推送。这里就波及到了音讯重试,持续向下剖析吧。

PS:无关IM音讯ID或序列号生成的专题文章能够浏览:
《IM音讯ID技术专题(一):微信的海量IM聊天音讯序列号生成实际(算法原理篇)》
《IM音讯ID技术专题(三):解密融云IM产品的聊天音讯ID生成策略》
《IM音讯ID技术专题(四):深度解密美团的分布式ID生成算法》
《IM音讯ID技术专题(五):开源分布式ID生成器UidGenerator的技术实现》
《IM音讯ID技术专题(六):深度解密滴滴的高性能ID生成器(Tinyid)》

7.5.3.2)ACK机制中的音讯重试:
音讯推给A的过程中失落了怎么办,比方:
1)A网络理论曾经不可达,但IM网关还没有感知到(ping呈现问题);
2)音讯在两头网络途中被某些中间设备丢掉了。
解决这个问题也是参考了TCP协定的重传机制。咱们会在客服端、IM网关、用户端都保护一个超时计时器,肯定工夫内如果没有收到对方回的ACK包,会从新取出该音讯进行重推。在重试肯定次数后,如果还是没有收到ACK,视为放弃。前端代码构造和成果如下:


上述图片中的数据只是模仿音讯重试,实在场景中执行频次必定要比这个工夫更久一些。7.5.3.3)音讯反复推送的问题:
如果在肯定工夫内没有收到ACK包,就会触发重试机制。收不到ACK的状况有两种,除了推送的音讯真正失落导致A不回ACK外,还可能是A回的ACK包自身丢了。解决方案是:发送方在发送音讯时携带一个msgid,msgid是全局惟一的,针对同一条重推的音讯msgid不变,接管方依据这个惟一的msgid进行去重,这样通过去重后,对于A来说,在聊天界面是不会看到反复的音讯,不影响应用体验。
7.5.3.4)保障音讯不会乱序:
音讯的一致性是十分重要的,在聊天过程中音讯程序不能错乱。咱们是这样思考的:1)以发送方的本地工夫戳为序号,然而这样有比拟大的问题,发送方的工夫戳是能够被改变的,这种形式不可取;2)IM网关服务是集群部署,会通过topic和seqid做为惟一索引,在接管到音讯落库之前会生成seqid,客服端和用户端接管到发送音讯的回执时须要依据返回的seqid(IM网关自增)进行音讯排序,这种形式可取。通过以上的剖析:客服IM音讯的可靠性就是通过ACK机制、重试机制、去重机制、排序机制来确保每一条音讯的残缺触达和精确排序。

八、音讯框架的分层实现:应用层

业务侧应用的时候间接实例化SDK即可,在音讯链路公布订阅中曾经提到了RxJS,此时在业务侧订阅应用即可。须要留神的是在实例化SDK的时候传递了一个filterMsgItem办法,次要是为非凡业务场景提供应用的。就拿咱们客服业务来说:有些特定音讯是不须要展现到聊天页面的(比方:用户发送音讯被篡改等)。当然咱们在业务侧从新对数据过滤或者渲染的时候也是能够做过滤的,这样操作是没什么问题,然而没有必要,如果不从源头过滤数据,后续参加二分、倒序查找的源数据也会减少。会有一些不必要的节约。当然也能够不增加这个参数,SDK都是全兼容的。

至此咱们就实现了整个SDK的实现以及在业务侧的应用,音讯发送和接管也都失常。成果如下:

九、本文小结

自研IM SDK还是蛮有挑战的一件事件,从单纯的基于第三方SDK二次开发到自研SDK并与咱们的理论业务场景绝对完满的联合。在SDK的整体设计以及和业务侧如何更完满的联合并不是欲速不达的,都是在理论业务场景中一直积攒教训,一直尝试才找到绝对完满的解决方案。这里列举一个简略的案例吧。
例如音讯发送,须要思考到断网场景下:
1)该如何进行音讯显示、排序、从新发送?
2)发送失败的场景下从新发送再次失败后又该如何显示、排序?
3)弱网场景下发送音讯触发重试机制该如何以最优的形式去重、排序?
4)发送音讯触发敏感词该如何解决?
5)断网重连后对于发送失败和触发敏感词的音讯又该如何解决?
6)如果在波及到文件又该如何解决?
……
在自研过程中除了关注业务场景外,还调研了行业内比拟好的一些Web利用在某些非凡场景的解决形式。很多优良的计划也都只能是借鉴一些核心思想,还是要以业务为外围,真正通过技术手段解决业务痛点才是最重要的。自研SDK收益还是十分大的,也积攒了很多IM方面的教训,实现自研IM SDK也只是一个开始,后续咱们将会在耗时工作、数据安全等方面继续深耕细作。

十、参考资料

[1] 从零到卓越:京东客服即时通讯零碎的技术架构演进历程
[2] 瓜子IM智能客服零碎的数据架构设计(整顿自现场演讲,有配套PPT)
[3] 从游击队到正规军(一):马蜂窝旅游网的IM零碎架构演进之路
[4] 一套高可用、易伸缩、高并发的IM群聊、单聊架构方案设计实际
[5] 浅谈IM零碎的架构设计
[6] 简述挪动端IM开发的那些坑:架构设计、通信协议和客户端
[7] 一套海量在线用户的挪动端IM架构设计实际分享(含具体图文)
[8] 一套原创分布式即时通讯(IM)零碎实践架构计划
[9] 一套亿级用户的IM架构技术干货(上篇):整体架构、服务拆分等
[10] 从老手到专家:如何设计一套亿级音讯量的分布式IM零碎
[11] 企业微信的IM架构设计揭秘:音讯模型、万人群、已读回执、音讯撤回等
[12] 阿里IM技术分享(三):闲鱼亿级IM音讯零碎的架构演进之路
[13] 基于实际:一套百万音讯量小规模IM零碎技术要点总结
[14] 跟着源码学IM(十):基于Netty,搭建高性能IM集群(含技术思路+源码)
[15] 一套十万级TPS的IM综合音讯零碎的架构实际与思考
[16] 直播零碎聊天技术(八):vivo直播零碎中IM音讯模块的架构实际
[17] 融云技术分享:全面揭秘亿级IM音讯的牢靠投递机制
(本文已同步公布于:http://www.52im.net/thread-4153-1-1.html)