从游击队到正规军(二):马蜂窝旅游网的IM客户端架构演进和实践总结

1、引言

移动互联网技术改变了旅游的世界,这个领域过去沉重的信息分销成本被大大下降。用户与服务供应商之间、用户与用户之间的沟通路径逐渐打通,沟通的场景也在不断扩展。这促使全部的移动应用开发者都要从用户视角出发,更好地知足用户需求。php

论坛时代的马蜂窝,用户之间的沟通形式比较单一,主要为单纯的回帖回复等。为了以较小的成本快速知足用户需求,当时采用的是非实时性消息的方案来实现用户之间的消息传递。html

随着行业和公司的发展,马蜂窝确立了「内容+交易」的独特商业模式。在用户规模不断增加及业务形态发生变化的背景下,为用户和商家提供稳定可靠的售前和售后技术支持,成为电商移动业务线的当务之急。算法

本文由马蜂窝电商业务 IM 移动端研发团队分享了马蜂窝电商业务 IM 移动端的架构演进过程,以及在IM技术力量和资源有限的状况下所踩过的坑等。数据库

系列文章:浏览器

关于马蜂窝旅游网:缓存

 

马蜂窝旅游网是中国领先的自由行服务平台,由陈罡和吕刚创立于2006年,从2010年正式开始公司化运营。马蜂窝的景点、餐饮、酒店等点评信息均来自上亿用户的真实分享,每一年帮助过亿的旅行者制定自由行方案。服务器

学习交流:微信

- 即时通信/推送技术开发交流5群:215477170 [推荐]网络

- 移动端IM开发入门文章:《新手入门一篇就够:从零开发移动端IM数据结构

(本文同步发布于:http://www.52im.net/thread-2796-1-1.html

2、设计思路与总体架构

咱们结合 B2C,C2B,C2C 不一样的业务场景设计实现了马蜂窝旅游移动端中的私信、用户咨询、用户反馈等即时通信业务;同时为了更好地为合做商家赋能,在马蜂窝商家移动端中加入与会话相关的咨询用户管理、客服管理、运营资源统计等功能。

目前 IM 涉及到的业务以下:

 

为了实现马蜂窝旅游 App 及商家 IM 业务逻辑、公共资源的整合复用及 UI 个性化定制,将问题拆解为如下部分来解决:

1)IM 数据通道与异常重连机制:解决不一样业务实时消息下发以及稳定性保障;

2)IM 实时消息订阅分发机制:解决消息定向发送、业务订阅消费,避免没必要要的请求资源浪费;

3)IM 会话列表 UI 绘制通用解决方案:解决不一样消息类型的快速迭代开发和管理复杂问题。

总体实现结构分为 4 个部分进行封装,分别为下图中的数据管理、消息注册分发管理、通用 UI 封装及业务管理。

 

3、技术原理和实现过程

3.一、通用数据通道

对于常规业务展现数据的获取,客户端须要主动发起请求,请求和响应的过程是单向的,且对实时性要求不高。但对于 IM 消息来讲,须要同时支持接收和发送操做,且对实时性要求高。为支撑这种要求,客户端和服务器之间须要建立一条稳定链接的数据通道,提供客户端和服务端之间的双向数据通讯。

3.1.1 数据通道基础交互原理

为了更好地提升数据通道对业务支撑的扩展性,咱们将全部通讯数据封装为外层结构相同的数据包,使多业务类型数据使用共同的数据通道下发通讯,统一分发处理,从而减小通道的建立数量,下降数据通道的维护成本。

 

常见的客户端与服务端数据交互依赖于 HTTP 请求响应过程,只有客户端主动发起请求才能够获得响应结果。结合马蜂窝的具体业务场景,咱们但愿创建一种可靠的消息通道来保障服务端主动通知客户端,实现业务数据的传递。目前采用的是 HTTP 长连接轮询的形式实现,各业务数据消息类型只需遵循约定的通用数据结构,便可实现经过数据通道下发给客户端。数据通道没必要关心数据的具体内容,只须要关注接收与发送。

