TCP数据的发送和接收

发布于:2025-07-06 ⋅ 阅读:(12) ⋅ 点赞:(0)

本篇文章结合实验对 TCP 数据传输中的重传机制、滑动窗口以及拥塞控制做简要的分析学习。

重传

实验环境

这里使用两台腾讯云服务器:vm-1(172.19.0.3)和vm-2(172.19.0.6)。

超时重传

首先 vm-1 作为服务端启动 nc,然后开启抓包,并使用 netstat 查看连接状态:

$ nc -k -l 172.19.0.3 9527

# 新开一个终端开启抓包
$ sudo tcpdump -s0 -X -nn "tcp port 9527" -w tcp.pcap --print

# 新开一个终端查看连接状态
$ while true; do sudo netstat -anpo | grep 9527 | grep -v LISTEN; sleep 1; done

然后我们在 vm-2 上使用 nc 连接 vm-1,三次握手成功后使用 iptables 拦截所有 vm-1 发来的包。

$ nc 172.19.0.3 9527

# 新开一个终端使用 iptables 拦截所有 vm-1 发来的包
$ sudo iptables -A INPUT -p tcp --sport 9527 -j DROP

准备好后我们从 vm-1 输入 abc 按下回车, vm-2 的 iptables 会将包丢弃,因此会触发 vm-1 进行重传,我们来看下 vm-1 的网络连接状态以及抓包结果:

  • 网络连接状态
tcp        0      0 172.19.0.3:9527         172.19.0.6:41278        ESTABLISHED 490833/nc            off (0.00/0/0)
tcp        0      4 172.19.0.3:9527         172.19.0.6:41278        ESTABLISHED 490833/nc            on (0.30/1/0)
tcp        0      4 172.19.0.3:9527         172.19.0.6:41278        ESTABLISHED 490833/nc            on (0.08/2/0)
tcp        0      4 172.19.0.3:9527         172.19.0.6:41278        ESTABLISHED 490833/nc            on (0.72/3/0)
tcp        0      4 172.19.0.3:9527         172.19.0.6:41278        ESTABLISHED 490833/nc            on (2.96/4/0)
...
tcp        0      4 172.19.0.3:9527         172.19.0.6:41278        ESTABLISHED 490833/nc            on (6.35/5/0)
...
tcp        0      4 172.19.0.3:9527         172.19.0.6:41278        ESTABLISHED 490833/nc            on (12.31/6/0)
...
tcp        0      4 172.19.0.3:9527         172.19.0.6:41278        ESTABLISHED 490833/nc            on (25.12/7/0)
...
tcp        0      4 172.19.0.3:9527         172.19.0.6:41278        ESTABLISHED 490833/nc            on (50.24/8/0)
...
tcp        0      4 172.19.0.3:9527         172.19.0.6:41278        ESTABLISHED 490833/nc            on (101.48/9/0)
...
tcp        0      4 172.19.0.3:9527         172.19.0.6:41278        ESTABLISHED 490833/nc            on (119.18/10/0)
...
tcp        0      4 172.19.0.3:9527         172.19.0.6:41278        ESTABLISHED 490833/nc            on (119.30/11/0)
...
tcp        0      4 172.19.0.3:9527         172.19.0.6:41278        ESTABLISHED 490833/nc            on (119.41/12/0)
...
tcp        0      4 172.19.0.3:9527         172.19.0.6:41278        ESTABLISHED 490833/nc            on (119.54/13/0)
...
tcp        0      4 172.19.0.3:9527         172.19.0.6:41278        ESTABLISHED 490833/nc            on (119.66/14/0)
...
tcp        0      4 172.19.0.3:9527         172.19.0.6:41278        ESTABLISHED 490833/nc            on (119.80/15/0)
...
  • 抓包结果

1. RTO 计算算法

三次握手后第 4 个包发送数据,其 length 为 4,我们输入了 abc 并按下回车,刚好四个字节,因为客户端收不到包,因此后续触发了重传。

TCP 重传是基于时间来判断的,这里有两个概念:

  • RTO(Retransmission TimeOut):重传超时时间
  • RTT(Round Trip Time):往返时间

TCP 会根据 RTT 来动态的计算 RTO,如果超时 RTO 会采用指数退避原则进行指数级增长,但最大不超过 120s。我们先来回顾下 RTO 的计算算法:

经典算法

RFC 793 中定义的 RTO 计算算法如下:

  1. 记录初始的几次 RTT 值
  2. 计算平滑 RTT 值(SRTT,Smoothed RTT),计算公式为如下:
# alpha 为平滑因子,取值在 0.8 到 0.9 之间,Linux 内核中默认是 0.875
SRTT = ( ALPHA * SRTT ) + ((1-ALPHA) * RTT)

可以看到,如果 alpha 值越大,标识系统越信任之前的计算结果,否则就会更信任新的 RTT 值。

  1. 计算 RTO 值,计算公式为如下:
RTO = min[Ubound,max[Lbound,(BETA*SRTT)]]
  • Ubound 为 RTO 上限,Linux 内核中默认是 120s
  • Lbound 为 RTO 下限,Linux 内核中默认是 200ms
  • Beta 为延迟方差因子,取值在 1.3 到 2.0 之间。
Karn 算法

上述算法的问题在于将所有包的 RTT 一视同仁,是对于重传的包,如果取第一次发送+ACK 包的 RTT 值,会导致 RTT 明显偏大;如果取重传的包,此时如果之前的 ACK 响应回来了,又会导致取值偏小。

为此 1987 年 Phil Karn/Craig Partridge 在论文 Improving Round-Trip Time Estimates in Reliable Transport Protocols 中提出了 Karn 算法,其最大的特点是将重传的包忽略掉,不用来做 RTT 的计算,同时一旦重传,RTO 会立即翻倍。

rfc6298 中规定,RTT 的采用必须采用 Karn 算法。

Jacobson/Karels 算法

RFC2988 中改进了重传算法,并在 rfc6298 中进行了更新,其规定的 RTO 计算算法如下:

对于初始 RTO,当第一个包的 RTT 获取到后:
SRTT = RTT
RTTVAR = RTT / 2
RTO = SRTT + max(K*RTTVAR, G) where K = 4 and G = 200ms

对于后续的 RTO 值计算,获取到新的 RTT 后:
RTTVAR = (1-Beta)*RTTVAR + Beta*|SRTT - RTT|
SRTT = (1-Alpha)*SRTT + Alpha*RTT

最后 RTO 的计算公式为:

RTO = SRTT + max(K*RTTVAR, G)

在 Linux 中,Alpha 取值为 0.125,Beta 取值为 0.25,K 取值为 4,G 取值为 200ms,其次还做了一些工程上的优化,这里先不深究,具体源码参考tcp_rtt_estimatortcp_set_rto

RTO 与 Delayed ACK

我们可以通过 ss -tip 命令查看某个连接的 rto,可以看到我们的连接初始 RTO 为 200ms,每次超时重传后都会翻倍,一直增长到 120s 后固定不变。

# 初始 RTO 为 200ms

