发送窗口
由于无法确认接收方是否能及时接收数据包,TCP 传输中不适合每发一个数据包就停下来等待确认,因为这样传输效率太低。最好的方式是一次性将所有数据包发送出去,然后一起等待确认。但是,实际情况存在一些限制:
- 接收方的缓存(接收窗口)可能无法一次性接收所有数据;
- 网络的带宽也不一定足够大,一次性发送过多的数据包可能导致数据丢失。
因此,在 TCP 传输中,发送窗口通过限制发送数据包的数量来平衡传输效率和数据可靠性。发送窗口的大小计算公式为 wnd = min(rwnd, cwnd * mss),其中 rwnd 表示接收方告知的接收窗口大小,cwnd 表示发送方的拥塞窗口大小。在此限制范围内,尽可能多地发送数据包,一次可以发送的数据量即为 TCP 发送窗口。
发送窗口大小对传输性能的影响非常大。下图显示了发送窗口大小为 1 个 MSS(即每个 TCP 包所能携带的最大数据量)和 2 个 MSS 时的差别。在相同的往返时间内,发送窗口大小为 2 个 MSS 时,传输的数据量是发送窗口大小为 1 个 MSS 的两倍。
在实际应用中,发送窗口通常可以达到数十个 MSS 的大小,因此发送窗口的大小会对 TCP 传输的效率和可靠性产生巨大影响。
发送窗口 VS MSS
发送窗口决定了一口气能发多少字节,而 MSS 决定了这些字节要分多少个包发完。例如:
发送窗口为 16000 字节,MSS 为 1000 字节时,需要发送 16000/1000=16 个包;而如果 MSS 等于 8000,那要发送的包数就是 16000/8000=2。
接收窗口
在 TCP 协议中,接收窗口是一项非常重要的参数,它决定了发送方在一个确定时间内可以发送多少数据。
在 TCP 协议初期,网络带宽非常有限,因此 TCP 的最大接收窗口被定义为 65535 字节。但随着网络带宽的提高,这个值已经无法满足现代网络传输的需求了。
- 如果抓包时没有抓到三次握手,Wireshark 就不知道该如何计算,所以有时候会很莫名地看到一些极小的接收窗口值。
- 如果防火墙识别不了 Window Scale,因此对方无法获得 Shift count,最终导致严重的性能问题。
1992 年,RFC 1323 提出了一种解决方案,即在三次握手时向对方发送自己的 Window Scaling 信息,Window Scaling 是一个 2 的指数,通过它可以计算出实际的 TCP 接收窗口大小。这个方案的好处是可以不需要修改 TCP 头的设计。
# 查看 Linux 内核 TCP Window Scaling sysctl net.ipv4.tcp_window_scaling > net.ipv4.tcp_window_scaling = 1 # 设置 Linux 内核 TCP Window Scaling sudo sysctl -w net.ipv4.tcp_window_scaling=0 > net.ipv4.tcp_window_scaling = 0
拥塞窗口
拥塞控制的基本思想是发送方通过维护一个虚拟的拥塞窗口,控制数据包的发送速度,以防止网络拥塞。
- 在连接建立初期,发送方对网络状况一无所知。由于一次发送过多数据可能会遭遇拥塞,因此发送方需要将拥塞窗口的初始值设置得很小。根据 RFC(请求评论文档)的建议,初始值为 2 个、3 个或者 4 个 MSS(最大报文段长度),具体取决于 MSS 的大小。
- 在慢启动过程中,拥塞窗口大小随着时间的推移而逐渐增加。此时,传输速度比较快,触碰拥塞点的风险也增加。因此,不能继续采用翻倍的慢启动算法,而是要缓慢增加拥塞窗口大小。根据 RFC 的建议,在每个往返时间中增加 1 个 MSS。例如,如果发送了 16 个 MSS 并得到全部确认,则拥塞窗口大小增加到 16+1=17 个 MSS。随后,拥塞窗口大小会增加到 18、19 等,这个过程称为拥塞避免。
- 在慢启动过渡到拥塞避免的临界窗口值方面,需要根据之前是否发生过拥塞来确定。如果发生过拥塞,则应将该拥塞点作为参考。如果从未发生过拥塞,则可以选择一个较大的值,例如与最大接收窗口相等。
具体怎么知道窗口多大会触发拥塞呢?
假设我们要计算的是某个 TCP 连接的拥塞点,而在该连接中存在一连串重传包。首先,我们需要找到重传包序列中的第一个包,然后根据其 Seq 值找到其对应的原始包,进而计算出原始包发送时刻的在途字节数。因为网络拥塞发生在该原始包发送的时刻,因此该时刻的在途字节数大致代表了拥塞点的大小。
在途字节数的计算公式应该是:
在途字节数 = Seq + Len - Ack
其中,Seq 是指包的序列号,Len 是包的长度,Ack 是指确认号。
具体步骤:
- Wireshark 上单击 Analyze 菜单,再单击 Expert Info 选项,得到重传统计表。
- 点击第一个重传包No.1225,可见它的 Seq=1012852。于是用“tcp.seq ==1012852”作为过滤条件
- 点击 Apply 过滤之后得到了原始包 No. 1053
- 选定 1053 号包,然后点击 Clear 清除过滤。可见上一个来自服务器端的包是 1051 号包
- 利用上述公式,可知当时的在途字节数为 1012852(No.1053 的 Seq)+816(No.1053 的 Len)-910546(No.1051 的 Ack)=103122 字节。
重传
在传输数据时,由于网络拥塞、硬件故障等原因导致数据包未能及时到达接收方,发送方会重新发送该数据包。
快速重传
在网络传输过程中,丢包是很常见的问题,不过有时候出现的丢包症状并不像严重拥塞时那么明显。一些因素如校验码不对可能导致单个包的丢失,或者只有少量的包丢失。当这些包的后续包能够正常到达接收方时,接收方会发现其 Seq 号比期望的大,为了提醒发送方重传这些包,接收方会每收到一个包就 Ack 一次期望的 Seq 号。当发送方接收到三个或以上的重复确认(Dup Ack)时,发送方便会意识到相应的包已经丢失,于是立即重传它。这个过程称为快速重传,与超时重传不同,它无需等待一段时间。
为什么要规定收到 3 个或以上的重复确认才会重传呢?这是因为网络包有时会乱序,乱序的包同样会触发重复的 Ack,但是为了乱序而重传却是不必要的。因为一般乱序的距离不会相差太大,比如 2 号包也许会跑到 4 号包后面,但不太可能跑到 6 号包后面。所以规定收到三个或以上的重复确认,可以在很大程度上避免因乱序而触发快速重传。
如下图所示,2 号包的丢失凑满了 3 个 Dup Ack,所以触发快速重传。而在右图中,2 号包跑到 4 号包后面,但因为凑不满 3 个 Ack,所以没有触发快速重传。
如果在拥塞避免阶段发生了快速重传,是否需要像发生超时重传一样处理拥塞窗口呢?