重传机制

TCP实现可靠传输的方式之一,就是通过序列号与确认应答。
在 TCP 中,当发送端的数据到达接收主机时,接收端主机会返回一个确认应答消息,表示已收到消息。
当由于网络原因出现丢包的现象时,TCP会采用重传机制解决。

超时重传

重传机制的其中一个方式,就是在发送数据时,设定一个定时器,当超过指定的时间后,没有收到对方的 ACK 确认应答报文,就会重发该数据,也就是我们常说的超时重传
TCP 会在以下两种情况发生超时重传:

  • 数据包丢失
  • 确认应答丢失

TCP重传、滑动窗口、流量控制、拥塞控制 - 图1
超时时间应该如何设置?
RTT(往返时延):数据从网络一端到另一端所需要的时间。也就是包往返的时间。
RTO(超时重传时间)
TCP重传、滑动窗口、流量控制、拥塞控制 - 图2
**

  • 当超时时间 RTO 较大时,重发就慢,丢了老半天才重发,没有效率,性能差;
  • 当超时时间 RTO 较小时,会导致可能并没有丢就重发,于是重发的就快,会增加网络拥塞。

超时重传时间RTO的值应该略大于报文往返RTT的时间
**
由于网络的波动性,RTO时间应该是一个动态变化的值。
估计往返时间,通常需要采样以下两个:

  • 需要 TCP 通过采样 RTT 的时间,然后进行加权平均,算出一个平滑 RTT 的值,而且这个值还是要不断变化的,因为网络状况不断地变化。
  • 除了采样 RTT,还要采样 RTT 的波动范围,这样就避免如果 RTT 有一个大的波动的话,很难被发现的情况。

每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为**先前值的两倍**。两次超时,就说明网络环境差,不宜频繁反复发送。
**

快速重传

TCP重传、滑动窗口、流量控制、拥塞控制 - 图3
当收到三个相同的ACK报文时,会在定时器过期之前,重传丢失的报文。

问题:重传之前的一个?还是重传所有?
根据TCP不同的实现,以上两种情况都是有可能的。
为了解决不知道该重传哪些 TCP 报文,于是就有 SACK 方法。

SACK 方法

这种方式需要在 TCP 头部「选项」字段里加一个 SACK 的东西,它可以将缓存的地图发送给发送方,这样发送方就可以知道哪些数据收到了,哪些数据没收到,知道了这些信息,就可以只重传丢失的数据
如果要支持 SACK,必须双方都要支持。在 Linux 下,可以通过 net.ipv4.tcp_sack 参数打开这个功能(Linux 2.4 后默认打开)。

Duplicate SACK

Duplicate SACK 又称 D-SACK,其主要使用了 SACK 来告诉「发送方」有哪些数据被重复接收了。
下面举例两个栗子,来说明 D-SACK 的作用。
栗子一号:ACK 丢包
TCP重传、滑动窗口、流量控制、拥塞控制 - 图4

  • 「接收方」发给「发送方」的两个 ACK 确认应答都丢失了,所以发送方超时后,重传第一个数据包(3000 ~ 3499)
  • 于是「接收方」发现数据是重复收到的,于是回了一个 SACK = 3000~3500,告诉「发送方」 3000~3500 的数据早已被接收了,因为 ACK 都到了 4000 了,也就意味着 4000 之前的所有数据都已收到,所以这个 SACK 就代表着 D-SACK
  • 这样「发送方」就知道了,数据没有丢,是「接收方」的 ACK 确认报文丢了。

栗子二号:网络延时
TCP重传、滑动窗口、流量控制、拥塞控制 - 图5

  • 数据包(1000~1499) 被网络延迟了,导致「发送方」没有收到ACK1500 的确认报文。
  • 而后面报文到达的三个相同的 ACK 确认报文,就触发了快速重传机制,但是在重传后,被延迟的数据包(1000~1499)又到了「接收方」;
  • 所以「接收方」回了一个 SACK=1000~1500,因为 ACK 已经到了 3000,所以这个 SACK 是 D-SACK,表示收到了重复的包。
  • 这样发送方就知道快速重传触发的原因不是发出去的包丢了,也不是因为回应的 ACK 包丢了,而是因为网络延迟了。

