TCP的4个定时器
T C P提供可靠的运输层。它使用的方法之一就是确认从另一端收到的数据。但数据和确认都有可能会丢失。 T C P通过在发送时设置一个定时器来解决这种问题。如果当定时器溢出时还没有收到确认,它就重传该数据。
只有数据设置有超时重传定时器。ACK无该定时器。
对每个连接, T C P管理4个不同的定时器。
1) 重传定时器,发送数据端(非发送ACK端),每发送一个数据都将设置一个超时时间,用于超时未收到ACK时进行重传。
2) 坚持定时器,如果接收端窗口为0了(快的发送端和慢的接收端),这时发送端将不再发送数据。随着接收端应用不断读取数据,这时接收端窗口增大了,向发送端通知窗口大小,该包不需要确认。如果该包丢失了,那么发送方将无法知道接收方窗口是否恢复了,这时发送方将发送一个探测包,探测一下窗口大小。(该定时器是发送数据方持有的,在收到通告窗口大小为0后启动,超过一定时间未收到窗口更新报文,将发送探测包。)
3) 保活定时器,可以理解为心跳包,当双方无数据往来时,依靠该定时器来发送心跳包,探测连接是否完好(另一端是否依然存在)。接收端每收到一次数据,就重新设置保活定时器,时间的设置通常是2小时。若2小时没有再收到数据,就发送一个探测报文段,以后每隔75秒发送一次。若连续发送10次都无应答,就认为出现了故障,接着关闭该连接。
4) 2MSL定时器,连接关闭时,主动 发起关闭连接的一方持有。在发送被动关闭FIN的确认时,启动该定时器,同时进入time_wait状态。
往返时间的测量
RTT:round trip time,往返时间。RTT 表示一次重返时间,用来衡量网络的时延。
RTO:retransmissiontimeout,重传超时时间。
目前TCP的实现,重传超时时间都是以RTT的幂次倍来实现的,即RTO = RTT * 2 ^n, n表示第几次重传,即超时时间是指数递增,而非线性递增,RTO的最大值为超过64秒。
需要注意的是,RTT测量采样时,不计入重传的报文段。这是因为,一个报文段一旦重传,无法区分收到的该报文段的ACK对应于哪次发送的报文段。
慢启动和拥塞避免
在实际中,这2个算法通常一起实现。
慢启动,为了控制一个连接初次进入网络(或发生超时后再发包)时的发包速度,控制初始发包速度。
慢启动,发送方通过设置cwnd控制初始发包速度,在不发生拥塞的情况下,发包速度呈指数增长。发送端能够发送的累积未确认的数据量是cwnd和接收端通告窗口大小中小者。慢启动是发送端对流量的控制,拥塞窗口是接收端对流量的控制。
另外需要注意,发送方发送数据的包的大小并不一定都是按照MSS来发送的,发送端能够发送累积未确认的数据量可以很大,但发送端的内核缓冲中,可能并没有这么多的数据需要发送。
拥塞避免算法是一种处理丢失分组的方法。
该算法假定由于分组受到损坏引起的丢失是非常少的(远小于1%),因此分组丢失就意味着在源主机和目的主机之间的某处网络上发生了拥塞。有两种分组丢失的指示:发生超时和接收到重复的确认。如果使用超时作为拥塞指示,则需要使用一个好的RTT算法。
拥塞避免算法和慢启动算法是两个目的不同、独立的算法。但是当拥塞发生时,我们希望降低分组进入网络的传输速率,于是可以调用慢启动来作到这一点。在实际中这两个算法通常在一起实现。
拥塞避免算法和慢启动算法需要对每个连接维持两个变量:一个拥塞窗口cwnd和一个慢启动门限ssthresh(slow start threshold,所谓的慢启动门限就是说,当拥塞窗口超过这个门限的时候,就使用拥塞避免算法,而在门限以内就采用慢启动算法。)。
拥塞避免算法:
1) 对一个给定的连接,初始化cwnd为1个报文段,ssthresh为65535个字节。
2) TCP输出例程的输出不能超过 cwnd和接收方通告窗口的大小。拥塞避免是发送方使用的流量控制,而通告窗口则是接收方进行的流量控制。前者是发送方感受到的网络拥塞的估计,而后者则与接收方在该连接上的可用缓存大小有关。
3) 当拥塞发生时(超时或收到重复确认),ssthresh被设置为当前窗口大小的一半(c w n d和接收方通告窗口大小的最小值,但最小为2个报文段)。此外,如果是超时引起了拥塞,则cwnd被设置为1个报文段(这就是慢启动)。
4) 当新的数据被对方确认时,就增加cwnd,但增加的方法依赖于我们是否正在进行慢启动或拥塞避免。如果 cwnd小于或等于ssthresh,则正在进行慢启动,否则正在进行拥塞避免。慢启动一直持续到cwnd回到当拥塞发生时所处位置的一半时候才停止(因为我们记录了在步骤 2中给我们制造麻烦的窗口大小的一半,因为此刻我们的慢启动门限ssthresh就是该大小,其实就是达到ssthresh之前执行慢启动,达到则启动之后,执行拥塞避免),然后转为执行拥塞避免。
慢启动算法初始设置 cwnd为1个报文段,此后每收到一个确认就加 1。这会使cwnd按指数方式增长。
拥塞避免算法要求每次收到一个确认时将 cwnd增加1 /cwnd(当cwnd个MSS都收到确认的时候,就相当于cwnd增加了1)。与慢启动的指数增加比起来,这是一种加性增长(additive increase)。在拥塞避免阶段,在一个往返时间内最多为cwnd增加1个报文段(不管在这个RT T中收到了多少个ACK),然而慢启动将根据这个往返时间中所收到的确认的个数增加cwnd。
注释:针对3),当拥塞发生(超时或重复ACK)时,ssthresh被设置为当前窗口的一半(这里的当前窗口 指的是cwnd和接收方通告窗口大小中的小者),但最小为2个报文段。如果是超时引起的拥塞,那么cwnd被设置为1个报文段;如果是重复ACK引起的拥塞,这时cwnd被设置为ssthresh+3,其中ssthresh是更新后的值,3是3 个报文段(MMS)。
注意:重复ACK的计算,比如ACK=40为重复ACK,当第一次收到ACK等于40时,是正常的,因为是第一次收到,不知道后面还后再发来ACK=40,因此第一次收到时,会增加cwnd(策略取决于当前cwnd与ssthresh的大小,无论哪种方式,cwnd都是连续增长的,而非跳跃式增长);当第二次和第三次收到ACK=40时,cwnd不会变,ssthresh也不变;当第四次收到ACK=40时,才认为发生了拥塞(这时才认为是收到了重复的ACK,即累积收到该ACK3次或以上,这从后面的快速重传和快速恢复算法可以看出)。这时,才会执行ssthresh变为原来的一半,而cwnd变为ssthresh+3个报文段大小。
一个报文段第一次的ACK不计入重复ACK。如收到3个重复ACK,则表示总共收到了4个该报文段的ACK。
另外,可以看出,在cwnd<=接收端通告窗口时,是由发送端的cwnd控制发送速度;当cwnd>接收端通告窗口时,是接收端通告窗口控制发送速度。
注意:拥塞发生的两种情况(一定要理解):
超时,是发送端发送了数据后,启动重传定时器,在规定时间内未收到ACK,于是发生了超时,可见,超时是发送端自己感知并触发;
重复ACK是指在接收方收到乱序报文时,所发出的一类TCP报文。重复ACK,是因为数据未按序到达接收端造成的,是由于接收端触发的,是对到目前为止收到的连续数据段的确认,重复ACK,是接收端触发并通知到发送端;例如,现在接收端已经收到0~35个字节,已给客户端回复ACK=36,第36以后的字节未收到;但这时收到了40~43字节,但由于第36~39字节并未收到,于是回复给发送端的ACK仍是ACK=36,这对发送端来说,ACK=36即是一个重复ACK;这时,又收到了44~47字节,仍是因为未收到36~39字节,所以必须回复给发送端ACK=36,这对发送端来说,ACK=36,又是一个重复确认的ACK。
另外,重复ACK也可以是接收端收到同一个数据包时的确认,这时,发送端无更多的后续数据需要发送。见下面的解释(有点勉强,如果不理解,可以去除,)
重复ACK,不是指“同一个数据包,收到多次,并对其确认多次”。但也存在这种情况,如接收端已回复ACK=60,发送端一直未收到该包的ACK,一直超时重传该包,且应用程序没有发送更多的数据,此时发送端TCP协议栈只有该超时的数据包要发送,无后续数据包,接收端一直收到该包,回复ACK(假设该ACK在回来的路上一直丢失),这时勉强可以将重复ACK理解为对同一包的确认。
针对4),对于3中设置的cwnd值,发送方根据该值可累积发送但未确认的cwnd个数据包(在实现上是先发根据老的cwnd值发送完数据包,然后再设置cwnd值)。每收到一个回复的ACK,就增加cwnd的值,增加的大小根据当前cwnd值与ssthresh的值确定。如果当前cwnd值小于ssthresh的值,处于慢启动阶段,则每次增加1个MSS的大小;如果当前cwnd值大于ssthresh的值,处于拥塞避免阶段,则每次增加1/cwnd个MSS的大小。需要注意:无论是哪种增加方式,cwnd的增长都是连续的,而非跳跃的;cwnd在这里被表示为以报文段为单位,其实际上是以字节为单位的。
同时可以看出,在3)中,如果是超时引起的拥塞,cwnd被设置为1个报文段,肯定小于ssthresh(最小是2个报文段),这时进入的是慢启动;如果是重复ACK引起的拥塞,cwnd被设置为ssthresh+3,这时进入的是拥塞避免;
1个报文段的大小,是 对方通告的MMS值的大小,与本地的MTU值的比较,取小者做为一个报文段的大小。
发送端能够发送的累积未收到ACK的数据量大小,受cwnd和接收通知窗口(对方滑动窗口的未使用大小)大小的限制,取二者之中的小值。
下图是慢启动和拥塞避免的一个可视化描述。我们以段为单位来显示cwnd和ssthresh,但它们实际上都是以字节为单位进行维护的。
在该图中,假定当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个报文段。
正如我们在这个图中看到的那样,术语“慢启动”并不完全正确。它只是采用了比引起拥塞更慢些的分组传输速率,但在慢启动期间进入网络的分组数增加的速率仍然是在增加的。只有在达到ssthresh拥塞避免算法起作用时,这种增加的速率才会慢下来。
cwnd的值可持续往上增长,可超过接收端的通告窗口大小。但即使cwnd大于了接收端通告窗口大小,但能够发送的累积未确认的字节数也只能是cwnd和接收端通告窗口大小的小者。当发生拥塞避免时,cwnd会再次被拉下,被拉下时,ssthresh的值会设置为cwnd和接收方通告窗口大小中小者的一半。
快速重传和快速恢复
在收到一个失序的报文段时, T C P立即需要产生一个 A C K(一个重复的 A C K)。这个重复的 A C K不应该被迟延。该重复的 A C K的目的在于让对方知道收到一个失序的报文段,并告诉对方自己希望收到的序号。
下面这段解释了为什么是3个重复的ACK。
由于发送端不知道一个重复的 A C K是由一个丢失的报文段引起的,还是由于仅仅出现了几
个报文段的重新排序,因此我们等待少量重复的 A C K到来。假如这只是一些报文段的重新排序,则在重新排序的报文段被处理并产生一个新的 A C K之前,只可能产生 1 ~ 2个重复的 A C K。如果一连串收到 3个或3个以上的重复 A C K,就非常可能是一个报文段丢失了。于是我们就重传丢失的数据报文段,而无需等待超时定时器溢出。
上段也反映出了重复ACK的两个原因:1. 报文段失序; 2. 报文段丢失(阻塞在网络中、损坏或TTL为0等)。
快速重传,即是收到3个或3个以上的重复ACK,就重传丢失的报文,而不等待定时器超时(定时器超时可能要等待6秒)。这样比等待定时器超时后再重传要快一点,因此叫快速重传。
快速重传后执行拥塞避免算法,即是快速恢复,因为拥塞避免算法,累积发送未确认包的起点较高(大于或等于ssthresh),相对于慢启动从1个报文段的cwnd大小累积发送未确认包的数量来说,对于发送速度的恢复更快,因此是快速恢复。
快速重传和快速恢复算法
1) 当收到第3个重复的A C K时,将s s t h re s h设置为当前拥塞窗口 c w n d的一半。重传丢失的报文段。设置c w n d为s s t h re s h加上3倍的报文段大小。
2) 每次收到另一个重复的 A C K时, c w n d增加1个报文段大小并发送 1个分组(如果新的c w n d允许发送)。
3) 当下一个确认新数据的 A C K到达时,设置c w n d为s s t h re s h(在第1步中设置的值)。这个A C K应该是在进行重传后的一个往返时间内对步骤 1中重传的确认。另外,这个 A C K也应该是对丢失的分组和收到的第 1个重复的A C K之间的所有中间报文段的确认。这一步采用的是拥塞避免,因为当分组丢失时我们将当前的速率减半。
注意,这里是将快速重传和快速恢复算法一起来讲的。
1),当收到第3个重复ACK时,将ssthresh设置为当前的拥塞窗口cwnd的一半(注意,这里是设置为cwnd的一半,而不是cwnd和接收方通告窗口中小者的一半;在拥塞避免中,是设置为二者中小者的一半)。
重传丢失的报文段。这里丢失的报文段,其实就是重复ACK所在的报文段;之所以收到重复ACK可能是该报文段丢失或报文段失序。
设置cwnd为ssthresh加3个报文段(MMS)大小。这里要理解为什么这样取值?
发送端之所以能够收到重复的ACK确认,是因为接收端有数据报(segment)被收到,但这些数据包是乱序的;虽然乱序,但是接收端收到了,这也证明,其实网络总体是好的,只是丢失了极小部分包(比如该重复ACK所在的包),因此是没有必要进行慢启动的(虽然慢启动能够较短时间内达到较快的发包速度,这里仍认为是一种浪费),那也就是说要进入拥塞避免算法,拥塞避免的条件是cwnd要大于ssthresh,因此cwnd至少要和ssthresh的值相等;那么这里为什么要加3呢?
上面解释了,收到重复ACK,证明网络整体状况是好的,所以在理解上可以将重复的ACK当做正常的ACK。这里是收到第3个重复的ACK,相当于3个正常的ACK,所以加3.(结合上面黄色背景的文字理解)
注意:步骤1)与后面的拥塞举例5有冲突。这里说设置cwnd为更新后的ssthresh的值+3;而在拥塞举例5中,cwnd为原来cwnd(未减半,减半了即和ssthresh的值相等)的值+3,而在拥塞举例5所配的文字说明中,也是以更新后的ssthresh为准。
以cwnd=ssthresh+3为准。
2),每次收到另一个重复的ACK,cwnd增加一个报文段。这里仍是将重复的ACK,当做是正常的ACK,cwnd增加1个报文段(注意:这时的cwnd是大于ssthresh的,这里是快速重传,而不使用拥塞避免的条件来限制)。
注意:如果一直收到重复的ACK,那么必将引起丢失的数据包超时,(由于超时而引起的拥塞),进入慢启动状态。
3),当下一个确认新数据的ACK到达时,设置cwnd为ssthresh。这里要理解,下一个确认新数据的ACK所代表的含义。上面步骤中,一直接收到的都是重复ACK。这里新数据的ACK(是一个新的ACK值),不再是上面的重复ACK。这个新的ACK能够收到,表明接收端已经收到了步骤1)中重传的丢失的报文。同时,该新的ACK之前的包都已经收到。
收到新确认的ACK,设置cwnd为ssthresh。表明网络丢包失序等现象基本消失,网络总体正常,将进入拥塞避免阶段。而进入拥塞避免的条件就是,cwnd>=ssthresh。既然cwnd本来就ssthresh要大(由步骤1和2可知,在设置cwnd为ssthresh之前,cwnd-ssthresh应>=3,可见,这里又将cwnd减小了),那么为什么这里还要重新设置呢?这是因为,虽然丢失的包通过重传已顺利到达接收方,但网络的状况,可能仍不太适合一次发送大量的包(发送端能够发送的累积未确认包由cwnd和接收方通告窗口中的小者决定),因此这里要将cwnd减小(但同时cwnd应>=ssthresh,因为要保证进入拥塞避免算法)。这时,进入拥塞避免算法,即是快速恢复。
拥塞避免举例
cwnd实际上是以字节为单位来表示的。而且,在进入拥塞避免阶段后,cwnd的值不再是MSS的整数倍了。
在该例中,建立连接时,第一个SYN超时。
假设MMS为256,发送端(发起连接端)初始设置cwnd为256(即1个报文段),ssthresh初始值设为65535,然后发出第一个SYN包。但该SYN包超时了,在定时器提示超时后,设置ssthresh为cwnd的一半,但ssthresh最小也必须是512(即2个报文段),于是设置ssthresh为512。接着发出第2个SYN包。然后收到了接收端的SYN+ACK包,这时自己这端的cwnd仍为256,仍处理慢启动,于是发出数据包,在发出数据包后接着设置自己这一端的ssthresh为2个报文段。(注意,这里在实现上是:接收到对端发来的数据包,参考自己当前的cwnd值,发出数据包,然后再设置自己的cwnd值------先发数据包后改cwnd)。
这里假设左端为A,右端为B。 A的第一个数据包超时了,在第2个数据包发出去之前,A端的cwnd为1个报文段,ssthresh为2个报文段。然后发出第2个数据包,在收到B的第1个数据包(SYN+ACK)后,A参考自己当前的cwnd值进行发包,这时,A端的cwnd值仍为1个报文段,于是A端发出第3个数据包。由于刚才收到了B发来的数据包,A需要设置自己的cwnd值,于是就在A发出第3个数据包后,设置自己的cwnd值,由于自己当前的cwnd值为1,而ssthresh值为2,可见还处于慢启动状态,于是按照慢启动的方式,将自己的cwnd值设置为当前cwnd值加1,即cwnd=cwnd + 1。
该例说明,这些拥塞的算法,在建立连接时就在起作用,而不止是建立连接后正常的数据收发才起作用。
另外,还需要注意,在发送端接收到对端的ACK后,虽然cwnd值增大(在收到ACK后,又发送了数据之后才增大的,即先发送数据后改的cwnd)了,但下次发送数据时,也许并不会发cwnd和对端通告窗口大小的数据量,这时因为socket内核缓冲中,可能并没有这么多的数据需要发送。还需要注意的一点,TCP发送数据,是流式的方式写入,在内核缓冲区中数据量足够的情况下,会按照MSS的大小来发送一个segment,但如果缓冲区的数据不够一个MSS的大小,也将做为一个segment被发送出去。
进入拥塞避免后,cwnd的计算方法,理论上并不是直接增加1/cwnd个报文段的大小,上面是计算公式。但为了计算机计算的方便及效率,直接当做1/cwnd个报文段的大小。
快速重传和快速恢复举例
该图中,两条线,一条表示cwnd的变化,一条表示序号(Seq,发送端序号)的变化。
Cwnd线,连续4个平点的地方,表示收到了3个重复的ACK,第一个是正常的ACK。
Seq线,凹点处,表示在收到3个重复ACK后,进入了快速重传丢失的数据包(未等定时器超时)。
在重传了丢失的数据包后,又收到了重复的ACK,这时每收到一个重复ACK,cwnd就增加1个报文段。(需要注意的是,这时的cwnd是大于ssthresh的,这里是快速重传算法,不使用拥塞避免算法的约束条件:“cwnd>ssthresh时,执行拥塞避免,cwnd += 1/cwnd”)
注意:收到重复的数据包后,ssthresh的值设置为当前cwnd的一半。另外(在快速重传算法那一章节,讲到在设置了ssthresh的值为当前cwnd的一半后,设置cwnd为ssthresh+3,ssthresh为更新后的值。但从该图上看,cwnd在重传完丢失的数据包后,cwnd增加的基础仍是原来的cwnd+3,而不是ssthresh+3). 以快速重传算法处为准,即cwnd设为ssthresh+3。
TCP中的ICMP差错报文
T C P能够遇到的最常见的I C M P差错就是源站抑制、主机不可达和网络不可达。
ICMP源站抑制报文:当目标主机的处理速度赶不上数据接收的速度,因为接受主机的IP层缓存会被占满,所以主机就会发出一个“我受不了”的一个ICMP报文。
T C P忽略I C M P主机不可达的差错并坚持重传。“软”的 I C M P差错没有引起 T C P连接终止,但这些差错被保存以便在连接非正常中止时能够报告这些软差错。即,TCP在重传过程中,会忽略ICMP主机不可达差错,但这些差错也被记录了下来,一旦最终重传失败(超过最大重传次数),这些ICMP差错报文,将会被反馈给应用层。
注意
对于一条TCP连接,如果一个中间设备给发送方回复了远端主机不可达,那么发送方会忽略掉该报文,认为网络只是暂时的不可达,而不是将TCP连接关闭。
如果是对于目标主机发来的主机不可达或端口不可达的处理,TCP会关闭该连接。
TCP拥塞控制算法融合
通常,我们说TCP拥塞控制的算法有4个。但事实上,这4个算法并非各自孤立的,而是互相融入的,在实现时,通常可以实在在一起。尤其中,慢启动和拥塞避免,快速重传和快速恢复。