ESTAB 0      0               172.19.0.3:9527                172.19.0.6:41278 users:(("nc",pid=490833,fd=4))
	 cubic wscale:7,7 rto:200 rtt:0.153/0.076 mss:8448 pmtu:8500 rcvmss:536 advmss:8448 cwnd:10 segs_in:2 send 4.42Gbps lastsnd:11221 lastrcv:11221 lastack:11221 pacing_rate 8.83Gbps delivered:1 app_limited rcv_space:57076 rcv_ssthresh:57076 minrtt:0.153 snd_wnd:59264

ESTAB 0      4               172.19.0.3:9527                172.19.0.6:41278 users:(("nc",pid=490833,fd=4))
	 cubic wscale:7,7 rto:12800 backoff:6 rtt:0.153/0.076 mss:8448 pmtu:8500 rcvmss:536 advmss:8448 cwnd:1 ssthresh:7 bytes_sent:32 bytes_retrans:28 segs_out:8 segs_in:2 data_segs_out:8 send 442Mbps lastsnd:1115 lastrcv:28668 lastack:28668 pacing_rate 8.83Gbps delivered:1 app_limited busy:14438ms unacked:1 retrans:1/7 lost:1 rcv_space:57076 rcv_ssthresh:57076 minrtt:0.153 snd_wnd:59264


ESTAB 0      4               172.19.0.3:9527                172.19.0.6:41278 users:(("nc",pid=490833,fd=4))
	 cubic wscale:7,7 rto:51200 backoff:8 rtt:0.153/0.076 mss:8448 pmtu:8500 rcvmss:536 advmss:8448 cwnd:1 ssthresh:7 bytes_sent:40 bytes_retrans:36 segs_out:10 segs_in:2 data_segs_out:10 send 442Mbps lastsnd:45728 lastrcv:112705 lastack:112705 pacing_rate 8.83Gbps delivered:1 app_limited busy:98475ms unacked:1 retrans:1/9 lost:1 rcv_space:57076 rcv_ssthresh:57076 minrtt:0.153 snd_wnd:59264


ESTAB 0      4               172.19.0.3:9527                172.19.0.6:41278 users:(("nc",pid=490833,fd=4))
	 cubic wscale:7,7 rto:102400 backoff:9 rtt:0.153/0.076 mss:8448 pmtu:8500 rcvmss:536 advmss:8448 cwnd:1 ssthresh:7 bytes_sent:44 bytes_retrans:40 segs_out:11 segs_in:2 data_segs_out:11 send 442Mbps lastsnd:2475 lastrcv:124748 lastack:124748 pacing_rate 8.83Gbps delivered:1 app_limited busy:110518ms unacked:1 retrans:1/10 lost:1 rcv_space:57076 rcv_ssthresh:57076 minrtt:0.153 snd_wnd:59264


$ sudo ss -tip | grep -A 1 9527
ESTAB 0      4               172.19.0.3:9527                172.19.0.6:41278 users:(("nc",pid=490833,fd=4))
	 cubic wscale:7,7 rto:120000 backoff:10 rtt:0.153/0.076 mss:8448 pmtu:8500 rcvmss:536 advmss:8448 cwnd:1 ssthresh:7 bytes_sent:48 bytes_retrans:44 segs_out:12 segs_in:2 data_segs_out:12 send 442Mbps lastsnd:4544 lastrcv:233313 lastack:233313 pacing_rate 8.83Gbps delivered:1 app_limited busy:219083ms unacked:1 retrans:1/11 lost:1 rcv_space:57076 rcv_ssthresh:57076 minrtt:0.153 snd_wnd:59264


$ sudo ss -tip | grep -A 1 9527
ESTAB 0      4               172.19.0.3:9527                172.19.0.6:41278 users:(("nc",pid=490833,fd=4))
	 cubic wscale:7,7 rto:120000 backoff:15 rtt:0.153/0.076 mss:8448 pmtu:8500 rcvmss:536 advmss:8448 cwnd:1 ssthresh:7 bytes_sent:68 bytes_retrans:64 segs_out:17 segs_in:2 data_segs_out:17 send 442Mbps lastsnd:2520 lastrcv:845689 lastack:845689 pacing_rate 8.83Gbps delivered:1 app_limited busy:831459ms unacked:1 retrans:1/16 lost:1 rcv_space:57076 rcv_ssthresh:57076 minrtt:0.153 snd_wnd:59264

从 ss 的信息中可以看到虽然 RTT 的大小始终是 rtt:0.153/0.076 ,代表 rtt 时间为 0.153ms,平均偏差为 0.076ms,但 RTO 时间最小也是 200ms,后续一直增加到120000 ms,看起来和 RTT 并没有关系。

这样是因为 Linux 内核规定了 RTO 的最小值和最大值分别为 200ms 和 120s,具体源码如下:

// 源码地址:https://elixir.bootlin.com/linux/v6.0/source/include/net/tcp.h#L141
#define TCP_RTO_MAX     ((unsigned)(120*HZ)) 
#define TCP_RTO_MIN     ((unsigned)(HZ/5))

HZ 表示 CPU 一秒种发出多少次时间中断–IRQ-0,通常使用 HZ 做时间片的单位,可以理解为 1HZ 就是 1s。

$ cat /boot/config-`uname -r` | grep '^CONFIG_HZ='
CONFIG_HZ=1000

# ubuntu @ vm-1 in ~ [15:44:15]
$ cat /proc/interrupts | grep timer && sleep 1 && cat /proc/interrupts | grep timer
LOC:  134957597  148734818   Local timer interrupts
LOC:  134957987  148735153   Local timer interrupts

这样做主要是为了给 Delayed ACK 留出时间。简单来说就是让 TCP 在收到数据包后稍微等一会,看有没有其他需要发送的数据,如果有就让 ACK 搭个便车一起发送回去,这样可以减少网络上小包的数量,提高网络传输效率。

重传超时时长

netstat 查看状态可以看到重传计时器在不断变化,从 200ms 开始不断翻倍,最终在传完 10 次后固定为 120s,最终显示已经重传了 15 次 on (119.80/15/0)。这里主要受 tcp_retries2 参数的控制,默认为 15。注意这里不是精确控制一定会重传 15 次,而是 tcp_retries2 结合 TCP_RTO_MIN(200ms)计算出一个超时时间来,tcp 连接不断重传,最终不能超过这个超时时间。源码如下:


// 源码地址:https://elixir.bootlin.com/linux/v6.0/source/net/ipv4/tcp_timer.c#L231
static int tcp_write_timeout(struct sock *sk)
{
	// ... 代码省略
	bool expired = false, do_reset;
	int retry_until = READ_ONCE(net->ipv4.sysctl_tcp_retries2);

	if (!expired)
		expired = retransmits_timed_out(sk, retry_until,
						icsk->icsk_user_timeout);
	if (expired) {
		/* Has it gone just too far? */
		tcp_write_err(sk);
		return 1;
	} 
}
// 源码地址:https://elixir.bootlin.com/linux/v6.0/source/net/ipv4/tcp_timer.c#L209
static bool retransmits_timed_out(struct sock *sk,
				  unsigned int boundary,
				  unsigned int timeout)
{
	// ... 代码省略
	unsigned int start_ts;
	unsigned int rto_base = TCP_RTO_MIN;
	timeout = tcp_model_timeout(sk, boundary, rto_base);
	return (s32)(tcp_time_stamp(tcp_sk(sk)) - start_ts - timeout) >= 0;
}


