因为工作的原因,对IM相关的技术接触比较多,这篇文章来聊一聊IM系统最重要的两个特性:即时性和可靠性。

即时性

即时性,通俗来讲就是指低延迟。在双方通信过程中,一方发送的消息,另一方需要在短时间内立即收到。IM系统的即时性,主要是靠长连接和断线重连机制来保证。
(还有一种实时性的提法,一般是针对语音、视频通话、直播等场景,这些场景对低时延的要求更高,但允许少量的信息丢失,因此通常基于UDP协议)

websocket长连接

对于web客户端来说,目前与服务器通信的方式主要有http request/SSE/websocket三种。

  1. http协议是web端最常用的通信协议,可能和很多人的理解不同,http连接并不一定是“短连接”,实际上在1.1版本后已经实现了对长连接的支持。在http1.0协议中,每次请求都会建立一个新的TCP连接,响应完成后立即关闭。具备一些网络知识的同学都知道,TCP连接的建立和关闭是很耗资源的(还记得三次握手和四次挥手吗);因此http协议在1.1版本对此进行了改进,增加了连接复用的keep-alive协议,在一个TCP连接中可以发送多个Request,接收多个Response。
    但是,虽然http1.1可以在一个连接中完成多个http请求,但由于协议自身的无状态性,每个请求均要携带完整的http header,造成信息交换的浪费;同时,http协议的通信方式只能是客户端发送request->服务器返回response,服务器无法主动向客户端推送信息,如果客户端想要实时获取信息,只能通过轮询的方式。

  2. SSE,全称Server-Sent Events,用于服务器向客户端推送消息。SSE基于http协议,连接建立后不会断开,客户端会一直等待接收服务器发送过来的数据流。SSE的通信是单向的,只能由服务器向客户端发送;如果浏览器向服务器发送信息,就变成了另一次请求。

  3. websocket是html5提出的一个新的通信协议,用于在客户端和服务器之间建立一个全双工的持续连接。websocket基于TCP,因此能够实现可靠通信;出于兼容性的考虑,websocket借用http协议来完成连接建立的握手阶段,连接建立后双方的信息通过数据帧传输,不再需要复杂的header。

基于上面的分析,websocket无疑是最符合IM系统即时性要求的通信方式,它能够提供低时延、高性能、全双工的“真·长连接”。

知识补充:websocket是如何建立连接的

  1. 客户端发送一个http握手请求
GET /chat HTTP/1.1
GET wss://ws.qiyukf.com/socket.io/?events=refresh&code=ysftest4&EIO=3&transport=websocket HTTP/1.1
Host: ws.qiyukf.com
Connection: Upgrade
Upgrade: websocket
Origin: http://ysftest4.qiyukf.com
Sec-WebSocket-Version: 13
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36
Sec-WebSocket-Key: gtWgniZtQRUVLSVVJiAQ3g==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

Connection: Upgrade Upgrade: websocket 字段告诉服务器当前发起的是Websocket协议

  1. 服务器发现自己支持websocket服务,则建立websocket连接并返回响应报文
HTTP/1.1 101 Switching Protocols
Server: nginx
Connection: upgrade
upgrade: websocket
sec-websocket-accept: ddUBPRBVf4vZ+goyOJpfUhss3Fk=
sec-websocket-extensions: permessage-deflate

101 Switching Protocols告诉客户端已成功切换协议,建立了websocket连接

  1. 客户端升级到websocket协议,后续的数据交换基于websocket协议

基于心跳机制的断线重连

名词解析:心跳
心跳一般是指某端每隔一定时间向对端发送自定义指令,以判断双方是否存活,因其按照一定间隔发送,类似于心跳,故被称为心跳指令。

断线重连很好理解,出于即时性的考虑,一旦连接断开需要尽快重连,以恢复正常的消息收发。而对连接是否断开的判断,则需要靠心跳机制来完成。上一节我们提到,websocket是基于TCP的,那么难道不能依靠TCP协议自身来判断连接状态,一定要在应用层加一个额外的心跳机制吗?为了解答这个疑问,我们可以先做几个实验。

首先实现一个简单的websocket应用:
server.js

// 基于ws库
var WebSocketServer = require('ws').Server,wss = new WebSocketServer({ port: 4000 });wss.on('connection', function (ws) {console.log('client connected');ws.on('message', function (message) {console.log(message);});ws.on('close', function () {console.log('disconnect');});
});

client.html

<script>ws = new WebSocket("ws://localhost:4000");ws.onopen = function (evt) {console.log("Connection open ...");};ws.onclose = function (evt) {alert("Connection close ...");};</script>

