目录
TCP重传机制
超时重传
快速重传
SACK 方法
Duplicate SACK
TCP 流量控制
滑动窗口
累积确认
窗口大小由哪一方决定?
接收窗口和发送窗口的大小是相等的吗?
流量控制
窗口关闭的后果
糊涂窗口综合症
TCP拥塞处理
为什么要有拥塞控制呀,不是有流量控制了吗?
什么是拥塞窗口?和发送窗口有什么关系呢?
怎么知道当前网络是否出现了拥塞呢?
拥塞控制算法
慢开始算法:
拥塞避免算法
拥塞发生算法
快速恢复算法
TCP重传机制
在 TCP 中,当发送端的数据到达接收主机时,接收端主机会返回一个确认应答消息,表示已收到消息。TCP 针对数据包丢失的情况,会用重传机制解决。
常见的重传机制:
-
超时重传
-
快速重传
-
SACK
-
D-SACK
超时重传
在发送数据时,设定一个定时器,当超过指定的时间后,没有收到对方的 ACK
确认应答报文,就会重发该数据,也就是我们常说的超时重传。
这种情况要么就是自己发过去的数据包丢失,要么就是对方的应答丢失
超时时间应该设置为多少呢?
首先来了解下什么是RTT:
RTT
指的是数据发送时刻到接收到确认的时刻的差值,也就是包的往返时间。
上图中有两种超时时间不同的情况:
-
当超时时间 RTO 较大时,重发就慢,丢了老半天才重发,没有效率,性能差;
-
当超时时间 RTO 较小时,会导致可能并没有丢就重发,于是重发的就快,会增加网络拥塞,导致更多的超时,更多的超时导致更多的重发。
所以,超时重传时间 RTO 的值应该略大于报文往返 RTT 的值,实际上「报文往返 RTT 的值」是经常变化的,因为我们的网络也是时常变化的。也就因为「报文往返 RTT 的值」 是经常波动变化的,所以「超时重传时间 RTO 的值」应该是一个动态变化的值
-
超时触发重传存在的问题是,超时周期可能相对较长。那是不是可以有更快的方式呢?
于是就可以用「快速重传」机制来解决超时重发的时间等待。
快速重传
快速重传机制,它不以时间为驱动,而是以数据驱动重传。
在上图,发送方发出了 1,2,3,4,5 份数据:
-
第一份 Seq1 先送到了,于是就 Ack 回 2;
-
结果 Seq2 因为某些原因没收到,Seq3 到达了,于是还是 Ack 回 2;
-
后面的 Seq4 和 Seq5 都到了,但还是 Ack 回 2,因为 Seq2 还是没有收到;
-
发送端收到了三个 Ack = 2 的确认,知道了 Seq2 还没有收到,就会在定时器过期之前,重传丢失的 Seq2。
-
最后,收到了 Seq2,此时因为 Seq3,Seq4,Seq5 都收到了,于是 Ack 回 6 。
所以,快速重传的工作方式是当收到三个相同的 ACK 报文时,会在定时器过期之前,重传丢失的报文段。
但快速重传机制只解决了一个问题,就是超时时间的问题,但是它依然面临着另外一个问题。就是重传的时候,是重传一个,还是重传所有的问题。
举个例子,假设发送方发了 6 个数据,编号的顺序是 Seq1 ~ Seq6 ,但是 Seq2、Seq3 都丢失了,那么接收方在收到 Seq4、Seq5、Seq6 时,都是回复 ACK2 给发送方,但是发送方并不清楚这连续的 ACK2 是接收方收到哪个报文而回复的, 那是选择重传 Seq2 一个报文,还是重传 Seq2 之后已发送的所有报文呢(Seq2、Seq3、 Seq4、Seq5、 Seq6) 呢?
-
如果只选择重传 Seq2 一个报文,那么重传的效率很低。因为对于丢失的 Seq3 报文,还得在后续收到三个重复的 ACK3 才能触发重传。
-
如果选择重传 Seq2 之后已发送的所有报文,虽然能同时重传已丢失的 Seq2 和 Seq3 报文,但是 Seq4、Seq5、Seq6 的报文是已经被接收过了,对于重传 Seq4 ~Seq6 折部分数据相当于做了一次无用功,浪费资源。
可以看到,不管是重传一个报文,还是重传已发送的报文,都存在问题。
SACK 方法
SACK
, 选择性确认。这种方式需要在 TCP 头部「选项」字段里加一个 SACK
的东西,它可以将已收到的数据的信息发送给「发送方」,这样发送方就可以知道哪些数据收到了,哪些数据没收到,知道了这些信息,就可以只重传丢失的数据。
如下图,发送方收到了三次同样的 ACK 确认报文,于是就会触发快速重发机制,通过 SACK
信息发现只有 200~299
这段数据丢失,则重发时,就只选择了这个 TCP 段进行重复。
Duplicate SACK
D-SACK
有这么几个好处:
-
可以让「发送方」知道,是发出去的包丢了,还是接收方回应的 ACK 包丢了;
-
可以知道是不是「发送方」的数据包被网络延迟了;
-
可以知道网络中是不是把「发送方」的数据包给复制了;
TCP 流量控制
滑动窗口
我们都知道 TCP 是每发送一个数据,都要进行一次确认应答。当上一个数据包收到了应答了, 再发送下一个。但这种方式的缺点是效率比较低的
为解决这个问题,TCP 引入了窗口这个概念。即使在往返时间较长的情况下,它也不会降低网络通信的效率。
窗口的实现实际上是操作系统开辟的一个缓存空间,发送方主机在等到确认应答返回之前,必须在缓冲区中保留已发送的数据。如果按期收到确认应答,此时数据就可以从缓存区清除
累积确认
假设窗口大小为 3
个 TCP 段,那么发送方就可以「连续发送」 3
个 TCP 段,并且中途若有 ACK 丢失,可以通过「下一个确认应答进行确认」。如下图:
图中的 ACK 600 确认应答报文丢失,也没关系,因为可以通过下一个确认应答进行确认,只要发送方收到了 ACK 700 确认应答,就意味着 700 之前的所有数据「接收方」都收到了。这个模式就叫累计确认或者累计应答。
窗口大小由哪一方决定?
TCP 头里有一个字段叫 `Window`,也就是窗口大小。
这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。
所以,通常窗口的大小是由接收方的窗口大小来决定的。发送方发送的数据大小不能超过接收方的窗口大小,否则接收方就无法正常接收到数据。
接收窗口和发送窗口的大小是相等的吗?
并不是完全相等,接收窗口的大小是约等于发送窗口的大小的。
因为滑动窗口并不是一成不变的。比如,当接收方的应用进程读取数据的速度非常快的话,这样的话接收窗口可以很快的就空缺出来。那么新的接收窗口大小,是通过 TCP 报文中的 Windows 字段来告诉发送方。那么这个传输过程是存在时延的,所以接收窗口和发送窗口是约等于的关系。
流量控制
对于发送端和接收端而言,TCP 需要把发送的数据放到发送缓存区,将接收的数据放到接收缓存区。 而流量控制要做的事情,就是在通过调整接收缓存区的大小,控制发送端的发送。如果对方的接收缓存区满了,就不能再继续发送了。
为了控制发送端的速率,接收端会告知客户端自己接收窗口大小(接收缓冲区中空闲的部分)。在请求数据的ACK确认(和TCP建立连接的ACK无关)中会带有当前接收端的窗口大小
发送方根据ACK确认响应中的客户端此时的接收窗口大小来调节自身发送滑动窗口的大小,将滑动窗口的数据都发送过去后,要等待ACK再次确认报告接收窗口大小
-
接收窗口大小为0,发送端阻塞
-
接收窗口大小不为零,发送端滑动窗口拓展到指定大小后进行后移操作
窗口关闭的后果
当服务端系统资源非常紧张的时候,操作系统可能会直接减少了接收缓冲区大小,这时应用程序又无法及时读取缓存数据,会出现数据包丢失的现象。为了防止这种情况发生,TCP 规定是不允许同时减少缓存又收缩窗口的,而是采用先收缩窗口,过段时间再减少缓存,这样就可以避免了丢包情况。
而如果假设服务端系统资源不是很紧张,接收缓冲区不需要变动,那么如果服务端业务读取慢,接收端口容易变为了0,就会阻止发送方给接收方传递数据,直到窗口变为非 0 为止,这就是窗口关闭。
窗口关闭的风险:
当发生窗口关闭时,接收方处理完数据后,会向发送方通告一个窗口非 0 的 ACK 报文,如果这个通告窗口的 ACK 报文在网络中丢失了,这会导致发送方一直等待接收方的非 0 窗口通知,接收方也一直等待发送方的数据,如不采取措施,这种相互等待的过程,会造成了死锁的现象。
为了解决这个问题,TCP 为每个连接设有一个持续定时器,只要 TCP 连接一方收到对方的零窗口通知,就启动持续计时器。
如果持续计时器超时,就会发送窗口探测报文,而对方在确认这个探测报文时,给出自己现在的接收窗口大小。
-
如果接收窗口仍然为 0,那么收到这个报文的一方就会重新启动持续计时器;
-
如果接收窗口不是 0,那么死锁的局面就可以被打破了。
窗口探测的次数一般为 3 次,每次大约 30-60 秒(不同的实现可能会不一样)。如果 3 次过后接收窗口还是 0 的话,有的 TCP 实现就会发 RST
报文来中断连接。
糊涂窗口综合症
如果接收方太忙了,来不及取走接收窗口里的数据,那么就会导致发送方的发送窗口越来越小。
到最后,如果接收方腾出几个字节并告诉发送方现在有几个字节的窗口,而发送方会义无反顾地发送这几个字节,这就是糊涂窗口综合症。
我们的
TCP + IP
头有40
个字节,为了传输那几个字节的数据,要搭上这么大的开销,这太不经济了。
于是,要解决糊涂窗口综合症,可以采用如下方案:
-
让接收方当「窗口大小」小于 min( MSS,缓存空间/2 ) ,也就是小于 MSS 与 1/2 缓存大小中的最小值时,就会向发送方通告窗口为
0
,也就阻止了发送方再发数据过来。反之就把真实窗口大小发过去 -
让发送方使用 Nagle 算法,要等到窗口大小 >=
MSS
并且 数据大小 >=MSS
或者收到之前发送数据的ack
回包;只要上面两个条件都不满足,发送方一直在囤积数据,直到满足上面的发送条件。
所以,接收方得满足「不通告小窗口给发送方」+ 发送方开启 Nagle 算法,才能避免糊涂窗口综合症。
TCP拥塞处理
为什么要有拥塞控制呀,不是有流量控制了吗?
前面的流量控制是避免「发送方」的数据填满「接收方」的缓存,但是并不知道网络的中发生了什么。一般来说,计算机网络都处在一个共享的环境。因此也有可能会因为其他主机之间的通信使得网络拥堵。
在网络出现拥堵时,如果继续发送大量数据包,可能会导致数据包时延、丢失等,这时 TCP 就会重传数据,但是一重传就会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,这个情况就会进入恶性循环被不断地放大....
所以,TCP 不能忽略网络上发生的事,当网络发送拥塞时,TCP 会自我牺牲,降低发送的数据量。于是,就有了拥塞控制,控制的目的就是避免「发送方」的数据填满整个网络。
为了在「发送方」调节所要发送数据的量,定义了一个叫做「拥塞窗口」的概念。
什么是拥塞窗口?和发送窗口有什么关系呢?
我们在前面提到过发送窗口 swnd
和接收窗口 rwnd
是约等于的关系,那么由于加入了拥塞窗口的概念后,此时
发送窗口的值是swnd = min(cwnd, rwnd),也就是拥塞窗口和接收窗口中的最小值。
拥塞窗口 cwnd
变化的规则:
-
只要网络中没有出现拥塞,
cwnd
就会增大; -
但网络中出现了拥塞,
cwnd
就减少;
怎么知道当前网络是否出现了拥塞呢?
其实只要「发送方」没有在规定时间内接收到 ACK 应答报文,也就是发生了超时重传,就会认为网络出现了拥塞。
拥塞控制算法
拥塞控制主要是四个算法:
-
慢启动
-
拥塞避免
-
拥塞发生
-
快速恢复
慢开始算法:
从1开始,由小到大逐渐增加拥塞窗口大小来控制发送速率,每经过一个往返时间RTT就更新(窗口大小 * 2)
为什么要使用慢启动:最初的TCP在连接建立成功后会向网络中发送大量的数据包
,这样很容易导致网络中路由器缓存空间耗尽,从而发生拥塞。因此新建立的连接不能够一开始就大量发送数据包,而只能根据网络情况逐步增加每次发送的数据量,以避免上述现象的发生。
慢开始算法以指数次幂增长过快,为了防止引起网络拥塞,还需设置一个慢开始门限ssthresh状态变量。当你通过慢开始算法增长窗口,当窗口大小大于慢开始门限后就会采用拥塞避免算法
拥塞避免算法
每经过一个往返时间RTT就把发送方的拥塞窗口cwnd加1,而不是加倍
无论是在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞(TCP认为网络拥塞的一种依据是它重传了一个报文段。上面提到过,TCP对每一个报文段都有一个定时器,称为重传定时器(RTO),当RTO超时且还没有得到数据确认,那么TCP就会对该报文段进行重传),就把慢开始门限设置为出现拥塞时的发送窗口大小的一半。然后把拥塞窗口设置为1,执行慢开始算法
拥塞发生算法
当网络出现拥塞,也就是会发生数据包重传,重传机制主要有两种:
-
超时重传
-
ssthresh
设为cwnd/2
, -
cwnd
重置为1
(是恢复为 cwnd 初始化值,我这里假定 cwnd 初始化值 1)
但是这种方式太激进,反应也很强烈,会造成网络卡顿。还有更好的方式,前面我们讲过「快速重传算法」。当接收方发现丢了一个中间包的时候,发送三次前一个包的 ACK,于是发送端就会快速地重传,不必等待超时再重传。
-
-
快速重传
-
cwnd = cwnd/2
,也就是设置为原来的一半; -
ssthresh = cwnd
; -
进入快速恢复算法
-
快速恢复算法
快速重传和快速恢复算法一般同时使用,快速恢复算法是认为,你还能收到 3 个重复 ACK 说明网络也不那么糟糕,所以没有必要像 RTO
超时那么强烈。
正如前面所说,进入快速恢复之前,cwnd
和 ssthresh
已被更新了:
-
cwnd = cwnd/2
,也就是设置为原来的一半; -
ssthresh = cwnd
;
然后,进入快速恢复算法如下:
-
拥塞窗口
cwnd = ssthresh + 3
( 3 的意思是确认有 3 个数据包被收到了); -
重传丢失的数据包;
-
如果再收到重复的 ACK,那么 cwnd 增加 1;
-
如果收到新数据的 ACK 后,把 cwnd 设置为第一步中的 ssthresh 的值,原因是该 ACK 确认了新的数据,说明从 duplicated ACK 时的数据都已收到,该恢复过程已经结束,可以回到恢复之前的状态了,也即再次进入拥塞避免状态;