浅谈TCP(1):状态机与重传机制

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅谈TCP(1):状态机与重传机制相关的知识,希望对你有一定的参考价值。

参考技术A

TCP协议比较复杂,接下来分两篇文章浅要介绍TCP中的一些要点。

本文介绍TCP的状态机与重传机制,下文讲解流量控制与拥塞控制。

TCP在网络OSI的七层模型中的第四层——Transport层,IP在第三层——Network层,ARP在第二层——Data Link层,在第二层上的数据,我们叫Frame,在第三层上的数据叫Packet,第四层的数据叫Segment。

应用层的数据首先会打到TCP的Segment中,然后TCP的Segment会打到IP的Packet中,然后再打到以太网Ethernet的Frame中,传到对端后,各个层解析自己的协议,然后把数据交给更高层的协议处理。

在正式讨论之前,先来看一下TCP头的格式:
[图片上传中...(-eed30f-1522722733998-0)]

注意:

其他字段参考下图:

其实, 网络传输是没有连接的——TCP所谓的“连接”,其实只不过是在通讯的双方维护一个“连接状态” ,让它看上去好像有连接一样。所以,TCP的状态转换非常重要。

下面是 简化 的“TCP协议状态机” 和 “TCP三次握手建连接 + 传数据 + 四次挥手断连接” 的对照图,两张图本质上都描述了TCP协议状态机,但场景略有不同。 这两个图非常重要,一定要记牢

TCP协议状态机,不区分client、server:

下图是经典的“TCP三次握手建连接 + 传数据 + 四次挥手断连接”,client发起握手,向server传输数据(server不向client传),最后发起挥手:

很多人会问, 为什么建连接要三次握手,断连接需要四次挥手?

主要是要 初始化Sequence Number 的初始值

通信的双方要同步对方ISN(初始化序列号,Inital Sequence Number)——所以叫SYN(全称Synchronize Sequence Numbers)。也就是上图中的 x 和 y。这个号在以后的数据通信中,在client端按发送顺序递增,在server端按递增顺序重新组织,以保证应用层接收到的数据不会因为网络问题乱序。

其实是 双方各自进行2次挥手

因为TCP是全双工的,client与server都占用各自的资源发送segment(同一通道,同时双向传输seq和ack),所以, 双方都需要关闭自己的资源(向对方发送FIN)并确认对方资源已关闭(回复对方Ack) ;而双方可以同时主动关闭,也可以由一方主动关闭带动另一方被动关闭。只不过,通常以一方主动另一方被动举例(如图,client主动server被动),所以看上去是所谓的4次挥手。

如果两边同时主动断连接,那么双方都会进入 CLOSING 状态,然后到达 TIME_WAIT 状态,最后超时转到 CLOSED 状态。下图是双方同时主动断连接的示意图(对应TCP状态机中的Simultaneous Close分支):

server收到client发的SYN并回复Ack(SYN)(此处称为Ack1)后,如果client掉线了(或网络超时),那么server将无法收到client回复的Ack(Ack(SYN))(此处称为Ack2),连接处于一个 中间状态 (非成功非失败)。

为了解决中间状态的问题,server如果在一定时间内没有收到Ack2,会重发Ack1 (不同于数据传输过程中的重传机制)。Linux下,默认重试5次,加上第一次最多共发送6次;重试间隔从1s开始翻倍增长(一种指数回退策略,Exponential Backoff),5次的重试时间分别为1s, 2s, 4s, 8s, 16s,第5次发出后还要等待32s才能判断第5次也超时。所以, 至多共发送6次,经过1s + 2s + 4s+ 8s+ 16s + 32s = 2^6 -1 = 63s,TCP才会认为SYN超时断开这个连接

可以利用建连接时的SYN超时机制发起 SYN Flood攻击 ——给server发一个SYN就立即下线,于是服务器默认需要占用资源63s才会断开连接。发SYN的速度是很快的,这样,攻击者很容易将server的SYN队列资源耗尽,使server无法处理正常的新连接。