// 源码地址:https://elixir.bootlin.com/linux/v6.0/source/net/ipv4/tcp_timer.c#L182
static unsigned int tcp_model_timeout(struct sock *sk,
				      unsigned int boundary,
				      unsigned int rto_base)
{
	unsigned int linear_backoff_thresh, timeout;
	linear_backoff_thresh = ilog2(TCP_RTO_MAX / rto_base);
	if (boundary <= linear_backoff_thresh)
		timeout = ((2 << boundary) - 1) * rto_base;
	else
		timeout = ((2 << linear_backoff_thresh) - 1) * rto_base +
			(boundary - linear_backoff_thresh) * TCP_RTO_MAX;
	return jiffies_to_msecs(timeout);
}

可以看到内核取 tcp_retries2 参数值作为 boundary,核心计算逻辑位于 tcp_model_timeout 函数中,首先会计算出小于 120s 时的指数退避次数为 9。因此重传次数在小于等于 9 次时,下一次的重传时间都是指数增加的,如果超过 9 次比如已经发生了 10 次重传,那下一次的重传时间就是 120s 了。从 netstat 的输出中我们可以验证这一点:

tcp        0      4 172.19.0.3:9527         172.19.0.6:41278        ESTABLISHED 490833/nc            on (101.48/9/0)
...
tcp        0      4 172.19.0.3:9527         172.19.0.6:41278        ESTABLISHED 490833/nc            on (119.18/10/0)

总超时的计算逻辑为:

  • tcp_retries2 <= 9 时, timeout = ((2 << boundary) - 1) * rto_base
  • tcp_retries2 > 9 时, timeout = ((2 << linear_backoff_thresh) - 1) * rto_base + (boundary - linear_backoff_thresh) * TCP_RTO_MAX;

基于上述逻辑,在 rto 为 200ms时,我们可以计算出 tcp_retries2 设置和总重传超时时间的关系:

tcp_retries2 重传超时时间 总超时时间
0 200ms 200ms
1 400ms 600ms
2 800ms 1.4s
3 1.6s 3s
4 3.2s 6.2s
5 6.4s 12.6s
6 12.8s 25.4s
7 25.6s 51s
8 51.2s 102.2s
9 102.4s 204.6s
10 120s 324.6s
11 120s 444.6s
12 120s 564.6s
13 120s 684.6s
14 120s 804.6s
15 120s 924.6s

tcp_retries2 默认是 15,因此默认情况下,TCP 发送数据失败后大约会在 924.6s,也就是 15 分钟左右才会放弃连接。如果实际 RTO 很大,也不会真的重传 15 次导致等待时间过长,而是在超过 924.6s 后放弃连接。下面我们使用 tc qdisc 将 vm-2 的延迟改为 2s 来模拟网络延迟在来看下重传的次数:

# ubuntu @ vm-2 in ~ [10:05:28]
$ sudo tc qdisc add dev eth0 root netem delay 2000ms

修改完成后重新建立连接并发送数据,通过 ss、netstat 查看,可以看到初始 RTO 已经成了 6s,抓包显示实际的重传次数为 11 次,超时时长为 973.2567 - 45.5127 = 927.744s,大约 15 分钟多一些,基本符合预期。

# 初始 RTO 为 6s
$ sudo ss -tip | grep -A 1 9527
ESTAB 0      0               172.19.0.3:9527                172.19.0.6:36856 users:(("nc",pid=1880252,fd=4))
	 cubic wscale:7,7 rto:6000 rtt:2000/1000 mss:8448 pmtu:8500 rcvmss:536 advmss:8448 cwnd:10 segs_in:3 send 338kbps lastsnd:25355 lastrcv:25355 lastack:24330 pacing_rate 676kbps delivered:1 app_limited retrans:0/1 rcv_space:57076 rcv_ssthresh:57076 minrtt:2000 snd_wnd:59264

# 超时时间翻倍到 120s 后,RTO 也变为 120000ms
$ sudo ss -tip | grep -A 1 9527
ESTAB 0      4               172.19.0.3:9527                172.19.0.6:39054 users:(("nc",pid=1910324,fd=4))
	 cubic wscale:7,7 rto:120000 backoff:5 rtt:2000/1000 mss:8448 pmtu:8500 rcvmss:536 advmss:8448 cwnd:1 ssthresh:7 bytes_sent:28 bytes_retrans:24 segs_out:7 segs_in:3 data_segs_out:7 send 33.8kbps lastsnd:74641 lastrcv:308618 lastack:307585 pacing_rate 676kbps delivered:1 app_limited busy:269672ms unacked:1 retrans:1/7 lost:1 rcv_space:57076 rcv_ssthresh:57076 minrtt:2000 snd_wnd:59264

# 从 6 s 开始翻倍,6、12、24、48、96,在传完 5 次后超时时间固定为 120s。最终重传完 11 次后,总时间超过了 900 多s,系统终止连接
$ while true; do sudo netstat -anpo | grep 9527 | grep -v LISTEN; sleep 1; done
tcp        0      0 172.19.0.3:9527         172.19.0.6:39054        ESTABLISHED 1910324/nc           off (0.00/0/0)
tcp        0      4 172.19.0.3:9527         172.19.0.6:39054        ESTABLISHED 1910324/nc           on (3.98/0/0)
tcp        0      4 172.19.0.3:9527         172.19.0.6:39054        ESTABLISHED 1910324/nc           on (2.96/0/0)
tcp        0      4 172.19.0.3:9527         172.19.0.6:39054        ESTABLISHED 1910324/nc           on (1.94/0/0)
tcp        0      4 172.19.0.3:9527         172.19.0.6:39054        ESTABLISHED 1910324/nc           on (0.92/0/0)
tcp        0      4 172.19.0.3:9527         172.19.0.6:39054        ESTABLISHED 1910324/nc           on (0.00/0/0)
....
tcp        0      4 172.19.0.3:9527         172.19.0.6:39054        ESTABLISHED 1910324/nc           on (5.24/0/0)
tcp        0      4 172.19.0.3:9527         172.19.0.6:39054        ESTABLISHED 1910324/nc           on (4.22/0/0)
tcp        0      4 172.19.0.3:9527         172.19.0.6:39054        ESTABLISHED 1910324/nc           on (3.20/0/0)
tcp        0      4 172.19.0.3:9527         172.19.0.6:39054        ESTABLISHED 1910324/nc           on (2.17/0/0)
tcp        0      4 172.19.0.3:9527         172.19.0.6:39054        ESTABLISHED 1910324/nc           on (1.15/0/0)
tcp        0      4 172.19.0.3:9527         172.19.0.6:39054        ESTABLISHED 1910324/nc           on (0.13/0/0)
tcp        0      4 172.19.0.3:9527         172.19.0.6:39054        ESTABLISHED 1910324/nc           on (11.25/1/0)
...
tcp        0      4 172.19.0.3:9527         172.19.0.6:39054        ESTABLISHED 1910324/nc           on (23.27/2/0)
...
tcp        0      4 172.19.0.3:9527         172.19.0.6:39054        ESTABLISHED 1910324/nc           on (47.80/3/0)
...
tcp        0      4 172.19.0.3:9527         172.19.0.6:39054        ESTABLISHED 1910324/nc           on (95.36/4/0)
...
tcp        0      4 172.19.0.3:9527         172.19.0.6:39054        ESTABLISHED 1910324/nc           on (119.48/5/0)
...
tcp        0      4 172.19.0.3:9527         172.19.0.6:39054        ESTABLISHED 1910324/nc           on (119.48/11/0)
...
tcp        0      4 172.19.0.3:9527         172.19.0.6:39054        ESTABLISHED 1910324/nc           on (2.70/11/0)
tcp        0      4 172.19.0.3:9527         172.19.0.6:39054        ESTABLISHED 1910324/nc           on (1.68/11/0)
...
tcp        0      4 172.19.0.3:9527         172.19.0.6:39054        ESTABLISHED 1910324/nc           on (0.00/11/0)
tcp        0      4 172.19.0.3:9527         172.19.0.6:39054        ESTABLISHED 1910324/nc           on (0.00/11/0)