在浏览器打开client页面,建立连接后,进行以下几个实验:
1. 浏览器关闭页面
结果:连接断掉,且服务端能够感知
2. 客户端断网
结果:客户端本身会收到close事件,但服务器无感知
3. 服务器终止socket服务/kill掉进程
结果:连接断掉,客户端能够感知
4. 链路中间的网络设备出现问题
连接实际上断开了,但客户端和服务器均无感知

2和4的结果说明,TCP连接的断开有时是无法探知的(起码是无法瞬时探知)。TCP连接实际上是一个虚拟的连接,只要通信双方完成了三次握手,连接就成功建立了;除非其中一方发出断开连接的报文,否则这个状态不会改变。这条通路上真实网络情况的变化,并不会被TCP感知。一个最简单的例子,假设我们通过ssh登录了远程主机,一不小心踢掉了网线,重新插上之后会发现连接仍然是可用的。

知识复习:TCP的三次握手

那么,TCP协议完全没有探测连接状态的能力吗?并不是,TCP提供了KeepAlive机制,开启状态下一旦连接长时间空闲,TCP协议会自动发送一个KeepAlive探针来检测连接是否断开。那么是否可以使用KeepAlive机制来替代应用层的心跳机制呢?答案是否定的。

一方面TCP KeepAlive的超时时间非常长,另一方面,KeepAlive是用于检测连接的死活,而心跳机制则附带一个额外的功能:检测通讯双方的存活状态。两者听起来似乎是一个意思,但实际上却大相径庭。考虑一种情况,某台服务器因为某些原因导致负载超高,CPU 100%,无法响应任何业务请求,但是使用TCP探针则仍旧能够确定连接状态,这就是典型的连接活着但业务提供方已死的状态,对客户端而言,这时的最好选择就是断线后重新连接其他服务器,而不是一直认为当前服务器是可用状态,一直向当前服务器发送些必然会失败的请求。同样的,假如客户端因为某些原因崩溃了,服务器也需要停止向客户端推送消息,并回收连接和资源。举一个现实生活中的例子,假设你和朋友小明打电话,结果打到一半小明睡着了,这时候通讯连接仍然是通畅的,但为了你的话费着想,你最好主动挂掉这通电话。

此外,心跳机制还有一个作用,那就是防止空闲的连接被网络运营商回收。因为IPv4的地址数量有限,运营商分配给手机终端的IP是运营商内网的IP ,手机要连接Internet就需要通过运营商的网关做一个网络地址转换(Network Address Translation,简称:NAT)。因此,大部分移动无线网络运营商都在链路一段时间没有数据通讯时,会淘汰 NAT 表中的对应项,造成链路中断。心跳机制能够有效避免这种情况。

总结:为什么基于TCP的websocket长连接仍然需要心跳机制
1. TCP连接的断开有时是无法瞬时探知的,因此不适合实时性高的场合
2. TCP协议的KeepAlive机制只能检测连接存活,而不能检测连接可用
3. 心跳机制能够避免连接被网络运营商回收

云信sdk相关代码

/** 重新建立连接*   - 如果还有可用地址, 那么尝试下一个地址;*   - 如果没有可用地址, 但是lbs重试次数还没达到上限, 那么重试lbs;* - 如果既没有可用地址, 重试次数也达到上限, 那么不再重新连接。*/
ProtocolFn.reconnect = function() {// debugger;var self = this;if (self.willReconnect()) {// 重置 socket, 这样才能换地址self.socket = null;self.retryCount++;// 指数退避算法计算delay时间var duration = self.backoff.duration();...setTimeout(function() {self.connect();}, duration);} else {self.notifyDisconnect();}
};ProtocolFn.connect = function() {var self = this;// 防止重复建立连接if (self.isConnected() || self.connecting) { return; }self.connecting = true;if (!self.socket) {// 如果有 url, 那么连此 url, 否则刷新 urlvar url = self.getNextSocketUrl();if (!!url) {self.connectToUrl(url);} else {self.refreshSocketUrl();}} else {self.socket.socket.connect();}
};ProtocolFn.getNextSocketUrl = function() {return this.socketUrls.shift();
};

可靠性

IM系统的可靠性,通常就是指消息投递的可靠性,即我们经常听到的“消息必达”。消息的可靠性(不丢失、不重复)无疑是IM系统的重要指标,也是IM系统实现中的难点之一。

在线消息收发流程

我们应该听过这样一个结论:TCP是一种可靠的传输层协议。那么上图的收发过程能够保证消息必达吗?答案是不能,因为网络层的可靠性不等于应用层的可靠性。上图中,clientA-server、server-clientB两条信道是可靠的,但clientA、server、clientB本身却无法保证可靠性。

如图,数据可靠抵达网络层之后,还需要一层层往上移交处理,在整个过程中,各种极端情况都可能出现(断网,用户 logout,disk full,OOM,crash,关机。。)等等,网络层以上的处理步骤越多,出错的可能性就越大。拿clientB举例,假设消息成功到达了网络层,但在应用程序消费之前,客户端崩溃了;这种情况下,网络层不会进行重发,如果不在应用层增加可靠性保障,这条消息对于clientB来说就丢失了。

