TCP-IP详解

news/2024/5/17 15:30:15 标签: TCP

TCP-IP详解:快速重传与快速恢复


快速重传算法

快速重传算法在之前的文章中有介绍,如果收到一个out-of-order的报文段时, TCP需要立刻产生一个ACK,这个ACK不应该被延时,目的在于让对方知道收到一个失序的报文,并告诉对方自己希望收到的报文seq,我们不知道这个重复的ACK的原因,因为还是会等待少量的重复ACK到来,如果连续收到3个或者3个以上的dup ACK,就被判断这个报文被丢失了,于是就需要立即重传丢失的数据段,这个地方不用等待定时器溢出。更详细的介绍可以参考:TCP-IP详解:超时重传机制


快速恢复算法

TCP Reno这个算法定义在RFC5681。快速重传和快速恢复算法一般同时使用。快速恢复算法是认为,你还有3个Duplicated Acks说明网络也不那么糟糕,所以没有必要像RTO超时那么强烈,并不需要重新回到慢启动进行,这样可能降低效率。所以协议栈会做如下工作

1.  cwnd = cwnd/2 

2. sshthresh = cwnd 

然后启动快速恢复算法:

1. 设置cwnd = ssthresh+ACK个数*MSS(一般情况下会是3个dup ACK)

2. 重传丢失的数据包(对于重传丢失的那个数据包,可以参考TCP-IP详解:SACK选项)

3. 如果只收到Dup ACK,那么cwnd = cwnd + 1, 并且在允许的条件下发送一个报文段

4. 如果收到新的ACK, 设置cwnd = ssthresh, 进入拥塞避免阶段


TCP Reno算法

其实TCP Reno算法就是在慢启动和拥塞避免的基础上增加了快速重传和快速恢复算法,避免了在拥塞不严重的状况下,过大的减小拥塞窗口,降低TCP的传输效率,这个算法的示意图如下,可以看到cwnd在遇到3个dup ACK的时候 cwnd减半,进入快速恢复模式.


这个算法存在的一个问题是,多个报文同时丢失的情况下会出现性能问题,系统会多次执行快速重传和快速恢复算法,多次降低cwnd的值,降低了传输的效率。

可以看下Reno丢失2个包的状况了
1. cwnd = 10 

2. 发送数据包,但是3包和6包丢失

3. 收到数据包1的ACK, cwnd = 11

4. 收到数据包2的ACK,  cwnd = 11

5. 收到数据包2的Dup ACK,cwnd = 11

6. 收到数据包2的Dup ACK, cwnd = 11

7. 收到数据包2的dup ACK, cwnd = 11 , 3个dup ACK,启动快速重传算法,立即发送数据包3,然后进入快速恢复阶段

8. ssh = 11/2 = 5  cwnd = 5+3 = 8

9. 收到一个dup ACK cwnd + 1  直到收到一个新的ACK,cwnd = ssh = 5  进入拥塞避免阶段

10. 可是又连续收到dup ACK,进行快速重传,cwnd又要减半进入快速恢复阶段.... 




TCP New Reno

这个算法是Reno算法的改进,没有使用SACK机制
  • 当sender这边收到了3个Duplicated Acks,进入Fast Retransimit模式,开始重传重复Acks指示的那个包。如果只有这一个包丢了,那么,重传这个包后回来的Ack会把整个已经被sender传输出去的数据ack回来。如果没有的话,说明有多个包丢了。我们叫这个ACK为Partial ACK。
  • 一旦Sender这边发现了Partial ACK出现,那么sender就可以推理出来有多个包被丢了,于是乎继续重传sliding window里未被ack的第一个包。直到再也收不到了Partial Ack,才真正结束Fast Recovery这个过程

超时重传机制

超时重传指的是,发送数据包在一定的时间周期内没有收到相应的ACK,等待一定的时间,超时之后就认为这个数据包丢失,就会重新发送。这个等待时间被称为RTO.  

检测丢失segment的方法从概念上讲还是比较简单的,每一次开始发送一个TCP segment的时候,就启动重传定时器,定时器的时间一开始是一个预设的值(Linux 规定为1s),随着通讯的变化以及时间的推移,这个定时器的溢出值是不断的在变化的,有相关算法计算RTO[参考:文章....],如果在ACK收到之前,定时器到期,协议栈就会认为这个片段被丢失,重新传送数据。

