Home Tags Posts tagged with "消息确认"

消息确认

最近系统的学习了网络编程的相关知识,当然也离不开Tcp这位重量级,而自己手里写的这个项目 基于Bio的聊天室 其本质也是消息的传递,最接近Tcp的实现原理,因此根据对Tcp的学习,引出本人对自己项目的思考。

起因是我考虑将该聊天室做的可靠,健壮,同时往消息队列的模型上靠拢,因此,必须考虑从客户端发往服务器端的消息的可靠性和安全性。

 

我们知道,基于Tcp实现的Socket编程,其本质就是Socket连接 = Tcp 连接,所以很多原本考虑在Socket层面实现的保证安全性和可靠性的机制其实Tcp已经实现了,Tcp具有以下机制:

保证可靠性的机制

  • 校验和
  • 序列号(按序到达)
  • 确认应答
  • 超时重传
  • 连接管理
  • 流量控制
  • 拥塞控制

提高性能的机制

  • 滑动窗口
  • 快速重传
  • 延迟应答
  • 捎带应答

 

 

针对这些机制,仔细梳理我所写的项目的实现

连接和断开

由于是基于Tcp实现的Socket编程,因此不用考虑连接和正常断开的问题(Tcp的三次握手和四次挥手)

但是如果已经建立了连接,而客户端突然故障,这种情况下开启Tcp的保活计时器,修改其参数,即开启其心跳机制,同时为了避免其Bug(详情可搜索另一篇博文),我在应用层实现了心跳机制,可以保证客户端突发故障,服务器端能快速知晓并关闭连接。

而服务器端突然故障,则没关系,因为客户端是在向服务器端写入的,比如 客户端发送消息 哈哈哈哈 成功(这个成功会有个显示,类似于QQ消息的已发送状态),而无论是在客户端下次发送之前还是发送中与服务器连接失败,客户端都会立即收到通知,即客户端显示 未发送成功 ,这样客户端就可以采取下一步动作了(采取什么动作需要再进行设计 例如重新连接 重传 等等)

 

消息确认的必要性

昨晚我思考了几个问题,在客户端发送消息给服务器端的过程中,可能出现以下几种情况:

消息还未发送

  1. 客户端刚准备发送消息,服务器端挂了,这个时候因为会抛出异常,客户端就知晓这条消息发不出去了,因此这条消息不会丢失也不会出错(丢失起码要先发出去)

消息已经在发送过程中

  1. 客户端的这条消息在传递过程中,服务器端挂了,也会抛出异常,但是客户端不知道这条消息是否发送成功(因为有可能服务器已经收到了才挂,也有可能是服务器端没收到就挂了),只知道服务器端挂了,而客户端此时不知道是否应该重发,导致迷茫

上述分析情况单纯的考虑了服务器端对消息的接收情况,而服务器端收到消息后,可能还有一些其他操作,例如转发,存储,加工等等,因此必须同时考虑服务器对消息的处理情况(建立在服务器端成功收到消息的基础上)

服务器端成功接收消息,但在对消息进行加工,转发,存储的时候,不管是成功还是失败,这个结果对客户端是不可知的,因为目前客户端只会对 “客户端和服务器端的连接中断或失败”这个事件作出反应,也就是抛出异常;这样肯定不行啊,我客户端一头雾水,一问三不知,不知道我的消息发送出去没,不知道服务器端收到消息没,不知道服务器端成功处理消息没。很显然,解决这个问题就是让客户端了解所发消息的状态

消息状态粗略的可分为:

  1. 未发送出去
  2. 服务器端成功接收了该消息但处理失败
  3. 服务器端成功处理了该消息

而确认应答就可以让客户端知道消息的状态,从而进行其他操作。

 

TCP的确认应答机制(ACK机制) 

 

TCP将每个字节的数据都进行了编号, 即为序列号.

每一个ACK都带有对应的确认序列号, 意思是告诉发送者, 我已经收到了哪些数据; 下一次你要从哪里开始发.
比如, 客户端向服务器发送了1005字节的数据, 服务器返回给客户端的确认序号是1003, 那么说明服务器只收到了1-1002的数据.
1003, 1004, 1005都没收到.
此时客户端就会从1003开始重发.

 

既然有了TCP的确认应答(ACK机制),为什么还需要我们在应用层实现应答机制?

我理解的是,TCP的确认应答确认的范围是在一条消息内,它对每个字节的数据进行标号,比如一条消息如果占300个字节,那么它可以确保这300个字节的消息中,如果有丢失的,可以重发

例如:这一条消息占300个字节,由于网络阻塞或其他原因,只发送了158个字节,那么服务器端的ACK只会回应159,这样的话客户端就会从159开始重发,确保这一条消息完全发送。

理解了TCP应答机制的实现,那么我们就知道了TCP的确认应答并不能解决我们上述所需要的 让客户端了解所发消息的状态,因此还需要我们在应用层实现应答机制。

 

应用层实现应答机制

