TCP如何保证消息顺序以及可靠性到达,以及TCP的流量控制,拥塞控制
1. TCP可靠性传输传输的工作原理:
- 停止等待协议
- 连续ARQ协议
2. TCP可靠性传输传输的实现
- 以字节为单位的滑动窗口
- 超时重传时间的选择
- 选择确定SACK
- 面向连接:意味着两个使用TCP的应用(通常是一个客户和一个服务器)在彼此交换数据之前必须先建立一个TCP连接
- 在一个TCP连接中,仅有两方进行彼此通信,广播和多播不能用于TCP。
3. TCP通过下列方式来提供可靠性:
- 应用数据被分割成TCP认为最适合发送的数据块。这和UDP完全不同,应用程序产生的数据报长度将保持不变。 (将数据截断为合理的长度)
- 当TCP发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段。 (超时重发)
- 当TCP收到发自TCP连接另一端的数据,它将发送一个确认。这个确认不是立即发送,通常将推迟几分之一秒 。 (对于收到的请求,给出确认响应) (之所以推迟,可能是要对包做完整校验)
- TCP将保持它首部和数据的检验和。这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP将丢弃这个报文段和不确认收到此报文段。 (校验出包有错,丢弃报文段,不给出响应,TCP发送数据端,超时时会重发数据)
- 既然TCP报文段作为IP数据报来传输,而IP数据报的到达可能会失序,因此TCP报文段的到达也可能会失序。如果必要,TCP将对收到的数据进行重新排序,将收到的数据以正确的顺序交给应用层。 (对失序数据进行重新排序,然后才交给应用层)
- 既然IP数据报会发生重复,TCP的接收端必须丢弃重复的数据。(对于重复数据,能够丢弃重复数据)
- TCP还能提供流量控制。TCP连接的每一方都有固定大小的缓冲空间。TCP的接收端只允许另一端发送接收端缓冲区所能接纳的数据。这将防止较快主机致使较慢主机的缓冲区溢出。(TCP可以进行流量控制,防止较快主机致使较慢主机的缓冲区溢出)TCP使用的流量控制协议是可变大小的滑动窗口协议。
什么是字节流服务:
- 两个应用程序通过TCP连接交换8bit字节构成的字节流
- TCP不在字节流中插入记录标识符。我们将这称为字节流服务(bytestreamservice)。
- TCP对字节流的内容不作任何解释。
- TCP不知道传输的数据字节流是二进制数据,还是ASCII字符、EBCDIC字符或者其他类型数据。
- 对字节流的解释由TCP连接双方的应用层解释。
4. TCP保证消息顺序:
大家都知道,TCP提供了最可靠的数据传输,它给发送的每个数据包做顺序化(这看起来非常烦琐),然而,如果TCP没有这样烦琐的操作,那么,可能会造成更多的麻烦。如造成数据包的重传、顺序的颠倒甚至造成数据包的丢失。那么,TCP具体是通过怎样的方式来保证数据的顺序化传输呢?
主机每次发送数据时,TCP就给每个数据包分配一个序列号并且在一个特定的时间内等待接收主机对分配的这个序列号进行确认,如果发送主机在一个特定时间内没有收到接收主机的确认,则发送主机会重传此数据包。接收主机利用序列号对接收的数据进行确认,以便检测对方发送的数据是否有丢失或者乱序等,接收主机一旦收到已经顺序化的数据,它就将这些数据按正确的顺序重组成数据流并传递到高层进行处理。
具体步骤如下:
- 为了保证数据包的可靠传递,发送方必须把已发送的数据包保留在缓冲区;
- 并为每个已发送的数据包启动一个超时定时器;
- 如在定时器超时之前收到了对方发来的应答信息(可能是对本包的应答,也可以是对本包后续包的应答),则释放该数据包占用的缓冲区;
- 否则,重传该数据包,直到收到应答或重传次数超过规定的最大次数为止。
- 接收方收到数据包后,先进行CRC校验,如果正确则把数据交给上层协议,然后给发送方发送一个累计应答包,表明该数据已收到,如果接收方正好也有数据要发给发送方,应答包也可方在数据包中捎带过去。
粘包问题
尽管TCP能够有效的保证每个字节序和包顺序是按照顺序接收的,但是仍然会存在粘包的问题,具体如下:
1. 粘包产生的原因
- 如果客户端连续不断的向服务端发送数据包时,服务端接收的数据会出现两个数据包粘在一起的情况,这就是TCP协议中经常会遇到的粘包以及拆包问题。
- UDP是基于报文发送的,在UDP首部采用了16bit来指示UDP数据报文长度,因此在应用层能很好的将不同的数据报文区分开,从而避免粘包和拆包问题。
- 原因有以下两点:
- TCP是基于字节流的,虽然应用层和传输层之间的数据交互是大小不等的数据块,但是TCP把这些数据块仅仅看成一连串无结构的字节流,没有边界;
- 在TCP的首部没有表示数据长度的字段,基于上面两点,在使用TCP传输数据时,才有粘包或者拆包现象发生的可能。
2、粘包/拆包的表现形式
现在假设客户端向服务端连续发送了两个数据包,用packet1和packet2来表示,那么服务端收到的数据可以分为三种,现列举如下:
-
第一种情况,接收端正常收到两个数据包,即没有发生拆包和粘包的现象,此种情况不在本文的讨论范围内。
-
第二种情况,接收端只收到一个数据包,由于TCP是不会出现丢包的,所以这一个数据包中包含了发送端发送的两个数据包的信息,这种现象即为粘包。这种情况由于接收端不知道这两个数据包的界限,所以对于接收端来说很难处理。
-
第三种情况,这种情况有两种表现形式,如下图。接收端收到了两个数据包,但是这两个数据包要么是不完整的,要么就是多出来一块,这种情况即发生了拆包和粘包。这两种情况如果不加特殊处理,对于接收端同样是不好处理的。
3、粘包/拆包发生的原因
发生TCP粘包或拆包有很多原因,现列出常见的几点:
1、要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。
2、待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。
3、要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包。
4、接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。
4、粘包/拆包的解决办法
通过以上分析,我们清楚了粘包或拆包发生的原因,那么如何解决这个问题呢?问题的关键在于如何给每个数据包添加边界信息,常用的方法如下:
- 发送端给每个数据包添加包首部,首部中应该至少包含数据包长度,这样接收端在接收到数据后,通过读取包首部长度字段便知道每一个数据包实际长度了。
- 发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。
- 可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。
如何设置合理的边界特殊符号呢?
实际上,所谓的边界符号也就是 Header(Magic Number) ,由于传输的数据可能出现各种各样的情形,因此选用一种合适的 Header 字段来避免数据字段中出现 Header 边界符号,需要根据如下的步骤进行分析:
- 根据实际需求,确定待传输数据的取值范围,例如:传输由12bit-ADC采集的电压值,其取值范围为 0x000 ~ 0xFFE,因此,直接取12bit来传输ADC数据 (0x000 ~ 0x0FFF)
- 根据数据取值范围,写出最大数据组成的TCP数据包,例如:0xFFE|FFE|FFE| ~
- 根据数据包情况,选择至少比原始数据多4bit的数据作为头Header,假设为0xXXXX,将粘包数据包拆包为4Bytes数据可以得到三种情形(4Bytes滑动窗口):
- FFEF:由于F位可能出现任意可能,因此此情形下的可能组合情况为 0xXXEX --> 第3位E处必须是F,才能够避免出现此情形的重复
- FEFF:由于F位可能出现任意可能,因此此情形下的可能组合情况为 0xXEXX --> 第2位E处必须是F,才能够避免出现此情形的重复
- EFFE:由于F位可能出现任意可能,因此此情形下的可能组合情况为 0xEXXE --> 第1位E处 或 第4位E处 任一处为F,即够避免出现此情形的重复
- 根据上述分析结果可知,Header的取值应该为 0xFFFX 或 0xXFFF 即可!
注: 其他类似情况分析如下(0x00000~0xFFFFE):
根据上图分析,可用的Header为 \(0x(F_{1}/X_{6})(F_{2}/X_{7})(F_{3}/X_{8})F | F(F_{6}/X_{1})(F_{7}/X_{2})(F_{8}/X_{1})\)
结论: 对于最大值为0XFFF...E取值范围 Nbits 的数据包,取Header为 N+4 bits,且 Header=0xFFFF+FE(最后的E为多出来的4Bit)