Skip to content

1. TCP特性

详见此文

1.1 重传机制

TCP 实现可靠传输的方式之一,是通过序列号与确认应答。

在 TCP 中,当发送端的数据到达接收主机时,接收端主机会返回一个确认应答ACK消息,表示已收到消息。

1.1.1 超时重传

超时重传:在发送数据时,设置一个定时器,如果超过了指定时间还没收到 ACK 确认应答报文,就重传这个数据。

tcp_retries1和tcp_retries2都是在TCP三次握手之后的场景。

TCP 建立连接后的数据包最大超时重传次数由 tcp_retries2 指定,默认值是 15 次:

$ cat /proc/sys/net/ipv4/tcp_retries2
15
在超时重传过程中,每一次重传的等待时间间隔是指数增长的。若达到最大等待时间后仍没有重传成功,则断开连接。

TCP 会在以下两种情况发生超时重传:

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

RTT(Round-Trip Time 往返时延):数据发送时刻到接收到确认的时刻的差值,也就是包的往返时间。 RTO (Retransmission Timeout 超时重传时间):超时重传时间。

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

根据上述的两种情况,我们可以得知,超时重传时间 RTO 的值应该略大于报文往返 RTT 的值。

因为我们的网络也是时常变化的,所以 RTT 也是经常变化,所以 RTO 应该是一个动态值。

估计往返时间,通常需要采样以下两个:

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

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

1.1.1.1 tcp_retries1和tcp_retries2

  • 当重传次数超过tcp_retries1就会指示 IP 层进行 MTU 探测、刷新路由等过程,并不会断开TCP连接,当重传次数超过 tcp_retries2 才会断开TCP流。
  • tcp_retries1 和 tcp_retries2 两个重传次数都是受一个 timeout 值限制的,timeout 的值是根据它俩的值计算出来的,当重传时间超过 timeout,就不会继续重传了,即使次数还没到达。

1.1.1.2 存在的问题

超时触发重传存在的问题是,超时周期可能相对较长。于是就可以用快速重传机制来解决超时重发的时间等待。

比如,假设发送方发了 6 个数据,编号的顺序是 Seq1 ~ Seq6 ,但是 seq2 ~ seq3 都丢失了,接收方在接收 seq4 ~ seq6 时,回复的都是 ACK2 ,但是发送方并不清楚这连续的 ACK2 是接收方收到哪个报文而回复的。

  • 若只重传 seq2 ,还要经过三次重复的 ACK3 才能重传 seq3
  • 若重传 seq2 之后的所有报文,对于已经接收到的报文就是浪费资源

为了解决不知道该重传哪些 TCP 报文,于是就有 SACK 方法

1.1.2 快速重传

快速重传(Fast Retransmit)机制,它不以时间为驱动,而是以数据驱动重传。

解释:

  • 第一份 Seq1 先送到了,于是就 Ack 回 2;
  • 结果 Seq2 因为某些原因没收到,Seq3 到达了,于是还是 Ack 回 2;
  • 后面的 Seq4 和 Seq5 都到了,但还是 Ack 回 2,因为 Seq2 还是没有收到;
  • 发送端收到了三个 Ack = 2 的确认,知道了 Seq2 还没有收到,就会在定时器过期之前,重传丢失的 Seq2。
  • 最后,收到了 Seq2,此时因为 Seq3,Seq4,Seq5 都收到了,于是 Ack 回 6 。

所以,快速重传的工作方式是当收到三个相同的 ACK 报文时,会在定时器过期之前,重传丢失的报文段。

1.1.2.1 存在的问题

快速重传机制只解决了一个问题,就是超时时间的问题,但是它依然面临着另外一个问题。就是重传的时候,是重传一个,还是重传所有的问题。

1.1.3 SACK 方法

SACK( Selective Acknowledgment), 选择性确认。

这种方法需要在TCP选项里加SACK的东西,它可以将已收到的数据信息发送给 发送方,这样发送放就知道哪些数据是已收到或未收到的,就可以只重传丢失的数据。

如果要支持 SACK,必须双方都要支持。在 Linux 下,可以通过 net.ipv4.tcp_sack 参数打开这个功能(Linux 2.4 后默认打开)。

1.1.4 D-SACK

D-SACK(Duplicate SACK):使用SACK告诉发送方哪些数据重复接收了。