可见,D-SACK 有这么几个好处:

  1. 可以让「发送方」知道,是发出去的包丢了,还是接收方回应的 ACK 包丢了;
  2. 可以知道是不是「发送方」的数据包被网络延迟了;
  3. 可以知道网络中是不是把「发送方」的数据包给复制了;


滑动窗口

TCP每发送一个数据,都要进行一次确认应答,当上一个被确认后,再发送下一个(效率低下,包往返的时间越长,网络的吞吐量越低)。

为解决这个问题,TCP 引入了窗口这个概念。即使在往返时间较长的情况下,它也不会降低网络通信的效率。那么有了窗口,就可以指定窗口大小,窗口大小就是指无需等待确认应答,就可以继续发送数据的最大值

窗口的实现实际上是操作系统开辟一个缓存空间,发送方主机在等到确认应答返回之前,必须在缓冲区中保留已发送的数据,如果按期望收到确认应答,此时数据就可以从缓存区清除。
TCP重传、滑动窗口、流量控制、拥塞控制 - 图6
这个模式叫做累计确认或者累计应答

窗口大小由哪一方决定?

TCP头中有一个字段叫Window,就是窗口大小。

这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据,于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。

通常窗口的大小是由接收方决定的。

发送方的滑动窗口

TCP重传、滑动窗口、流量控制、拥塞控制 - 图7

程序如何表示发送方的四个部分

TCP 滑动窗口方案使用三个指针来跟踪在四个传输类别中的每一个类别中的字节。其中两个指针是绝对指针(指特定的序列号),一个是相对指针(需要做偏移)。

TCP重传、滑动窗口、流量控制、拥塞控制 - 图8

  • SND.WND:表示发送窗口的大小(大小是由接收方指定的);
  • SND.UNA:是一个绝对指针,它指向的是已发送但未收到确认的第一个字节的序列号,也就是 #2 的第一个字节。
  • SND.NXT:也是一个绝对指针,它指向未发送但可发送范围的第一个字节的序列号,也就是 #3 的第一个字节。
  • 指向 #4 的第一个字节是相对指针,它需要 SND.UNA 指针加上 SND.WND 大小的偏移量,就可以指向 #4 的第一个字节。

可用窗口的大小:
可用窗口大小 = SND.WND -(SND.NXT - SND.UNA)
**

接收方的滑动窗口

TCP重传、滑动窗口、流量控制、拥塞控制 - 图9

  • 1 + #2 是已成功接收并确认的数据(等待应用进程读取);

  • 3 是未收到数据但可以接收的数据;

  • 4 未收到数据并且不可以接收的数据;

接收窗口和发送窗口的大小相等么?

并不是完全相等,接收窗口的大小是约等于发送窗口的大小的。
因为滑动窗口并不是一成不变的。比如,当接收方的应用进程读取数据的速度非常快的话,这样的话接收窗口可以很快的就空缺出来。那么新的接收窗口大小,是通过 TCP 报文中的 Windows 字段来告诉发送方。那么这个传输过程是存在时延的,所以接收窗口和发送窗口是约等于的关系。

流量控制

发送方不能无脑的发数据给接收方,要考虑接收方处理能力。
如果一直无脑的发数据给对方,但对方处理不过来,那么就会导致触发重传机制,从而导致网络流量的无端的浪费。
为了解决这种现象发生,TCP 提供一种机制可以让**「发送方」根据「接收方」的实际接收能力控制发送的数据量**,这就是所谓的流量控制。

操作系统缓冲区与滑动窗口的关系
实际上,发送窗口和接收窗口中所存放的字节数,都是放在操作系统内存缓冲区中的,而操作系统的缓冲区,会
被操作系统调整**。
当应用进程没办法及时读取缓冲区的内容时,也会对我们的缓冲区造成影响。