针对该问题,Linux提供了一个 tcp_syncookies 参数解决这个问题—— 当SYN队列满了后,TCP会通过源地址端口、目标地址端口和时间戳构造一个特别的Sequence Number发回去,称为SYN Cookie,如果是攻击者则不会有响应,如果是正常连接,则会把这个SYN Cookie发回来,然后server端可以通过SYN Cookie建连接 (即使你不在SYN队列中)。至于SYN队列中的连接,则不做处理直至超时关闭。请注意, 不要用 tcp_syncookies 参数来处理正常的大负载连接情况 ,因为SYN Cookie本质上也破坏了建连接的SYN超时机制,是妥协版的TCP协议。

对于正常的连接请求,有另外三个参数可供选择:

最终,设计了多种ISN增长算法,普遍 使ISN随时钟动态增长,并具有一定的随机性 。RFC793中描述了一种简单的ISN增长算法:ISN会和一个假的时钟绑在一起,这个时钟会在每4微秒对ISN做加一操作,直到超过2^32,又从0开始。这样,一个ISN的周期大约是4.55(<font color="red"> 我算的4.77??? </font>)个小时。定义segment在网络上的最大存活时间为MSL(Maximum Segment Lifetime),网络中存活时间超过MSL的分组将被丢弃。因此,如果使用RFC793中的ISN增长算法,则MSL的值必须小于4.55小时,以保证不会在相邻的连接中重用ISN( TIME_WAIT 也有该作用)。同时,这间接限制了网络的大小(当然,4.55小时的MSL已经能构造非常大的网络了)。

在TCP状态机中,从 TIME_WAIT 状态到CLOSED状态,有一个超时时间 2 * MSL。为什么需要 TIME_WAIT 状态,且超时时间为2 * MSL?主要有两个原因:

一个常见问题是大规模出现TIME_WAIT,通常是在高并发短连接的场景中,会消耗很多资源。

网上大部分文章都是教你打开两个参数, tcp_tw_reuse 或 tcp_tw_recycle 。这两个参数默认都是关闭的, tcp_tw_recycle 比 tcp_tw_reuse 更为激进;要想使用二者,还需要打开 tcp_timestamps (默认打开),否则无效。不过, 打开这两个参数可能会让TCP连接出现诡异的问题 :如上所述,如果不等待超时就重用连接的话,新旧连接的数据可能会混在一起,比如新连接握手期间收到了旧连接的FIN,则新连接会被重置。因此, 使用这两个参数时应格外小心

各参数详细如下:

补充一个参数:

总之, TIME_WAIT 出现在主动发起挥手的一方 ,即,谁发起挥手谁就要牺牲资源维护那些等待从 TIME_WAIT 转换到 CLOSED 状态的连接。 TIME_WAIT 的存在是必要的,因此, 与其通过上述参数破协议来逃避 TIME_WAIT ,不如好好优化业务 (如改用长连接等),针对不同业务优化 TIME_WAIT 问题。

对于HTTP服务器,可以设置HTTP的KeepAlive参数,在应用层重用TCP连接来处理多个HTTP请求(需要浏览器配合),让client端(即浏览器)发起挥手,这样 TIME_WAIT 只会出现在client端。

下图是我从Wireshark中截了个我在访问coolshell.cn时的有数据传输的图,可以参照理解Seq与Ack是怎么变的(使用Wireshark菜单中的Statistics ->Flow Graph… ):

可以看到, Seq与Ack的增加和传输的字节数相关 。上图中,三次握手后,来了两个Len:1440的包,因此第一个包为Seq(1),第二个包为Seq(1441)。然后收到第一个Ack(1441),表示1~1440的数据已经收到了,期待Seq(1441)。另外,可以看到一个包可以同时充当Ack与Seq,在一次传输中携带数据与响应。

TCP协议通过重传机制保证所有的segment都可以到达对端,通过滑动窗口允许一定程度的乱序和丢包 (滑动窗口还具有流量控制等作用,暂不讨论)。注意,此处重传机制特指数据传输阶段,握手、挥手阶段的传输机制与此不同。

TCP是面向字节流的, Seq与Ack的增长均以字节为单位 。在最朴素的实现中,为了减少网络传输, 接收端只回复最后一个连续包的Ack ,并相应移动窗口。比如,发送端发送1,2,3,4,5一共五份数据(假设一份数据一个字节),接收端快速收到了Seq 1, Seq 2,于是回Ack 3,并移动窗口;然后收到了Seq 4,由于在此之前未收到过Seq 3(乱序),如果仍在窗口内,则只填充窗口,但不发送Ack 5,否则丢弃Seq 3(与丢包的效果相似);假设在窗口内,则等以后收到Seq 3时,发现Seq 4及以前的数据包都收到了,则回Ack 5,并移动窗口。