1.1.4.1 例1 ACK 丢失

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

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

D-SACK 有这么几个好处:

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

在 Linux 下可以通过 net.ipv4.tcp_dsack 参数开启/关闭这个功能(Linux 2.4 后默认打开)。

1.2 滑动窗口

1.2.1 引入窗口概念的原因

TCP 是每发送一个数据,都要进行一次确认应答。当上一个数据包收到了应答了, 再发送下一个。

但是这种交互模式效率低下:数据包的往返时间越长,通信的效率就越低。。

为解决这个问题,TCP 引入了窗口这个概念。即使在往返时间较长的情况下,它也不会降低网络通信的效率。

那么有了窗口,就可以指定窗口大小,窗口大小就是指无需等待确认应答,而可以继续发送数据的最大值。

窗口的实现实际上是操作系统开辟的一个缓存空间,发送方主机在等到确认应答返回之前,必须在缓冲区中保留已发送的数据。如果按期收到确认应答,此时数据就可以从缓存区清除。

累计确认/累计应答:接收方只需要发送一个确认序号,表示已成功接收到这个确认序号之前的所有数据包。 这样可以减少确认报文的数量,提高网络传输效率。这意味着即使中间的确认报文丢失,也不会进行数据重传。

1.2.2 窗口大小

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

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

所以,通常窗口的大小是由接收方的窗口大小来决定的。

发送方发送的数据大小不能超过接收方的窗口大小,否则接收方就无法正常接收到数据。

1.2.3 发送方的滑动窗口

下图是发送方缓存的数据,根据处理的情况分成四个部分,其中深蓝色方框是发送窗口,紫色方框是可用窗口:

当收到之前发送的数据 32~36 字节的 ACK 确认应答后,如果发送窗口的大小没有变化,则滑动窗口往右边移动 5 个字节,因为有 5 个字节的数据被应答确认。

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

  • SND.WND: 滑动窗口大小,由接收方指定
  • SND.UNA: 绝对指针,已发送但未确认的第一个字节的序列号
  • SND.NXT: 绝对指针,未发送但可发送的第一个字节的序列号
  • 指向 #4 的第一个字节是个相对指针,它需要 SND.UNA 指针加上 SND.WND 大小的偏移量,就可以指向 #4 的第一个字节了。
可用窗口大小 = SND.WND -(SND.NXT - SND.UNA)

1.2.4 接收方的滑动窗口

划分成三个部分:

使用两个指针进行划分: - RCV.WND: 接收窗口的大小,会告知发送方 - RCV.NXT: 绝对指针,它指向从发送方发来的下一个数据字节的序列号 - 指向 #4 的第一个字节是个相对指针,它需要 RCV.NXT 指针加上 RCV.WND 大小的偏移量,就可以指向 #4 的第一个字节了。

1.2.5 接收窗口和发送窗口的大小是相等的吗

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

1.3 流量控制

详见此文

TCP提供了一种机制,让发送方根据接收方的实际接收能力控制发送数据量,即所谓的流量控制。

举个例子,为了简单起见,假设以下场景:

  • 客户端是接收方,服务端是发送方。本次例子是把服务端作为发送方,所以没有画出服务端的接收窗口。
  • 假设接收窗口和发送窗口相同,都为 200
  • 假设两个设备在整个传输过程中都保持相同的窗口大小,不受外界影响

说明:

  1. 服务端接收到请求之后,发送确认报文和80字节数据,于是可用窗口变成了 120 字节;同时 SND.NXT 向后偏移 80 字节,指向 321,意味着下次发送数据的时候,序列号是321。
  2. 客户端收到80字节数据后,接收窗口向右滑动80字节,RCV.NXT指向 321,意味着客户端期望的下一个报文的序列号是321,接着发送确认报文给服务端
  3. 服务端再次发送了120字节,SND.NXT指向 441,可用窗口为0,服务端无法再发送数据
  4. 客户端收到120字节的数据后,于是接收窗口向右移动120字节,RCV.NXT指向441,接着发送确认报文给服务端。
  5. 服务端接收到对 80 字节数据的确认报文之后,SND.UNA 向右偏移80字节,指向321,于是可用窗口 Usable 增大到 80。
  6. 服务端接收到对 120 字节数据的确认报文之后,SND.UNA 向右偏移80字节,指向441,于是可用窗口 Usable 增大到 200。
  7. 服务端可以发送了,于是发送了160字节后,SND.NXT指向601,于是可用窗口 Usable 减少到 40。
  8. 客户端接收到160字节数据后,接收窗口向右偏移了160字节,RCV.NXT指向601,接着发送确认报文给服务端。
  9. 服务端收到对 160 字节数据的确认报文后,发送窗口往右移动了 160 字节,于是 SND.UNA 指针偏移了 160 后指向 601,可用窗口 Usable 也就增大至了 200。