TCP在实现重传机制的时候,需要保证能够在同一时刻有效的处理多个没有被确认的ACK,也保证在合适的时候对每一个片段进行重传,有这样几点原则:

1 . 这些被发送的片段放在一个窗口中,等待被确认,没有确认不会从窗口中移走,定时器在重传时间到期内,每个片段的位置不变,这个地方其实在滑动窗口的时候也有提到过

2 .只有等到ACK收到的时候,变成发送并ACK的片段,才会被从窗口中移走。

3 .如果定时器到期没有收到对应ACK, 就重传这个TCP segment

重传之后也没有办法完全保证,数据段一定被收到,所以仍然会重置定时器,等待ACK,如果定时器到期还是没有收到ACK,继续重传,这个过程重传的TCP segment一直留着队列之内。

举个重传的例子:

1. Server 发送80个字节 Part1,seq = 1 

2. Server 发送120个字节Part2,Seq = 81

3. Server发送160个字节Part3,Seq = 201,此包由于其他原因丢失

4. Client收到前2个报文段,并发送ACK = 201

5. Server发送140个字节Part4, Seq = 361

7. Server收到Client对于前两个报文段的ACK,将2个报文从窗口中移除,窗口有200个字节的余量

8. 报文3的重传定时器到期,没有收到ACK,进行重传

9. 这个时候Client已经收到报文4,存放在缓冲区中,也不会发送ACK【累计通知,发送ACK就表示3也收到了】,等待报文3,报文3收到之后,一块对3,4进行确认

10. Server收到确认之后,将报文3,4移除窗口,所有数据发送完成


这种方式会面临一个问题:客户端在等待报文3的时候,服务器如何处理报文4, 客户端这个期间内并没有发送任何报文,服务器并不知道报文3和报文4的状态,报文4可能会丢失,也可能会被客户端接收,那么如果超时了,我到底值该发送报文3 ,还是报文3和报文4 呢?

总结起来就是2中处理

1. 定时器溢出,重传3

2. 定时器溢出,重传3,4

对于怎么传的问题,在RFC2018中已经提供了一种方案: SACK,    详细可参考文章:TCP-IP详解:SACK选项(Selective Acknowledgment)

对于重传时间是如何计算的问题,在RFC2988中也提供了一种至今Linux使用的方案,详细介绍可以参考文章:TCP-IP详解 RTT and RTO

对于定时器溢出的问题,就来介绍一下 快速重传机制。


快速重传机制

在超时重传中,重点是定时器溢出超时了才认为发送的数据包丢失,快速重传机制,实现了另外的一种丢包评定标准,即如果我连续收到3次dup ACK,发送方就认为这个seq的包丢失了,立刻进行重传,这样如果接收端回复及时的话,基本就是在重传定时器到期之前,提高了重传的效率。

在传输过程中会出现out-of-order的现象,但是在滑动窗口中会有严格的顺序控制,假设有4,5,6三个待接收的数据包,先收到了5,6,协议栈是不会回复对5,6包的确认,而是根据TCP协议的规定,当接收方收到乱序片段时,需要重复发送ACK, 在这个地方会发送报文4 seq的ACK,表明需要报文4没有被接收到,如果此后收到的是报文7,那么仍然要回报文4 seq的ACK,如果连续发送3个 dup ACK,接收端认为这个片段已经丢失,进行快速重传。

看一个简单的例子:这是下载过程中网络不好抓的tcpdump

1. 145/153/170 是3个dup ACK

2.  171包,快速重传


不过快速重传能够解决超时的问题,但是对于之前讨论的究竟重传哪些包的问题,依然不能有效的解决,这就需要TCP中提供的SACK机制来解决。


在文章TCP-IP详解:超时重传机制中,有介绍到快速重传和超时重传都会面临到一个重传什么包的问题,因为发送端也不清楚丢失包后面传送的数据是否有成功的送到。主要原因还是对于TCP的确认系统,不是特别的好处理这种不连续确认的状况了,只有低于ACK number的片段都被收到才有进行ACK,out-of-order的片段只能是等待,同时,这个时间窗口是无法向右移动的。