当发送方发现等待Seq 3的Ack(即Ack 4) 超时 后,会认为Seq 3发送“失败”,重传Seq 3 。一旦接收方收到Seq 3,会立即回Ack 4。

这种方式有些问题:假设目前已收到了Seq 4;由于未收到Seq 3,导致发送方重传Seq 3,在收到重传的Seq 3之前,包括新收到的Seq 5和刚才收到的Seq 4都不能回复Ack,很容易引发发送方重传Seq 4、Seq5。接收方之前已经将Seq 4、Seq 5保存到窗口中,此时重传Seq 4、Seq 5明显造成浪费。

也就是说,超时重传机制面临“ 重传一个还是重传所有 ”的问题,即:

可知,两种方法都属于 超时重传机制 ,各有利弊,但二者都需要等待timeout,是 基于时间驱动 的,性能与timeout的长度密切相关。如果timeout很长(普遍情况),则两种方法的性能都会受到较大影响。

最理想的方案是:在超时之前,通过某种机制要求发送方尽快重传timeout的包(即Seq 3),如 快速重传机制 (Fast Retransmit)。这种方案浪费资源(浪费多少取决于“重传一个还是重传所有”,见下),但效率非常高(因为不需要等待timeout了)。

快速重传机制不基于时间驱动,而 基于数据驱动 : 如果包没有连续到达,就Ack最后那个可能被丢了的包;如果发送方连续收到3次相同的Ack,就重传对应的Seq

比如:假设发送方仍然发送1,2,3,4,5共5份数据;接收方先收到Seq 1,回Ack 2;然后Seq 2因网络原因丢失了,正常收到Seq 3,继续回Ack 2;后面Seq 4和Seq 5都到了,最后一个可能被丢了的包还是Seq 2,继续回Ack 2;现在,发送方已经连续收到4次(大于等于3次)相同的Ack(即Ack 2),知道最大序号的未收到包是Seq 2,于是重传Seq 2,并清空Ack 2的计数器;最后,接收方收到了Seq 2,查看窗口发现Seq 3、4、5都收到了,回Ack 6。示意图如下:

快速重传解决了timeout的问题,但依然面临“重传一个还是重传所有”的问题。对于上面的示例来说,是只重传Seq 2呢还是重传Seq 2、3、4、5呢?

如果只使用快速重传,则必须重传所有:因为发送方并不清楚上述连续的4次Ack 2是因为哪些Seq传回来的。假设发送方发出了Seq 1到Seq 20供20份数据,只有Seq 1、6、10、20到达了接收方,触发重传Ack 2;然后发送方重传Seq 2,接收方收到,回复Ack 3;接下来,发送方与接收方都不会再发送任何数据,两端陷入等待。因此,发送方只能选择“重传所有”,这也是某些TCP协议的实际实现,对于带宽未满时重传效率的提升非常明显。

一个更完美的设计是:将超时重传与快速重传结合起来, 触发快速重传时,只重传局部的一小段Seq(局部性原理,甚至只重传一个Seq),其他Seq超时后重传

TCP超时与重传机制

TCP超时与重传机制 
   
  TCP协议是一种面向连接的可靠的传输层协议,它保证了数据的可靠传输,对于一些出错,超时丢包等问题TCP设计的超时与重传机制。其基本原理:在发送一个数据之后,就开启一个定时器,若是在这个时间内没有收到发送数据的ACK确认报文,则对该报文进行重传,在达到一定次数还没有成功时放弃并发送一个复位信号。 
  这里比较重要的是重传超时时间,怎样设置这个定时器的时间(RTO),从而保证对网络资源最小的浪费。因为若RTO太小,可能有些报文只是遇到拥堵或网络不好延迟较大而已,这样就会造成不必要的重传。太大的话,使发送端需要等待过长的时间才能发现数据丢失,影响网络传输效率。 
  由于不同的网络情况不一样,不可能设置一样的RTO,实际中RTO是根据网络中的RTT(传输往返时间)来自适应调整的。具体关系参考相关算法。 
  通过图来了解重传机制: 