3.1.2 客户端数据通道实现原理

客户端数据通道管理的核心是维护一个业务场景请求栈,在不一样业务场景切换过程当中入栈不一样的业务场景参数数据。每次 HTTP 长连接请求使用栈顶请求数据,能够模拟在特定业务场景 (如与不一样的用户私信) 的不一样处理。数据相关处理都集中封装在数据通道管理中,业务层只需在数据通道管理中注册对应的接收处理便可获得须要的业务消息数据。

 

3.二、消息订阅与分发

在软件系统中,订阅分发本质上是一种消息模式。非直接传递消息的一方被称为「发布者」,接受消息处理称为「订阅者」。发布者将不一样的消息进行分类后分发给对应类型的订阅者,完成消息的传递。应用订阅分发机制的优点为便于统一管理,能够添加不一样的拦截器来处理消息解析、消息过滤、异常处理机制及数据采集工做。

3.2.1 消息订阅

业务层只专一于消息处理,并不关心消息接收分发的过程。订阅的意义在于更好地将业务处理和数据通道处理解耦,业务层只须要订阅关注的消息类型,被动等待接收消息便可。

 

业务层订阅须要处理的业务消息类型,在注册后会自动监控当前页面的生命周期,并在页面销毁后删除对应的消息订阅,从而避免手动编写成对的订阅和取消订阅,下降业务层的耦合,简化调用逻辑。订阅分发管理会根据各业务类型维护订阅者队列用于消息接收的分发操做。

3.2.2 消息分发

数据通道的核心在于维护多消息类型各自对应的订阅者集合,并将解析的消息分发到业务层。

 

数据通道由多业务消息共用,在每次请求收到新消息列表后,根据各自业务类型从新拆分红多个消息列表,分发给各业务类型对应的订阅处理器,最终传递至业务层交予对应页面处理展现。

3.三、会话消息列表绘制

基于不一样的场景,如社交为主的私信、用户服务为主的咨询反馈等,都须要会话列表的展现形式;但各场景又不彻底相同,须要分析当前会话列表的共通性及可封装复用的部分,以更好地支撑后续业务的扩展。

3.3.1 消息在列表展现的组成结构

IM 消息列表的特色在于消息类型多、UI 展现多样化,所以须要创建各种型消息和布局的对应关系,在收到消息后根据消息类型匹配到对应的布局添加至对应消息列表。

 

3.3.2 消息类型与展现布局管理原理

对于不一样消息类型及展现,问题的核心在于创建消息类型、消息数据结构、消息展现布局管理的映射关系。以上三者在实现过程当中经过创建映射管理表来维护,各自创建列表存储消息类型/消息体封装结构/消息展现布局管理,设置对应关系关联 3 个列表来完成查找。 

 

3.3.3 一次收发消息 UI 绘制过程

各种型消息在内容展现上各有不一样,但总体会话消息展现样式能够分为 3 种,分别是接收消息、发送消息和处于页面中间的消息样式,区别只在于内部的消息样式。因此消息 UI 的绘制能够拆分红 2 个步骤,首先是建立通用的展现容器,而后再填充各消息具体的展现样式。

 

拆分的目的在于使各种型消息 UI 处理只须要关注特有数据。而如通用消息如头像、名称、消息时间、是否可举报、已读未读状态、发送失败/重试状态等均可以统一处理,下降修改维护的成本,同时使各消息 UI 处理逻辑更少、更清晰,更利于新类型的扩展管理。

收发到消息后,根据消息类型判断是「发送接收类型」仍是「居中展现类型」,找到外层的布局样式,再根据具体消息类型找到特有的 UI 样式,拼接在外层布局中,获得完整的消息卡片,而后设置对应的数据渲染到列表中,完成整个消息的绘制。

4、细节优化 & 踩坑经验

在实现上述 IM 系统的过程当中,咱们遇到了不少问题,也作了不少细节优化。在这里总结实现时须要考虑的几点,以供你们借鉴。

4.一、消息去重

