一、引言
移动互联网技术改变了旅游的世界,这个领域过去沉重的信息分销成本被大大降低。用户与服务供应商之间、用户与用户之间的沟通路径逐渐打通,沟通的场景也在不断扩展。这促使所有的移动应用开发者都要从用户视角出发,更好地满足用户需求。
论坛时代的马蜂窝,用户之间的沟通形式比较单一,主要为单纯的回帖回复等。为了以较小的成本快速满足用户需求,当时采用的是非实时性消息的方案来实现用户之间的消息传递。
随着行业和公司的发展,马蜂窝确立了「内容+交易」的独特商业模式。在用户规模不断增长及业务形态发生变化的背景下,为用户和商家提供稳定可靠的售前和售后技术支持,成为电商移动业务线的当务之急。
本文由马蜂窝电商业务 IM 移动端研发团队分享了马蜂窝电商业务 IM 移动端的架构演进过程,以及在IM技术力量和资源有限的情况下所踩过的坑等。
系列文章:
关于马蜂窝旅游网:
马蜂窝旅游网是中国领先的*行服务平台,由陈罡和吕刚创立于2006年,从2010年正式开始公司化运营。马蜂窝的景点、餐饮、酒店等点评信息均来自上亿用户的真实分享,每年帮助过亿的旅行者制定*行方案。
学习交流:
- 即时通讯/推送技术开发交流5群:215477170 [推荐]
- 移动端IM开发入门文章:《新手入门一篇就够:从零开发移动端IM》
(本文同步发布于:http://www.52im.net/thread-2796-1-1.html)
二、设计思路与整体架构
我们结合 B2C,C2B,C2C 不同的业务场景设计实现了马蜂窝旅游移动端中的私信、用户咨询、用户反馈等即时通讯业务;同时为了更好地为合作商家赋能,在马蜂窝商家移动端中加入与会话相关的咨询用户管理、客服管理、运营资源统计等功能。
目前 IM 涉及到的业务如下:
为了实现马蜂窝旅游 App 及商家 IM 业务逻辑、公共资源的整合复用及 UI 个性化定制,将问题拆解为以下部分来解决:
1)IM 数据通道与异常重连机制:解决不同业务实时消息下发以及稳定性保障;
2)IM 实时消息订阅分发机制:解决消息定向发送、业务订阅消费,避免不必要的请求资源浪费;
3)IM 会话列表 UI 绘制通用解决方案:解决不同消息类型的快速迭代开发和管理复杂问题。
整体实现结构分为 4 个部分进行封装,分别为下图中的数据管理、消息注册分发管理、通用 UI 封装及业务管理。
三、技术原理和实现过程
3.1、通用数据通道
对于常规业务展示数据的获取,客户端需要主动发起请求,请求和响应的过程是单向的,且对实时性要求不高。但对于 IM 消息来说,需要同时支持接收和发送操作,且对实时性要求高。为支撑这种要求,客户端和服务器之间需要创建一条稳定连接的数据通道,提供客户端和服务端之间的双向数据通信。
3.1.1 数据通道基础交互原理
为了更好地提高数据通道对业务支撑的扩展性,我们将所有通信数据封装为外层结构相同的数据包,使多业务类型数据使用共同的数据通道下发通信,统一分发处理,从而减少通道的创建数量,降低数据通道的维护成本。
常见的客户端与服务端数据交互依赖于 HTTP 请求响应过程,只有客户端主动发起请求才可以得到响应结果。结合马蜂窝的具体业务场景,我们希望建立一种可靠的消息通道来保障服务端主动通知客户端,实现业务数据的传递。目前采用的是 HTTP 长链接轮询的形式实现,各业务数据消息类型只需遵循约定的通用数据结构,即可实现通过数据通道下发给客户端。数据通道不必关心数据的具体内容,只需要关注接收与发送。
3.1.2 客户端数据通道实现原理
客户端数据通道管理的核心是维护一个业务场景请求栈,在不同业务场景切换过程中入栈不同的业务场景参数数据。每次 HTTP 长链接请求使用栈顶请求数据,可以模拟在特定业务场景 (如与不同的用户私信) 的不同处理。数据相关处理都集中封装在数据通道管理中,业务层只需在数据通道管理中注册对应的接收处理即可得到需要的业务消息数据。
3.2、消息订阅与分发
在软件系统中,订阅分发本质上是一种消息模式。非直接传递消息的一方被称为「发布者」,接受消息处理称为「订阅者」。发布者将不同的消息进行分类后分发给对应类型的订阅者,完成消息的传递。应用订阅分发机制的优势为便于统一管理,可以添加不同的拦截器来处理消息解析、消息过滤、异常处理机制及数据采集工作。
3.2.1 消息订阅
业务层只专注于消息处理,并不关心消息接收分发的过程。订阅的意义在于更好地将业务处理和数据通道处理解耦,业务层只需要订阅关注的消息类型,被动等待接收消息即可。
业务层订阅需要处理的业务消息类型,在注册后会自动监控当前页面的生命周期,并在页面销毁后删除对应的消息订阅,从而避免手动编写成对的订阅和取消订阅,降低业务层的耦合,简化调用逻辑。订阅分发管理会根据各业务类型维护订阅者队列用于消息接收的分发操作。
3.2.2 消息分发
数据通道的核心在于维护多消息类型各自对应的订阅者集合,并将解析的消息分发到业务层。
数据通道由多业务消息共用,在每次请求收到新消息列表后,根据各自业务类型重新拆分成多个消息列表,分发给各业务类型对应的订阅处理器,最终传递至业务层交予对应页面处理展示。
3.3、会话消息列表绘制
基于不同的场景,如社交为主的私信、用户服务为主的咨询反馈等,都需要会话列表的展示形式;但各场景又不完全相同,需要分析当前会话列表的共通性及可封装复用的部分,以更好地支撑后续业务的扩展。
3.3.1 消息在列表展示的组成结构
IM 消息列表的特点在于消息类型多、UI 展示多样化,因此需要建立各类型消息和布局的对应关系,在收到消息后根据消息类型匹配到对应的布局添加至对应消息列表。
3.3.2 消息类型与展示布局管理原理
对于不同消息类型及展示,问题的核心在于建立消息类型、消息数据结构、消息展示布局管理的映射关系。以上三者在实现过程中通过建立映射管理表来维护,各自建立列表存储消息类型/消息体封装结构/消息展示布局管理,设置对应关系关联 3 个列表来完成查找。
3.3.3 一次收发消息 UI 绘制过程
各类型消息在内容展示上各有不同,但整体会话消息展示样式可以分为 3 种,分别是接收消息、发送消息和处于页面中间的消息样式,区别只在于内部的消息样式。所以消息 UI 的绘制可以拆分成 2 个步骤,首先是创建通用的展示容器,然后再填充各消息具体的展示样式。
拆分的目的在于使各类型消息 UI 处理只需要关注特有数据。而如通用消息如头像、名称、消息时间、是否可举报、已读未读状态、发送失败/重试状态等都可以统一处理,降低修改维护的成本,同时使各消息 UI 处理逻辑更少、更清晰,更利于新类型的扩展管理。
收发到消息后,根据消息类型判断是「发送接收类型」还是「居中展示类型」,找到外层的布局样式,再根据具体消息类型找到特有的 UI 样式,拼接在外层布局中,得到完整的消息卡片,然后设置对应的数据渲染到列表中,完成整个消息的绘制。
四、细节优化 & 踩坑经验
在实现上述 IM 系统的过程中,我们遇到了很多问题,也做了很多细节优化。在这里总结实现时需要考虑的几点,以供大家借鉴。
4.1、消息去重
在前面的架构中,我们使用 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.2、本地化 Push
当我们在会话页或列表页的环境下,可以通过界面的变化很直观地观察到收取了新消息并更新未读数。但从会话页或者列表页退出之后,就无法单纯地从界面上获取这些信息,这时需要有其他的机制,让用户获知当前消息的状态。
系统推送与第三方推送是一个可行的选择,但本质上推送也是基于长链接提供的服务。为弥补推送不稳定性与风险,我们采用数据通道+本地通知的形式来完善消息通知机制。通过数据通道下发的消息如需达到推送的提示效果,则携带对应的 Push 展示数据。同时会对当前所处的页面进行判断,避免对当前页面的消息内容进行重复提醒。
通过这种数据通道+本地通知展示的机制,可以在应用处于运行状态的时间内提高消息抵达率,减少对于远程推送的依赖,降低推送系统的压力,并提升用户体验。
4.3、数据通道异常重连机制
当前数据通道通过 HTTP 长链接轮询 (Polling) 实现。
不同业务场景下对 Polling 的影响如下图所示:
由于用户手机所处网络请求状态不一,有时候会遇到网络中断或者服务端异常的情况,从而终止 Polling 的请求。为能够让用户在网络恢复后继续会话业务,需要引入重连机制。
在重试机制 1.0 版本中,对于可能出现较多重试请求的情况,采取的是添加 60s 内连续 5 次报错延迟重试的限制。
具体流程如下:
在实践中发现以下问题:
1)当服务端突然异常并持续超过 1 分钟后,客户端启动执行重试机制,并每隔 1 分钟重发一次重连请求。这对服务器而言就相当于遭受一次短暂集中的「攻击」,甚至有可能拖垮服务器;
2)当客户端断网后立刻进行重试也并不合理,因为用户恢复网络也需要一定时间,这期间的重连请求是无意义的。
基于以上问题分析改进,我们设计了第二版重试机制。此次将 5 次以下请求错误的延迟时间修改为 5 - 20 秒随机重试,将客户端重试请求分散在多个时间点避免同时请求形成对服务器对瞬时压力。同时在客户端断网情况下也进行延迟重试。
Polling 机制修改后请求量划分,相对之前请求分布比较均匀,不再出现集中请求的问题。
4.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.1、数据通道实现方式升级为 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.2、业务功能的扩展
计划将 IM 移动端功能模块打造成通用的即时通讯组件,能够更容易地赋予各业务 IM 能力,使各业务快速在自有产品线上添加聊天功能,降低研发 IM 的成本和难度。目前的 IM 功能实现主要有两个组成,分别是公用的数据通道与 UI 组件。
随着马蜂窝业务发展,在现有 IM 系统上还有很多可以建设和升级的方向。比如消息类型的支撑上,扩展对短视频、语音消息、快捷消息回复等支撑,提高社交的便捷性和趣味性;对于多人场景希望增加群组,兴趣频道,多人音视频通信等场景的支撑等。
相信未来通过对更多业务功能的扩展及应用场景的探索,马蜂窝移动端 IM 将更好地提升用户体验,并持续为商家赋能。
附录:更多IM架构设计方面的文章
(本文同步发布于:http://www.52im.net/thread-2796-1-1.html)
从游击队到正规军(二):马蜂窝旅游网的IM客户端架构演进和实践总结的更多相关文章
-
从游击队到正规军:马蜂窝旅游网的IM系统架构演进之路
本文引用自马蜂窝公众号,由马蜂窝技术团队原创分享. 一.引言 今天,越来越多的用户被马蜂窝持续积累的笔记.攻略.嗡嗡等优质的分享内容所吸引,在这里激发了去旅行的热情,同时也拉动了马蜂窝交易的增长.在帮 ...
-
从游击队到正规军(三):基于Go的马蜂窝旅游网分布式IM系统技术实践
本文由马蜂窝技术团队电商交易基础平台研发工程师"Anti Walker"原创分享. 一.引言 即时通讯(IM)功能对于电商平台来说非常重要,特别是旅游电商. 从商品复杂性来看,一个 ...
-
基于Go的马蜂窝旅游网分布式IM系统技术实践
一.引言 即时通讯(IM)功能对于电商平台来说非常重要,特别是旅游电商. 从商品复杂性来看,一个旅游商品可能会包括用户在未来一段时间的衣.食.住.行等方方面面.从消费金额来看,往往单次消费额度较大.对 ...
-
2019 途牛旅游网java面试笔试题 (含面试题解析)
本人5年开发经验.18年年底开始跑路找工作,在互联网寒冬下成功拿到阿里巴巴.今日头条.途牛旅游网等公司offer,岗位是Java后端开发,因为发展原因最终选择去了途牛旅游网,入职一年时间了,也成为 ...
-
同程旅游网开放平台SDK开发完成
最近利用业余时间,使用了同程旅游网的开放平台,并对大部分的方法进行了调用,发现有很多不好用的地方,比如 1.同一个开放平台居然有几个调用地址,景区调用http://tcopenapi.17usoft. ...
-
Vue 旅游网首页开发2 - 首页编写
Vue 旅游网首页开发2 - 首页编写 项目结构 首页开发 效果图 项目开发组件化 将页面的各个部分划分成不同的组件,有助于项目的开发和维护. 项目代码初始化 项目结构修改 1.删除整个 compin ...
-
Vue 旅游网首页开发1-工具安装及码云使用
Vue 旅游网首页开发-工具安装及码云使用 环境安装 安装 node.js node.js 官网:https://nodejs.org/en/ 注册码云,创建私密仓库存储项目 码云:https://g ...
-
2F+1模式才是高可用 途牛旅游网 还是通过proxy层
2F+1模式才是高可用 途牛旅游网 还是通过proxy层 f f f f f f f f f
-
KEUC首次落地中国,网易云深度剖析Kubernetes优化与实践
本文由 网易云发布. 10 月 15 日,聚焦 Kubernetes 中国行业应用与技术落地的首届中国 Kubernetes 用户大会(KEUC)在杭州成功举办.本次大会吸引了来自全球各地的技术精英 ...
随机推荐
-
js获取url信息
设置或获取对象指定的文件名或路径. alert(window.location.pathname) 设置或获取整个 URL 为字符串. alert(window.location.href); 设置或 ...
-
CF 19D Points 【线段树+平衡树】
在平面上进行三种操作: 1.add x y:在平面上添加一个点(x,y) 2.remove x y:将平面上的点(x,y)删除 3.find x y:在平面上寻找一个点,使这个点的横坐标大于x,纵坐标 ...
-
(@WhiteTaken)设计模式学习——建造者模式
Builder模式,也就是建造者模式或者生成器模式,是GoF提出的23种设计模式的一种. 这种模式是用来隐式的创建复合对象而提出来的. 创建复合对象的过程,不在主客户端显示. 下面直接上代码.以修建房 ...
-
Android JSON 解析关键代码
Android Json 解析其实还是蛮重要的知识点,为什么这么说呢,因为安卓通信大部分的协议都是使用 json 的方式传输,我知道以前大部分是使用的 xml ,但是时代在发展社会在进步,json 成 ...
-
Oracle学习笔记:trunc函数
在Oracle中可以使用trunc函数进行日期截取和数字截取,具体使用方法如下: 1.trunc(for dates) 日期截取 语法:trunc(date,[fmt]) select trunc(s ...
-
比較两个 List 的值是否相等
public static <T extends Comparable<T>> boolean compare(List<T> a, List<T> b ...
-
利用JavaScript计算引擎进行字符串公式运算
1.通过js计算引擎计算(java自带) 2.计算公式除了支持基本的方法之外还支持简单js脚本分支计算 3.通过设定map传入参数 4.默认返回最后一个计算结果,如果需返回特定值,将变量补写在公式最后 ...
-
Android应用开发基础之五:网络编程(二)
HttpClient 发送get请求 创建一个客户端对象 HttpClient client = new DefaultHttpClient(); 创建一个get请求对象 HttpGet hg = n ...
-
BZOJ3110:[ZJOI2013]K大数查询(整体二分版)
浅谈离线分治算法:https://www.cnblogs.com/AKMer/p/10415556.html 题目传送门:https://lydsy.com/JudgeOnline/problem.p ...
-
AngularJS 重复HTML元素
data-ng-repeat指令会重复一个HTML元素 <!DOCTYPE html><html><head><meta http-equiv="C ...