总结: - 发送方发送数据的时候SND.NXT向右偏移 - 发送方接收确认报文时SND.UNA 向右偏移,并调整发送窗口大小 - 接收方接收数据的时候RCV.NXT向右偏移,并调整接收窗口大小

1.3.1 操作系统缓冲区与滑动窗口的关系

实际上,发送窗口和接收窗口中所存放的字节数,都是放在操作系统内存缓冲区中的,而操作系统的缓冲区,会被操作系统调整。

1.3.1.1 例1 窗口调整的正常情况

根据上图的流量控制,流程说明:

  1. 客户端发送 140 字节,可用窗口变成了220(360 - 140)
  2. 服务端收到了140字节,但是服务端非常繁忙,应用进程只读取了 40 个字节,还有 100 字节占用着缓冲区,于是接收窗口收缩到了 260 (360 - 100),最后发送确认信息时,将窗口大小通告给客户端。
  3. 客户端收到确认和窗口通告报文后,发送窗口减少为 260。
  4. 客户端发送 180 字节数据,此时可用窗口减少到 80。
  5. 服务端收到 180 字节数据,但是应用程序没有读取任何数据,这 180 字节直接就留在了缓冲区,于是接收窗口收缩到了 80 (260 - 180),并在发送确认信息时,通过窗口大小给客户端。
  6. 客户端收到确认和窗口通告报文后,发送窗口减少为 80。
  7. 客户端发送 80 字节数据后,可用窗口耗尽。
  8. 服务端收到 80 字节数据,但是应用程序依然没有读取任何数据,这 80 字节留在了缓冲区,于是接收窗口收缩到了 0,并在发送确认信息时,通过窗口大小给客户端。
  9. 客户端收到确认和窗口通告报文后,发送窗口减少为 0。

可见最后窗口都收缩为 0 了,也就是发生了窗口关闭。详见下文。

1.3.1.2 例2 窗口调整的异常情况

当服务端系统资源非常紧张的时候,操作系统可能会直接减少了接收缓冲区大小,这时应用程序又无法及时读取缓存数据,那么这时候就有严重的事情发生了,会出现数据包丢失的现象。

说明:

  1. 客户端发送 140 字节的数据,于是可用窗口减少到了 220。
  2. 服务端因为现在非常的繁忙,操作系统于是就把接收缓存减少了 120 字节,当收到 140 字节数据后,又因为应用程序没有读取任何数据,所以 140 字节留在了缓冲区中,于是接收窗口大小从 360 收缩成了 100,最后发送确认信息时,通告窗口大小给对方。
  3. 此时客户端因为还没有收到服务端的通告窗口报文,所以不知道此时接收窗口收缩成了 100,客户端只会看自己的可用窗口还有 220,所以客户端就发送了 180 字节数据,于是可用窗口减少到 40。
  4. 服务端收到了 180 字节数据时,发现数据大小超过了接收窗口的大小,于是就把数据包丢失了。
  5. 客户端收到第 2 步时,服务端发送的确认报文和通告窗口报文,尝试减少发送窗口到 100,把窗口的右端向左收缩了 80(这里我感觉作者说法有误,个人认为收缩值应是 361-(141+100) = 120),此时可用窗口的大小就会出现诡异的负值。

所以,如果发生了先减少缓存,再收缩窗口,就会出现丢包的现象。

为了防止这种情况发生,TCP 规定是不允许同时减少缓存又收缩窗口的,而是采用先收缩窗口,过段时间再减少缓存,这样就可以避免了丢包情况。

1.3.2 窗口关闭

如果窗口大小为 0 时,就会阻止发送方给接收方传递数据,直到窗口变为非 0 为止,这就是窗口关闭。

接收方是通过ACK报文通知发送方接收窗口大小的。