举个例子:

1. 服务发送4个片段给客户端,seg1(seq=1,len=80),seg2(seq=81,len=120), seg3(seq=201,len=160),seg4(seq=361,len=140)

2. 服务器收到seg1和seg2的ACK = 201,所以此时seg1 seg2变成发送并已经确认范畴的数据包,被移除滑动窗口,此时服务器又可以多发80+120 byte数据

3. 假设seg3由于某些原因丢失,这个时候服务器仍然可以像客户端发送数据,但是服务器会等待seg3的ACK,否则窗口无法滑动,卡主了

4. seg3丢失了,即使后面的seg4收到了,客户端也无法告知服务器已经收到了seg4,试想一下,如果窗口也够大,服务器可以继续持续发送更多的片段,那么这些片段被客户端接收,只能存放到队列中,无法进行确认

正式因为后续OUT-OF-ORDER的报文段的发送状况也不清楚,所以Server也不是特别清楚要如何去处理这种状况,不过一般来说只能有2中状况:

1. 只重传超时的数据包,这种方法是最常想到的,比较实用与后面的数据包都能够正常接收的状况,只重传超时的数据包,但是如果比较坏的情况下,丢失了很多封包呢?  那就需要一个一个的等待超时了,很浪费时间。

2. 重传这个片段以及之后的所有包,这种方法在最坏的状况下,看起来效率还是挺高的,但是如果只有一个包丢失,就去重传后面所有接受到的包,流量浪费也是很严重的。

总之对于上面阐述的问题,没有想到一个好的思路来解。但是RFC2018提供了一个SACK的方法,有效的解决这个问题


SACK(Selective Acknowledgment)

SACK是一个TCP的选项,来允许TCP单独确认非连续的片段,用于告知真正丢失的包,只重传丢失的片段。要使用SACK,2个设备必须同时支持SACK才可以,建立连接的时候需要使用SACK Permitted的option,如果允许,后续的传输过程中TCP segment中的可以携带SACK option,这个option内容包含一系列的非连续的没有确认的数据的seq range,这些

SYN包中SACK Permitted 选项,双方都支持才对

SACK option格式

Kind 5  Length  剩下的都是没有确认的segment的range了 比如说segment 501-600 没有被确认,那么Left Edge of 1st Block = 501,Right Edge of 1st Block = 600,TCP的选项不能超过40个字节,所以边界不能超过4组


可以看下实际的tcpdump抓包中的SACK option,如下图,使用tcp.option.sack进行过滤,可以看到这个SACK option只有一个片段,接收并没有进行确认,范围是18761~20101


再来将上面的例子

客户端收到seg4的时候,发送seg3的ACK 会产生一个SACK的option(361~500),Server收到这个ACK后,就知道seg3丢失了,但是seg4已经收到了但是并没有确认,所以就只会重传seg3


SACK的产生,RFC2018

SACK通常是由数据接收方产生,如果在connection建立的时候,SYN包中有SACK-Permitted 的选项为true,同时自身也支持SACK,那么可以在接收异常的时候,产生SACK option. 如果要发送,SACK中需要携带接收队列中所有没有被确认的数据段信息。

如果接收方选择发送带有SACK的ACK,需要遵循如下规则:

1. 第一个block需要指出是哪一个segment触发SACK option ,我认为就是谁乱序了,才会导致SACK

2. 尽可能多的把所有的block填满

3. SACK 要报告最近接收的不连续的数据块

接收端的行为:

1. 数据没有被确认前,都会保持在滑动窗口内

2. 每一个数据包都有一个SACKed的标志,对于已经标示的segment,重新发送的时候会忽略

3. 如果SACK丢失,超时重传之后,重置所有数据包SACKed 标志


D-SACK RFC2883

D-SACK主要是使用了SACK来告诉发送方有哪些数据被重复接收了,如果是D-SACK,那么SACK option的第一个block代表被重复发送的序列片段

需要注意的点:

1. D-SACK仅仅是接收端报告一个重复的连续的片段

2. 每个重复的连续片段只能在一个block中

3.  重复片段的序列号

4. 第二个block指的是data没有被确认的

分析一下RFC2883的例子:

1. Reproting a Duplicate Segment

如下图: 发送端发送seg1和seg2,但是接收端的ACK都被drop掉了,超时重传seg1,然后接收端就发了一个D-SACK,告诉发送端3000~3499重复接收,需要接收4000的。


2. 报告OUT-OF-ORDER段和重传段

从图中可以看出seg4 out-of-order会触发SACK,说明已经收到但是没有确认的数据,但是这个丢掉了,通过发送方又重传了seg1,然后接收方此时会生成D-SACK,block1中存放了dup的segment,block2中存放了收到但没有确认的segment


还有比较多的例子,详细的可以参考 RFC2883。

总起来说D-SACK还是带了诸多的好处,能否让发送方了解是ACK丢了还是发送的数据包丢了,重复发送就说明是ACK丢了呗,同时也能够掌握网络上的一些事情,比如out-of-order,超时重传等,这样了解了网络,才能更好的做流控。



http://www.niftyadmin.cn/n/1313404.html

相关文章

【转】Entity Framework Extended Library (EF扩展类库,支持批量更新、删除、合并多个查询等)...

E文好的可以直接看https://github.com/loresoft/EntityFramework.Extended 也可以在nuget上直接安装这个包&#xff0c;它的说明有点过时了&#xff0c;最新版本已经改用对IQueryable<T>的扩展&#xff0c;而不是DbSet<T>&#xff08;已经标记为弃用&#xff09;,所…

Java中的浮点数比较

前几天有位同学问我一个问题&#xff0c;为什么float和double不能直接用比较&#xff1f; 例如&#xff1a; [java] view plain copy System.out.println(0.1d 0.1f); 结果会是flase当时我只是简单的回答&#xff0c;因为精度丢失&#xff0c;比较结果是不对的。 那么&#…

Linux下的vim常用操作

在vim里面有3种模式&#xff1a;命令模式&#xff0c;编辑模式&#xff0c;末行模式 命令模式下的操作&#xff1a; 打开文件 vim 文件路径和名称&#xff0c;如果文件不存在则新建一个文件 vim xxx 代表打开这个文件从多少行开始 光标的移动 分为下面几个级别&#xff1a; 字…

TCP DUP ACK

最近在跟一个CDN服务器端做对接&#xff0c;从CDN服务器下载内容&#xff08;http&#xff09;&#xff0c;发现抓包出现非常多的Tcp Dup Ac​k异常提示。通过查阅质料得知Tcp Dup Ack xxx#y 代表了数据段丢失TCP状态&#xff0c;xxx代表数据丢失的位置&#xff0c;#后代表第几…

Tomcat4 源代码分析 (15) How Tomcat Works 第十五章

Chapter 15 : Digester 概览 在之前的章节中&#xff0c;我们用Bootstrap类来实例化connector, context, wrappers 以及其他的组件。当把组件注入到父级组件中时&#xff0c;我们使用的是set方法&#xff0c;比如说&#xff1a; connector.setContainer(context); context.setP…

应用程序无法正常启动0xc0150002解决方案

我也遇到过此问题&#xff0c;解决办法&#xff1a; 方案一&#xff1a; 在项目的“属性&#xff5c;配置属性&#xff5c;链接器&#xff5c;常规”中的“启用增量链接”选择“否”。此方法阻断了问题产生的源头&#xff0c;其每次生成exe文件时都直接嵌入清单文件&#xff0c…

学习 Docker 操作系统版本选择

近来有时间一直在捣鼓 Docker。因为服务器选择的是 CentOS 版本&#xff0c;所以实验的环境选择的一直是 CentOS。如果是个人玩 Docker&#xff0c;优先选择 ubuntu。如果需要选择 CentOS 的话&#xff0c;单机的话&#xff0c;可以选择 CentOS 7.0 和 CentOS 7.2 。如果想研究…

TCP 报文-wireshark抓包常见提示含义解析

wireshark抓包常见提示含义解析1&#xff0e;[Packet size limited during capture]当你看到这个提示&#xff0c;说明被标记的那个包没有抓全。以图1的4号包为例&#xff0c;它全长有171字节&#xff0c;但只有前96个字节被抓到了&#xff0c;因此Wireshark给了此提示。图1这种…