抓包结果如下:

快速重传

可以看到依赖于 RTO 的重传会因为 TCP_RTO_MIN 的影响,导致重传超时时间很长,效率很低。为此 RFC 5681 中提出了快速重传(Fast Retransmit),该算法不以时间作为重传依据,而是按照收到的重复 ACK 来判断是否需要重传。

RFC 规定,当接收方收到的包乱序时,要立即响应一个 Duplicate ACK,比如有 1、2、3、4、5 共5个包,在收到 1 后接收方 ACK 为 2,表示希望接下来收到 2 号包,但此时如果收到了 3、4、5 号包,此时接收方需要立即响应 duplicate ACK 给发送方。

RFC 规定发送方在收到 3 个 Duplicate ACK 后,会立即重传,这样判断的依据是,有两种情况会导致接收方收到的包乱序:乱序丢包

  • 如果是乱序,接收方通常会稍后收到预期的包,比如在收到 3 后才收到 2 号包,此时发送方一般只会收到 1 ~ 2 次 Duplicate ACK。

  • 如果是丢包,就会导致接收方多次响应 Duplicate ACK,此时发送方就可以认为是数据包丢失从而引发进行快速重传。

下面使用 scapy 来模拟快速重传的过程。代码如下:

  • 服务端程序
import socket
import time 

def start_server(host, port, backlog):
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.bind((host, port))
    server.listen(backlog)
    client, _ = server.accept()
    client.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) # 禁用 Nagle 算法

    client.sendall(b"a" * 1460)
    time.sleep(0.01) # 避免协议栈合并包的方式,不严谨但是凑合能工作
    client.sendall(b"b" * 1460)
    time.sleep(0.01)
    client.sendall(b"c" * 1460)
    time.sleep(0.01)
    client.sendall(b"d" * 1460)
    time.sleep(0.01)
    client.sendall(b"e" * 1460)
    time.sleep(0.01)
    client.sendall(b"f" * 1460)
    time.sleep(0.01)
    client.sendall(b"g" * 1460)

    time.sleep(10000)


if __name__ == '__main__':
    start_server('172.19.0.3', 9527, 8)
  • 客户端程序
import threading
import time
from scapy.all import *
from scapy.layers.inet import *


class ACKDataThread(threading.Thread):
    def __init__(self):
        super().__init__()
        self.first_data_ack_seq = 0

    def run(self):
        def packet_callback(packet):
            ip = IP(dst="172.19.0.3")

            resp_tcp = packet[TCP]

            # 收到第二次握手包
            if 'SA' in str(resp_tcp.flags):
                recv_seq = resp_tcp.seq
                recv_ack = resp_tcp.ack
                print(f"received SYN, seq={recv_seq}, ACK={recv_ack}")
                send_ack = recv_seq + 1
                tcp = TCP(sport=9528, dport=9527, flags='A', seq=2, ack=send_ack)
                print(f"send ACK={send_ack}")
                # 第三次握手
                send(ip/tcp)
                return
            # 收到数据包
            elif resp_tcp.payload:
                print("-" * 50)
                print(f"Received TCP packet")
                print(f"Flags: {resp_tcp.flags}")
                print(f"Sequence: {resp_tcp.seq}")
                print(f"ACK: {resp_tcp.ack}")
                print(f"Payload: {resp_tcp.load}")
                # send_ack = resp_tcp.seq + len(resp_tcp.load)
                if self.first_data_ack_seq == 0:
                    self.first_data_ack_seq = resp_tcp.seq + len(resp_tcp.load)
                send_ack = self.first_data_ack_seq
                tcp = TCP(sport=9528, dport=9527, flags='A', seq=2, ack=send_ack)
                print(f"send ACK={send_ack}")
				# 发送 4 次重复的 ACK
                send(ip/tcp)
                send(ip/tcp)
                send(ip/tcp)
                send(ip/tcp)

        interface = "eth0"  # 根据实际络接口名称更改
        sniff(iface=interface, prn=packet_callback, filter="tcp and port 9527", store=0)


def main():
    thread = ACKDataThread()
    thread.start()

    time.sleep(1)

    ip = IP(dst="172.19.0.3")
    tcp = TCP(sport=9528, dport=9527, flags='S', seq=1, options=[('MSS', 1460)])

    # 第一次握手
    print("send SYN, seq=0")
    send(ip/tcp)

    thread.join()


if __name__ == "__main__":
    main()

启动程序

# vm-1
# 启动服务端
$ python3 server.py
# 开启抓包
$ sudo tcpdump -S -s0 -nn "tcp port 9527" -w tcp-fast-retra.pcap --print


# vm-2
# 丢弃 RST 包
$ sudo iptables -A OUTPUT -p tcp --tcp-flags RST RST --dport 9527 -j DROP

# 启动客户端
$ python3 client.py

我们将抓包结果放到 Wireshark 中做分析,其标识了 Duplicate ACK 的包和快速重传的包,可以看到在服务端 0.018s 发送了数据包,然后在 0.072s 进行了快速重传,中间只差了 54ms,比 RTO 要小很多。然后在 0.285s 又进行了一次重传,这个和之前的快速重传包差了大约 200ms,已经是超时重传在进行了,后续在 0.709s、1.589s 进行的重传,时间间隔基本符合指数退避的规律。

Wireshark -> 统计 -> TCP 流图形 -> 序列号(tcptrace)窗口中可以看到重传的标识,其中的蓝色竖线表示有包发生了重传。

虽然 RFC 规定收到 3 个 Duplicate ACK 后才需要快速重传,但 Linux 提供了参数 net.ipv4.tcp_reordering来控制,默认为 3,如果我们修改为 1 可以看到在收到一个 Duplicate ACK 后就会立即重传。当然,生产环境中不建议修改这些参数。

$ sudo sysctl -w net.ipv4.tcp_reordering=1
net.ipv4.tcp_reordering = 1

SACK(Selective ACK)

SACK 选择性是 TCP 提供的一种选择重传机制,允许发送方在收到乱序包时,只重传丢失的包,而不是重传整个窗口的数据。

图片来自:TCP/IP Guide

SACK 需要双方协商,在握手时需要发送方在选项中携带 SACK 选项,接收方在收到后会启用 SACK 机制。在 Linux 下 由 net.ipv4.tcp_sack 参数控制。