那操作系统的缓冲区,是如何影响发送窗口和接收窗口的呢?

1、窗口关闭
2、丢包(TCP规定不允许同时减少缓存又收缩窗口的,而时采用先收缩窗口,过段时间再减少缓存,避免丢包)

其实可以这么理解:缓冲区保证了滑动窗口的可行性。滑动窗口设计的初衷在于提高传输的效率,不需要逐一确认包是否正常抵达,而缓冲区就是对于这一机制安全性的兜底策略。

窗口关闭

在前面我们都看到了,TCP 通过让接收方指明希望从发送方接收的数据大小(窗口大小)来进行流量控制。
如果窗口大小为 0 时,就会阻止发送方给接收方传递数据,直到窗口变为非 0 为止,这就是窗口关闭。
**

窗口关闭的潜在危险?

接收方向发送方通告窗口大小,通过ACK报文来通告。

当发生窗口关闭时,接收方处理完数据后,会向发送方通告一个窗口非 0 的 ACK 报文,如果这个通告窗口的 ACK 报文在网络中丢失,那麻烦就大了。(但是如果一个包丢失的话不会有超时重传的机制的么?)

导致发送方一直等待接收方的非 0 窗口通知,接收方也一直等待发送方的数据,如果不采取措施,这种相互等待的过程,会造成了死锁的现象。

TCP 是如何解决窗口关闭时,潜在的死锁现象呢?

为了解决这个问题,TCP 为每个连接设有一个持续定时器,只要 TCP 连接一方收到对方的**零窗口通知就启动持续计时器**。
如果持续计时器超时,就会发送窗口探测 ( Window probe ) 报文,而对方在确认这个探测报文时,给出自己现在的接收窗口大小。

  • 如果接收窗口仍然为 0,那么收到这个报文的一方就会重新启动持续计时器;
  • 如果接收窗口不是 0,那么死锁的局面就可以被打破了。


糊涂窗口综合症

如果接收方腾出几个字节并告诉发送方现在有几个字节的窗口,而发送方会义无反顾地发送这几个字节,这就是糊涂窗口综合症
要知道,我们的 TCP + IP 头有 40 个字节,为了传输那几个字节的数据,要达上这么大的开销,这太不经济了。

例子:就好像一辆可以承载50人的大巴车,每次来一两个人就发车,解决办法就是等乘客超过25人才发车。

糊涂窗口综合征的现象可以发生在发送方和接收方:

  • 接收方可以通告一个小的窗口
  • 而发送方可以发送小数据

解决办法

  • 接收方不通知小窗口给发送方
  • 让发送方避免发送小数据

拥塞控制

为什么有流量控制还要拥塞控制?

流量控制是避免【发送方】的数据填满【接收方】的缓存,拥塞控制的目的就是避免发送方的数据填满整个网络。计算机网络都处于一个共享的环境中,因此有可能因为其他主机之间的通信使得网络拥堵。

在网络出现拥堵时,如果继续发送大量数据包,可能会导致数据包时延、丢失等,这时 TCP 就会重传数据,但是一重传就会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,这个情况就会进入恶性循环被不断地放大….

当网络拥堵的时候,TCP会自我牺牲,降低发送的数据量

为了在发送方调节所要发送数据的量,定义一个叫做【拥塞窗口】的概念。

什么是拥塞窗口?和发送窗口有什么关系?

拥塞窗口cwnd是发送方维护的一个状态变量,它会根据网络的拥塞程度动态变化的
发送窗口 swnd 和接收窗口 rwnd 是约等于的关系,那么由于有了拥塞窗口的概念后,此时发送窗口的值是swnd = min(cwnd, rwnd),也就是拥塞窗口和接收窗口中的最小值。

拥塞窗口cwnd变化的规则:

  • 只要网络中没有出现拥塞,cwnd就会增大
  • 网络出现拥塞,cwnd就会减小

怎么知道当前网络是否出现了拥塞呢?