技术分享图片

 

从图可以知道,发送方连续发送3个数据包,其中第二个丢失,没有被接收到,因此不会返回对应的ACK,没发送一个数据包,就启动一个定时器,当第二个包的定时器溢出了还没有收到ack,这时就进行重传。

 

TCP慢启动

  慢启动是TCP的一个拥塞控制机制,慢启动算法的基本思想是当TCP开始在一个网络中传输数据或发现数据丢失并开始重发时,首先慢慢的对网路实际容量进行试探,避免由于发送了过量的数据而导致阻塞。 
  慢启动为发送方的TCP增加了另一个窗口:拥塞窗口(congestion window),记为cwnd。当与另一个网络的主机建立TCP连接时,拥塞窗口被初始化为 1个报文段(即另一端通告的报文段大小)。每收到一个ACK,拥塞窗口就增加一个报文段(cwnd以字节为单位,但是慢启动以报文段大小为单位进行增加)。发送方取拥塞窗口与通告窗口中的最小值作为发送上限。拥塞窗口是发送方使用的流量控制,而通告窗口则是接收方使用的流量控制。发送方开始时发送一个报文段,然后等待 ACK。当收到该ACK时,拥塞窗口从1增加为2,即可以发送两个报文段。当收到这两个报文段的 A C K时,拥塞窗口就增加为4。这是一种指数增加的关系。

 

拥塞避免算法

  网络中拥塞的发生会导致数据分组丢失,需要尽量避免。在实际中,拥塞算法与慢启动通常在一起实现,其基本过程: 
   1. 对一个给定的连接,初始化cwnd为1个报文段,ssthresh为65535个字节。 
   2. TCP输出例程的输出不能超过cwnd和接收方通告窗口的大小。拥塞避免是发送方使用 的流量控制,而通告窗口则是接收方进行的流量控制。前者是发送方感受到的网络拥塞的估 计,而后者则与接收方在该连接上的可用缓存大小有关。 
   3. 当拥塞发生时(超时或收到重复确认),ssthresh被设置为当前窗口大小的一半(cwnd 和接收方通告窗口大小的最小值,但最少为2个报文段)。此外,如果是超时引起了拥塞,则 cwnd被设置为1个报文段(这就是慢启动)。 
   4. 当新的数据被对方确认时,就增加cwnd,但增加的方法依赖于是否正在进行慢启动或拥塞避免。如果cwnd小于或等于ssthresh,则正 在进行慢启动,否则正在进行拥塞避免。慢启动一直持续到回到当拥塞发生时所处位置的半时候才停止(因为记录了在步骤2 中制造麻烦的窗口大小的一半),然后转为执行拥塞避免。 
   慢启动算法初始设置cwnd为1个报文段,此后每收到一个确认就加 1。那样,这会使窗口按指数方式增长:发送 1个报文段,然后是2个,接着是4个……。

技术分享图片

