零基础 IM 开发入门 (三):什么是 IM 系统的可靠性?
本文编写时引用了“聊聊IM系统的即时性和可靠性”一文的部分内容和图片,感谢原作者。
1、引言
上一篇《零基础IM开发入门(二):什么是IM系统的实时性?》讲到了IM系统的“立足”之本——“实时性”这个技术特征,本篇主要讲解IM系统中的“可靠性”这个话题,内容尽量做到只讲原理不深入展开,避开深层次的技术性探讨,确保通俗易懂。
学习交流:开源IM框架源码 https://github.com/JackJiang2011/MobileIMSDK
2、系列文章
《零基础IM开发入门(三):什么是IM系统的可靠性?》(* 本文)
《零基础IM开发入门(四):什么是IM系统的消息时序一致性?》
《零基础IM开发入门(五):什么是IM系统的安全性? (稍后发布)》
《零基础IM开发入门(六):什么是IM系统的的心跳机制? (稍后发布)》
《零基础IM开发入门(七):如何理解并实现IM系统消息未读数? (稍后发布)》
《零基础IM开发入门(八):如何理解并实现IM系统的多端消息漫游? (稍后发布)》
3、正文概述
一般来说,IM系统的消息“可靠性”,通常就是指聊天消息投递的可靠性(准确的说,这个“消息”是广义的,因为还存用户看不见的各种指令,为了通俗,统称“消息”)。
从用户行为来讲,消息“可靠性”应该分为两种类型:
1)在线消息的可靠性:即发送消息时,接收方当前处于“在线”状态;
2)离线消息的可靠性:即发送消息时,接收方当前处于“离线”状态。
从具体的技术表现来讲,消息“可靠性”包含两层含义:
1)消息不丢:这很直白,发出去的消息不能像进了黑洞一样,一脸懵逼可不行;
2)消息不重:这是丢消息的反面,消息重复了也不能容忍。
对于“消息不丢”这个特征来说,细化下来,它又包含两重含义:
1)已明确被对方收到;
2)已明确未被对方收到。
是的,对于第1)重含义好理解,第2)重含义的意思是:当对方没有成功收到时,你的im系统也必须要感知到,否则,它同样属于被“丢”范畴。
总之,一个成型的im系统,必须包含这两种消息“可靠性”逻辑,才能堪用,缺一不可。
消息的可靠性(不丢失、不重复)无疑是IM系统的重要指标,也是IM系统实现中的难点之一。本文以下文字,将从在线消息的可靠性和离线消息的可靠性进行讨论。
4、典型的在线消息收发流程
先看下面这张典型的im消息收发流程:
是的,这是一个典型的服务端中转型IM架构。
所谓“服务端中转型IM架构”是指:一条消息从客户端A发出后,需要先经过 IM 服务器来进行中转,然后再由 IM 服务器推送给客户端B,这种模式也是目前最常见的 IM 系统的消息分发架构。
你可能会说,IM不可以是P2P模式的吗?是的,目前来说主流IM基本都是服务器中转这种方式,P2P模式在IM系统中用的很少。
原因是以下两个很明显的弊端:
1)P2P模式下,IM运营者很容易被用户架空(无法监管到用户行为,用户涉黄了怕不怕?);
2)P2P模式下,群聊这种业务形态,很难实现(我要在千人群中发消息给,不可能我自已来分发1000次吧)。
话题有点跑偏,我们回到正题:在上面这张图里,客户A发送消息到服务端、服务端中转消息给客户B,假设这两条数据链接中使用的通信协议是TCP,你认为在TCP所谓可靠传输协议加持下,真的能保证IM聊天消息的可靠性吗?
答案是否定的。我们继续看下节。
5、TCP并不能保证在线消息的“可靠性”
接上节,在一个典型的服务端中转型IM架构中,即使使用“可靠的传输协议”TCP,也不能保证聊天消息的可靠性。为什么这么说?
要回答这个问题,网上的很多文章,都会从服务端的角度举例:比如消息发送时操作系统崩溃、网络闪断、存储故障等等,总之很抽象,不太容易理解。
这次我们从客户端角度来理解,为什么使用了可靠传输协议TCP的情况下IM聊天消息仍然不可靠的问题。
具体来说:如何确保 IM 消息的可靠性是个相对复杂的话题,从客户端发送数据到服务器,再从服务器送达目标客户端,最终在 UI 成功展示,其间涉及的环节很多,这里只取其中一环「接收端如何确保消息不丢失」来探讨,粗略聊下我接触过的两种设计思路。
说到可靠送达:第一反应会联想到 TCP 的可靠性。数据的可靠送达是个通用性的问题,无论是网络二进制流数据,还是上层的业务数据,都有可靠性保障问题,TCP 作为网络基础设施协议,其可靠性设计的可靠性是毋庸置疑的,我们就从 TCP 的可靠性说起。
在 TCP 这一层:所有 Sender 发送的数据,每一个 byte 都有标号(Sequence Number),每个 byte 在抵达接收端之后都会被接收端返回一个确认信息(Ack Number), 二者关系为 Ack = Seq + 1。简单来说,如果 Sender 发送一个 Seq = 1,长度为 100 bytes 的包,那么 receiver 会返回一个 Ack = 101 的包,如果 Sender 收到了这个Ack 包,说明数据确实被 Receiver 收到了,否则 Sender 会采取某种策略重发上面的包。
第一个问题是:既然 TCP 本身是具备可靠性的,为什么还会出现消息接收端(Receiver)丢失消息的情况?
看下图一目了然:
(▲ 上图引用自《从客户端的角度来谈谈移动端IM的消息可靠性和送达机制》)
一句话总结上图的含义:网络层的可靠性不等同于业务层的可靠性。
数据可靠抵达网络层之后,还需要一层层往上移交处理,可能的处理有:
1)安全性校验;
2)binary 解析;
3)model 创建;
4)写 db;
5)存入 cache;
6)UI 展示;
7)以及一些边界问题:比如断网、用户突然退出登陆、磁盘已满、内存溢出、app奔溃、突然关机等等。
项目的功能特性越多,网络层往上的处理出错的可能性就越大。
举个最简单的场景为例子:消息可靠抵达网络层之后,写 db 之前 IM APP 崩溃(不稀奇,是 App 都有崩溃的可能),虽然数据在网络层可靠抵达了,但没存进 db,下次用户打开 App 消息自然就丢失了,如果不在业务层再增加可靠性保障(比如:后面要提到的网络层面的消息重发保障),那么意味着这条消息对于接收端来说就永远丢失了,也就自然不存在“可靠性”了。
从客户端角度理解IM的可能性以及解决办法,可以详细阅读:《从客户端的角度来谈谈移动端IM的消息可靠性和送达机制》,本节引用的是该文中“4、TCP协议的可靠性之外还会出现消息丢失?”一节的文字。
6、为在线消息增加“可靠性”保障
那么怎样在应用层增加可靠性保障呢?
有一个现成的机制可供我们借鉴:TCP协议的超时、重传、确认机制。
具体来说就是:
1)在应用层构造一种ACK消息,当接收方正确处理完消息后,向发送方发送ACK;
2)假如发送方在超时时间内没有收到ACK,则认为消息发送失败,需要进行重传或其他处理。
增加了确认机制的消息收发过程如下:
我们可以把整个过程分为两个阶段。
阶段1: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,此时可以提示用户发送失败,手动进行重发。
阶段2: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为止。
关于IM聊天消息的可靠性保障问的深入讨论,可以详读:《IM消息送达保证机制实现(一):保证在线实时消息的可靠投递》,该文会深入讨论这个话题。
7、典型的离线消息收发流程
说完在线消息的“可靠性”问题,我们该了解一下离线消息了。
7.1 离线消息的收发也存在“不可靠性”
下图是一张典型的IM离线消息流程图:
如上图所示,和在线消息收发流程类似。
离线消息收发流程也可划分为两个阶段:
阶段1:clientA -> server
1-1:clientA向server发送消息(msg-Req) ;
1-2:server发现clientB离线,将消息存入offline-DB。
阶段2:server -> clientB
2-1:clientB上线后向server拉取离线消息(pull-Req) ;
2-2:server从offline-DB检索相应的离线消息推送给clientB(pull-res),并从offline-DB中删除。
显然:离线消息收发过程同样存在消息丢失的可能性。
举例来说:假设pull-res没有成功送达clientB,而offline-DB中已删除,这部分离线消息就彻底丢失了。
7.2 离线消息的“可靠性”保障
与在线消息收发流程类似,我们同样需要在应用层增加可靠性保障机制。
下图是增加了可靠性保障后的离线消息收发流程:
与初始的离线消息收发流程相比,上图增加了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。在某此后IM的实现中是将所有的离线消息按会话进行分组,每组回复一个ACK,假如某个ACK丢失,则只需要重传该会话的所有离线消息。
有关离线消息的可靠性保障机制的详细讨论,可以详读:《IM消息送达保证机制实现(二):保证离线消息的可靠投递》、《IM开发干货分享:如何优雅的实现大量离线消息的可靠投递》,这两篇文章可以给你更深入具体的答案。
8、聊天消息重复的问题
上面章节中,通过在应用层加入重传、确认机制后,我们确实是杜绝了消息丢失的可能性。
但由于重试机制的存在,我们会遇到一个新的问题:那就是同一条消息可能被重复发送。
举一个最简单的例子:假设client成功收到了server推送的消息,但其后续发送的ACK丢失了,那么server将会在超时后再次推送该消息,如果业务层不对重复消息进行处理,那么用户就会看到两条完全一样的消息。
消息去重的方式其实非常简单,一般是根据消息的唯一标志(id)进行过滤。
具体过程在服务端和客户端可能有所不同:
1)客户端 :我们可以通过构造一个map来维护已接收消息的id,当收到id重复的消息时直接丢弃;
2)服务端 :收到消息时根据id去数据库查询,若库中已存在则不进行处理,但仍然需要向客户端回复Ack(因为这条消息很可能来自用户的手动重发)。
关于消息的去重问题,在一对一聊天的情况下,逻辑并不复杂,但在群聊模式下,会将问题复杂化,有关群聊消息不丢和去重的详细讨论,可以深入阅读:《IM群聊消息如此复杂,如何保证不丢不重?》。
9、本文小结
保证消息的可靠性是IM系统设计中很重要的一环,能不能做到“消息不丢”、“消息不重”,对用户的体验影响极大。
所谓“可靠的传输协议”TCP也并不能保障消息在应用层的可靠性。
我们一般通过在应用层的ACK应答和重传机制,来实现IM消息的可靠性保障。但由此带来的消息重复问题,需要我们额外进行处理,最简单的方法就是通过消息ID进行幂等去重。
关于IM系统消息可靠性的理论基础,我们就探讨到这里,有疑问的读者,可以在本文末尾留意,欢迎积极讨论。
10、参考资料
[1] IM消息送达保证机制实现(一):保证在线实时消息的可靠投递
[2] IM消息送达保证机制实现(二):保证离线消息的可靠投递
[3] IM开发干货分享:如何优雅的实现大量离线消息的可靠投递
[4] 从客户端的角度来谈谈移动端IM的消息可靠性和送达机制
[5] 聊聊IM系统的即时性和可靠性
(本文同步发布于:http://www.52im.net/thread-3182-1-1.html)
评论