$ sysctl net.ipv4.tcp_sack
net.ipv4.tcp_sack = 1

我们使用 nc 作为服务端,Scapy 作为客户端来复现 SACK 的情况。

 nc -k -l 172.19.0.15  9527

客户端代码

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import time
from scapy.all import *
from scapy.layers.inet import *


def main():
    ip = IP(dst="172.19.0.15")

    myself_seq = 1
    tcp = TCP(sport=9528, dport=9527, flags='S', seq=myself_seq, options=[("SAckOK", '')])
    print("send SYN, seq=0")
    resp = sr1(ip/tcp, timeout=2)
    if not resp:
        print("recv timeout")
        return

    resp_tcp = resp[TCP]
    if 'SA' in str(resp_tcp.flags):
        recv_seq = resp_tcp.seq
        recv_ack = resp_tcp.ack
        print(f"received SYN, seq={recv_seq}, ACK={recv_ack}")

        myself_seq += 1
        send_ack = recv_seq + 1
        tcp = TCP(sport=9528, dport=9527, flags='A', seq=myself_seq, ack=send_ack)
        print(f"send ACK={send_ack}")
        send(ip/tcp)

        # 特意注释掉,让发的数据有空洞
        # send data
        # payload = b"a" * 10
        # tcp = TCP(sport=9528, dport=9527, flags='A', seq=myself_seq, ack=send_ack)
        # send(ip/tcp/payload)
        myself_seq += 10

        payload = b"b" * 10
        tcp = TCP(sport=9528, dport=9527, flags='A', seq=myself_seq, ack=send_ack)
        send(ip/tcp/payload)
        myself_seq += 10

        # 特意注释掉,让发的数据有空洞
        # payload = b"c" * 10
        # tcp = TCP(sport=9528, dport=9527, flags='A', seq=myself_seq, ack=send_ack)
        # send(ip/tcp/payload)
        myself_seq += 10

        payload = b"d" * 10
        tcp = TCP(sport=9528, dport=9527, flags='A', seq=myself_seq, ack=send_ack)
        send(ip/tcp/payload)


    elif 'R' in str(resp_tcp.flags):
        print(f"received RST")
    else:
        print("received different TCP flags")

    time.sleep(100)


if __name__ == "__main__":
    main()

因为是使用 Scapy 伪造的 SYN 请求,内核中是没有 TCP 连接的,服务端的响应回来后内核会返回 RST 来终止连接。我们需要注意在客户端机器添加 iptables 规则将 RST 包屏蔽掉。

sudo iptables -A OUTPUT -p tcp --tcp-flags RST RST -s 172.19.0.11 -j DROP

然后开启抓包并运行客户端程序,可以看到 SACK 相关的相关信息。

滑动窗口

TCP 在发送数据时必须保证接收端能够正常接收数据,如果接收端已经没有空间接收数据了,发送端应该暂停发送数据,这一机制是通过 滑动窗口(Sliding Window) 实现的。

发送端会维护一个发送窗口结构对要发送的数据进行管理,如图所示:

图片来自 TCP/IP Guide

发送窗口以字节为单位管理数据,将数据分为四类:

  • #1 已经发送且已被确认的数据
  • #2 已经发送但未被确认的数据
  • #3 尚未发送但可以发送的数据(此时接收端还有空间)
  • #4 等待被发送的数据(此时接收端没有足够空间接收这些数据)

“黑色框”就是发送数据的窗口,当第二类的数据被确认后,它就可以向右滑动,这样后续的数据就可以继续发送了。

图片来自 TCP/IP Guide

TCP 连接的窗口大小是在三次握手时确定的,相关字段和计算方式参考 # TCP 连接的建立与关闭抓包分析,这里不在赘述。

对于还存在的 TCP 连接,可以通过 ss 命令查看其 wscale,示例如下,其 wscale 为 7,则其真实的窗口大小为 window * (2 ^7)。

$ sudo ss -tip | grep -A 1 9527
ESTAB 105856 0        172.19.0.15:9527      172.19.0.11:43120 users:(("python3",pid=710678,fd=4))
	 cubic wscale:7,7 rto:204 rtt:0.175/0.087 ato:80 mss:8448 pmtu:8500 rcvmss:8448 advmss:8448 cwnd:10 bytes_received:105856 segs_out:8 segs_in:22 data_segs_in:17 send 3.86Gbps lastsnd:2032 lastrcv:1944 lastack:1920 pacing_rate 7.72Gbps delivered:1 app_limited rcv_rtt:0.287 rcv_space:57076 rcv_ssthresh:57076 minrtt:0.175

抓包查看信息符合我们的计算:

下面我们用代码结合抓包看下滑动窗口的工作过程。

  • 服务端代码
import socket
import time

def start_server(host, port, backlog):
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.bind((host, port))
    # 只监听端口,不读取数据。
    server.listen(backlog)
    client, _ = server.accept()
    time.sleep(10000)


if __name__ == '__main__':
    start_server('172.19.0.15', 9527, 8)
  • 客户端代码
import socket
import time

def start_client(host, port):
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client.connect((host, port))
    client.setblocking(False)

    send_size = 0
    data = b"a" * 100000
    # 每秒发送数据
    while True:
        try:
            size = client.send(data)
            if size > 0:
                send_size += size
                print(f"send size: {size}")
                print(f"total send size: {send_size}\n")
                time.sleep(1)
        except BlockingIOError:
            time.sleep(0.1)
            pass

if __name__ == '__main__':
    start_client('172.19.0.15', 9527)

零窗口探测

运行程序后分析抓包信息,可以看到数据在发送一段时间之后,窗口会变为 0 。tcpdump 在第 28 行输出了 win 0,在 Wireshark 中第 28 个展示为 Zero Window,可以通过 tcp.analysis.zero_window 来过滤该类包。

发送端在收到 Zero Window 包后就停止发送数据了,为了在接收端窗口恢复正常时继续发送数据,发送端会触发零窗口探测(Zero Window Probe),定时发送探活包去探听接收端的窗口大小,查看发送端的 socket 状态可以看到启用了 probe 计时器来计算 ZWP 探活包的发送时间。

$ while true; do sudo netstat -anpo | grep -E "Recv-Q|9527" ;echo ; sleep 1; done
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name     Timer

Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name     Timer

Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name     Timer
tcp        0  74656 172.19.0.11:34408       172.19.0.15:9527        ESTABLISHED 493448/python3       on (0.17/0/0)

Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name     Timer
tcp        0  90912 172.19.0.11:34408       172.19.0.15:9527        ESTABLISHED 493448/python3       probe (0.20/0/0)

Silly Window Syndrome

上述基于滑动窗口的流控会导致所谓的 “糊涂窗口综合征”,每当发送端检测到接收端有一点窗口释放出来后就立即发送数据,这会导致大量的小包传输,严重影响网络传输性能。解决办法就是避免对这类小窗口进行处理。具体方法有:

  • 对于接收端,RFC 1122 规定可用空间必须不小于 Recieve Buffer 的一半与发送方一个完整 MSS 的最小值。比如我们的 Receive Buffer 为 1024byte,而发送端的 MSS 为 600 bytes,则只有接收端的可用 buffer > Min(1024/2,600)=512 时,才会告知发送端其真实 window 大小,否则还是返回 Zero Window。

  • 对于发送端,就是大名鼎鼎的 Nagle 算法了,RFC 1122中作了说明,和 Delayed ACK 一样也是延迟发送的思路,其规定当发送端存在未被 ACK 的数据时,其会延迟发送数据,直到其 1)收到了 ACK 或 2)待发送数据超过了 SMSS。Nagle 算法是默认打开的,并且没有全局的开关设置,对于像 SSH 这种交互性强的场景,通常需要频繁发送小包,此时 Nagle 算法会影响性能。可以通过设置 socket 的 TCP_NODELAY 来关闭。

