AlexiaChen / AlexiaChen.github.io

My Blog https://github.com/AlexiaChen/AlexiaChen.github.io/issues
88 stars 11 forks source link

即时通信消息的可靠性保证---在线篇 #70

Closed AlexiaChen closed 1 year ago

AlexiaChen commented 4 years ago

title: 即时通信消息的可靠性保证---在线篇 date: 2017-10-29 19:26:00 tags:

前言

IM应用发展至今,基于都需要实现一种叫Qos的机制,主要目的就是保证消息的可靠性,简而言之就是消息不会丢失,必然会到达对方目标地址上。这篇文章的正文会整理讲解其实现Qos的思路,所以在此之前,我们可以从问题本身出发,网络本身是不可靠的,那么我们应该怎样采用在应用层的手段来保证消息的可靠,不丢失,不重复呢?那么很简单,可以商量一个协议,就是发送给对方消息的一段时间内(超时)如果收到对方回复“我收到你的消息啦”那么就说明消息到达,如果在一定时间内没有收到,那么就认为消息没有被对方收到,就需要重发消息,这个就是ACK机制。一般的MQ等中间件就是类似这样的思路实现的Qos。

可靠性的概念

消息的可靠性,简而言之就是消息的不丢失(ACK机制)和不重复(去重),是IM类应用的一个重点和难点。

消息报文的类型

IM的Client端与Server端通过发送报文来完成消息的传递。

为了简化问题,暂时把报文分为三种,实际情况不会那么简单:

Request报文是Client端主动向Server端发出的报文。

Ack报文是Server端被动应答Client端的报文,一个Request肯定有一个与之对应的Ack,http协议就是这样一种类似的协议。

Notify报文是Server端主动发送给客户端的报文。

最简单的普通消息投递的流程

  1. Client-A向IM-Server发送一个Reuqest消息。

  2. IM-Server在成功处理了Request消息之后,回复给Client-A一个Ack消息

  3. 如果此刻Client-B在线(这个状态由Server端来判断),则IM-Server就主动向Client-B发送一个Notify的消息,如果Client-B不在线,则消息内容会离线存储在IM-Server上。

看了上面的流程的之后,这样的一个流程有没有BUG呢? 答案是,有!

比如IM-Server发送Notify消息给Client-B的过程中,消息丢失了怎么办?(由于网络抖动等复杂原因) IM-Server是不知道Client-B收到没收到消息的,所以Client-B需要有一个Ack包来告诉IM-Server说明Notify消息已经收到。

还有概率更小的事情,IM-Server崩溃了,导致Notify消息没有发送给Client-B。又或者Client-B崩溃了,Notify消息直接未接收到,这时候Client-B连Ack消息都不可能回发给IM-Server。

最后导致一个严重的问题就是,接收方Client-B是否能收到Notify消息,发送方Client-A完全不可控,那怎么办呢?

应用层确认和IM消息可靠投递的六个报文

在解决问题之前,先回顾下网络传输层的协议基本概念: UDP是一种不可靠的传输层协议,会乱序,会丢包。TCP是一种可靠的传输层协议,那么TCP的可靠性是怎么做到的呢? 答案是:超时,重传,确认。

在实际的IM应用中,无论在传输层采用是UDP还是TCP协议,都需要在应用层保证Qos机制,原因在于IM的通信是两个Client端与Server端的通信,而非传统的C/S或B/S只有两方参与的通信。所以即使底层采用TCP这样的可靠协议,也需要应用层来保证消息的可靠,因为TCP的消息可靠只保证双方参与的通信

想要实现应用层的消息可靠性,必须加入应用层的确认机制。所以想要之前那样的场景,想要让发送方Client-A确保接收方Client-B一定收到了消息,必须让接收方Client-B给一个消息确认。

这样应用层的通信流程得基于之前的流程增加3个步骤:

  1. Client-B在接收到IM-Server的Notify消息后,一定要向Server回复一个ACK消息

  2. IM-Server在成功处理了Client-B回复的Ack消息后,又会回复一个Ack给Client-B

  3. 然后IM-Server再主动向Client-A发送一个Ack消息,表示Client-B接收到了消息

至此,Client-A才知道自己最初给Client-B的消息,Client-B已经收到,可以放心了。这6个步骤有严格的顺序,必须一步一步的发起,以下是六个步骤的整理,每个步骤对应一种属于它自己的类型的报文:

  1. Client-A向IM-Server发送一个Reuqest消息。(Message:Request)

  2. IM-Server在成功处理了Request消息之后,回复给Client-A一个Ack消息 (Message: Ack)

  3. 如果此刻Client-B在线(这个状态由Server端来判断),则IM-Server就主动向Client-B发送一个Notify的消息,如果Client-B不在线,则消息内容会离线存储在IM-Server上。(Message: Notify)

  4. Client-B在接收到IM-Server的Notify消息后,一定要向Server回复一个ACK消息 (Ack: Request)

  5. IM-Server在成功处理了Client-B回复的Ack消息后,又会回复一个Ack给Client-B (Ack : Ack)

  6. 然后IM-Server再主动向Client-A发送一个Ack消息,表示Client-B接收到了消息 (Ack: notify)

以上是最核心关键的6种类型的消息报文,如果某个IM应用系统没有实现类似的6种类型的报文和步骤,那么是不可能保证消息可靠的。当然,实际IM应用肯定是还有其他基于这6种报文的扩展,或者增添其他类型的消息报文。

上述六个步骤存在的问题

虽然以上的六种类似的报文是保证消息可靠性的基础,但是由于网络的复杂性,还是会出现问题:

消息的超时和重传

Client-A发出了Message:Reuqest并之后收到了Message:Ack之后,在一个期待的时间内,如果没有收到IM-Server发过来的Ack:Notify,那么Client-A会尝试将Message:Request重发。由于即时聊天是不停地在交流打字或者语音。所以可能Client-A会“同时”发出了很多Message:Reuqest,所以需要Client-A在本地内存中维护一个等待Ack:Notify的队列,并配合Timer超时机制,来记录哪些Message:Request没有收到对应的Ack:Notify,以定时重发。

一旦Client-A收到了Ack:Notify,说明Client-B收到了,就将对应的Message:Request从“等待Ack:Notify的队列”中移除。

注意, 消息重传一定需要Client端来实现,以保证Server端的无状态。不然Server端还需要记录一些收到或者没收到等状态信息在数据库中,这样增大复杂度,也很没必要。

消息的去重

方法很简单,由发送方Client-A生成一个消息去重的Message ID,这个ID可以是MD5,SHA1等Hash算法,为了优化可以Hash算法生成的字符串的后5位或者前5位来表示Message ID。

这个Message ID保存在“等待Ack:Notify队列”里面,同一条消息使用相同的Message ID来重传,Client-B依据这些ID来去重,而不影响用户体验。

其他注意的地方