在前面的架构中,咱们使用 msg_id 来标记消息列表中的每一条消息,msg_id 是根据客户端上传的数据,进行存储后生成的。

 

客户端 A 请求 IM 服务器以后生成 msg_id,再经过请求返回和 Polling 分发到客户端 A 和客户端 B。当流程成立的时候,客户端 A 和客户端 B 经过服务端分发的 msg_id 来进行本地去重。

但这种方案存在如下问题:

 

当客户端 A 由于网络出现问题,没法接受对应发送消息的请求返回的时候,会触发重发机制。此时虽然 IM 服务器已经接受过一次客户端 A 的消息发送请求,可是由于没法肯定两个请求是否来自同一条原始消息,只能再次接受,这就致使了重复消息的产生。解决的方法是引入客户端消息标识 id。由于咱们已经依附旧有的 msg_id 作了不少工做,不打算让客户端的消息 id 代替 msg_id 的职能,所以从新定义一个 random_id。

 

random_id = random + time_stamp。random_id 标识了惟一的消息体,由一个随机数和生成消息体的时间戳生成。当触发重试的时候,两次请求的 random_id 会是相同的,服务端能够根据该字段进行消息去重。

4.二、本地化 Push

当咱们在会话页或列表页的环境下,能够经过界面的变化很直观地观察到收取了新消息并更新未读数。但从会话页或者列表页退出以后,就没法单纯地从界面上获取这些信息,这时须要有其余的机制,让用户获知当前消息的状态。

系统推送与第三方推送是一个可行的选择,但本质上推送也是基于长连接提供的服务。为弥补推送不稳定性与风险,咱们采用数据通道+本地通知的形式来完善消息通知机制。经过数据通道下发的消息如需达到推送的提示效果,则携带对应的 Push 展现数据。同时会对当前所处的页面进行判断,避免对当前页面的消息内容进行重复提醒。

 

经过这种数据通道+本地通知展现的机制,能够在应用处于运行状态的时间内提升消息抵达率,减小对于远程推送的依赖,下降推送系统的压力,并提高用户体验。

4.三、数据通道异常重连机制

当前数据通道经过 HTTP 长连接轮询 (Polling) 实现。

不一样业务场景下对 Polling 的影响以下图所示:

 

因为用户手机所处网络请求状态不一,有时候会遇到网络中断或者服务端异常的状况,从而终止 Polling 的请求。为可以让用户在网络恢复后继续会话业务,须要引入重连机制。

在重试机制 1.0 版本中,对于可能出现较多重试请求的状况,采起的是添加 60s 内连续 5 次报错延迟重试的限制。

具体流程以下:

 

在实践中发现如下问题:

1)当服务端忽然异常并持续超过 1 分钟后,客户端启动执行重试机制,并每隔 1 分钟重发一次重连请求。这对服务器而言就至关于遭受一次短暂集中的「攻击」,甚至有可能拖垮服务器;

2)当客户端断网后马上进行重试也并不合理,由于用户恢复网络也须要必定时间,这期间的重连请求是无心义的。

基于以上问题分析改进,咱们设计了第二版重试机制。这次将 5 次如下请求错误的延迟时间修改成 5 - 20 秒随机重试,将客户端重试请求分散在多个时间点避免同时请求造成对服务器对瞬时压力。同时在客户端断网状况下也进行延迟重试。

 

Polling 机制修改后请求量划分,相对以前请求分布比较均匀,再也不出现集中请求的问题。

 

4.四、惟一会话标识

4.4.1 为什么引入消息线 ID

消息线就是用来表示会话的聊天关系,不一样消息线表明不一样对象的会话,从 DB 层面来看须要一个张表来存储这种关系 uid + object_id + busi_type = 消息线 ID。

 

在 IM 初期实现中,咱们使用会话配置参数(包含业务来源和会话参数)来标识会话 id,有三个做用:

1)查找商家 id,获取咨询来源,进行管家分配;

2)查找已存在的消息线;