那么如何设计该应用层的应答机制呢?首先需要区分消息状态是从客户端本身还是从服务器端来进行反馈

消息状态由客户端本身进行反馈(ACK)

从客户端A发送消息到该消息在网上进行传输之前,如果服务器端连接断开或客户端本身的原因,已经无法写入,则反馈 未发送成功

类似于QQ发送消息,结果本地网络已经中断了,会反馈发送失败,这个状态由客户端自身反馈。

消息状态由服务器端进行反馈(ACK)

客户端A发送消息后,由于网络拥堵等原因,消息无法到达服务器端B,因为服务器端根本没收到消息,则无法回应,而客户端本身也无法反馈该消息状态,这种情况必须在客户端使用超时重传策略进行解决;(问题一:消息在客户端发给服务器端的过程中丢失)

客户端A发送消息后,服务器端成功接收,但是在处理的时候失败了,这个时候需要由服务器端向客户端反馈状态,同样的,如果处理成功也需要反馈,也就是说服务器端只向客户端反馈对消息进行处理之后的状态;那么此处必须要考虑到的就是 由客户端本身进行反馈还好,该反馈一般不会丢失,但是由服务器端进行的反馈是有可能丢失的,如果这个ACK丢失了,客户端就还是不知道该消息的状态 因此 仅仅有确认应答机制是不够的。(问题二:ACK在服务器端发向客户端的途中丢失)

TCP则是使用超时重传机制来解决 消息在客户端发给服务器端的过程中丢失 和 ACK丢失 两个问题的

 

TCP超时重传机制

 

主机A发送数据给B之后, 可能因为网络拥堵等原因, 数据无法到达主机B
如果主机A在一个特定时间间隔内没有收到B发来的确认应答, 就会进行重发
但是主机A没收到确认应答也可能是ACK丢失了.

这种情况下, 主机B会收到很多重复数据.
那么TCP协议需要识别出哪些包是重复的, 并且把重复的丢弃.
这时候利用前面提到的序列号, 就可以很容易做到去重.

 

既然有了TCP的超时重传,为什么还需要应用层实现超时重传?

还是一个范围问题,TCP解决的是字节层面(或者称之为数据包层面的问题)而应用层则解决的是所发送的消息层面的问题

 

应用层实现超时重传

客户端A发送消息,服务器端接收并处理消息,反馈消息状态给客户端A,这是消息确认应答的流程

在这个流程中,如果确认应答过程中ACK丢失,则使用超时重传的策略来解决;

具体来说就是:

客户端A发送消息给服务器端,可能因为网络拥堵,数据无法到达服务器端(即消息在发送过程中,服务器端还没接收到消息就丢失了),在客户端A使用超时重传机制,如果客户端A在一个特定的时间间隔内没有收到服务器端发来的确认应答, 就进行重发;

发生超时重传的情景有两个:

  1. 第一个就是客户端消息在发送过程中丢失,
  2. 第二个就是来自服务器端的ACK丢失;

第一种情景还好,可是第二种情景,服务器端已经收到了该消息,客户端进行超时重传势必会导致服务器端收到许多重复消息;

那么必须在服务器端进行消息去重,由TCP的超时重传引出

关于消息去重我们可以用以下做法

每条发送的消息有一个唯一的序列号,在服务器端收到一条消息后,会先比较该ID表示是否存在,存在则表示该消息在服务器里面了,反馈客户端就好,不需要再对这条重复的消息进行太多操作

那么超时时间如何确定呢?

最理想的情况下, 找到一个最小的时间, 保证 “确认应答一定能在这个时间内返回”.
但是这个时间的长短, 随着网络环境的不同, 是有差异的.
如果超时时间设的太长, 会影响整体的重传效率; 如果超时时间设的太短, 有可能会频繁发送重复的包.

TCP为了保证任何环境下都能保持较高性能的通信, 因此会动态计算这个最大超时时间.

Linux中(BSD Unix和Windows也是如此), 超时以500ms为一个单位进行控制, 每次判定超时重发的超时时间都是500ms的整数倍.
如果重发一次之后, 仍然得不到应答, 等待 2*500ms 后再进行重传. 如果仍然得不到应答, 等待 4*500ms 进行重传.
依次类推, 以指数形式递增. 累计到一定的重传次数, TCP认为网络异常或者对端主机出现异常, 强制关闭连接.

到此,我们来回顾一下,我们为了让客户端了解它所发送的消息的状态并根据状态进行其他操作,使用了确认应答机制,而确认应答机制无法解决两个问题(ACK丢失和消息在发送的时候丢失),为了解决和完善确认应答机制,我们使用了超时重传机制,而超时重传在ack丢失的情况下会导致服务器端收到重复消息,我们又使用最开始设计的传递的消息的唯一序列号来进行去重,环环相扣,确保了消息从客户端传递到服务器端的可靠性。

 

 

参考:https://blog.csdn.net/sinat_36629696/article/details/80740678