滑动窗口是 接受数据 端使用的窗口大小,用来告知发送端接收端的缓存大小,以此可以控制发送端发送数据的大小,从而达到流量控制的目的,对应==>rwnd:接收端窗口(receiver window)
对于流量控制,是一个端对端的概念。由接收端返回的rwnd控制。
那么对于数据的 发送数据 端就是拥塞窗口,拥塞窗口不代表缓存,拥塞窗口指某一源端数据流在一个RTT内可以最多发送的 数据包 数,cwnd:发送端窗口( congestion window )。
为了保证顺序性,每⼀个包都有⼀个ID。在建⽴连接的时候,会商定起始的ID是什么,然后 按照ID⼀个个发送。为了保证不丢包,对于发送的包都要进⾏应答,但是这个应答也不是⼀个⼀个来的,⽽是会应答某个之前 的ID,表示都收到了,这种模式称为累计确认或者累计应答(cumulative acknowledgment)。 为了记录所有发送的包和接收的包,TCP也需要发送端和接收端分别都有缓存来保存这些记录。发送端的缓存⾥是按照包的ID ⼀个个排列,根据处理的情况分成四个部分。 第⼀部分:发送了并且已经确认的。这部分就是你交代下属的,并且也做完了的,应该划掉的。 第⼆部分:发送了并且尚未确认的。这部分是你交代下属的,但是还没做完的,需要等待做完的回复之后,才能划掉。 第三部分:没有发送,但是已经等待发送的。这部分是你还没有交代给下属,但是⻢上就要交代的。 第四部分:没有发送,并且暂时还不会发送的。这部分是你还没有交代给下属,⽽且暂时还不会交代给下属的。 在TCP⾥,接收端会给发送端报⼀个窗⼝的⼤⼩,叫Advertised window。这个窗 ⼝的⼤⼩应该等于上⾯的第⼆部分加上第三部分,就是已经交代了没做完的加上⻢上要交代的。超过这个窗⼝的,接收端做不 过来,就不能发送了。 于是,发送端需要保持下⾯的数据结构。
![image-20200308202456487](
file://C:/Users//AppData/Roaming/Typora/typora-user-images/image-20200308202456487.png?lastModify=1583671098)LastByteAcked:第⼀部分和第⼆部分的分界线 LastByteSent:第⼆部分和第三部分的分界线 LastByteAcked + AdvertisedWindow:第三部分和第四部分的分界线 对于接收端来讲,它的缓存⾥记录的内容要简单⼀些。
拥塞控制:拥塞控制是作用于网络的,它是防止过多的数据注入到网络中,避免出现网络负载过大的情况;常用的方法就是:
流量控制:流量控制是作用于接收者的,它是控制发送者的发送速度从而使接收者来得及接收,防止分组丢失的。
第⼀部分:接受并且确认过的。也就是我领导交代给我,并且我做完的。 第⼆部分:还没接收,但是⻢上就能接收的。也即是我⾃⼰的能够接受的最⼤⼯作量。 第三部分:还没接收,也没法接收的。也即超过⼯作量的部分,实在做不完。 对应的数据结构就像这样。
![image-20200308202704928](file://C:/Users/TYH/AppData/Roaming/Typora/typora-user-images/image-20200308202704928.png?lastModify=1583671098)
MaxRcvBuffer:最⼤缓存的量; LastByteRead之后是已经接收了,但是还没被应⽤层读取的; NextByteExpected是第⼀部分和第⼆部分的分界线。 第⼆部分的窗⼝有多⼤呢? NextByteExpected和LastByteRead的差其实是还没被应⽤层读取的部分占⽤掉的MaxRcvBuffer的量,我们定义为A。 AdvertisedWindow其实是MaxRcvBuffer减去A。 也就是:AdvertisedWindow=MaxRcvBuffer-((NextByteExpected-1)-LastByteRead)。 那第⼆部分和第三部分的分界线在哪⾥呢?NextByteExpected加AdvertisedWindow就是第⼆部分和第三部分的分界线,其实 也就是LastByteRead加上MaxRcvBuffer。 其中第⼆部分⾥⾯,由于受到的包可能不是顺序的,会出现空挡,只有和第⼀部分连续的,可以⻢上进⾏回复,中间空着的部 分需要等待,哪怕后⾯的已经来了。
接收方每次收到数据包,可以在发送确定报文的时候,同时告诉发送方自己的缓存区还剩余多少是空闲的,我们也把缓存区的剩余大小称之为接收窗口大小,用变量win来表示接收窗口的大小。
发送方收到之后,便会调整自己的发送速率,也就是调整自己发送窗口的大小,当发送方收到接收窗口的大小为0时,发送方就会停止发送数据,防止出现大量丢包情况的发生。
当发送方停止发送数据后,该怎样才能知道自己可以继续发送数据?
我们可以采用这样的策略:当接收方处理好数据,接受窗口 win > 0 时,接收方发个通知报文去通知发送方,告诉他可以继续发送数据了。当发送方收到窗口大于0的报文时,就继续发送数据。
不过这时候可能会遇到一个问题,假如接收方发送的通知报文,由于某种网络原因,这个报文丢失了,这时候就会引发一个问题:接收方发了通知报文后,继续等待发送方发送数据,而发送方则在等待接收方的通知报文,此时双方会陷入一种僵局。
为了解决这种问题,我们采用了另外一种策略:当发送方收到接受窗口 win = 0 时,这时发送方停止发送报文,并且同时开启一个定时器,每隔一段时间就发个测试报文去询问接收方,打听是否可以继续发送数据了,如果可以,接收方就告诉他此时接受窗口的大小;如果接受窗口大小还是为0,则发送方再次刷新启动定时器。
我们在开始假定:
cwnd的大小取决于网络的拥塞程度,并且动态地在变化。发送方让自己的发送窗口等于拥塞窗口,另外考虑到接受方的接收能力,发送窗口可能小于拥塞窗口。
慢开始算法的思路就是,不要一开始就发送大量的数据,先探测一下网络的拥塞程度,也就是说由小到大逐渐增加拥塞窗口的大小。
这里用报文段的个数作为拥塞窗口的大小举例说明慢开始算法,实际的拥塞窗口大小是以字节为单位的。
一个传输轮次所经历的时间其实就是往返时间RTT,而且没经过一个传输轮次(transmission round),拥塞窗口cwnd就加倍。
为了防止cwnd增长过大引起网络拥塞,还需设置一个慢开始门限ssthresh状态变量。ssthresh的用法如下:当cwnd<ssthresh时,使用慢开始算法。 当cwnd>ssthresh时,改用拥塞避免算法。 当cwnd=ssthresh时,慢开始与拥塞避免算法任意
注意,这里的“慢”并不是指cwnd的增长速率慢,而是指在TCP开始发送报文段时先设置cwnd=1,然后逐渐增大,这当然比按照大的cwnd一下子把许多报文段突然注入到网络中要“慢得多”。
拥塞避免算法让拥塞窗口缓慢增长,即每经过一个往返时间RTT就把发送方的拥塞窗口cwnd加1,而不是加倍。这样拥塞窗口按线性规律缓慢增长。
无论是在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞(其根据就是没有按时收到确认,虽然没有收到确认可能是其他原因的分组丢失,但是因为无法判定,所以都当做拥塞来处理),就把慢开始门限ssthresh设置为出现拥塞时的发送窗口大小的一半(但不能小于2)。然后把拥塞窗口cwnd重新设置为1,执行慢开始算法。这样做的目的就是要迅速减少主机发送到网络中的分组数,使得发生拥塞的路由器有足够时间把队列中积压的分组处理完毕。
乘法减小(Multiplicative Decrease)和加法增大(Additive Increase)
“乘法减小”指的是无论是在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞,就把慢开始门限ssthresh设置为出现拥塞时的发送窗口大小的一半,并执行慢开始算法,所以当网络频繁出现拥塞时,ssthresh下降的很快,以大大减少注入到网络中的分组数。“加法增大”是指执行拥塞避免算法后,使拥塞窗口缓慢增大,以防止过早出现拥塞。常合起来成为AIMD算法。
注意:“拥塞避免”并非完全能够避免了阻塞,而是使网络比较不容易出现拥塞。
滑动窗口是接受数据端使用的窗口大小,用来告知发送端接收端的缓存大小,以此可以控制发送端发送数据的大小,从而达到流量控制的目的,对应==>rwnd:接收端窗口(receiver window)
对于流量控制,是一个端对端的概念。由接收端返回的rwnd控制。
那么对于数据的发送数据端就是拥塞窗口,拥塞窗口不代表缓存,拥塞窗口指某一源端数据流在一个RTT内可以最多发送的 数据包 数,cwnd:发送端窗口( congestion window )。
为了保证顺序性,每⼀个包都有⼀个ID。在建⽴连接的时候,会商定起始的ID是什么,然后 按照ID⼀个个发送。为了保证不丢包,对于发送的包都要进⾏应答,但是这个应答也不是⼀个⼀个来的,⽽是会应答某个之前 的ID,表示都收到了,这种模式称为累计确认或者累计应答(cumulative acknowledgment)。 为了记录所有发送的包和接收的包,TCP也需要发送端和接收端分别都有缓存来保存这些记录。发送端的缓存⾥是按照包的ID ⼀个个排列,根据处理的情况分成四个部分。第⼀部分:发送了并且已经确认的。这部分就是你交代下属的,并且也做完了的,应该划掉的。第⼆部分:发送了并且尚未确认的。这部分是你交代下属的,但是还没做完的,需要等待做完的回复之后,才能划掉。第三部分:没有发送,但是已经等待发送的。这部分是你还没有交代给下属,但是⻢上就要交代的。第四部分:没有发送,并且暂时还不会发送的。这部分是你还没有交代给下属,⽽且暂时还不会交代给下属的。在TCP⾥,接收端会给发送端报⼀个窗⼝的⼤⼩,叫Advertised window。这个窗 ⼝的⼤⼩应该等于上⾯的第⼆部分加上第三部分,就是已经交代了没做完的加上⻢上要交代的。超过这个窗⼝的,接收端做不 过来,就不能发送了。于是,发送端需要保持下⾯的数据结构。
LastByteAcked:第⼀部分和第⼆部分的分界线 LastByteSent:第⼆部分和第三部分的分界线 LastByteAcked + AdvertisedWindow:第三部分和第四部分的分界线 对于接收端来讲,它的缓存⾥记录的内容要简单⼀些。
拥塞控制:拥塞控制是作用于网络的,它是防止过多的数据注入到网络中,避免出现网络负载过大的情况;常用的方法就是:
流量控制:流量控制是作用于接收者的,它是控制发送者的发送速度从而使接收者来得及接收,防止分组丢失的。
第⼀部分:接受并且确认过的。也就是我领导交代给我,并且我做完的。第⼆部分:还没接收,但是⻢上就能接收的。也即是我⾃⼰的能够接受的最⼤⼯作量。第三部分:还没接收,也没法接收的。也即超过⼯作量的部分,实在做不完。对应的数据结构就像这样。
MaxRcvBuffer:最⼤缓存的量; LastByteRead之后是已经接收了,但是还没被应⽤层读取的; NextByteExpected是第⼀部分和第⼆部分的分界线。 第⼆部分的窗⼝有多⼤呢?NextByteExpected和LastByteRead的差其实是还没被应⽤层读取的部分占⽤掉的MaxRcvBuffer的量,我们定义为A。 AdvertisedWindow其实是MaxRcvBuffer减去A。 也就是:AdvertisedWindow=MaxRcvBuffer-((NextByteExpected-1)-LastByteRead)。 那第⼆部分和第三部分的分界线在哪⾥呢?NextByteExpected加AdvertisedWindow就是第⼆部分和第三部分的分界线,其实 也就是LastByteRead加上MaxRcvBuffer。 其中第⼆部分⾥⾯,由于受到的包可能不是顺序的,会出现空挡,只有和第⼀部分连续的,可以⻢上进⾏回复,中间空着的部 分需要等待,哪怕后⾯的已经来了。
接收方每次收到数据包,可以在发送确定报文的时候,同时告诉发送方自己的缓存区还剩余多少是空闲的,我们也把缓存区的剩余大小称之为接收窗口大小,用变量win来表示接收窗口的大小。
发送方收到之后,便会调整自己的发送速率,也就是调整自己发送窗口的大小,当发送方收到接收窗口的大小为0时,发送方就会停止发送数据,防止出现大量丢包情况的发生。
640830×573
当发送方停止发送数据后,该怎样才能知道自己可以继续发送数据?
我们可以采用这样的策略:当接收方处理好数据,接受窗口 win > 0 时,接收方发个通知报文去通知发送方,告诉他可以继续发送数据了。当发送方收到窗口大于0的报文时,就继续发送数据。
不过这时候可能会遇到一个问题,假如接收方发送的通知报文,由于某种网络原因,这个报文丢失了,这时候就会引发一个问题:接收方发了通知报文后,继续等待发送方发送数据,而发送方则在等待接收方的通知报文,此时双方会陷入一种僵局。
为了解决这种问题,我们采用了另外一种策略:当发送方收到接受窗口 win = 0 时,这时发送方停止发送报文,并且同时开启一个定时器,每隔一段时间就发个测试报文去询问接收方,打听是否可以继续发送数据了,如果可以,接收方就告诉他此时接受窗口的大小;如果接受窗口大小还是为0,则发送方再次刷新启动定时器。
640867×696
我们在开始假定:
cwnd的大小取决于网络的拥塞程度,并且动态地在变化。发送方让自己的发送窗口等于拥塞窗口,另外考虑到接受方的接收能力,发送窗口可能小于拥塞窗口。
慢开始算法的思路就是,不要一开始就发送大量的数据,先探测一下网络的拥塞程度,也就是说由小到大逐渐增加拥塞窗口的大小。
这里用报文段的个数作为拥塞窗口的大小举例说明慢开始算法,实际的拥塞窗口大小是以字节为单位的。
一个传输轮次所经历的时间其实就是往返时间RTT,而且没经过一个传输轮次(transmission round),拥塞窗口cwnd就加倍。
为了防止cwnd增长过大引起网络拥塞,还需设置一个慢开始门限ssthresh状态变量。ssthresh的用法如下:当cwnd<ssthresh时,使用慢开始算法。当cwnd>ssthresh时,改用拥塞避免算法。当cwnd=ssthresh时,慢开始与拥塞避免算法任意
注意,这里的“慢”并不是指cwnd的增长速率慢,而是指在TCP开始发送报文段时先设置cwnd=1,然后逐渐增大,这当然比按照大的cwnd一下子把许多报文段突然注入到网络中要“慢得多”。
拥塞避免算法让拥塞窗口缓慢增长,即每经过一个往返时间RTT就把发送方的拥塞窗口cwnd加1,而不是加倍。这样拥塞窗口按线性规律缓慢增长。
无论是在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞(其根据就是没有按时收到确认,虽然没有收到确认可能是其他原因的分组丢失,但是因为无法判定,所以都当做拥塞来处理),就把慢开始门限ssthresh设置为出现拥塞时的发送窗口大小的一半(但不能小于2)。然后把拥塞窗口cwnd重新设置为1,执行慢开始算法。这样做的目的就是要迅速减少主机发送到网络中的分组数,使得发生拥塞的路由器有足够时间把队列中积压的分组处理完毕。
乘法减小(Multiplicative Decrease)和加法增大(Additive Increase)
“乘法减小”指的是无论是在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞,就把慢开始门限ssthresh设置为出现拥塞时的发送窗口大小的一半,并执行慢开始算法,所以当网络频繁出现拥塞时,ssthresh下降的很快,以大大减少注入到网络中的分组数。“加法增大”是指执行拥塞避免算法后,使拥塞窗口缓慢增大,以防止过早出现拥塞。常合起来成为AIMD算法。
注意:“拥塞避免”并非完全能够避免了阻塞,而是使网络比较不容易出现拥塞。
一个连接的时延带宽积可表示为:capacity(b)=bandwidth(b/s)×round-triptime(s)。也可称它为两端的管道大小。具有大的带宽时延乘积的网络被称为长肥网络(LongFatNetwork,即LFN),而一个运行在LFN上的TCP连接被称为长肥管道。使用长肥管道会遇到多种问题。
快重传要求接收方在收到一个失序的报文段后就立即发出重复确认(为的是使发送方及早知道有报文段没有到达对方,可提高网络吞吐量约20%)而不要等到自己发送数据时捎带确认。快重传算法规定,发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段,而不必继续等待设置的重传计时器时间到期。如下图:
另外一种更好的方式叫:Selective Acknowledgment (SACK)(参看RFC 2018),这种方式需要在TCP头里加一个SACK的东西,ACK还是Fast Retransmit的ACK,SACK则是汇报收到的数据碎版。参看下图:
这样,在发送端就可以根据回传的SACK来知道哪些数据到了,哪些没有到。于是就优化了Fast Retransmit的算法。当然,这个协议需要两边都支持。在 linux下,可以通过tcp_sack参数打开这个功能(Linux 2.4后默认打开)。
这里还需要注意一个问题——接收方Reneging,所谓Reneging的意思就是接收方有权把已经报给发送端SACK里的数据给丢了。这样干是不被鼓励的,因为这个事会把问题复杂化了,但是,接收方这么做可能会有些极端情况,比如要把内存给别的更重要的东西。所以,发送方也不能完全依赖SACK,还是要依赖ACK,并维护Time-Out,如果后续的ACK没有增长,那么还是要把SACK的东西重传,另外,接收端这边永远不能把SACK的包标记为Ack。
注意:SACK会消费发送方的资源,试想,如果一个攻击者给数据发送方发一堆SACK的选项,这会导致发送方开始要重传甚至遍历已经发出的数据,这会消耗很多发送端的资源。详细的东西请参看《TCP SACK的性能权衡》。
重传定时器是用来计算TCP报文段的超时重传时间的(至于超时重传时间的确定,这里涉及到一大堆的算法,我这里不细谈了)。每发送一个报文段就会启动重传定时器,如果在定时器时间到后还没收到对该报文段的确认,就重传该报文段,并将重传定时器复位,重新计算;如果在规定时间内收到了对该报文段的确认,则撤销该报文段的重传定时器。
专门为对付零窗口通知而设立的。
主要是为了应付零窗口大小通知可能导致的死锁问题。如果接收端在向发送端发送了零窗口报文段后不久,接收端的接收缓存又有了一些存储空间,于是接收端向发送端发送了一个非零窗口大小的报文段,然而这个报文段在传送过程中丢失了,发送端没有收到该报文段,就一直等待接收端发送非零窗口的报文通知,而接收端并不知道报文段丢失了,而是觉得已经告诉发送端了,就会一直等待发送端发送数据,如果没有任何措施的话,这话死锁的局面会一直延续下去。
为了解决这个问题,TCP为每一个连接设有一个坚持定时器(也叫持续计数器)。只要TCP连接的一方收到对方的零窗口通知,就启动坚持定时器。若坚持定时器设置的时间到期,就发送一个零窗口控测报文段(该报文段只有一个字节的数据,它有一个序号,但该序号永远不需要确认,因此该序号可以持续重传),之后会出现以下三种情况:
保活定时器是为了应对两个TCP连接间出现长时间的没有数据传输的情况。如果客户已与服务器建立了TCP连接,但后来客户端主机突然故障,则服务器就不能再收到客户端发来的数据了,而服务器肯定不能这样永久地等下去,保活定时器就是用来解决这个问题的。服务器每收到一次客户端的数据,就重新设置保活定时器,通常为2小时,如果2小时没有收到客户端的数据,服务端就发送一个探测报文,以后每隔75秒发送一次,如果连续发送10次探测报文段后仍没有收到客户端的响应,服务器就认为客户端出现了故障,就可以终止这个连接。
2MSL定时器测量一个连接处于TIME—WAIT状态的时间,通常为2MSL(报文段寿命的两倍)。2MSL定时器的设置主要是为了确保发送的最后一个ACK报文段能够到达对方,并防止之前与本连接有关的由于延迟等原因而导致已失效的报文被误判为有效。在连接终止期使用,当TCP关闭连接时,并不认为这个连接就真正关闭了,在时间等待期间,连接还处于一种中间过度状态。这样就可以使重复的fin报文段在到达终点后被丢弃。
TIME_WAIT 确保有足够的时间让对端收到了ACK,如果被动关闭的那方没有收到 ACK,就会触发被动端重发 FIN。因为最后一次确认应答 ACK 报文段很有可能丢失,因而使被动关闭方处于在LIST_ACK 状态的,此时被动关闭方会重发这个 FIN+ACK 报文段,在这等待的 2MSL 时间内主动关闭方重新收到这个被动关闭方重发的 FIN+ACK 报文段,因此,主动关闭方会重新发送确认应答信息,从而重新启动 2MSL 计时器,直到通信双方都进入 CLOSED 状态。如果主动关闭方在 TIME_WAIT 状态不等待一段时间就直接释放连接并进入 CLOSED 状态,那么主动关闭方无法收到来自被动关闭方重发的 FIN+ACK 报文段,也就不会再发送一次确认 ACK 报文段,因此被动关闭方就无法正常进入CLOSED 状态。
该定时器使得有足够的时间让这个连接不会跟后面的连接混在一起。防止已失效的请求连接出现在本连接中。在连接处于 2MSL 等待时,任何迟到的报文段将被丢弃,因为处于 2MSL等待的、由该插口(插口是IP和端口对的意思,socket)定义的连接在这段时间内将不能被再用,这样就可以使下一个新的连接中不会出现这种旧的连接之前延迟的报文段。