3)判断客户端页面状态,决定要不要下发推送,进行消息提醒。

这种方式存在两个问题:

1)经过业务来源和会话参数来解析对应的商家 id,两个参数缺失一个都会致使商家 id 解析错误,还要各类查询数据库才能获得商家 id,影响效率;

2)经过会话类型切换接口标识当前会话类型,切换页面会频繁触发网络请求;若是请求接口发生意外容易引起消息内容错误问题,严重依赖客户端的健壮性。

用业务来源和会话参数帮助咱们进行管家分配是不可避免的,但咱们能够经过引入消息线 ID 来绑定消息线的方式,替代业务来源和会话参数查找消息线的做用。另外针对下发推送的问题已经过上方讲述的本地推送通知机制解决。

4.4.2 什么时候建立消息线

1)当进入会话页发消息时,检查 DB 中是否存在对应消息线,不存在则将这条消息 id 看成消息线 id 使用,存在即复用;

2)当进入会话时,根据用户 id 、业务类型 id 等检查在 DB 中是否已存在对应消息线,不存在则建立消息线,存在即复用。

4.4.3 引入消息线目的

1)减小服务端查询消息线的成本;

2)移除旧版状态改变相关的接口请求,间接提升了推送触达率;

3)下降移动端对于用户消息匹配的复杂度。

5、展望及近期优化

5.一、数据通道实现方式升级为 Websocket

WebSocket 是一种在单个 TCP 链接上进行全双工通讯的协议。WebSocket 使得客户端和服务器之间的数据交换变得更加简单,容许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只须要完成一次握手,二者之间就直接能够建立持久性的链接,并进行双向数据传输。

与目前的 HTTP 轮询实现机制相比, Websocket 有如下优势:

1)较少的控制开销:在链接建立后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小。在不包含扩展的状况下,对于服务器到客户端的内容,此头部大小只有 2 至 10 字节(和数据包长度有关);对于客户端到服务器的内容,此头部还须要加上额外的 4 字节的掩码。相对于 HTTP 请求每次都要携带完整的头部,开销显著减小;

2)更强的实时性:因为协议是全双工的,服务器能够随时主动给客户端下发数据。相对于 HTTP 须要等待客户端发起请求服务端才能响应,延迟明显更少;即便是和 Comet 等相似的长轮询比较,其也能在短期内更屡次地传递数据;

3)保持链接状态:与 HTTP 不一样的是,Websocket 须要先建立链接,这就使其成为一种有状态的协议,在以后通讯时能够省略部分状态信息。而 HTTP 请求可能须要在每一个请求都携带状态信息(如身份认证等);

4)更好的二进制支持:Websocket 定义了二进制帧,相对 HTTP,能够更轻松地处理二进制内容;

5)支持扩展:Websocket 定义了扩展,用户能够扩展协议、实现部分自定义的子协议,如部分浏览器支持压缩等;

6)更好的压缩效果:相对于 HTTP 压缩,Websocket 在适当的扩展支持下,能够沿用以前内容的上下文,在传递相似的数据时,能够显著地提升压缩率。

为了进一步优化咱们的数据通道设计,咱们探索验证了 Websocket 的可行性,并进行了调研和设计:

 

近期将对 HTTP 轮询实现方案进行替换,进一步优化数据通道的效率。

5.二、业务功能的扩展

计划将 IM 移动端功能模块打形成通用的即时通信组件,可以更容易地赋予各业务 IM 能力,使各业务快速在自有产品线上添加聊天功能,下降研发 IM 的成本和难度。目前的 IM 功能实现主要有两个组成,分别是公用的数据通道与 UI 组件。

随着马蜂窝业务发展,在现有 IM 系统上还有不少能够建设和升级的方向。好比消息类型的支撑上,扩展对短视频、语音消息、快捷消息回复等支撑,提升社交的便捷性和趣味性;对于多人场景但愿增长群组,兴趣频道,多人音视频通讯等场景的支撑等。

相信将来经过对更多业务功能的扩展及应用场景的探索,马蜂窝移动端 IM 将更好地提高用户体验,并持续为商家赋能。