那么,当发生窗口关闭时,接收方处理完数据后,会向发送方通告一个窗口非 0 的 ACK 报文。 如果这个通告窗口的 ACK 报文在网络中丢失了,那麻烦就大了。发送方会一直等待接收方的ACK通知报文,接收方会一直等待发送方发送数据,这就造成了死锁。

解决方法:

TCP 为每个连接设有一个持续定时器,只要 TCP 连接一方收到对方的零窗口通知,就启动持续计时器。

如果持续计时器超时,就会发送窗口探测 ( Window probe ) 报文,而对方在确认这个探测报文时,会给出自己现在的接收窗口大小。

如果接收窗口仍然为 0,那么收到这个报文的一方就会重新启动持续计时器。

窗口探测报文并不需要使用特定的标志位进行设置,而是通过设置TCP头部中的Window字段为0,然后发送给目标主机进行探测。

窗口探测的次数一般为 3 次,每次大约 30-60 秒(不同的实现可能会不一样)。如果 3 次过后接收窗口还是 0 的话,有的 TCP 实现就会发 RST 报文来中断连接。

1.3.3 糊涂窗口综合症

如果接收方太忙了,来不及取走接收窗口里的数据,那么就会导致发送方的发送窗口越来越小。

到最后,如果接收方腾出几个字节并告诉发送方现在有几个字节的窗口,而发送方会义无反顾地发送这几个字节,这就是糊涂窗口综合症。

要知道,我们的 TCP + IP 头有 40 个字节,为了传输那几个字节的数据,要搭上这么大的开销,这太不经济了。

要解决糊涂窗口综合症,就要同时*解决上面两个问题:

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

通常的策略:

MSS:最大分段大小。

当「窗口大小」小于 min( MSS,缓存空间/2 ),也就是小于 MSS 与 1/2 缓存大小中的最小值时,就会向发送方通告窗口为 0,也就阻止了发送方再发数据过来。

等到接收方处理了一些数据后,窗口大小 >= MSS,或者接收方缓存空间有一半可以使用,就可以把窗口打开让发送方发送数据过来。

1.3.3.2 发送方避免发送小数据

发送方通常的策略

使用 Nagle 算法,该算法的思路是延时处理,只有满足下面两个条件中的任意一个条件,才可以发送数据:

  • 条件一:要等到窗口大小 >= MSS 并且 数据大小 >= MSS
  • 条件二:收到之前发送数据的 ack 回包

Nagle 算法默认是打开的,如果对于一些需要小数据包交互的场景的程序,比如,telnet 或 ssh 这样的交互性比较强的程序,则需要关闭 Nagle 算法。

可以在 Socket 设置 TCP_NODELAY 选项来关闭这个算法(关闭 Nagle 算法没有全局参数,需要根据每个应用自己的特点来关闭)

setsockopt(sock_fd, IPPROTO_TCP, TCP_NODELAY, (char *)&value, sizeof(int));

1.3.4 减少小报文传输的两种策略

减少小报文传输的两种策略:

  • nagle 算法
  • 延迟确认

其他文章:TCP之Nagle、Cork、Delay ACK

1.3.4.1 Nagle 算法

Nagle 伪代码如下:

if 有数据要发送 {
    if 可用窗口大小 >= MSS and 可发送的数据 >= MSS {
        立刻发送MSS大小的数据
    } else {
        if 有未确认的数据 {
            将数据放入缓存等待接收ACK
        } else {
            立刻发送数据
        }
    }
}

该算法的思路是延时处理,只有满足下面两个条件中的任意一个条件,才能可以发送数据:

  • 可用窗口大小 >= MSS 并且 要发送的数据 >= MSS
  • 收到ACK确认报文

只要上面两个条件都不满足,发送方一直在囤积数据。

可见,在最开始的时候,Nagel 算法一定会有一个小报文。

1.3.4.2 延迟确认(Delayed ACK)

没有携带数据的 ACK,它的网络效率也是很低的,因为它也有 40 个字节的 IP 头 和 TCP 头,但却没有携带数据报文。

TCP 延迟确认在Linux中是默认打开的。

TCP 延迟确认的策略:

  • 当有响应数据要发送时,ACK随着响应报文一起发送出去
  • 当没有响应数据要发送时,CK 将会延迟一段时间,以等待是否有响应数据可以一起发发送。如果等待期内有数据要发送则带上 ACK 一起,没有则发送ACK
  • 在等待的时候收到了对方发送的第二个报文,不管有没有数据要发送,此时立即发送ACK报文

