8 月底,“IM 进阶实战高手课·第二讲”围绕“Web & Electron 平台即时通讯产品的技术选型”进行了详细拆解。
融云讲师巧用比喻等方法,生动而又逻辑清晰地对 IM 场景前端技术方案进行了分析对比,并分享了融云的最佳实践。下期聚焦 IM 全能力 ,就在 9 月 20 日 关注【融云全球互联网通信云】了解更多
IM 常见业务形态及核心功能
即时通讯产品常见的业务形态有以下几种:聊天室、单群聊、超级群、实时通知、在线广播。而底层功能就像基础零件,可以用不同的方法拼接出上层的不同业务形态。
基础功能单元模块,大概就分为三类:
最基础的是连接管理类的需求,这是即时通讯业务的基础。接着是两端基于连接的数据传输,这里我们要关注的是前后端通讯时的数据传输协议,也就是对于数据的序列化和反序列化的过程管理。最后就是基于既有数据的查询功能,我们主要分享前端的持久化存储技术。
即时通讯场景下,对于这三个技术点的一些技术要求各不相同。
- 连接管理 - 持续、稳定、及时的双向网络连接
- 数据传输 - 安全、高效、易拓展的前后端数据传输协议
- 记录查询 - 前端数据持久化存储方案
网络连接方案对比
我们通过五个指标来横向对比连接方案,分别是:连接速度、传输效率、即时性、安全性、兼容性。
WebSocket 是前端的首选方案,它也是 Web 平台上构建长连接业务的原生技术方案。因为浏览器安全沙箱的存在,我们不能在 Web 浏览器内直接访问传输层协议,但是 Electron 主进程内是百无禁忌的,我们可以在 Electron 场景下选用 TCP。
基于 HTTP 的模拟双工协议解决方案,并不是单纯的 HTTP 协议本身,因为在长连接业务中,短连接特性的 HTTP 协议并不匹配需求场景。
连接速度
连接速度是从发起连接到连接建立的耗时。
TCP 的连接需要进行三次握手,对于发起端是发两次收一次,对于应答端就是发一次收两次。
为什么要做三次握手?其实这也是一个很常见的面试题。打个比方来说,两个人要达成一次有效对话,就需要确定两个信息:一是自己的耳朵和嘴巴没有问题;二是对方的耳朵和嘴巴没有问题。只有在这个基础之上,两者之间的对话才是有效的。三次握手就是要完成这么一个能力确认过程。嘴巴=发送能力,耳朵=接收能力。
当然这个比喻也不严谨,双方语言不通时也沟通不来。这是第二个技术方向数据封装协议要解决的问题,也就是对对端意图理解问题。
WebSocket 连接要建立在 TCP 连接基础之上,因为 WebSocket 是一个应用层协议,也就是运维们经常说的 7 层协议,而 TCP 是一个传输层协议,是 4 层协议。
TCP 握手完成之后,通过 HTTP 报文向服务器协议提出升级申请,服务通过 HTTP response 报文来响应申请,至此 WebSocket 协商完成。比 TCP 多了两个动作,所以 WebSocket 的连接速度是慢于 TCP 的。HTTP 是短连接协议,连接无法稳定保持,每次通讯要重建连接,所以连接速度对它没有意义。
传输效率
我们把传输效率定义为同一块数据在传输过程中产生的流量消耗、时间消耗、算力消耗,以辅助横向对比不同网络协议,消耗越大,效率越低。
这里我们主要看流量消耗和时间消耗,算力消耗在 Web 和 Electron 几乎可以忽略不计。先看一下 OSI 参考模型,TCP 是 4 层协议,WebSocekt 是 7 层协议,这些其实就是 OSI 参考模型里的层级概念。
OSI 参考模型里,数据从两个网络节点之间传递,是一个 U 型传递过程:发送端从上到下传递,每层需要在数据包中增加不同的协议头信息,以确保接收端同层可以解析;接收端从下往上传递,传递过程逐层剥离数据中的头信息,并将数据向上传递。
所以数据包从上到下的传递过程中,数据体积是不断加码的。同一段数据,直接通过 TCP 发送,流量消耗低于 WebSocket 协议。我们看一下通过 WebSocket 协议传递数据的额外流量有哪些。
首先 WebSocket 数据传递的最小单元是数据帧。一段数据会被分割组装为最少一个数据帧写入到 TCP 的缓冲区,如果数据比较大,就会被分拆为多个数据帧,然后对端接收到之后再进行数据帧还原。
下图的二进制序列结构就是数据帧中的数据组成。
HTTP 是文本型协议,没有最小发送单元,或者说 HTTP 的报文数据的最小发送单元,就是 TCP 协议的最小发送单元。它不像 WebSocket 会将数据分装为 N 个数据帧,每个数据帧增加 WebSocket 头信息后再交给 TCP。它不主动分割数据,只在数据首部增加首行和 Headers 信息,并最终把完整报文信息写入 TCP 缓冲区,交由 TCP 去管理传输过程。
那么,HTTP 的传输效率是否就优于 WebSocket 呢?并不一定。
首先,一个 WebSocket 数据帧最多额外增加 2 - 14 个字节的头信息,但是 HTTP 协议本身的首行和 Headers 信息消耗的空间是远大于 14 字节的,这也是因为 HTTP 协议的特性导致的。它是文本型协议,一个字符最少需要一个字节的容量来存储。
其次,HTTP 协议的拓展,会额外增加报文 Headers 信息,这些信息也是字符型的,且是键值对形式。另外,HTTP 本身是短连接,意味着服务在收到请求时首先要确认数据发送者的身份,所以报文数据中不可避免地在每次的请求中携带鉴权信息,但是长连接协议是不需要这些额外开销的。
所以,WebSocket 的总体传输效率是优于 HTTP 的,除非待传输的数据大到了使 WebSocket 数据帧数量的头信息空间总和超过了 HTTP 报文头的程度,但是这种情况一般发生在文件上传等低频场景。大部分业务数据往来中,单次发送的数据都不会很大。
即时性
即时性是数据准备完成,到被写入到 TCP 缓冲区可能经历哪些等待时长。
对于即时通讯场景,数据的上下行是同时在发生的,这也意味着 HTTP 的短特性很吃亏,因为下行数据会受阻,服务器无法通过短连接的 HTTP 协议完成数据的主动推送。
这里我们先普及一下网络协议的一些基础概念,因为它跟我们要去比较的即时性是息息相关。
第一类概念,是对于连接持续性的描述。
长连接,通俗点讲就是连接在建立后是持续存在的,双端可以通过已存在的连接互发数据,只有当一端主动终止连接,连接才会被关闭。TCP 和 WebSocket 都属于长连接协议。(关于长连接的更多分享,点此了解)
短连接,是说当我需要与对端通信时建立连接,通信完毕后立即关闭连接。HTTP 就是一个短连接协议。发起请求的时候建立连接,收到响应之后连接就会关闭。当然,也可以利用 KeepAlive 去保持 TCP 连接复用,不过它还是不能保证连接不能被关闭。因为连接的持续有效,长连接的即时性是优于短连接的。因为它避免了数据发送时要建立连接的等待过程。
第二类概念,是对于字节流数据流向控制的定义。
全双工协议,是说字节流可以在连接中双向自由流动,因为这种自由流动,所以这类协议的即时性是最好的,也通常是长连接协议。只要缓冲区够大,就基本没有等待过程。TCP、WebSocket 都是属于全双工协议。
半双工协议,是说字节流可以在两个方向上流动,但同一时刻只能存在一个方向上的流动数据。半双工协议就像一条路上只有一个车道,对向有来车时,本方向的车就不能进车道,否则路就堵死了。HTTP 协议就是一个典型的半双工协议,它实际上是允许数据双向流动的,但是它的响应必须在请求数据接收完成之后,同一时刻不存在双向流动的字节流。半双工协议的即时性要低于全双工,因为它有对连接的使用等待过程,当有对向的数据流时,数据要延迟发送。
单工协议,就是数据只能单向流动,比如 HTTP 协议中的 SSE 功能。因为单工协议不能独立完成双向数据流动,不符合即时通讯的需求,所以我们就不考虑了。归类来看,WebScoekt 和 TCP 的即时性基本属于同一级别的,HTTP 则比他们要弱。
当然,HTTP 单独拎出来一个请求是不能和长连接协议比的,我们还要看一下通过 HTTP 协议的并发多连接请求能不能弥补它自身的不足。这里,我们再深入分析一下基于 HTTP 协议的长连接模拟方案。我们先想一下,基于 HTTP 协议构建的解决方案,要解决的核心问题是什么?
第一点,客户端发送数据时,等待连接建立造成的发送延迟。因为 HTTP 的短连接性质,所以在上行数据传输过程中,需要等 TCP 连接建立才能发送 HTTP 报文。针对这一点,就 HTTP 协议来说,目前是没有解决方案的,HTTP 的 KeepAlive 特性可以缓解,但不能彻底解决。
第二点,服务器数据无法主动推送到端造成的下行数据延迟。这也是 HTTP 的短连接特性造成的。当服务器有下行数据时,并没有一个持续的有效连接能够让它把数据推下去,所以只能等待客户端来主动建立连接,顺道把下行数据带下去。我们要介绍的方案就是围绕解决第二点展开的。市面上比较流行的前端基于 HTTP 协议构建的解决方案,主要有三种。
Comet 部分缓解了下行延迟问题。
HTTP + SSE 方案与 Comet 类似,只是把下行通道从 HTTP 请求变成了 SSE 实现。
SSE 的特性是长连接、单工协议,它的整体效果优于 Comet,因为没有额外的连接等待时间。它作为下行数据的通道,单工协议也完全符合要求。可以说 HTTP + SSE 的下行即时性基本是与 WebSocekt 等同的,基本解决了下行延迟问题。
Long-Pulling,定时向服务器去发送请求,以此来把下行数据带回来,基本属于常规操作,两个核心问题,基本一个也没有解决。总结而言,HTTP + SSE > Comet > Long-Pulling
这个结论有一个前提,是抛开了兼容性的。
SSE 方案虽好,但仅限于浏览器,如果我们想把 JS 代码复用到其他环境比如小程序,该方案就无法实现了。目前各小程序 Runtime 对于 SSE 的支持几乎是 0。
安全性
我们看一下 OSI 参考模型,着重看一下应用层和传输层之间的部分,这里是 SSL/TLS 所处的位置,也就是我们常说的 HTTPS 中的那个 S。
OSI 模型中定义中,会话层负责两端的会话维持、身份鉴别等,表示层负责对数据的加解密。在此之上,应用层协议的安全性是等同的,HTTPS 和 WSS 协议就是安全版的 HTTP 和 WebSocket 协议。
TCP 是比 SSL/TLS 更底层的协议,所以直接经由 TCP 发送的数据是可以有更多的安全选择的,TLS 只是备选项之一。使用 HTTPS 或 WSS 协议时,由 Runtime 提供 TLS/SSL 支持,开发者无需关注数据传输过程中的安全问题。使用 TCP 协议时,需要由开发者自行保证数据传输过程中的安全性(对接 TLS/SSL 或其他自定义安全方案)。
数据传输协议方案 & 前端持久化存储
数据传输协议方案
我们通过信息密度、拓展性、安全性、多端一致、兼容性五个指标来做对比数据传输协议。除了我们并不推荐的自研方案,常用的数据传输协议有两种:Protocol Buffer(PB),TLV 格式二进制数据;JSON,纯文本键值对数据。
五个指标对比来看,信息密度指 A 向 B 传达信息需要消耗的流量,密度越高,消耗得越少,PB 的信息密度比 JSON 高。
传输结构上,PB 是一个 TLV 格式二进制数据序列,JSON 是纯字符串系列键值对,JSON 描述数据结构要远大于 PB 描述数据结构。
安全性与可读性相反,我们要读一个二进制的 PB 数据,需要知道它序列化过程中的 PB 数据定义的结构。 我们常说的 PB 文件定义的可读性文件,这是前后端的一个约定,基于这个文件我们才能去序列化和反序列化这个二进制数据。而 JSON 是纯字符串,可读性良好,可以比较直观去了解数据里的信息。
兼容性方面,JSON 有很多原生语言库可选,PB 的兼容性在前端来看也就是 JS 对 ArrayBuffer 的支持,目前也都是支持的。
拓展性上,双方是等同的,PB 有一个优点,因为他传输的数据不包含键信息,所以两端的键信息可以不同。JSON 的传输信息包含键信息,意味着键信息是不可以随意变更的。
多端一致与兼容性一致,即时通讯场景涉及很多平台,多端就要去对数据做多端传输,数据的序列化和反序列化的实现的过程要保持一致,PB 跟 JSON 这方面都比较好,JSON 是原生支持的,PB 可以使用 Google 提供的相应三方库。总之,PB 比 JSON 更优。
前端持久化存储对比
最后是前端的可持久化存储技术:LocalStorage、IndexDB 和 Sqlite。持久化存储本身可选方案不多,需要考虑容量、兼容性、数据一致性等方面。
- LocalStorage,最常用的方案,容量比较低
- IndexDB,浏览器器上唯一可用的持久化大容量存储方案
- Sqlite - Electron Only,常用前端数据库
- Sqlite - WebAssembly,研发成本比较高
除了 LocalStorage,其他三种容量相当。
就数据一致性而言,除了 Electron 主进程内使用 Sqlite,其他三种方案都不太好处理数据竞争问题,很难保证数据一致性。在 Electron 平台下,把数据库操作放到主进程去完成,当渲染进程需要操控本地数据库时,依赖 IPC 去跟主进程通信,由主进程去处理真实的数据库事务。主进程是唯一一个数据库的访问点,因此可以妥善完成对数据竞争和数据一致性的保障。
融云的落地实践分享
经过三大项的方案对比,融云在落地的时候按照前面所分享的原则,在可用的方案下尽可能选择最优解,比如,Electron 平台上最优解就是 TCP+Sqlite ,数据封装用 PB。
Web 平台上没有 TCP+Sqlite 可用,就用 WebSocket 作为解决方案,把 HTTP 作为次优解做相应的技术落地。
小程序上,PB、WebSocket 都用不了,则采用 HTTP 方案备选,选型用的是 Comet。
这个降级过程对于集成的开发者来说是无感的,但在业务落地过程中是需要关注的,比如消息查询,有没有数据库就是两种应用体验。