那么怎样在应用层增加可靠性保障呢?有一个现成的机制可供我们借鉴:TCP协议的超时、重传、确认机制。具体来说,就是在应用层构造一种ACK消息,当接收方正确处理完消息后,向发送方发送ACK;假如发送方在超时时间内没有收到ACK,则认为消息发送失败,需要进行重传或其他处理。增加了确认机制的消息收发过程如下:

我们可以把整个过程分为两个阶段:

clientA->server

1-1、clientA向server发送消息(msg-Req);
1-2、server收取消息,回复ACK(msg-Ack)给clientA;
1-3、一旦clientA收到ACK即可认为消息已成功投递,第一阶段结束。

无论msg-A或ack-A丢失,clientA均无法在超时时间内收到ACK,此时可以提示用户发送失败,手动进行重发。

server->clientB

2-1、server向clientB发送消息(Notify-Req);
2-2、clientB收取消息,回复ACK(Notify-ACk)给server;
2-3、server收到ACK之后将该消息标记为已发送,第二阶段结束。

无论msg-B或ack-B丢失,server均无法在超时时间内收到ACK,此时需要重发msg-B,直到clientB返回ACK为止。

云信sdk相关代码

// 调用 socket 发送命令
ProtocolFn.doSendCmd = function(cmd) {var self = this;var ser = cmd.SER;// 存储超时self.timerMap[ser] = setTimeout(function() {self.markCallbackInvalid(ser, NIMError.newTimeoutError());}, config.cmdTimeout);// 发送命令self.socket.send(JSON.stringify(cmd));
};/*** 接收到服务端的消息** @private* @param {String} data 包数据* @return {Void}*/
ProtocolFn.onMessage = function(data){var self = this;self.heartbeat()var packet = self.parser.parseResponse(data);...self.callPacketAckCallback(packet);
};// 调用回包确认回调函数
ProtocolFn.callPacketAckCallback = function(packet) {var self = this;if (!!packet && !!packet.raw) {var ser = packet.raw.ser;if (!!ser) {self.clearTimerWithSer(ser);var callback = self.getCallbackWithSer(ser);...callback(packet.error, packet.obj);}}
};

离线消息收发流程

和在线消息收发流程类似,离线消息收发流程也可划分为两个阶段:

clientA->server

1-1、clientA向server发送消息(msg-Req)
1-2、server发现clientB离线,将消息存入offline-DB

server->clientB

2-1、clientB上线后向server拉取离线消息(pull-Req)
2-2、server从offline-DB检索相应的离线消息推送给clientB(pull-res),并从offline-DB中删除

显然,这个过程同样存在消息丢失的可能性。举例来说,假设pull-res没有成功送达clientB,而offline-DB中已删除,这部分离线消息就彻底丢失了。与在线消息收发流程类似,我们同样需要在应用层增加可靠性保障机制。

与初始的离线消息收发流程相比,上图增加了1-3、2-4、2-5步骤:

1-3、server将消息存入offline-DB后,回复ACK(msg-Ack)给clientA,clientA收到ACK即可认为消息投递成功;
2-4、clientB收到推送的离线消息,回复ACK(res-Ack)给server;
2-5、server收到res-ACk后确定离线消息已被clientB成功收取,此时才能从offline-DB中删除。

性能优化:
当离线消息的量较大时,如果对每条消息都回复ACK,无疑会大大增加客户端与服务器的通信次数。这种情况我们通常使用批量ACK的方式,对多条消息仅回复一个ACK。在云信的实现中是将所有的离线消息按会话进行分组,每组回复一个ACK;假如某个ACK丢失,则只需要重传该会话的所有离线消息。

消息去重

在应用层加入重传、确认机制后,我们完全杜绝了消息丢失的可能性;但由于重试机制的存在,我们会遇到一个新的问题,那就是同一条消息可能被重复发送。举一个最简单的例子,假设client成功收到了server推送的消息,但其后续发送的ACK丢失了,那么server将会在超时后再次推送该消息;如果业务层不对重复消息进行处理,那么用户就会看到两条完全一样的消息。
消息去重的方式其实非常简单,一般是根据消息的唯一标志(id)进行过滤。具体过程在服务端和客户端可能有所不同:

  1. 客户端
    我们可以通过构造一个map来维护已接收消息的id,当收到id重复的消息时直接丢弃;
  2. 服务端
    收到消息时根据id去数据库查询,若库中已存在则不进行处理,但仍然需要向客户端回复Ack(因为这条消息很可能来自用户的手动重发)。