延迟等待的时间是在 Linux 内核中定义的:

#define TCP_DELACK_MAX ((unsigned)(HZ/5))  # 最大延迟确认时间
#define TCP_DELACK_MIN ((unsigned)(HZ/25))  # 最小延迟确认时间

关键就需要 HZ 这个数值大小,HZ 是跟系统的时钟频率有关,每个操作系统都不一样,假设 HZ 大小是 1000,那么:

  • 最大延迟确认时间是 200 ms(1000/5)
  • 最短延迟确认时间是 40 ms(1000/25)
1.3.4.2.1 如何关闭

如果需要关闭延迟确认可以通过TCP_QUICKACK这个选项来启动快速ACK:

// 1 表示开启 TCP_QUICKACK,即关闭 TCP 延迟确认机制
int value = 1;
setsockopt(socketfd, IPPROTO_TCP, TCP_QUICKACK, (char*)& value, sizeof(int));

如果在快速的ACK模式下,ACK被立即发送。这个flag并不是永久的,系统会判定是交互数据流,仍然会启动delay ACK,所以这个flag在recv之后需要重新设置。

1.3.4.3 两者混合使用的问题

很明显,这两个同时使用会导致额外的时延。解决方法:

  • 要么关闭发送方的 Nagel 算法
  • 要么关闭接收方的TCP延迟确认

1.4 拥塞控制

拥塞窗口 cwnd(Congestion Window)是发送方维护的一个的状态变量,它会根据网络的拥塞程度动态变化,从而调节所要发送数据的量。

我们在前面提到过发送窗口 swnd 和接收窗口 rwnd 是约等于的关系,那么由于加入了拥塞窗口的概念后,此时发送窗口的值是swnd = min(cwnd, rwnd),也就是拥塞窗口和接收窗口中的最小值

拥塞窗口 cwnd 变化的规则:

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

如何知道拥塞了:只要发送方没有在规定的时间内收到ACK应答报文,即发生了超时重传,就认为网络出现了拥塞。

拥塞控制主要是四个算法:

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

1.4.1 和流量控制的区别

流量控制是避免「发送方」的数据填满「接收方」的缓存,但是并不知道网络的中发生了什么。

拥塞控制,控制的目的就是避免「发送方」的数据填满整个网络。

1.4.2 慢启动

TCP刚建立连接之后,首先就是慢启动的过程,一点点的提高发送数据包的数量。

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

可见,慢启动(slow start)算法中的发包数量是 2 的指数级增加。

注意:初始值在TCP协议中是根据具体的拥塞控制算法和实现进行设置的,而不是固定的数值。不同的操作系统、TCP实现和网络环境可能会有不同的默认初始值。

不过增长是有限制的,它就是慢启动门限 ssthresh (slow start threshold)

  • 当 cwnd < ssthresh 时,使用慢启动算法。
  • 当 cwnd >= ssthresh 时,就会使用拥塞避免算法

1.4.3 拥塞避免

当拥塞窗口 cwnd 「超过」慢启动门限 ssthresh 就会进入拥塞避免算法。

一般情况下,ssthresh 大小是 65535 字节。

规则:每当收到一个 ACK 时,cwnd 增加 1/cwnd。

接上前面的慢启动的例子,现假定 ssthresh 为 8:

当 8 个 ACK 应答确认到来时,每个确认增加 1/8,8 个 ACK 确认 cwnd 一共增加 1,于是这一次能够发送 9 个 MSS 大小的数据,变成了线性增长。

就这么一直增长着后,网络就会慢慢进入了拥塞的状况了,于是就会出现丢包现象,这时就需要对丢失的数据包进行重传。

当触发了重传机制,也就进入了拥塞发生算法

1.4.4 拥塞发生

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

  • 超时重传
  • 快速重传

这两种使用的拥塞发生算法是不同的。

1.4.4.1 发生超时重传的拥塞发生算法

ssthresh 和 cwnd 的值会发生变化:

  • ssthresh 会变成 cwnd/2
  • cwnd 重置为初始值

接着,就重新开始慢启动。这种方式很激进,反应也很强烈,会造成网络卡顿。