其实只要【发送方】没有在规定时间内接收到ACK应答报文,就是发生了**超时重传**,就会认为网络中出现了拥塞。
**

拥塞控制有哪些算法?

  • 慢启动
  • 拥塞避免
  • 拥塞发生
  • 快速恢复

慢启动
1、慢启动不是指 cwnd 的增长速度慢(指数增长),而是指 TCP 开始发送设置 cwnd=1。
2、思路:不要一开始就发送大量的数据,先探测一下网络的拥塞程度,也就是说由小到大逐渐增加拥塞窗口的大小。
这里用报文段的个数的拥塞窗口大小举例说明慢开始算法,实时拥塞窗口大小是以字节为单位的。
3、 为了防止 cwnd 增长过大引起网络拥塞,设置一个慢启动门限(ssthresh 状态变量)
当 cnwd<ssthresh,使用慢启动算法
当 cnwd = ssthresh,既可使用慢启动算法,也可以使用拥塞避免算法
当 cnwd>ssthresh,使用拥塞避免算法

流程

  • 连接建立完成后,一开始初始化 cwnd = 1,表示可以传一个 MSS 大小的数据。
  • 当收到一个 ACK 确认应答后,cwnd 增加 1,于是一次能够发送 2 个
  • 当收到 2 个的 ACK 确认应答后, cwnd 增加 2,于是就可以比之前多发2 个,所以这一次能够发送 4 个
  • 当这 4 个的 ACK 确认到来的时候,每个确认 cwnd 增加 1, 4 个确认 cwnd 增加 4,于是就可以比之前多发 4 个,所以这一次能够发送 8 个。

规则:当发送方每收到一个ACK,拥塞窗口的大小就会加1。

拥塞避免**
1、拥塞避免并非完全能够避免拥塞,是说在拥塞避免阶段将拥塞窗口控制为按线性规律增长,使网络比较不容易出现拥塞。
2、思路:让拥塞窗口 cwnd 缓慢地增大,每收到一个ACK时,cwnd增加1/cwnd。

无论是在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞(其根据就是没有收到确认,虽然没有收到确认可能是其他原因的分组丢失,但是因为无法判定,所以都当做拥塞来处理),就把慢开始门限设置为出现拥塞时的发送窗口大小的一半。然后把拥塞窗口设置为 1,执行慢开始算法。 如图所示:

image.png
拥塞发生
当网络出现拥塞,也就是会发生数据包重传,重传机制主要有两种:

  • 超时重传
  • 快速重传

这两种使用的拥塞发送算法是不同的,接下来分别来说说。

超时重传的拥塞发生算法

  • ssthresh 设为 cwnd/2
  • cwnd 重置为 1

TCP重传、滑动窗口、流量控制、拥塞控制 - 图11
快速重传的拥塞发生算法
当接收方发现丢了一个中间包的时候,发送三次前一个包的 ACK,于是发送端就会快速地重传,不必等待超时再重传。

TCP 认为这种情况不严重,因为大部分没丢,只丢了一小部分,则 ssthreshcwnd 变化如下:

  • cwnd = cwnd/2 ,也就是设置为原来的一半;
  • ssthresh = cwnd;
  • 进入快速恢复算法

快速恢复
快速重传和快速恢复算法一般同时使用,快速恢复算法是认为,你还能收到 3 个重复 ACK 说明网络也不那么糟糕,所以没有必要像 RTO 超时那么强烈。

正如前面所说,进入快速恢复之前,cwndssthresh 已被更新了:

  • cwnd = cwnd/2 ,也就是设置为原来的一半;
  • ssthresh = cwnd;

然后,进入快速恢复算法如下:

  • 拥塞窗口 cwnd = ssthresh + 3 ( 3 的意思是确认有 3 个数据包被收到了)
  • 重传丢失的数据包
  • 如果再收到重复的 ACK,那么 cwnd 增加 1
  • 如果收到新数据的 ACK 后,设置 cwnd 为 ssthresh,接着就进入了拥塞避免算法

TCP重传、滑动窗口、流量控制、拥塞控制 - 图12