聊聊IM系统的即时性和可靠性相关推荐

  1. 【余压监控系统】实时性、数字化、智能化,自动化,连续动态监控

    余压监控系统在某高层住宅的应用方案 安科瑞 崔远航 摘要 本文介绍了余压监控系统的基本架构和功能,结合某高层住宅建设实例分析了高层民用建筑中设置此系统的优点与必要性,总结了余压监控系统的功能用于高层建 ...

  2. QNX系统的实时性分析-实时性能测试标准

    锋影 e-mail:174176320@qq.com QNX的优势在于过了车规级ASIL-D, 符合ISO-26262的标准,分布式操作系统. 其在汽车领域和轨道交通领域大有应用,其系统兼容POSIX ...

  3. 在线客服系统IM即时通讯聊天源码

    源码简介 在线客服系统IM即时通讯聊天源码 现在腾讯出来个差不多的玩意,没啥用了拿出来给大伙乐呵乐呵无后门,无论你用什么检测工具随便检测,嘎嘎用就完了 学习资料源码:PHP客服系统全开源无限制版.zi ...

  4. 软件和系统可追溯性学习(一)

    ** Traceability Strategy(可追溯性策略) ** 1. Traceability Fundamentals(可追溯性基础) 1.1 Essential Traceability ...

  5. 从零开始学习VIO笔记 --- 第四讲:滑动窗口(基于滑动窗口算法的 VIO 系统:可观性和一致性)

    从零开始学习VIO笔记 --- 第四讲:滑动窗口(基于滑动窗口算法的 VIO 系统:可观性和一致性) 一. 从高斯分布到信息矩阵 1.1 高斯分布 1.2 高斯分布和协方差矩阵 1.3 信息矩阵 二. ...

  6. 完整性约束延迟性和即时性

    完整性约束 默认情况下,完整性约束检查在SQL语句执行完以后进行,为什么不在SQL语句执行期间检查呢? 因为在同一时间只有一条语句处理数据才是正常的. 下面演示一下完整性约束的延迟和即时性 [auto ...

  7. 在线考试系统的并发性介绍

    随着互联网技术的发展,在线考试的方式慢慢代替了传统纸质考试,在线考试的方式可以有效的提高我们组织考试的工作效率. 不过有的人会担心,在线考试的人数承载问题.现在的教育机构或者学校,学生人数是几千甚至是 ...

  8. 定期清理卡巴的report文件夹,否则严重影响系统的流畅性

    定期清理卡巴的report文件夹,否则严重影响系统的流畅性 提醒:请定期清理卡巴的report文件夹,否则严重影响系统的流畅性. 我说这几天打个游戏都觉得有点卡,我还以为是系统垃圾多了就用优化大师卸, ...

  9. 即时消息发送模块 java_五个模块,以促进模块化的即时性

    即时消息发送模块 java I designed my modular to be used with a computer. At times, it feels like I'm in the m ...

最新文章

  1. 反正我不信!马斯克谈元宇宙:没人愿意把屏幕贴脸上
  2. 面向对象——构造方法(重载)
  3. SAP Spartacus全局配置里和路由Route相关的配置
  4. osgi简介_OSGi:简介
  5. 胃癌2019csco指南_2019 CSCO胃癌诊疗指南精华来了!
  6. 学会这个BBC,你的图也可以上新闻啦!
  7. Atitit 企业常见100个职能 组织职能 社会职能 政府职能 家庭职能 团队职能
  8. 视频人像磨皮插件:Beauty Box 4.2
  9. HTML静态网页作业-网上花店4个页面(HTML+CSS+JS)
  10. 高效人士的七个管理习惯
  11. OSChina 周四乱弹 ——Iphone7出了开始做牛做马了
  12. 树莓派基础实验24:超声波测距传感器实验
  13. Silverlight 2.5D RPG游戏技巧与特效处理(Game Effects):目录
  14. 系统突然访问变慢,如何排查和解决?
  15. 【Laravel笔记】16. Cookie和Session
  16. Office2003/2007/2010/2013强力卸载工具下载
  17. MATLAB应用1——MATLAB傅里叶变换函数封装
  18. Android 常用技巧
  19. rpmbuild 介绍
  20. Jfree实现统计图

热门文章

  1. [原创] 树莓派使用多个联通4G上网卡
  2. 美国排名前10芯片公司的特点!
  3. 这些初中物理口诀,还不快快点赞收藏?
  4. [python]如何学习python的第三方库(wheel轮子)
  5. 有用的东东---信用卡利息计算过程
  6. 正则表达式-不包含某个字符串
  7. Library-Files-维持权限--对比自带CLSID打开程序
  8. wildfly 21的domain配置
  9. 【ABAP】创建局部Macro和全局Macro
  10. 什么是网络安全?网络安全的主要内容是什么?