tcp的拥塞控制
滑动窗口
滑动窗口的单位是字节。其值可以为MSS*N(mss的整数倍)
如果我们在任一时间点对于这一过程做一个“快照”,那么我们可以将TCP buffer中的数据分为以下四类,并把它们看作一个时间轴:
- 状态1 已发送已确认数据流中最早的字节已经发送并得到确认。这些数据是站在发送设备的角度来看的。如下图所示,31个字节已经发送并确认。
- 状态2 已发送但尚未确认已发送但尚未得到确认的字节。发送方在确认之前,不认为这些数据已经被处理。下图所示14字节为第2类。
- 状态3 未发送而接收方已Ready设备尚未将数据发出,但接收方根据最近一次关于发送方一次要发送多少字节确认自己有足够空间。发送方会立即尝试发送。如图,第3类有6字节。
- 状态4 未发送而接收方Not Ready由于接收方not ready,还不允许将这部分数据发出。
注意: 在tcp的一次报文传输过程中,会发送多个字节,其作为一个整体,对端只会对此报文的最后一个字节进行ack,表明此报文的所有字节都接收成功。相应的,客户端也会将这些字节放入状态2,滑动窗口往前移动。
拥塞窗口
实际的发送窗口,是min(cwnd, wnd).这就保证了
- 发送的流量不会超过服务器缓存剩余的空间
- 发送的流量不会超过当前网络链路的负荷
这里为了表述简单,我们将拥塞窗口大小称为N,N的值为N mss字节.
tcp的拥塞控制主要有4个算法
- 慢启动
- 拥塞避免
- 快重传
- 快恢复
ssthresh
关于慢启动的ssthresh值,有篇文章讲的比较好。TCP核心概念-慢启动,ssthresh,拥塞避免,公平性的真实含义,同时也剪藏在evernote中。
- 2N = rttr (r为发送的速率)
N指什么呢?N是指字节数。当我们站在上帝视角,我们能清楚的知道这条链路的承载能力的,即这条TCP链路某一时刻承载的最大字节数。但是主机是不知道的,主机并不知道连接的TCP链路状况,因此主机要做的就是使ssthresh迫近N. - 2*N = wnd (N是确定但未知的,而wnd是需要我们不断迫近的)
从理论上讲,wnd的值等于当前链路N*2,N=wnd/2.但是可惜的是我们不知道N,也就无从知道理论wnd是多少。我们的目标就是找到理论wnd和N,在寻找的过程中,我们给N起个变量名: ssthresh. 这就清楚了,ssthresh就是我们假定当前链路的承载能力。
疑问: 为什么2*N = wnd
我们假设当前的N为4,即:当前的TCP链路可承载4字节的信息。那么,我们该将wnd设置为多大,才能充分利用此链路呢?
充分利用链路,就是说,此链路充满数据,服务器从接收一个,客户端发送一个,数据流不断。
可见,在理想的情况下,当wnd=8的时候,客户端不用等待ack可以一直发送数据,否则客户端需要收到ack并将滑动窗口向前移动,才能发送新的数据。
注意: 在一个rtt中,共发送了wnd 字节的流量。
慢启动
初始化cwnd=1, 在每收到一个对新的报文段的确认后,把拥塞窗口增加至多一个MSS的数值。用这样的方法逐步增大发送方的拥塞窗口 cwnd ,可以使分组注入到网络的速率更加合理。
当cwnd<ssthresh时,采用慢启动算法,当cwnd>ssthresh时,采用拥塞避免算法。
那么这个值具体的有什么讲究呢?这个在上面的链接中讲的比较清楚了,我把我自己的理解再简单的阐述一下。
ssthresh表明了当前网络链路中可容纳的最大字节数。当新建tcp链接的时候,并不清楚此tcp链路的状况,所以将sshthresh设置为一个巨大的值。然后每收到一个报文的ack,都将cwnd的值加1。因此,经过一个rtt, cwnd是呈指数级在增长。
但是一般来说cwnd是达不到初始 ssthresh的值的,在cwnd增长的过程中,可能初始ssthresh的值远远大于链路的带宽,因此很快链路就会出现阻塞。注意这里区分两种情况:
- 1 网络出现阻塞
- 2 网络出现丢包,但是后续的报文被服务端成功收到。
当网络出现阻塞的时候,需要进行如下操作: - ssthresh 减少为当前cwnd的一半
- cwnd 设置为1 并开始新一轮的计算
需要考虑一个问题,为什么ssthresh需要设置为原来的一半?
当前的cwnd值过大,根本原因是我们假定当前链路的承载能力ssthresh过大,需要缩减,ssthresh = cwnd/2,就是说,在当前的cwnd情况下,其对应的ssthresh_new为 cwnd/2。因此我们将ssthresh_old替换为ssthresh_new,即减为当前cwnd值得一半,并开始新一轮的迫近过程。
举个例子
理论情况:ssthresh=2 cwnd=4
初始化: ssthresh=5 cwnd=1
rtt1: ssthresh 5 cwnd=1
rtt2: ssthresh 5 cwnd=2(cwnd==ssthresh,改为拥塞避免算法)
rtt3: ssthresh 5 cwnd=3
rtt4: ssthresh 5 cwnd=4
rtt5: ssthresh 5 cwnd=5
rtt6: ssthresh 5 cwnd=6(可能开始出现阻塞丢包)
rtt7: ssthresh 3 cwnd=1(reset)
...
疑问:那会不会存在cwnd/2>ssthresh_old的情况呢?这样不是ssthresh反而加大了?
拥塞避免
wnd = rtt*r
在一个rtt中,共发送了wnd 字节的流量。如果这wnd字节的流量都成功ack,那么我们将cwnd+1
即:一个rtt 结束后,没有网络阻塞的话 ,cwnd+1。
可以很明显看到拥塞避免和慢启动的差异,慢启动时一个rtt后,cwnd增加一倍。