本文编写时引用了“聊聊IM系统的即时性和可靠性”一文的部分内容和图片,感谢原作者。

1、引言

上一篇《零基础IM开发入门(二):什么是IM系统的实时性?》讲到了IM系统的“立足”之本——“实时性”这个技术特征,本篇主要讲解IM系统中的“可靠性”这个话题,内容尽量做到只讲原理不深入展开,避开深层次的技术性探讨,确保通俗易懂。


阅读对象:本系列文章主要阅读对象为零IM基础的开发者或产品经理,目标是告诉你“IM系统是什么?”,尽量不深入探讨具体的技术实现,确保通俗易懂,老少皆宜。

如您想从技术维度系统学习IM技术并着手自已的IM开发(即解决“IM系统要怎么做?”这个疑问),请从此文开始:《新手入门一篇就够:从零开发移动端IM》。

学习交流:

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

2、系列文章

零基础IM开发入门(一):什么是IM系统?
零基础IM开发入门(二):什么是IM系统的实时性?
零基础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系统的即时性和可靠性

[6] 学习笔记4——IM系统如何保证消息的可靠性

[7] IM群聊消息如此复杂,如何保证不丢不重?

附录:更多IM开发热门技术点

移动端IM开发者必读(一):通俗易懂,理解移动网络的“弱”和“慢”
移动端IM开发者必读(二):史上最全移动弱网络优化方法总结
现代移动端网络短连接的优化手段总结:请求速度、弱网适应、安全保障
移动端IM中大规模群消息的推送如何保证效率、实时性?
移动端IM开发需要面对的技术问题
开发IM是自己设计协议用字节流好还是字符流好?
请问有人知道语音留言聊天的主流实现方式吗?
如何保证IM实时消息的“时序性”与“一致性”?
一个低成本确保IM消息时序的方法探讨
IM单聊和群聊中的在线状态同步应该用“推”还是“拉”?
IM群聊消息如此复杂,如何保证不丢不重?
谈谈移动端 IM 开发中登录请求的优化
移动端IM登录时拉取数据如何作到省流量?
浅谈移动端IM的多点登录和消息漫游原理
完全自已开发的IM该如何设计“失败重试”机制?
微信对网络影响的技术试验及分析(论文全文)
IM开发基础知识补课(五):通俗易懂,正确理解并用好MQ消息队列
微信技术分享:微信的海量IM聊天消息序列号生成实践(算法原理篇)
IM开发基础知识补课(六):数据库用NoSQL还是SQL?读这篇就够了!
IM里“附近的人”功能实现原理是什么?如何高效率地实现它?
IM的扫码登录功能如何实现?一文搞懂主流应用的扫码登录技术原理
IM消息ID技术专题(一):微信的海量IM聊天消息序列号生成实践(算法原理篇)
IM消息ID技术专题(二):微信的海量IM聊天消息序列号生成实践(容灾方案篇)
IM消息ID技术专题(三):解密融云IM产品的聊天消息ID生成策略
IM消息ID技术专题(四):深度解密美团的分布式ID生成算法
IM消息ID技术专题(五):开源分布式ID生成器UidGenerator的技术实现
IM消息ID技术专题(六):深度解密滴滴的高性能ID生成器(Tinyid)
IM开发宝典:史上最全,微信各种功能参数和逻辑规则资料汇总

本文已同步发布于“即时通讯技术圈”公众号。

▲ 本文在公众号上的链接是:点此进入,原文链接是:http://www.52im.net/thread-3182-1-1.html


JackJiang
1.6k 声望808 粉丝

专注即时通讯(IM/推送)技术学习和研究。