我们修改下服务端程序,让其正常接收数据,然后再次抓包分析 TCP 的传输过程。

import socket
import time

def start_server(host, port, backlog):
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.bind((host, port))
    server.listen(backlog)
    client, _ = server.accept()
    client.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) # 禁用 Nagle 算法

    while True:
        for i in range(5):
            client.recv(4096)
        time.sleep(1)
    time.sleep(10000)


if __name__ == '__main__':
    start_server('172.19.0.15', 9527, 8)

抓包后其传输过程如图:

绿色线表示的就是接收窗口的大小,黄线表示 ACK 的数据变化。可以看到数据得到确认,黄线会上涨,同时接收窗口也会增长。蓝色点代表数据发送,每次接收窗口变化后数据也随之发送。

这里可以与第一次的抓包做对比,因为服务端不会主动接受数据,因此其黄线和绿线是不变的,进而导致发送端停止发送数据。

最后笔者在抓包时也遇到了巨帧(Jumbo Frames)的问题,可以看到很多数据包的大小明显超过了 MTU 8500 的限制。

这是 Linux 的 GRO/GSO/TSO 机制导致的,它们主要是为了优化数据传输的性能,其功能分别是:

名称
全称
方向 层级 用处
TSO TCP Segmentation Offload 发送 NIC(网卡) 让网卡把大 TCP 包拆小包
GSO Generic Segmentation Offload 发送 内核协议栈 让内核暂不拆包,延迟到驱动层
GRO Generic Receive Offload 接收 内核协议栈 把多个小包合并成大包再交给协议栈处理
因为 MTU 的原因,在发送数据时对于较大的数据包通常需进行分片操作,可以看到 TSO/GSO 的作用是将分片操作延迟到网卡驱动层;而 GRO 则是反过来,在收到包时将其合并成大包后再交给系统的协议栈处理,这样可以降低系统的开销。

三种机制都是默认开启的,可以通过如下命令查看:

$ sudo ethtool -k eth0 | grep -E "generic-segmentation-offload|generic-receive-offload|tcp-segmentation-offload"
tcp-segmentation-offload: on
generic-segmentation-offload: on
generic-receive-offload: on

上述三种机制是在网卡或者内核驱动层生效的,比抓包更加的底层,因此会导致我们抓到巨帧。如果需要可以临时关闭,命令如下:

$ sudo ethtool -K eth0 gso off

$ sudo ethtool -K eth0 gro off

$ sudo ethtool -K eth0 tso off

拥塞控制

上面提到的滑动窗口指的是接收方的接收窗口(Receiver Window),用来解决发送端和接收端的速率匹配问题,保证发送端的发送速度不会超过接收方的接收速度。除此之外,数据的发送速度受到网络环境的影响,如同我们发送获取到港口出口,为了及时发出去,除了港口的吞吐速度,还要考虑路上是不是堵车。

拥塞控制作为 TCP 协议最复杂的部分,相关算法层出不穷,到今天也在不断研究演进中。这里我们只关注最主要四个传统算法

四种传统算法

拥塞控制作为 TCP 协议最复杂的部分,相关算法层出不穷,到今天也在不断研究演进中。这里我们只关注最主要四个传统算法:

  • 1988年,TCP-Tahoe 提出了 慢启动(Slow start)、拥塞避免(Congestion Avoidance)、快速重传(fast retransmit)。
  • 1990 年 TCP Reno 在 Tahoe 的基础上增加快速恢复(Fast Recovery)。
慢启动 Slow start

顾名思义,慢启动的意思就是在 TCP 开始发送数据时,一点一点的逐步提高发送速度,不要一下子全力发送,把整个网络给占满,如果我们刚上高速时要逐步加速汇入主干道。

其实现主要依赖 cwnd(Congestion window,拥塞窗口),Linux 3.0 以后默认为 10 且不可更改。cwnd 表示的是 TCP 在收到 ACK 时最多能够发送的包的个数。也就是说最开始 Linux 最多发送 10 个数据包,最大数据量为 MSS * 10。

其工作过程如下:

  • cwnd 初始化为 10。
  • 每收到 1 个 ACK, 则线性增加 cwnd++
  • 每超过一个RTT,则指数增加 cwnd 翻倍
  • 到达 ssthresh(slow start threshold)上限后,进入拥塞避免算法。
拥塞避免(Congestion Avoidance)

上面提到了 ssthresh(slow start threshold),这是慢启动的上限。超过这个界限后,TCP 会采用拥塞避免算法,将 cwnd 改为线性增长,慢慢的找到适合网络的最佳值。具体方式是:

  • 收到一个ACK时,cwnd = cwnd + 1/cwnd。
  • 每过一个RTT时,cwnd = cwnd + 1。

拥塞时的处理

目前提到的算法都是基于丢包来判断网络是否堵塞的。当丢包 TCP 会进行重传,此时有两种情况:

  1. RTO 超时重传

TCP 会认为这是比较严重的网络问题,此时会将:
- sshthresh 降为 cwnd /2
- cnwd 重置为 1
- 进入慢启动状态

可以看到超时重传会极大的影响 TCP 的传输性能。

  1. 快速重传

TCP Tahoe的实现和上面的超时重传一样,TCP Reno 则提出了不同的实现:

  • cwnd = cwnd /2
  • sshthresh = cwnd
  • 进入快速恢复算法
快速恢复(Fast Recovery)

快速算法是基于快速重传来实现的,当收到 3 个duplicated ACK 时,它认为网络没有想象的那么糟糕,没必要像超时重传那样降 cwnd 粗暴的重置为 1。其在 cwnd 降为 cwnd /2,sshthresh = cwnd 后:

    • cwnd = sshthresh + 3 * MSS (3的意思是确认有3个数据包被收到了)
  • 重传 duplicated ACK 对应的数据包

Cubic 算法

Linux 内核在 2.6.19 后默认的拥塞控制是 CUBIC 算法,它使用三次函数作为其拥塞窗口的算法,并且使用函数拐点作为拥塞窗口的设置值,具体细节可以参考 Cubic 论文。Linux 中通过如下几个参数来设置拥塞算法:

$ sysctl -a | grep congestion

# 允许使用的拥塞算法
net.ipv4.tcp_allowed_congestion_control = reno cubic

# 内核中已经加载可用的拥塞算法
net.ipv4.tcp_available_congestion_control = reno cubic

# 当前默认的拥塞算法
net.ipv4.tcp_congestion_control = cubic

我们创建一个 4GB 大小的文件在两台机器之间传输。

# 服务端,收到的数据全部丢弃
$ nc -k -l 172.19.0.15  9527 > /dev/null

