TCP数据的发送和接收

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

重传

实验环境

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

超时重传

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

bash 复制代码
$ 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 发来的包。

bash 复制代码
$ 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 的网络连接状态以及抓包结果:

  • 网络连接状态
bash 复制代码
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),计算公式为如下:
sh 复制代码
# alpha 为平滑因子,取值在 0.8 到 0.9 之间,Linux 内核中默认是 0.875
SRTT = ( ALPHA * SRTT ) + ((1-ALPHA) * RTT)

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

  1. 计算 RTO 值,计算公式为如下:
sh 复制代码
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 后固定不变。

bash 复制代码
# 初始 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,具体源码如下:

c 复制代码
// 源码地址: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。

bash 复制代码
$ 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 连接不断重传,最终不能超过这个超时时间。源码如下:

c 复制代码
// 源码地址: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 的输出中我们可以验证这一点:

sh 复制代码
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 来模拟网络延迟在来看下重传的次数:

bash 复制代码
# 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 分钟多一些,基本符合预期。

bash 复制代码
# 初始 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 来模拟快速重传的过程。代码如下:

  • 服务端程序
python 复制代码
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)
  • 客户端程序
python 复制代码
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 后就会立即重传。当然,生产环境中不建议修改这些参数。

bash 复制代码
$ 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

客户端代码

python 复制代码
#!/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

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

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

  • 服务端代码
python 复制代码
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)
  • 客户端代码
python 复制代码
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 的传输过程。

python 复制代码
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 中通过如下几个参数来设置拥塞算法:

bash 复制代码
$ 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,因此是支持的只需要启用下即可,方式如下:

bash 复制代码
# 检查内核配置文件是否支持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:

bash 复制代码
$ 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 章节又重读了一遍,整个阅读体验和收获和之前硬啃完全不一样。初读时更像是一种填鸭式的硬啃,啃完过段时间也就忘了。做完实验后重读时,整个阅读体验类似有点品读的意思,读到相关章节之前都能回想起实验时的场景以及相关的知识点,大脑会自动的与书中内容做对比,查缺补漏,校对细节,从而构建更坚固的理解和记忆,这样读下来的收获是填鸭式阅读远远不能比的。

相关推荐
ifeng091824 分钟前
HarmonyOS网络请求优化实战:智能缓存、批量处理与竞态处理
网络·缓存·harmonyos
qq_401700411 小时前
Linux磁盘配置与管理
linux·运维·服务器
恒创科技HK1 小时前
香港大带宽服务器能降低ping值吗 ?
运维·服务器
llilian_161 小时前
智能数字式毫秒计在实际生活场景中的应用 数字式毫秒计 智能毫秒计
大数据·网络·人工智能
爱代码的小黄人1 小时前
华硕主板BIOS设置台式机电脑“Restore AC Power Loss”(断电后恢复状态)设置
运维·服务器·电脑
打码人的日常分享2 小时前
基于信创体系政务服务信息化建设方案(PPT)
大数据·服务器·人工智能·信息可视化·架构·政务
武汉唯众智创2 小时前
职业院校网络安全靶场实训演练系统建设方案
网络·安全·web安全·网络安全·网络安全靶场实训演练系统·网络安全靶场实训·网络安全实训演练系统
G31135422732 小时前
判断 IP 地址纯净度
服务器·网络
北京盛世宏博2 小时前
如何利用技术手段来甄选一套档案馆库房安全温湿度监控系统
服务器·网络·人工智能·选择·档案温湿度
濊繵3 小时前
Linux网络--Socket 编程 TCP
linux·网络·tcp/ip