在该图中,假定当cwnd为32个报文段时就会发生拥塞。于是设置 ssthresh为16个报文段,而cwnd为1个报文段。在时刻 0发送了一个报文段,并假定在时刻 1接收到它的ACK,此时cwnd增加为2。接着发送了2个报文段,并假定在时刻 2接收到它们的ACK,于是cwnd增加为4(对每个ACK增加1次)。这种指数增加算法一直进行到在时刻3和4之间收到8个ACK后cwnd等于ssthresh时才停止,从该时刻起,cwnd以线性方式增加,在每个往返时间内最多增加 1个报文段。

 

 

 

 

  什么是计时器呢?我们可以理解成一块闹钟,隔一段时间响一次,提醒TCP做特定的事情。TCP要正常工作,必须要有特定的计时器。那么TCP中有哪些计时器呢?

  TCP中有四种计时器(Timer),分别为:

    1.重传计时器:Retransmission Timer

    2.坚持计时器:Persistent Timer

    3.保活计时器:Keeplive Timer

    4.时间等待计时器:Timer_Wait Timer

  (1)重传计时器

    大家都知道TCP是保证数据可靠传输的。怎么保证呢?带确认的重传机制。在滑动窗口协议中,接受窗口会在连续收到的包序列中的最后一个包向接收端发送一个ACK,当网络拥堵的时候,发送端的数据包和接收端的ACK包都有可能丢失。TCP为了保证数据可靠传输,就规定在重传的“时间片”到了以后,如果还没有收到对方的ACK,就重发此包,以避免陷入无限等待中。

  当TCP发送报文段时,就创建该特定报文的重传计时器。可能发生两种情况:

  1.若在计时器截止时间到之前收到了对此特定报文段的确认,则撤销此计时器。

  2.若在收到了对此特定报文段的确认之前计时器截止时间到,则重传此报文段,并将计时器复位。

  (2)持久计时器

  先来考虑一下情景:发送端向接收端发送数据包知道接受窗口填满了,然后接受窗口告诉发送方接受窗口填满了停止发送数据。此时的状态称为“零窗口”状态,发送端和接收端窗口大小均为0.直到接受TCP发送确认并宣布一个非零的窗口大小。但这个确认会丢失。我们知道TCP中,对确认是不需要发送确认的。若确认丢失了,接受TCP并不知道,而是会认为他已经完成了任务,并等待着发送TCP接着会发送更多的报文段。但发送TCP由于没有收到确认,就等待对方发送确认来通知窗口大小。双方的TCP都在永远的等待着对方。

  要打开这种死锁,TCP为每一个链接使用一个持久计时器。当发送TCP收到窗口大小为0的确认时,就坚持启动计时器。当坚持计时器期限到时,发送TCP就发送一个特殊的报文段,叫做探测报文。这个报文段只有一个字节的数据。他有一个序号,但他的序号永远不需要确认;甚至在计算机对其他部分的数据的确认时该序号也被忽略。探测报文段提醒接受TCP:确认已丢失,必须重传。

  坚持计时器的值设置为重传时间的数值。但是,若没有收到从接收端来的响应,则需发送另一个探测报文段,并将坚持计时器的值加倍和复位。发送端继续发送探测报文段,将坚持计时器设定的值加倍和复位,直到这个值增大到门限值(通常是60秒)为止。在这以后,发送端每个60秒就发送一个探测报文,直到窗口重新打开。

  (3)保活计时器

    保活计时器使用在某些实现中,用来防止在两个TCP之间的连接出现长时间的空闲。假定客户打开了到服务器的连接,传送了一些数据,然后就保持静默了。也许这个客户出故障了。在这种情况下,这个连接将永远的处理打开状态。

  要解决这种问题,在大多数的实现中都是使服务器设置保活计时器。每当服务器收到客户的信息,就将计时器复位。通常设置为两小时。若服务器过了两小时还没有收到客户的信息,他就发送探测报文段。若发送了10个探测报文段(每一个像个75秒)还没有响应,就假定客户除了故障,因而就终止了该连接。

  这种连接的断开当然不会使用四次握手,而是直接硬性的中断和客户端的TCP连接。

  (4)时间等待计时器

  时间等待计时器是在四次握手的时候使用的。四次握手的简单过程是这样的:假设客户端准备中断连接,首先向服务器端发送一个FIN的请求关闭包(FIN=final),然后由established过渡到FIN-WAIT1状态。服务器收到FIN包以后会发送一个ACK,然后自己有established进入CLOSE-WAIT.此时通信进入半双工状态,即留给服务器一个机会将剩余数据传递给客户端,传递完后服务器发送一个FIN+ACK的包,表示我已经发送完数据可以断开连接了,就这便进入LAST_ACK阶段。客户端收到以后,发送一个ACK表示收到并同意请求,接着由FIN-WAIT2进入TIME-WAIT阶段。服务器收到ACK,结束连接。此时(即客户端发送完ACK包之后),客户端还要等待2MSL(MSL=maxinum segment lifetime最长报文生存时间,2MSL就是两倍的MSL)才能真正的关闭连接。

 

 

 

 

技术分享图片

 

以上是关于浅谈TCP(1):状态机与重传机制的主要内容,如果未能解决你的问题,请参考以下文章

详解 TCP 超时与重传机制——长文预警

TCP/IP详解 卷1 第二十一章 TCP的超时与重传

TCP超时与重传

TCP/IP 协议——十四章:TCP超时与重传

[转帖]4000字详解TCP超时与重传,看完没收获算我输

当bgp的邻居状态机处于啥状态是,标志着与邻居的tcp连接已经正常建立