# 客户端,创建 4GB 的文件并传输
$ dd if=/dev/zero of=testfile bs=1M count=4096

$ nc 172.19.0.15 9527 < testfile

执行后抓包如下,可以看到传输过程还是比较丝滑的,cwnd 基本维持在 40 左右。

我们使用 tc 在客户端机器添加一定的丢包率

$ sudo tc qdisc replace dev eth0 root netem loss 5%

再次执行请求后抓包,可以看到传输速度从 21M 降到了 17M,看 tcptrace 会发现很多红色线代表重传。请求过程中查看 cwnd 会发现因为丢包会被不断重置为 1,从而影响发送效率。

BBR 算法

BBR 算法是近些年研究最为活跃的拥塞控制算法,其发送速率控制完全不在意丢包,自己会不断探测整个传输链路的带宽和时延,最终让发送数据稳定在带宽时延积。因此相比于上述算法,理论上 BBR 算法的传输性能会更有。

Linux 内核从 4.9 开始就支持 BBR 算法了,我们的内核版本是 5.15.0-139-generic,因此是支持的只需要启用下即可,方式如下:

# 检查内核配置文件是否支持BBR,如果是 y 说明已经内置,可以直接启用;如果是 m 说明是基于模块存在,需要加载模块;如果没有需要更新内核。
$ sudo cat /boot/config-$(uname -r) | grep CONFIG_TCP_CONG_BBR
CONFIG_TCP_CONG_BBR=m

# BBR 需要配合 fq 调度器使用,看是否已支持,输出是 m 说明支持。
# ubuntu @ vm-02 in ~ [10:02:06]
$ sudo cat /boot/config-$(uname -r) | grep CONFIG_NET_SCH_FQ
CONFIG_NET_SCH_FQ_CODEL=m
CONFIG_NET_SCH_FQ=m
CONFIG_NET_SCH_FQ_PIE=m

# 加载 bbr 模块
$ sudo modprobe tcp_bbr

# 查看可用算法
$ sysctl net.ipv4.tcp_available_congestion_control
net.ipv4.tcp_available_congestion_control = reno cubic bbr

bbr 算法可用后,修改 tcp_congestion_control 和 qdisc 配置即可启用 BBR:

$ sysctl -w net.ipv4.tcp_congestion_control=bbr net.core.default_qdisc=fq
net.core.default_qdisc=fq
net.ipv4.tcp_congestion_control=bbr

启用 BBR 算法后我们再次执行上述文件传输并抓包,在设置 5% 的丢包率前后其传输性能没有较大差异,均为 40M/s 左右。


cwnd 也没有出现重置为 1 的情况,实验时一直稳定在 36。

# 5% 丢包率启用 BBR 算法时的 cwnd 变化情况。
$ while true; do ss -i | grep -A 1 9527; sleep 1; done
tcp   ESTAB  0      1969152                  172.19.0.11:45832         172.19.0.15:9527
	 bbr wscale:7,7 rto:204 backoff:1 rtt:1.444/0.949 mss:8448 pmtu:8500 rcvmss:536 advmss:8448 cwnd:36 ssthresh:24 bytes_sent:1447680 bytes_retrans:25344 bytes_acked:1422337 segs_out:184 segs_in:57 data_segs_out:181 bbr:(bw:472Mbps,mrtt:0.105,pacing_gain:1.25,cwnd_gain:2) send 1.68Gbps lastsnd:412 lastrcv:676 lastack:200 pacing_rate 584Mbps delivery_rate 73Mbps delivered:179 busy:672ms rwnd_limited:668ms(99.4%) retrans:0/3 rcv_space:57088 rcv_ssthresh:57088 notsent:1969152 minrtt:0.105
tcp   ESTAB  0      1969152                  172.19.0.11:45832         172.19.0.15:9527
	 bbr wscale:7,7 rto:204 backoff:2 rtt:1.444/0.949 mss:8448 pmtu:8500 rcvmss:536 advmss:8448 cwnd:36 ssthresh:24 bytes_sent:1447680 bytes_retrans:25344 bytes_acked:1422337 segs_out:185 segs_in:57 data_segs_out:181 bbr:(bw:472Mbps,mrtt:0.105,pacing_gain:1.25,cwnd_gain:2) send 1.68Gbps lastsnd:1416 lastrcv:1680 lastack:1204 pacing_rate 584Mbps delivery_rate 73Mbps delivered:179 busy:1676ms rwnd_limited:1672ms(99.8%) retrans:0/3 rcv_space:57088 rcv_ssthresh:57088 notsent:1969152 minrtt:0.105
tcp   ESTAB  0      1969152                  172.19.0.11:45832         172.19.0.15:9527
	 bbr wscale:7,7 rto:204 backoff:3 rtt:1.444/0.949 mss:8448 pmtu:8500 rcvmss:536 advmss:8448 cwnd:36 ssthresh:24 bytes_sent:1447680 bytes_retrans:25344 bytes_acked:1422337 segs_out:186 segs_in:58 data_segs_out:181 bbr:(bw:472Mbps,mrtt:0.105,pacing_gain:1.25,cwnd_gain:2) send 1.68Gbps lastsnd:2420 lastrcv:2684 lastack:948 pacing_rate 584Mbps delivery_rate 73Mbps delivered:179 busy:2680ms rwnd_limited:2676ms(99.9%) retrans:0/3 rcv_space:57088 rcv_ssthresh:57088 notsent:1969152 minrtt:0.105
tcp   ESTAB  0      1969152                  172.19.0.11:45832         172.19.0.15:9527
	 bbr wscale:7,7 rto:204 backoff:4 rtt:1.444/0.949 mss:8448 pmtu:8500 rcvmss:536 advmss:8448 cwnd:36 ssthresh:24 bytes_sent:1447680 bytes_retrans:25344 bytes_acked:1422337 segs_out:187 segs_in:59 data_segs_out:181 bbr:(bw:472Mbps,mrtt:0.105,pacing_gain:1.25,cwnd_gain:2) send 1.68Gbps lastsnd:3424 lastrcv:3688 lastack:288 pacing_rate 584Mbps delivery_rate 73Mbps delivered:179 busy:3684ms rwnd_limited:3680ms(99.9%) retrans:0/3 rcv_space:57088 rcv_ssthresh:57088 notsent:1969152 minrtt:0.105
tcp   ESTAB  0      1969152                  172.19.0.11:45832         172.19.0.15:9527
	 bbr wscale:7,7 rto:204 backoff:4 rtt:1.444/0.949 mss:8448 pmtu:8500 rcvmss:536 advmss:8448 cwnd:36 ssthresh:24 bytes_sent:1447680 bytes_retrans:25344 bytes_acked:1422337 segs_out:187 segs_in:59 data_segs_out:181 bbr:(bw:472Mbps,mrtt:0.105,pacing_gain:1.25,cwnd_gain:2) send 1.68Gbps lastsnd:4432 lastrcv:4696 lastack:1296 pacing_rate 584Mbps delivery_rate 73Mbps delivered:179 busy:4692ms rwnd_limited:4688ms(99.9%) retrans:0/3 rcv_space:57088 rcv_ssthresh:57088 notsent:1969152 minrtt:0.105
