Closed AlexiaChen closed 1 year 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端主动发送给客户端的报文。
Client-A向IM-Server发送一个Reuqest消息。
IM-Server在成功处理了Request消息之后,回复给Client-A一个Ack消息
如果此刻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完全不可控,那怎么办呢?
在解决问题之前,先回顾下网络传输层的协议基本概念: UDP是一种不可靠的传输层协议,会乱序,会丢包。TCP是一种可靠的传输层协议,那么TCP的可靠性是怎么做到的呢? 答案是:超时,重传,确认。
在实际的IM应用中,无论在传输层采用是UDP还是TCP协议,都需要在应用层保证Qos机制,原因在于IM的通信是两个Client端与Server端的通信,而非传统的C/S或B/S只有两方参与的通信。所以即使底层采用TCP这样的可靠协议,也需要应用层来保证消息的可靠,因为TCP的消息可靠只保证双方参与的通信。
想要实现应用层的消息可靠性,必须加入应用层的确认机制。所以想要之前那样的场景,想要让发送方Client-A确保接收方Client-B一定收到了消息,必须让接收方Client-B给一个消息确认。
这样应用层的通信流程得基于之前的流程增加3个步骤:
Client-B在接收到IM-Server的Notify消息后,一定要向Server回复一个ACK消息
IM-Server在成功处理了Client-B回复的Ack消息后,又会回复一个Ack给Client-B
然后IM-Server再主动向Client-A发送一个Ack消息,表示Client-B接收到了消息
至此,Client-A才知道自己最初给Client-B的消息,Client-B已经收到,可以放心了。这6个步骤有严格的顺序,必须一步一步的发起,以下是六个步骤的整理,每个步骤对应一种属于它自己的类型的报文:
Client-A向IM-Server发送一个Reuqest消息。(Message:Request)
IM-Server在成功处理了Request消息之后,回复给Client-A一个Ack消息 (Message: Ack)
如果此刻Client-B在线(这个状态由Server端来判断),则IM-Server就主动向Client-B发送一个Notify的消息,如果Client-B不在线,则消息内容会离线存储在IM-Server上。(Message: Notify)
Client-B在接收到IM-Server的Notify消息后,一定要向Server回复一个ACK消息 (Ack: Request)
IM-Server在成功处理了Client-B回复的Ack消息后,又会回复一个Ack给Client-B (Ack : Ack)
然后IM-Server再主动向Client-A发送一个Ack消息,表示Client-B接收到了消息 (Ack: notify)
以上是最核心关键的6种类型的消息报文,如果某个IM应用系统没有实现类似的6种类型的报文和步骤,那么是不可能保证消息可靠的。当然,实际IM应用肯定是还有其他基于这6种报文的扩展,或者增添其他类型的消息报文。
虽然以上的六种类似的报文是保证消息可靠性的基础,但是由于网络的复杂性,还是会出现问题:
Message:Request, Message:Ack报文可能丢失。 没关系,直接提示Client-A消息发送失败即可
Message:Notify, Ack:Request, Ack:Ack, Ack:Notify这四种类型的报文都可能丢失。 这样千万不能提示Client-A发送失败了,因为网络的不可靠发生的概率稍大了,此时Client-A会收不到Ack:Notify的报文,就不能确认Client-B是否收到了自己发出的消息,需要引入消息的超时和重传。
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来去重,而不影响用户体验。
如果Client-B不在线,IM-Server保存了离线消息以后,要伪造一个Ack-Notify消息发送给Client-A
离线消息的拉取,为了保证消息的可靠性,也需要有Ack机制,但是由于拉取离线消息不存在Ack:Notify报文,故实现逻辑要简单,就是当Client-B登录Server的时候,向Server先发送offline:Request拉取消息,当收到Server端发过来的offline:Notify消息后(里面包含Client-A发来的消息内容),再发送offline:Ack给Server端,Server端成功处理该消息就删除数据库中对应的这条离线消息。
文章目前讨论的都是一对一的单聊,而不是群聊,群聊的情况复杂得多,暂时不作讨论。
文章目前主要讨论的是对端Client-B在线的情况,离线的处理方式没有过多论述,下一篇会讲解离线对端的处理情况。
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端主动发送给客户端的报文。
最简单的普通消息投递的流程
Client-A向IM-Server发送一个Reuqest消息。
IM-Server在成功处理了Request消息之后,回复给Client-A一个Ack消息
如果此刻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个步骤:
Client-B在接收到IM-Server的Notify消息后,一定要向Server回复一个ACK消息
IM-Server在成功处理了Client-B回复的Ack消息后,又会回复一个Ack给Client-B
然后IM-Server再主动向Client-A发送一个Ack消息,表示Client-B接收到了消息
至此,Client-A才知道自己最初给Client-B的消息,Client-B已经收到,可以放心了。这6个步骤有严格的顺序,必须一步一步的发起,以下是六个步骤的整理,每个步骤对应一种属于它自己的类型的报文:
Client-A向IM-Server发送一个Reuqest消息。(Message:Request)
IM-Server在成功处理了Request消息之后,回复给Client-A一个Ack消息 (Message: Ack)
如果此刻Client-B在线(这个状态由Server端来判断),则IM-Server就主动向Client-B发送一个Notify的消息,如果Client-B不在线,则消息内容会离线存储在IM-Server上。(Message: Notify)
Client-B在接收到IM-Server的Notify消息后,一定要向Server回复一个ACK消息 (Ack: Request)
IM-Server在成功处理了Client-B回复的Ack消息后,又会回复一个Ack给Client-B (Ack : Ack)
然后IM-Server再主动向Client-A发送一个Ack消息,表示Client-B接收到了消息 (Ack: notify)
以上是最核心关键的6种类型的消息报文,如果某个IM应用系统没有实现类似的6种类型的报文和步骤,那么是不可能保证消息可靠的。当然,实际IM应用肯定是还有其他基于这6种报文的扩展,或者增添其他类型的消息报文。
上述六个步骤存在的问题
虽然以上的六种类似的报文是保证消息可靠性的基础,但是由于网络的复杂性,还是会出现问题:
Message:Request, Message:Ack报文可能丢失。 没关系,直接提示Client-A消息发送失败即可
Message:Notify, Ack:Request, Ack:Ack, Ack:Notify这四种类型的报文都可能丢失。 这样千万不能提示Client-A发送失败了,因为网络的不可靠发生的概率稍大了,此时Client-A会收不到Ack:Notify的报文,就不能确认Client-B是否收到了自己发出的消息,需要引入消息的超时和重传。
消息的超时和重传
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来去重,而不影响用户体验。
其他注意的地方
如果Client-B不在线,IM-Server保存了离线消息以后,要伪造一个Ack-Notify消息发送给Client-A
离线消息的拉取,为了保证消息的可靠性,也需要有Ack机制,但是由于拉取离线消息不存在Ack:Notify报文,故实现逻辑要简单,就是当Client-B登录Server的时候,向Server先发送offline:Request拉取消息,当收到Server端发过来的offline:Notify消息后(里面包含Client-A发来的消息内容),再发送offline:Ack给Server端,Server端成功处理该消息就删除数据库中对应的这条离线消息。
文章目前讨论的都是一对一的单聊,而不是群聊,群聊的情况复杂得多,暂时不作讨论。
文章目前主要讨论的是对端Client-B在线的情况,离线的处理方式没有过多论述,下一篇会讲解离线对端的处理情况。