Linux 针对每一个 TCP 连接的 cwnd 初始化值是 10,也就是 10 个 MSS,我们可以用 ss -nli 命令查看每一个 TCP 连接的 cwnd 初始化值。

1.4.4.2 发生快速重传的拥塞发生算法

ssthresh 和 cwnd 的值会发生变化:

  • cwnd = cwnd/2
  • ssthred = cwnd
  • 进入快速恢复算法

1.4.5 快速恢复

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

进入快速恢复之前,cwnd 和 ssthresh 已被更新了:

  • cwnd = cwnd/2
  • ssthresh = cwnd

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

  • 拥塞窗口 cwnd = ssthresh+3 (意思是收到了3个ACK)
  • 重传丢失的数据包
  • 后面如果再收到 重复的ACK,cwnd就+1
  • 如果收到新的数据之后,设置 cwnd = ssthresh即发生拥塞时的一半,重新进入到拥塞避免状态。原因是该 ACK 确认了新的数据,说明从 duplicated ACK 时的数据都已收到,该恢复过程已经结束。

首先,快速恢复是拥塞发生后慢启动的优化,其首要目的仍然是降低 cwnd 来减缓拥塞,所以必然会出现 cwnd 从大到小的改变。

其次,过程2(cwnd逐渐加1)的存在是为了尽快将丢失的数据包发给目标,从而解决拥塞的根本问题(三次相同的 ACK 导致的快速重传),所以这一过程中 cwnd 反而是逐渐增大的。

1.5 TCP Fast Open

客户端在向服务端发起 HTTP GET 请求时,一个完整的交互过程,需要 2.5 个 RTT 的时延。

由于第三次握手是可以携带数据的,这时如果在第三次握手发起 HTTP GET 请求,需要 2 个 RTT 的时延。

在 Linux 3.7 内核版本中,提供了 TCP Fast Open 功能,这个功能可以减少 TCP 连接建立的时延。

在客户端首次建立连接时的过程:

  1. 客户端发送 SYN 报文,该报文包含 Fast Open 选项,且该选项的 Cookie 为空,这表明客户端请求 Fast Open Cookie;
  2. 支持 TCP Fast Open 的服务器生成 Cookie,并将其置于 SYN-ACK 数据包中的 Fast Open 选项以发回客户端;
  3. 客户端收到 SYN-ACK 后,本地缓存 Fast Open 选项中的 Cookie。
  4. 所以,第一次发起 HTTP GET 请求的时候,还是需要正常的三次握手流程。

之后,如果客户端再次向服务器建立连接时的过程:

  1. 客户端发送 SYN 报文,该报文包含「数据」(对于非 TFO 的普通 TCP 握手过程,SYN 报文中不包含「数据」)以及此前记录的 Cookie;
  2. 支持 TCP Fast Open 的服务器会对收到 Cookie 进行校验:如果 Cookie 有效,服务器将在 SYN-ACK 报文中对 SYN 和「数据」进行确认,服务器随后将「数据」递送至相应的应用程序;如果 Cookie 无效,服务器将丢弃 SYN 报文中包含的「数据」,且其随后发出的 SYN-ACK 报文将只确认 SYN 的对应序列号;
  3. 如果服务器接受了 SYN 报文中的「数据」,服务器可在握手完成之前发送「数据」,这就减少了握手带来的 1 个 RTT 的时间消耗;
  4. 客户端将发送 ACK 确认服务器发回的 SYN 以及「数据」,但如果客户端在初始的 SYN 报文中发送的「数据」没有被确认,则客户端将重新发送「数据」;
  5. 此后的 TCP 连接的数据传输过程和非 TFO 的正常情况一致。

开启了 TFO 功能,cookie 的值是存放到 TCP 头部 option 字段里的。

注意:TCP Fast Open 是TCP层面的优化,和 HTTP 没有必然的联系。客户端缓存了 cookie 之后就可以一直使用,可以不断重复 TCP Fast Open 直到服务端认为该 cookie 无效。

可以通过设置 net.ipv4.tcp_fastopen 内核参数,来打开 Fast Open 功能:

  • 0 关闭
  • 1 作为客户端使用 Fast Open 功能
  • 2 作为服务端使用 Fast Open 功能
  • 3 无论作为客户端还是服务器,都可以使用 Fast Open 功能