tcp   ESTAB  0      1969152                  172.19.0.11:45832         172.19.0.15:9527
	 bbr wscale:7,7 rto:204 backoff:4 rtt:1.444/0.949 mss:8448 pmtu:8500 rcvmss:536 advmss:8448 cwnd:36 ssthresh:24 bytes_sent:1447680 bytes_retrans:25344 bytes_acked:1422337 segs_out:187 segs_in:59 data_segs_out:181 bbr:(bw:472Mbps,mrtt:0.105,pacing_gain:1.25,cwnd_gain:2) send 1.68Gbps lastsnd:5436 lastrcv:5700 lastack:2300 pacing_rate 584Mbps delivery_rate 73Mbps delivered:179 busy:5696ms rwnd_limited:5692ms(99.9%) retrans:0/3 rcv_space:57088 rcv_ssthresh:57088 notsent:1969152 minrtt:0.105
wntcp   ESTAB  0      1969152                  172.19.0.11:45832         172.19.0.15:9527
	 bbr wscale:7,7 rto:204 backoff:4 rtt:1.444/0.949 mss:8448 pmtu:8500 rcvmss:536 advmss:8448 cwnd:36 ssthresh:24 bytes_sent:1447680 bytes_retrans:25344 bytes_acked:1422337 segs_out:187 segs_in:59 data_segs_out:181 bbr:(bw:472Mbps,mrtt:0.105,pacing_gain:1.25,cwnd_gain:2) send 1.68Gbps lastsnd:6440 lastrcv:6704 lastack:3304 pacing_rate 584Mbps delivery_rate 73Mbps delivered:179 busy:6700ms rwnd_limited:6696ms(99.9%) retrans:0/3 rcv_space:57088 rcv_ssthresh:57088 notsent:1969152 minrtt:0.105
dtcp   ESTAB  0      1969152                  172.19.0.11:45832         172.19.0.15:9527
	 bbr wscale:7,7 rto:204 backoff:5 rtt:1.444/0.949 mss:8448 pmtu:8500 rcvmss:536 advmss:8448 cwnd:36 ssthresh:24 bytes_sent:1447680 bytes_retrans:25344 bytes_acked:1422337 segs_out:188 segs_in:60 data_segs_out:181 bbr:(bw:472Mbps,mrtt:0.105,pacing_gain:1.25,cwnd_gain:2) send 1.68Gbps lastsnd:7448 lastrcv:7712 lastack:888 pacing_rate 584Mbps delivery_rate 73Mbps delivered:179 busy:7708ms rwnd_limited:7704ms(99.9%) retrans:0/3 rcv_space:57088 rcv_ssthresh:57088 notsent:1969152 minrtt:0.105
tcp   ESTAB  0      1969152                  172.19.0.11:45832         172.19.0.15:9527
	 bbr wscale:7,7 rto:204 backoff:5 rtt:1.444/0.949 mss:8448 pmtu:8500 rcvmss:536 advmss:8448 cwnd:36 ssthresh:24 bytes_sent:1447680 bytes_retrans:25344 bytes_acked:1422337 segs_out:188 segs_in:60 data_segs_out:181 bbr:(bw:472Mbps,mrtt:0.105,pacing_gain:1.25,cwnd_gain:2) send 1.68Gbps lastsnd:8452 lastrcv:8716 lastack:1892 pacing_rate 584Mbps delivery_rate 73Mbps delivered:179 busy:8712ms rwnd_limited:8708ms(100.0%) retrans:0/3 rcv_space:57088 rcv_ssthresh:57088 notsent:1969152 minrtt:0.105
tcp   ESTAB  0      1969152                  172.19.0.11:45832         172.19.0.15:9527
	 bbr wscale:7,7 rto:204 backoff:5 rtt:1.444/0.949 mss:8448 pmtu:8500 rcvmss:536 advmss:8448 cwnd:36 ssthresh:24 bytes_sent:1447680 bytes_retrans:25344 bytes_acked:1422337 segs_out:188 segs_in:60 data_segs_out:181 bbr:(bw:472Mbps,mrtt:0.105,pacing_gain:1.25,cwnd_gain:2) send 1.68Gbps lastsnd:9456 lastrcv:9720 lastack:2896 pacing_rate 584Mbps delivery_rate 73Mbps delivered:179 busy:9716ms rwnd_limited:9712ms(100.0%) retrans:0/3 rcv_space:57088 rcv_ssthresh:57088 notsent:1969152 minrtt:0.105
tcp   ESTAB  0      1969152                  172.19.0.11:45832         172.19.0.15:9527
	 bbr wscale:7,7 rto:204 backoff:5 rtt:1.444/0.949 mss:8448 pmtu:8500 rcvmss:536 advmss:8448 cwnd:36 ssthresh:24 bytes_sent:1447680 bytes_retrans:25344 bytes_acked:1422337 segs_out:188 segs_in:60 data_segs_out:181 bbr:(bw:472Mbps,mrtt:0.105,pacing_gain:1.25,cwnd_gain:2) send 1.68Gbps lastsnd:10460 lastrcv:10724 lastack:3900 pacing_rate 584Mbps delivery_rate 73Mbps delivered:179 busy:10720ms rwnd_limited:10716ms(100.0%) retrans:0/3 rcv_space:57088 rcv_ssthresh:57088 notsent:1969152 minrtt:0.105
tcp   ESTAB  0      1969152                  172.19.0.11:45832         172.19.0.15:9527
	 bbr wscale:7,7 rto:204 backoff:5 rtt:1.444/0.949 mss:8448 pmtu:8500 rcvmss:536 advmss:8448 cwnd:36 ssthresh:24 bytes_sent:1447680 bytes_retrans:25344 bytes_acked:1422337 segs_out:188 segs_in:60 data_segs_out:181 bbr:(bw:472Mbps,mrtt:0.105,pacing_gain:1.25,cwnd_gain:2) send 1.68Gbps lastsnd:11468 lastrcv:11732 lastack:4908 pacing_rate 584Mbps delivery_rate 73Mbps delivered:179 busy:11728ms rwnd_limited:11724ms(100.0%) retrans:0/3 rcv_space:57088 rcv_ssthresh:57088 notsent:1969152 minrtt:0.105

笔者这里只用了两台内网机器做实验,理论上距离更远的传输路径,BBR 更好用,鉴于篇幅这里不再过多赘述,挖个坑后面在专门写篇 BBR 相关的实验。关于 BBR 更详细的论文可以参考其 论文和Google 的 Github 项目。

总结

本篇实验基本将 TCP 数据传输遇到的点都做了涉猎,我觉得初学 TCP 的小伙伴最好都从类似的实验开始,动手做一遍后再去读理论性强的书籍和 RFC 资料,学起来会更加事半功倍。

笔者在做完TCP 连接的建立与关闭抓包分析和本篇实验后将 《TCP/IP 详解(英文版)》的 TCP 章节又重读了一遍,整个阅读体验和收获和之前硬啃完全不一样。初读时更像是一种填鸭式的硬啃,啃完过段时间也就忘了。做完实验后重读时,整个阅读体验类似有点品读的意思,读到相关章节之前都能回想起实验时的场景以及相关的知识点,大脑会自动的与书中内容做对比,查缺补漏,校对细节,从而构建更坚固的理解和记忆,这样读下来的收获是填鸭式阅读远远不能比的。