本文由得物技术WWQ分享,原题“客服发送一条消息背后的技术和思”,本文有修订和改动。
在企业IM客服场景中,客服发送一条消息的背后,需要考虑网络通信、前端展示、后端存储以及安全性等多个方面的技术支持。单从前端层面来说,就需要考虑到消息的显示、状态更新、稳定传输以及极限操作消息不卡顿等场景。随着IM系统的不断更新迭代,已经实现了从外采到自研再到一站式全场景工作台的搭建,我们能够很明显地感知到客服对于IM的体验要求越来越高了,因此客服发送一条消息背后所涉及的技术和思考也越来越重要。
本文将探秘得物自研客服IM中收发聊天消息背后的技术逻辑和思考实现,帮助大家了解如何在IM聊天场景中提供高效、安全、可靠和良好的用户体验。
技术交流:
- 移动端IM开发入门文章:《新手入门一篇就够:从零开发移动端IM》
- 开源IM框架源码:https://github.com/JackJiang2011/MobileIMSDK(备用地址点此)
(本文已同步发布于:http://www.52im.net/thread-4483-1-1.html)
IM聊天消息是客服和用户之间最快速、最直观、最高效的双向沟通方式之一。
IM聊天的重要性体现在以下几个方面:
综上:IM聊天消息的重要性在于提高用户满意度、提高客服作业效率,这也意味着IM消息的可靠、高效、安全尤为重要,接下来本文就从前端视角对客服发送一条消息背后的技术和思考进行详细的讲述。
以下是得物客服IM消息发展的历程,列举的都是核心技术专项的里程碑节点。
在这个过程中,我们积累了一定的经验和技能,同时也遇到了各种各样的问题和挑战。比如:消息丢失、消息发送失败、消息重复、消息乱序等等方面的问题。
针对这些问题我们也都通过技术专项的方式去逐个解决并达到了预期效果,我们相信,随着技术的不断发展和创新,我们可以更好地提供更加高效便捷的服务。
站在用户/客服角度,发送消息不就是输入消息后点击回车键或点击发送按钮就完成了吗。
看似非常简单,但是从开始输入消息到对方收到消息这个过程实际上有非常强大的技术在高效、稳定支撑。
我们客服IM消息链路会涉及到三个核心端口:
以下将以客服发送一条消息到IM网关这个过程简单描述一下涉及到的技术点,反之用户侧发送消息也是类似的。
从上述流程图中可以看到:一条消息的旅程还是非常丰富的,当然其中有一些细节点还没有完全列举出来。
例如:IM网关的超时重推机制、前端的异常处理(网络异常、超时异常、重试无果等)。我们可以很清晰地看到当客服开始输入消息的时候就开始进行通知对方正常输入,触发消息发送后需要进行消息体的创建、排序、去重检测、网络检测、聊天列表渲染、推入超时重试队列、放入消息拦截器中统一进行消息格式转化并发送。
到这里只仅仅是完成了前端层面的发送工作而已,此时消息是否发送成功还是未知的,还需要监听消息的发送结果。如果在一定时间未收到响应结果会进行第二次消息的重发,直到发送成功或到达最大重试次数就表示该消息的生命周期结束。
一旦收到消息的响应结果就会对消息的状态进行更新(此时消息已完成了排序,不需要进行二次排序),至此第一个环节就完成了处理,IM网关到客户端也会有类似的处理过程。
纵观整个消息发送以及接收链路,任何一个环节出现问题都会导致消息发送出现问题,就需要非常稳定可靠的技术手段进行保障,主要从以下几个方面讲解一下。
消息的可靠性传递确保了消息收发双方信息的一致性。这也是我们为什么把消息可靠性传递放在第一个进行讲解。
我们试想一下这样一个场景:经常有消息丢失,客服频繁反馈,每次都要投入研发资源去排查问题。这还是次要的,有可能因为消息的丢失导致用户体验的急剧下降,这就得不偿失了。所以消息的可靠性传递是非常有必要的,而且也是必须的。
那么何为可靠性传递?至少要满足3个方面,我们进行详细分享。
我们使用IM最重要的一方面就是希望对方能够实时接收到我们发送的消息并能够给予回复,这对于提升用户体验尤为重要。如果不在乎实时性我们完全可以使用其他方式,例如邮件、写信甚至飞鸽传书…
一条消息发送给IM网关,网关大致需要经历以下5个环节的处理:
从消息的实时性的来说,没有绝对的实时,只能尽量优化。核心的处理逻辑都在IM网关,无论是前端还是客户端,处理过程都是非常快的,都在毫秒级别。
我们IM网关是Go语言开发的,并发处理的能力也是非常高的,所以整个闭合链路的耗时还是非常低的。
众所周知,TCP本身就是具有可靠性的,但是它只能保障传输层可靠,而应用层之间的可靠性并不能保证(可详读《为何基于TCP协议的移动端IM仍然需要心跳保活机制?》、《从客户端的角度来谈谈移动端IM的消息可靠性和送达机制》、《不为人知的网络编程(十二):彻底搞懂TCP协议层的KeepAlive保活机制》)。我们后续会有针对性的专项文章进行发表,本次就不再赘述。
那我们该如何保障应用之间的可靠性呢?
可靠性的保障就是让发送方知道接收方接收到了消息,这样就表示消息成功传递了。
我们再回头看一下上面讲述消息丢失的场景,消息丢失的问题也是我们在IM消息研发过程中遇到的一个让人头疼的问题,排查一个问题需要投入的技术资源是非常巨大的,需要涉及到H5、IM网关、服务端以及客户端,对于用户以及客服的使用体验是非常差的。很简单的一个场景,用户发了消息,客服没有收到,没有回复用户,用户以为客服故意不回复,会影响到用户的满意度。
那这个问题该如何解决呢?
大家可以看下《得物从0到1自研客服IM系统的技术实践之路》,其中有讲解过,核心是参考TCP协议的ACK机制,实现一套基于业务层的ACK协议。
这里特别的要注意的是针对批量消息(客服刷新会话、新会话进线等场景),我们采用的是批量ACK机制,如果每一个消息都回复ACK,成本会比较高。我们当初是通过一个IM架构升级技术专项协同各端完成了IM整体消息触达实现0丢失,保证触达,满足At least once(通过数据埋点验证后得到100%的触达率)。上线后该场景符合预期效果,相应的问题排查投入也减少了至少70%+。
在开发IM过程中有这样一个非常常见的场景,用户问A问题后又问题了B问题,在客服侧B问题排到A问题的前面,导致客服的回复也出现了错乱。
当然这只是IM消息乱序的一种场景而已。诸如此类的还有很多。
消息乱序产生的原因有很多,例如发送文件后再立即发送消息,文件需要前端先上传到OSS获取到URL后再发送给用户,上传文件这个过程,用户以及客服都是可以发送消息的,这种场景处理不好就极易出现消息乱序。
不做IM是真不会想到客服操作的效率会有多高,之前在处理消息乱序问题的时候有遇到客服连续发送了2条消息,间隔只有300毫秒,这种高频密集的操作场景在客服的工作场景下是持续性的。
看似一个乱序问题,不考虑清楚用户群体、极限场景、临界值等都不会彻底解决掉这个问题。
再说回我们客服IM,我们是如何处理消息排序的呢?
在整个开发过程也是比较曲折的,最终是以IM网关维护的Seq为准,然后返回到发送方,发送再根据消息序号进行排序,确保发送方和接收方消息的排序是一致的。
前端处理的流程如下:
说到消息的幂等性,我们要思考一个问题,为什么会收到多条(>1)相同的消息呢?
肯定是发送方重复发送导致的,那在什么场景下会重复发送?
前面刚讲过应用层的ACK机制,如果没有收到对方的ACK,会在超时时间到达后继续重复发送直到最大重试次数。参考下面的截图会更容易理解,只是模拟消息重试,真实场景中执行频次肯定要比这个时间更久一些。
既然要保证消息的可靠性,消息的重复就是无法避免的。就有可能出现消息幂等性问题。
那怎么解决呢?
我们是利用消息的Message ID做去重的,这里会涉及到一个性能问题,排序、去重以及风控信息验证等都需要一定的计算成本,如何保证处理过程系统不卡顿是一个核心问题。
想要了解我们客服IM是如何做的,请继续向下看。
我们来想一下为什么会出现卡顿?
什么样的场景才能够被视为卡顿呢?
我们一般都会说是因为在16ms内无法完成渲染导致的。那么为什么需要在16ms内完成呢?
这里我们就要了解一下刷新率(RefreshRate)与帧率(FrameRate):
如果:帧率为每秒钟60帧,而屏幕刷新率为30Hz,那么就会出现屏幕上半部分还停留在上一帧的画面,屏幕的下半部分渲染出来的就是下一帧的画面,这种情况被称为画面撕裂。
相反:如果帧率为每秒钟30帧,屏幕刷新率为60Hz,那么就会出现相连两帧显示的是同一画面,这就出现了卡顿。所以单方面的提升帧率或者刷新率是没有意义的,需要两者同时进行提升。浏览器都采用的60Hz的刷新率,为了使帧率也能达到60FPS,那么就要求在16.67ms内要完成一帧的绘制(1000ms/60Frame = 16.666ms / Frame)。
IM消息处理中出现卡顿的情况非常常见,到一定的量级都是一个很难避免的问题。对比我们经常使用电脑,打开多个浏览器页签,稍微时间长点不关机重启,也会感觉到卡顿。但对于IM消息处理还是有很多方式进行优化的。
主要涉及以下几方面的优化策略,我来展开讨论。
众所周知JS是单线程的,所以采用异步处理机制可以将优先级低的任务推入异步任务队列,让出主线程给优先级高的任务。
比如:客服在输入完消息后需要立即显示的聊天页面,如果存在短暂的不显示,会被认为是系统卡顿了,所以发送消息的优先级是高于接收消息的。
我们对各场景任务优先级做了区分,低优先级的任务都通过异步的方式进行处理。
这里主要针对聊天消息列表,对于大量消息的会话处理,只渲染可视区域的消息降低浏览器的负担,提升响应速度。
列表优化的方案有很多,如下。
1)方案 1:
使用定时器setTimeout来实现分批渲染。
这种方式我们一般不推荐,因为在setTimeout中对DOM进行操作,必须要等到屏幕下次绘制时才能更新到屏幕上,如果两者步调不一致,就可能导致中间某一帧的操作被跨越过去,而直接更新下一帧的元素,从而导致丢帧现象。
2)方案 2:
采用requestAnimationFrame。相比之下,requestAnimationFrame的优势还是非常明显的。
主要体现在以下几个方面:
3)方案 3:
采用IntersectionObserver。
IntersectionObserver接口(从属于Intersection Observer API)为开发者提供了一种可以异步监听目标元素与其祖先或视窗(viewport)交叉状态的手段。祖先元素与视窗(viewport)被称为根(root)。
可以看到,交叉了就是说明当前元素在视窗里,当前就是可见的了。是代替监听滚动加载的不错方案。
当然还有其他方案,还是要根据实际的业务场景选择合适的方案,IM消息分段加载的难点在于消息的不定高(多种不同类型的消息),计算成本还是有一些昂贵的。所以优化还是要验证一下临界值的,有时候优化不一定会有效。
上面我们讲到消息排序、去重以及消息状态更新等等,多个会话大量的聊天消息,如果处理不当,卡顿是必现的。
可以先看一下我们优化之前的处理流程,采用的是第三方的SDK,一堆for循环,消息量大一些基本卡住没反应了。
那我们是如何处理这个问题的呢?
基于现有的业务场景重写三方SDK,将会话维护成独立的实例,核心算法就是采用二分法。
感兴趣的同学可以看之前的这篇文章《得物从0到1自研客服IM系统的技术实践之路》,讲述得比较详细。重写了IM SDK之后,客服再也没有反馈过聊天相关的卡顿,聊天首响提升了20%,成果还是比较显著的。
在IM系统中,消息的安全性是非常重要,开发同学需要具备较强的安全意识,将安全融入到开发流程中,增强系统的安全性和健壮性。
消息安全性方面的事情我们做了很多,这里也不再详细讲解了,有兴趣可以读读下面的文章:
消息发送和接收的延迟直接影响用户的使用体验和沟通效率,在上面我们已经分析过一条消息的旅程,出现延迟的原因也比较好分析。
主要有以下4点:
既然能分析出原因,我们就能对症下药,可以通过一些优化策略来降低发送和接收的延迟。
目前规划从以下2个方面来进行优化。
1)前端方面:
延迟主要在消息的处理和编解码方面,目前我们IM消息的数据格式是JSON,存在序列化和反序列化的过程,这里我们会采用ProtoBuf 替换JSON,目前已完成了相关技术调研和测试验证。我们简单来看一下ProtoBuf(Protocol Buffers)和 JSON 处理耗时的对比。
编码时间:ProtoBuf 的编码时间比 JSON 快得多,因为 ProtoBuf 的编码是二进制的,不需要进行编码转换以及无需进行冗余类型的转换。相对而言,JSON 的编码时间较慢。
解码时间:相比编码,ProtoBuf 的解码效率要稍微低一些。但是,由于 ProtoBuf 的优势在数据量大、结构复杂的情况下更为明显,对于小型数据解码时,两者的效率差异可能不太明显。
有关Profobuf的更多资料,可以详读以下文章:
2)网络延迟:
网络延迟我们很难控制,但是可以通过降低消息传输体积进行相关优化。
刚讲了Protobuf 替换JSON,Protobuf是二进制格式,比JSON格式更加紧凑,能够使数据包大小大幅度减小,在网络传输中能够减少带宽占用和流量费用。
在IM系统中,由于用户数量庞大,消息发送频繁,在数据占用和网络带宽方面是一个巨大的问题,使用ProtoBuf能够显著地减少网络带宽消耗,提高系统的性能。
还有一方面就是消息压缩,但是压缩的深度和压缩算法需要慎重选择、验证。
所以使用ProtoBuf格式代替JSON格式基本可以解掉一大半延迟问题,也是接下来IM优化的一个方向。
说到坐席体验和交互方面,我们还是积累了不少经验的。不仅仅是IM,体验和交互是所有产品都无法绕开的一个话题。
自从做IM以来,体验可谓是鞭策我们不断前进的动力,卡顿是一直环绕在我耳边的一个话题。
客服理解的卡顿和我们正常理解的卡顿还是有点不一样的,前期我们也以为是系统卡住导致无法使用了,类似掉帧的场景。
实际却不是:
以上都会被归为卡顿。
针对这些方面:我们也是不断的进行职场调研、数据分析、优化,客服的满意度提升到了18%。可能在大家看来做了这么久提升18%并不是一个比较好的数据,但是针对客服域,提升18%也是一个相对比较难逾越的数据了。
主要的原因在2个方面:
1)第一个方面是很多客服都是3个月以内入职的,对于我们做的一些功能优化对比体验是无法感知或缺少功能使用对比的;
2)第二个方面是很多一线客服都来自一线大厂的客服服务团队。
其实反过来想一下,这也是一种正向的驱动,至少我们每次调研都能收集到新的反馈,同更加成熟、优秀产品的体验差距。
体验不是一蹴而就的,不要想着一下子就做到位,一个优秀的用户体验和交互设计需要始终与用户需求和反馈相结合,并不断改进和完善。在实际设计和开发过程中,需要进行不断的测试和优化,以确保系统的质量和可接受性。同时,需要与用户进行积极的沟通和反馈,以便更好地理解用户需求和意见。这一点我们之前是做的不够好的,尤其是新版本的推广,系统的易用性并未达到客服的期望,也是我们后期需要持续改进的一个方面。
体验是以绝大数用户需求为核心的,不能仅仅为了一小部分用户而去牺牲其他用户的使用体验,尤其不能因为某一个用户的反馈意见而做出过多的改变或者牺牲其他用户的利益。体验优化过程的不妥协也是非常重要的策略,在体验优化过程中,必须保持理性和客观,根据用户调研和数据分析进行合理的权衡和决策,以实现最佳的用户体验。
一些小细节的优化也可以起到事半功倍的效果,在IM系统中,一些细节的优化包括:及时的消息提示、清晰的消息展示、精确的消息发送时间等等。这些小细节的优化可以直接提高客服的使用效率和体验,从而提高客服满意度。IM的体验优化我们会一直做下去,有志者事竟成。
上述技术和思考的细节中有讲到消息的可靠性传递、卡顿优化处理、安全性、效率以及体验等。
接下来的一段时间我们还是以这几个方面为主线进行,持续优化、完善IM相关能力。
主要考虑以下几个方面的规划:
上述几个方面我们会优先去做重要且紧急的技术改造,并不会一味的创新、优化,还是会以业务为主,紧紧围绕业务和坐席体验展开。
客服发送一条消息在IM应用中看似简单,背后需要考虑的技术细节点是很多的。首先,这需要考虑到消息的发送机制和可靠性。即使是一条简单的消息,也需要经过一系列的加密、编码、传输、安全合规等等处理才能被成功接收。
最重要的是要考虑到数据实时性的问题,各种极限场景下的操作,客服发送的消息需要被及时展示到聊天页并传输给用户,客服同学在一对多的场景下工作,需要确保各会话消息不会出现不一致(丢失、重复),还有消息拦截和异常情况等问题。
因此,客服发送一条消息不仅需要技术能力和数据处理能力,还需要思考坐席体验和数据实时性等方面的问题。开发过程中需要细致入微地处理各种问题并持续优化,从而为客服提供一个稳定、流畅、安全、友好的IM应用。
[1] 得物基于Electron开发客服IM桌面端的技术实践
[2] 得物从0到1自研客服IM系统的技术实践之路
[3] 为何基于TCP协议的移动端IM仍然需要心跳保活机制?
[4] 从客户端的角度来谈谈移动端IM的消息可靠性和送达机制
[5] 不为人知的网络编程(十二):彻底搞懂TCP协议层的KeepAlive保活机制
[6] 微信新一代通信安全解决方案:基于TLS1.3的MMTLS详解
[7] 基于Netty的IM聊天加密技术学习:一文理清常见的加密概念、术语等
[8] IM通讯协议专题学习(一):Protobuf从入门到精通,一篇就够!
[9] IM通讯协议专题学习(二):快速理解Protobuf的背景、原理、使用、优缺点
[10] IM通讯协议专题学习(三):由浅入深,从根上理解Protobuf的编解码原理
[11] 请问有人知道语音留言聊天的主流实现方式吗?
[12] IM消息送达保证机制实现(一):保证在线实时消息的可靠投递
[13] IM消息送达保证机制实现(二):保证离线消息的可靠投递
[14] 如何保证IM实时消息的“时序性”与“一致性”?
[15] 一套亿级用户的IM架构技术干货(下篇):可靠性、有序性、弱网优化等
[16] 融云技术分享:全面揭秘亿级IM消息的可靠投递机制
(本文已同步发布于:http://www.52im.net/thread-4483-1-1.html)