附录:更多IM架构设计方面的文章

浅谈IM系统的架构设计
简述移动端IM开发的那些坑:架构设计、通讯协议和客户端
一套海量在线用户的移动端IM架构设计实践分享(含详细图文)
一套原创分布式即时通信(IM)系统理论架构方案
从零到卓越:京东客服即时通信系统的技术架构演进历程
蘑菇街即时通信/IM服务器开发之架构选择
腾讯QQ1.4亿在线用户的技术挑战和架构演进之路PPT
微信后台基于时间序的海量数据冷热分级架构设计实践
微信技术总监谈架构:微信之道——大道至简(演讲全文)
如何解读《微信技术总监谈架构:微信之道——大道至简》
快速裂变:见证微信强大后台架构从0到1的演进历程(一)
17年的实践:腾讯海量产品的技术方法论
移动端IM中大规模群消息的推送如何保证效率、实时性?
现代IM系统中聊天消息的同步和存储方案探讨
IM开发基础知识补课(二):如何设计大量图片文件的服务端存储架构?
IM开发基础知识补课(三):快速理解服务端数据库读写分离原理及实践建议
IM开发基础知识补课(四):正确理解HTTP短链接中的Cookie、Session和Token
WhatsApp技术实践分享:32人工程团队创造的技术神话
微信朋友圈千亿访问量背后的技术挑战和实践总结
王者荣耀2亿用户量的背后:产品定位、技术架构、网络方案等
IM系统的MQ消息中间件选型:Kafka仍是RabbitMQ?
腾讯资深架构师干货总结:一文读懂大型分布式系统设计的方方面面
以微博类应用场景为例,总结海量社交系统的架构设计步骤
快速理解高性能HTTP服务端的负载均衡技术原理
子弹短信光鲜的背后:网易云信首席架构师分享亿级IM平台的技术实践
知乎技术分享:从单机到2000万QPS并发的Redis高性能缓存实践之路
IM开发基础知识补课(五):通俗易懂,正确理解并用好MQ消息队列
微信技术分享:微信的海量IM聊天消息序列号生成实践(算法原理篇)
微信技术分享:微信的海量IM聊天消息序列号生成实践(容灾方案篇)
新手入门:零基础理解大型分布式架构的演进历史、技术原理、最佳实践
一套高可用、易伸缩、高并发的IM群聊、单聊架构方案设计实践
阿里技术分享:深度揭秘阿里数据库技术方案的10年变迁史
阿里技术分享:阿里自研金融级数据库OceanBase的艰辛成长之路
社交软件红包技术解密(一):全面解密QQ红包技术方案——架构、技术实现等
社交软件红包技术解密(二):解密微信摇一摇红包从0到1的技术演进
社交软件红包技术解密(三):微信摇一摇红包雨背后的技术细节
社交软件红包技术解密(四):微信红包系统是如何应对高并发的
社交软件红包技术解密(五):微信红包系统是如何实现高可用性的
社交软件红包技术解密(六):微信红包系统的存储层架构演进实践
社交软件红包技术解密(七):支付宝红包的海量高并发技术实践
社交软件红包技术解密(八):全面解密微博红包技术方案
社交软件红包技术解密(九):谈谈手Q红包的功能逻辑、容灾、运维、架构等
即时通信新手入门:一文读懂什么是Nginx?它可否实现IM的负载均衡?
即时通信新手入门:快速理解RPC技术——基本概念、原理和用途
多维度对比5款主流分布式MQ消息队列,妈妈不再担忧个人技术选型了
从游击队到正规军(一):马蜂窝旅游网的IM系统架构演进之路
从游击队到正规军(二):马蜂窝旅游网的IM客户端架构演进和实践总结
IM开发基础知识补课(六):数据库用NoSQL仍是SQL?读这篇就够了!
>> 更多同类文章 ……
 

(本文同步发布于:http://www.52im.net/thread-2796-1-1.html

相关文章
相关标签/搜索