跟我一起学TCP/IP

Web、Mail、Ftp、DNS、Proxy、VPN、Samba、LDAP 等基础网络服务
723937936@qq.com
帖子: 51
注册时间: 2023-02-26 9:59
系统: ubuntu

Re: 跟我一起学TCP/IP

#31

帖子 723937936@qq.com » 2023-03-27 10:24

第十八章:TCP Connection Establishment and Termination

连接建立:

1. 客户端发送SYN segment,目的是:通告自己的ISN和MSS
2. 服务端发送SYN+ACK segment,目的是:除了通告自己的ISN和MSS外,顺带确认已收到客户端发送的SYN segment
3. 客户端发送ACK segment,目的是:确认收到服务端发送的SYN+ACK segment

上述过程也被称为:three-way handshake

几点说明:
1. 第1步的SYN segment是由connect函数触发的
2. 第2步的SYN+ACK segment是TCP模块自动生成的
3. 客户端收到SYN+ACK segment后,connect函数才返回
4. 服务端收到ACK segment后,accept函数才返回

先发送SYN segment的一端(一般是客户端)称为执行一个active open
后发送SYN+ACK segment的一端(一般是服务端)称为执行一个passive open
另外还可能发生同时打开(simultaneous open)的情况,也就是两端都执行active open

连接终止:

为了便于描述,下文称两端为A端和B端

1. A端发送FIN segment,通知B端我已经没有数据要发送了
2. B端发送ACK segment,确认已收到A端发送的FIN segment
3. B端发送FIN segment,通知A端我已经没有数据要发送了
4. A端发送ACK segment,确认已收到B端发送的FIN segment

几点说明:
1. FIN segment是由close或shutdown函数触发的
2. ACK segment是TCP模块自动生成的
3. 收到FIN的一端的应用的read/recv/recvmsg等操作会返回EOF


先发送FIN segment的一端称为执行一个active close
后发送FIN segment的一端称为执行一个passive close
另外还可能发生同时关闭(simultaneous close)的情况,也就是两端都执行active close

两端都需要发送一个FIN segment,是因为TCP是全双工协议,两端都要执行关闭连接的操作,每一端的关闭操作称为half-close(关闭连接的发送方向)

观察连接建立与终止

代码: 全选

$ sudo ./mytcpdump port 8888
 0.000 ( 0.000) IP 192.168.0.3.50396 > 192.168.0.6.8888: flags [S], seq 634664549, win 65535, options [mss 1460], length 0
 0.000 ( 0.000) IP 192.168.0.6.8888 > 192.168.0.3.50396: flags [SA], seq 3060868865, ack 634664550, win 65160, options [mss 1460], length 0
 0.001 ( 0.001) IP 192.168.0.3.50396 > 192.168.0.6.8888: flags [A], seq 634664550, ack 3060868866, win 2058, options [], length 0

 5.990 ( 5.989) IP 192.168.0.3.50396 > 192.168.0.6.8888: flags [FA], seq 634664550, ack 3060868866, win 2058, options [], length 0
 5.990 ( 0.000) IP 192.168.0.6.8888 > 192.168.0.3.50396: flags [FA], seq 3060868866, ack 634664551, win 510, options [], length 0
 5.990 ( 0.000) IP 192.168.0.3.50396 > 192.168.0.6.8888: flags [A], seq 634664551, ack 3060868867, win 2058, options [], length 0
在上一个帖子已经描述了上述的输出,这里再强调两点:
1. SYN segment和FIN segment 都消耗一个sequence number
2. SYN segment除了通告对端自己的ISN外,还通告对端自己期望接收的MSS(表示TCP segment里data的最大字节数)


TCP模块发送的segment大小主要由两个值决定:
1.对端通告的MSS
2.自己发现的pMTU

TCP模块发送的segment大小由这两个值中的较小值决定

注意:
MSS指的对segment里data的大小限制
pMTU指的是对整个IP Datagram的大小限制


观察连接建立超时重传

在arp cache里添加一个不存在的主机的硬件地址(避免IP模块调用ARP请求)

代码: 全选

$ sudo arp -s 192.168.0.8 90:9c:4a:c0:be:d1
$ arp -n
Address                  HWtype  HWaddress           Flags Mask            Iface
192.168.0.3              ether   90:9c:4a:c0:be:d0   C                     enp0s3
192.168.0.8              ether   90:9c:4a:c0:be:d1   CM                    enp0s3
192.168.0.1              ether   2c:61:04:ba:ff:fa   C                     enp0s3
然后使用nc命令建立连接:

代码: 全选

$ time nc 192.168.0.8 8888

real	2m9.874s
user	0m0.002s
sys	0m0.000s
mytcpdump的输出:

代码: 全选

$ sudo ./mytcpdump -i enp0s3 port 8888
 0.000 ( 0.000) IP 192.168.0.6.39996 > 192.168.0.8.8888: flags [S], seq 1324841680, win 64240, options [mss 1460], length 0
 1.005 ( 1.005) IP 192.168.0.6.39996 > 192.168.0.8.8888: flags [S], seq 1324841680, win 64240, options [mss 1460], length 0
 3.025 ( 2.020) IP 192.168.0.6.39996 > 192.168.0.8.8888: flags [S], seq 1324841680, win 64240, options [mss 1460], length 0
 7.309 ( 4.284) IP 192.168.0.6.39996 > 192.168.0.8.8888: flags [S], seq 1324841680, win 64240, options [mss 1460], length 0
15.438 ( 8.129) IP 192.168.0.6.39996 > 192.168.0.8.8888: flags [S], seq 1324841680, win 64240, options [mss 1460], length 0
31.569 (16.131) IP 192.168.0.6.39996 > 192.168.0.8.8888: flags [S], seq 1324841680, win 64240, options [mss 1460], length 0
64.428 (32.859) IP 192.168.0.6.39996 > 192.168.0.8.8888: flags [S], seq 1324841680, win 64240, options [mss 1460], length 0
从上述的输出看:
1. time命令报告的时间大概130s(最后一个SYN segment需要再过大约32.859*2=65.718秒后超时,总时间大约为64.428+65.718=130.146)
2. tcp模块一共重传了6次,每次重传的时间间隔(RTO)都是上次的2倍(括号里的时间是距离前一个packet的时间)

建立连接过程中的syn的重传次数可以通过/proc/sys/net/ipv4/tcp_syn_retries文件配置

代码: 全选

$ cat /proc/sys/net/ipv4/tcp_syn_retries
6
TCP Half-Close

我理解half-close有两个含义:
1. TCP的half-close:指的是一端关闭发送方向,即触发FIN segment,从这个角度来说,close和shutdown(how=1)都执行TCP的half-close
2. FD的half-close:指的是关闭文件描述符的write方向,这个只能通过shudown函数(how=1),同样触发TCP的half-close,即:触发FIN segment

另外:
shutdown还可以指定how=0,关闭文件描述符的读方向,但是并不会触发TCP的half-close,即:不会触发FIN segment

我们通常说的half-close,一般指的是通过shudown函数(how=1)来触发TCP的half-close
上次由 723937936@qq.com 在 2023-03-27 20:18,总共编辑 1 次。
723937936@qq.com
帖子: 51
注册时间: 2023-02-26 9:59
系统: ubuntu

Re: 跟我一起学TCP/IP

#32

帖子 723937936@qq.com » 2023-03-27 19:03

TCP State Transition Diagram
Screen Shot 2023-03-27 at 6.57.17 PM.png
如上图所示:
对于客户端,通常的状态过渡由粗实线表示
对于服务端,通常的状态过渡由粗虚线表示

下面我们用netstat命令观察各种连接状态

客户端的状态过渡:

SYN_SENT状态

我们知道客户端调用connect函数会触发SYN segment的发送,然后客户端进入SYN_SENT状态
为了让客户端停留在这个状态,我们用iptables过滤掉相关的输入segments

代码: 全选

$ sudo iptables -t filter -A INPUT -p tcp -m tcp --sport 8888 -j DROP    # 过滤源端口为8888的输入segments
使用nc命令建立tcp连接

代码: 全选

$ nc 192.168.0.1 8888
使用netstat命令查看连接

代码: 全选

$ netstat -4tan
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
...        # 删除一些无关的行
tcp        0      1 192.168.0.6:55472       192.168.0.1:8888        SYN_SENT
...        # 删除一些无关的行
netstat报告该连接(192.168.0.6:55472 192.168.0.1:8888)处于SYN_SENT状态

ESTABLISHED状态

先清除掉前面添加的规则

代码: 全选

$ sudo iptables -F
使用nc命令建立tcp连接

代码: 全选

$ nc 192.168.0.1 80           # 路由器一般都有web管理功能,所以可以连接80端口
使用netstat命令查看连接

代码: 全选

$ netstat -4tan
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
...        # 删除一些无关的行
tcp        0      0 192.168.0.6:49852       192.168.0.1:80          ESTABLISHED
...        # 删除一些无关的行
FIN_WAIT1状态

先使用nc命令建立tcp连接

代码: 全选

$ nc 192.168.0.1 80
^C            # 等下一步做完后,再按Ctrl-C杀掉nc进程
然后用iptables过滤掉源端口为80的输入segments

代码: 全选

$ sudo iptables -t filter -A INPUT -p tcp -m tcp --sport 80 -j DROP
使用netstat命令查看连接

代码: 全选

$ netstat -4tan
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
...        # 删除一些无关的行
tcp        0      1 192.168.0.6:45836       192.168.0.1:80          FIN_WAIT1
...        # 删除一些无关的行
FIN_WAIT2状态

先清除掉前面添加的过滤规则,然后添加规则过滤掉输入的FIN segments

代码: 全选

$ sudo iptables -F
$ sudo iptables -t filter -A INPUT -p tcp -m tcp --sport 80 --tcp-flags FIN FIN -j DROP
先使用nc命令建立tcp连接

代码: 全选

$ nc 192.168.0.1 80
^C            # 立刻杀掉nc进程
然后使用netstat命令查看连接

代码: 全选

$ netstat -4tan
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
...        # 删除一些无关的行
tcp        0      0 192.168.0.6:50222       192.168.0.1:80          FIN_WAIT2
...        # 删除一些无关的行
TIME_WAIT状态

清除掉前面添加的过滤规则,然后使用nc命令建立tcp连接,然后直接按Ctrl-C杀掉nc进程,然后使用netstat命令查看连接

代码: 全选

$ sudo iptables -F
$ nc 192.168.0.1 80
^C
$ netstat -4tan
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
...        # 删除一些无关的行
tcp        0      0 192.168.0.6:39490       192.168.0.1:80          TIME_WAIT
...        # 删除一些无关的行
服务端的状态过渡:

LISTEN状态

使用nc命令启动一个tcp server,然后使用netstat命令查看连接

代码: 全选

$ nc -l 8888 &
$ netstat -4tan
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
...        # 删除一些无关的行
tcp        0      0 0.0.0.0:8888            0.0.0.0:*               LISTEN
...        # 删除一些无关的行
SYN_RECV状态
在上一步的基础上,使用iptables添加规则,过滤掉输入的ACK segment

代码: 全选

$ sudo iptables -t filter -A INPUT -p tcp -m tcp --dport 8888 --tcp-flags ACK ACK -j DROP
然后使用nc发起tcp连接

代码: 全选

$ nc 192.168.0.6 8888                # 这条命令是在192.168.0.3主机上执行的

然后使用netstat命令查看连接

代码: 全选

$ netstat -4tan
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
...        # 删除一些无关的行
tcp        0      0 0.0.0.0:8888            0.0.0.0:*               LISTEN
tcp        0      0 192.168.0.6:8888        192.168.0.3:53862       SYN_RECV
...        # 删除一些无关的行
原来处在LISTEN状态的socket还在,多了一个处于SYN_RECV状态的socket

ESTABLISHED状态

清除掉前面添加的过滤规则

代码: 全选

$ sudo iptables -F
使用nc命令启动一个tcp server,然后使用nc命令发起tcp连接,然后使用netstat命令查看连接

代码: 全选

$ nc -l 8888 &
[1] 32680
$ nc 192.168.0.6 8888 &
[2] 32681
$ netstat -4tan
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
...        # 删除一些无关的行
tcp        0      0 0.0.0.0:8888            0.0.0.0:*               LISTEN
tcp        0      0 192.168.0.6:8888        192.168.0.6:55962       ESTABLISHED
...        # 删除一些无关的行
原来处在LISTEN状态的socket还在,多了一个处于ESTABLISHED状态的socket

CLOSE_WAIT状态

从上面的TCP State Transition Diagram图看:

从ESTABLISHED状态过渡到CLOSE_WAIT状态是因为TCP收到FIN segment触发的,当连接进入CLOSE_WAIT状态时,会向应用进程报告EOF
由于nc命令收到EOF后,会立刻调用close关闭socket,从而立刻触发发送FIN segment,然后进入LAST_ACK状态
我们没有办法通过packet过滤来让连接停留在CLOSE_WAIT状态,因为从CLOSE_WAIT状态过渡到LAST_ACK状态是close操作触发的,并不是因为收到某个segment触发的。因此为了观察到连接停留在CLOSE_WAIT状态,我开发一个简单的simpletcpserver程序,该程序不调用close操作,从而使连接停留在CLOSE_WAIT状态

执行simpletcpserver启动一个tcp server

代码: 全选

$ ./simpletcpserver 8888 &
[1] 1810
$ netstat -4tan                       # 这条命令在下一步执行完后再执行
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
...        # 删除一些无关的行
tcp        0      0 0.0.0.0:8888            0.0.0.0:*               LISTEN
tcp        1      0 192.168.0.6:8888        192.168.0.3:54062       CLOSE_WAIT
...        # 删除一些无关的行
然后使用nc发起tcp连接

代码: 全选

$ nc 192.168.0.6 8888                # 这条命令是在192.168.0.3主机上执行的
^C                                               # 立刻按Ctrl-C杀掉nc进程
LAST_ACK状态

在192.168.0.6主机上启动一个tcp server

代码: 全选

$ nc -l 8888
在192.168.0.3主机上发起连接

代码: 全选

$ nc 192.168.0.6 8888
在192.168.0.6主机上使用iptables过滤外出的FIN segment(阻止服务端发送FIN segment)

代码: 全选

$ sudo iptables -t filter -A OUTPUT -p tcp -m tcp --sport 8888 --tcp-flags FIN FIN -j DROP
然后使用netstat命令查看连接

代码: 全选

$ netstat -4tan
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
...        # 删除一些无关的行
tcp        0      1 192.168.0.6:8888        192.168.0.3:54097       LAST_ACK
...        # 删除一些无关的行
723937936@qq.com
帖子: 51
注册时间: 2023-02-26 9:59
系统: ubuntu

Re: 跟我一起学TCP/IP

#33

帖子 723937936@qq.com » 2023-03-28 14:05

TIME_WAIT状态

TIME_WAIT状态又称为2MSL状态
MSL(maximum segment lifetime):是一个segment可以在网络中传输的最长时间(受IP数据报的TTL字段控制)

执行active close的一端,在发送最后一个ACK segment后,连接会进入TIME_WAIT状态,连接在这个状态停留2MSL的时长

两个问题:
1. 为什么主动关闭的一端要进入TIME_WAIT状态?
答:当主动关闭的一端发送最后的ACK segment后,需要确认对端是否收到了这个ACK segment,所以要等待

2. 为什么要等待2MSL的时长?
这是基于一个事实:如果对端没有收到最后的这个ACK segment的话,对端会重传FIN segment
要等待的最大时长(2MSL)=传输最后的这个ACK segment的最大时长+传输对端重传的那个FIN segment的最大时长

另外:
1. 如果在2MSL时间内没有收到FIN segment的话(对端没有重传FIN segment),就表示对端收到了ACK segment
2. 如果在2MSL时间内收到了对端重传的FIN segment,会再次发送ACK segment,然后重置定时器为2MSL

linux的2MSL的值是60s,这是在内核头文件include/net/tcp.h里定义的TCP_TIMEWAIT_LEN宏常量

连接处于TIME_WAIT状态的另一个效果

我们前面说过连接的唯一标识是:四元组(local ip address, local port, remote ip address, remote port),也称为socket pair
既然是连接的唯一标识,所以两个连接不能使用相同的四元组,这是连接的唯一标识的定义!

当一个连接处于TIME_WAIT状态时,这个连接还没有释放,所以新的连接不能使用与处于TIME_WAIT状态的连接相同的四元组。这是由连接的定义决定的,其实与连接是否处于TIME_WAIT状态无关。

与TIME_WAIT状态有关的是,当连接处于TIME_WAIT状态时,有一个额外的更严格的限制:新的连接(socket)也不能重用(bind)local port,应用程序可以通过SO_REUSEADDR套接字选项解除这个额外的限制

一般情况下是客户端发起active close,然后客户端的连接会进入TIME_WAIT状态,因为客户端很少会显式调用bind指定local port,而是让内核选择一个ephemeral port,所以当客户端连接处于TIME_WAIT状态时,不影响随后的客户端重启(再次启动使用了一个不同的ephemeral port)

然而服务端也可能发起active close,然后服务端的连接会进入TIME_WAIT状态,因为服务端一般都调用bind指定local port,当服务端的连接处于TIME_WAIT状态时,重启服务端时bind函数会报EADDRINUSE(Address already in use)错误(可以通过SO_REUSEADDR套接字选项解除这个限制)

服务端可能是有意设计为主动关闭连接(比如web server),也可能因为服务端进程crash,然后被systemd自动重新拉起

测试linux的2MSL大小

先用sock程序启动一个tcp server

代码: 全选

$ sock -s 6666
hello
^C               # 按^C杀掉sock进程
在另一个终端发起连接

代码: 全选

$ nc localhost 6666
hello              # 输入hello
连接建立后,杀掉sock进程,然后立即执行下面这个脚本

代码: 全选

$ for _ in `seq 100`; do date ; sock -s 6666 ; sleep 1 ; done
2023年 03月 28日 星期二 11:10:26 CST
can't bind local address: Address already in use
...     # 省略中间一些行
2023年 03月 28日 星期二 11:11:25 CST
can't bind local address: Address already in use
2023年 03月 28日 星期二 11:11:26 CST
从上面的输出看出,过了60s后重启server才成功

SO_REUSEADDR套接字选项

需要强调的是:当连接处于TIME_WAIT状态时,标识连接的四元组(local ip address, local port, remote ip address, remote port)仍在使用中,使用SO_REUSEADDR套接字选项理论上并不能建立具有相同四元组的连接,下面做实验进行验证

用我们的simpletcpserver启动一个tcp server

代码: 全选

linux $ ./simpletcpserver -A 8888            # 注意这里也使用了-A选项(如果这里不使用-A选项,后面的simpletcpclient使用-A也不能bind相同的了localport)
connection from 192.168.0.3:58411        # 在192.168.0.3(macos)主机上发起一个tcp连接
^C                 # 连接建立后,按Ctrl-C杀掉simpletcpserver进程
linux $ netstat -4tan       # 查看连接进入了TIME_WAIT状态
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
...        # 删除一些无关的行
tcp        0      0 192.168.0.6:8888        192.168.0.3:58411       TIME_WAIT
...        # 删除一些无关的行
用我们的simpletcpclient尝试建立使用相同四元组的连接

代码: 全选

linux $ ./simpletcpclient -h 192.168.0.6 -p 8888 192.168.0.3 58411
bind: Address already in use       # bind报告EADDRINUSE错误
使用-A选项设置SO_REUSEADDR套接字选项,再次尝试建立使用相同四元组的连接

代码: 全选

linux $ ./simpletcpclient -A -h 192.168.0.6 -p 8888 192.168.0.3 58411
connect: Connection refused           # 尽然成功了(bind没有报错,connect报的错误符合预期)
linux $ netstat -4tan | grep TIME_WAIT    # 查看TIME_WAIT是否还存在
linux $              # TIME_WAIT尽然消失了
从上面的输出看,在linux上使用SO_REUSEADDR套接字选项时,在建立使用相同四元组的连接时,如果前一个incarnation处于TIME_WAIT状态,则前一个incarnation会被立刻释放

在另一台主机发起相同四元组的连接

首先在linux主机运行simpletcpserver

代码: 全选

linux $ ./simpletcpserver -A 8888
connection from 192.168.0.3:50000
^C         # 连接建立后,按Ctrl-C杀掉simpletcpserver进程
linux $ netstat -4tan         # 查看连接进入了TIME_WAIT状态
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
...        # 删除一些无关的行
tcp        0      0 192.168.0.6:8888        192.168.0.3:50000       TIME_WAIT
...        # 删除一些无关的行
linux $ ./simpletcpserver -A 8888         # 再次启动server
connection from 192.168.0.3:50000      # 连接成功建立了,在另一个终端窗口用netstat命令查看连接,只有一个ESTABLISHED状态的连接,前一个处于TIME_WAIT状态的incarnation消失了
在macos上运行simpletcpclient命令如下:

代码: 全选

macos $ ./simpletcpclient -h 192.168.0.3 -p 50000 192.168.0.6 8888          # 第一次建立连接
macos $  ./simpletcpclient -h 192.168.0.3 -p 50000 192.168.0.6 8888         # 第二次建立连接
macos $
从上面的实验看出,在linux上使用SO_REUSEADDR套接字选项会让前一个处于TIME_WAIT状态incarnation立刻释放

前后使用相同四元组的连接互称为incarnation(化身)
723937936@qq.com
帖子: 51
注册时间: 2023-02-26 9:59
系统: ubuntu

Re: 跟我一起学TCP/IP

#34

帖子 723937936@qq.com » 2023-03-28 16:14

Quiet Time Concept

关于TIME_WAIT状态,还有几点强调:
1. 当连接处于TIME_WAIT状态期间,连接上收到的segment都会被丢弃
2. 当连接从TIME_WAIT状态过渡到CLOSED状态后,原则上网络中所有包含该该连接的四元组的segment都应该超时被丢弃了
3. 当连接处于TIME_WAIT状态时,如果应用未使用SO_REUSEADDR套接字选项(在Linux上),则不允许建立新的使用相同四元组的连接
4. 当连接处于TIME_WAIT状态时,如果应用使用了SO_REUSEADDR套接字选项(在Linux上),则在建立新的使用相同四元组的连接时,旧的连接会被立刻释放

TIME_WAIT状态可以提供一种保护,该保护是:避免新的连接接收到该连接之前的化身的segments,这种保护是基于上面提到的第2、3两点
然而:
1. 如果使用了SO_REUSEADDR套接字选项(在Linux上),TIME_WAIT状态就不能提供这种保护了
2. 如果有一个连接处于TIME_WAIT状态时,重启主机,然后建立使用相同四元组的连接,这个新的连接也有可能会接收到该连接之前的化身的segments

针对第2个情况,RFC793要求主机启动后,在MSL时间内(这段时间称为quiet time),禁止创建TCP连接(可能没有实现遵守RFC793的这个要求)
针对第1个情况呢?我没有查到相关资料

FIN_WAIT2状态

我们知道状态过渡的触发,要么是因为TCP模块收到了一个segment,要么是应用调用了某个系统调用

有两个状态可以长久的停留,一个是LISTEN状态,另一个是ESTABLISHED状态
其他状态要么过渡到ESTABLISHED状态,要么最终过渡到CLOSED状态

比如SYN_SENT状态,如果收到了对端的SYN+ACK,则过渡到ESTABLISHED状态,否则经过几次重传SYN后,会过渡到CLOSED状态
但是FIN_WAIT2状态要过渡到TIME_WAIT状态(然后过渡到CLOSED状态),取决于对端的应用进程调用close系统调用,这就变得不那么可控了
为了避免连接一直处于FIN_WAIT2状态,需要设置一个超时值,在Linux上这个超时值可以由/proc/sys/net/ipv4/tcp_fin_timeout文件配置

事情还没那么简单:
1. 我们之前学过,如果一端(A端)调用shutdown(how=1)发起half-close,目的是告诉对端(B端),我发送完了,但是我还要接收数据,这个时候A端就过渡到了FIN_WAIT2状态,B端过渡到了CLOSE_WAIT状态,此时如果B端一直不调用close,那么A端就永远处于FIN_WAIT2状态
2. 如果一端(A端)调用close发起关闭(也称为complete-close),同样A端过渡到了FIN_WAIT2状态,B端过渡到了CLOSE_WAIT状态,此时如果B端一直不调用close,那么A端会设置一个定时器,超时后A端会直接过渡到CLOSED状态

下面做实验验证:

先验证调用shutdown的情况:

先使用simpletcpserver启动一个server

代码: 全选

linux $ ./simpletcpserver 8888
connection from 192.168.0.6:37362
再使用tcpclient_fin_wait_2发起连接

代码: 全选

linux $ ./tcpclient_fin_wait_2
usage: tcpclient_fin_wait_2 [-s] host port
	-s	use shutdown
linux $ ./tcpclient_fin_wait_2 -s 192.168.0.6 8888

使用netstat查看连接状态:(无关行已删除)

代码: 全选

linux $ netstat -4tan
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
tcp        0      0 0.0.0.0:8888            0.0.0.0:*               LISTEN
tcp        0      0 192.168.0.6:37362       192.168.0.6:8888        FIN_WAIT2
tcp        1      0 192.168.0.6:8888        192.168.0.6:37362       CLOSE_WAIT
上面的倒数第二行是客户端维护的连接所处状态,倒数第一行是服务端维护的连接所处状态
大概等了十几分钟,再次运行netstat,结果与上面输出一致

要说明的是:
1. 虽然使用shutdown会导致客户端维护的连接永远处于FIN_WAIT2状态,但这仍然被认为是可控的,因为客户端可以调用close来启动FIN_WAIT2状态的定时器,并不依赖服务端
2. 同样服务端处于CLOSE_WAIT状态,也是可控的,因为这也只取决于服务端自己是否调用close,不依赖客户端

再验证调用close的情况


先使用simpletcpserver启动一个server

代码: 全选

linux $ ./simpletcpserver 8888
connection from 192.168.0.6:50414
在使用tcpclient_fin_wait_2发起连接

代码: 全选

linux $ ./tcpclient_fin_wait_2 192.168.0.6 8888

使用netstat查看连接状态:(无关行已删除)

代码: 全选

linux $ netstat -4tanc
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
tcp        0      0 0.0.0.0:8888            0.0.0.0:*               LISTEN
tcp        0      0 192.168.0.6:50414       192.168.0.6:8888        FIN_WAIT2
tcp        1      0 192.168.0.6:8888        192.168.0.6:50414       CLOSE_WAIT
使用netstat命令的-c选项,会每秒输出一次连接状态,我观察了大概60秒后,FIN_WAIT2状态消失了
723937936@qq.com
帖子: 51
注册时间: 2023-02-26 9:59
系统: ubuntu

Re: 跟我一起学TCP/IP

#35

帖子 723937936@qq.com » 2023-03-28 21:43

Reset Segments

我们之前提到过RST segment是TCP模块用来报告错误的,另外使用TCP协议的应用也可以使用RST segment报告错误
因此,RST segment可以由内核自动产生,也可以由应用触发

TCP模块使用RST segment报告错误

RST segment的最常见的例子是请求一个不存在的端口:

当服务端收到SYN segment,该segment里指定的目的端口是一个没有进程监听的端口时,服务端的TCP模块自动发送一个RST segment给客户端
回顾前面学习UDP时,这种情况下,服务端的UDP模块会自动发送一个ICMP port unreachable消息给客户端

观察RST segment

先在linux上执行mytcpdump命令

代码: 全选

linux $ sudo ./mytcpdump port 8888
 0.000 ( 0.000) IP 192.168.0.3.60342 > 192.168.0.6.8888: flags [S], seq 4233718391, win 65535, options [mss 1460], length 0
 0.000 ( 0.000) IP 192.168.0.6.8888 > 192.168.0.3.60342: flags [RA], seq 0, ack 4233718392, win 0, options [], length 0
在macos上发起连接

代码: 全选

macos $ nc -v 192.168.0.6 8888
nc: connectx to 192.168.0.6 port 8888 (tcp) failed: Connection refused
nc收到RST segment后,connect函数返回ECONNREFUSED(Connection refused)

TCP模块还可能遇到其他一些异常行为,也使用RST segment来报告错误,后面遇到时在进行学习

应用使用RST segment报告错误(Aborting a Connection)

当close用于TCP socket时,它的行为可以通过SO_LINGER套接字选项改变(我称之为定时关闭或延迟关闭)

SO_LINGER套接字选项的值的结构如下:

代码: 全选

struct linger {
	int l_onoff; /* 0=off, nonzero=on */
	int l_linger; /* linger time, POSIX specifies units as seconds */
};
UNP第7章第5小节的Figure 7.12很好的总结了close的3种行为,如下图:
Screen Shot 2023-03-28 at 8.39.30 PM.png
这里我们只关心第二种行为,即:l_onoff=1, l_linger=0的情况,这种行为称之为abortive release;close的默认行为称为orderly release(也叫graceful release)
当SO_LINGER选项设置为该值时,close会立即触发发送RST segment,然后连接直接进入CLOSED状态,socket send buffer和socket receive buffer的内容都被丢弃

我理解close的第二种行为,一般是服务端使用,当客户端发送的数据格式不正确时,服务端可以立即终止连接,理由是:
1. 立即丢弃掉socket receive buffer和socket send buffer的内容,因为没有必要继续接收或发送了
2. 避免连接进入TIME_WAIT状态,以免遭受恶意客户端的DOS攻击(比如故意发送错误消息格式,促使服务端出现大量处于TIME_WAIT状态的连接)
3. 对于接受RST segment的客户端,RST segment也是一个信号(服务端检测到了某个异常)

Detecting Half-Open Connections

当建立连接的两端,其中一端(通常是客户端)的主机突然断电重启,另一端(通常是服务端)维护的连接就称为half-open connection
这种情况是很常见的,尤其是在物联网环境中,因此检测半开连接就特别重要

下文的描述称断电重启的那一端为客户端,维护半开连接的一端为服务端

如果服务端继续发送数据,那么当data segment到达客户端时,会立刻引发RST segment,因为客户端断电重启后,内存里维护的连接信息已经全部丢失了
如果服务端再也不发送数据了,那服务端永远也不会知道客户端不见了,而且半开连接会永远存在(如果客户端经常性断电重启,会造成服务端存在大量的半开连接)

第23章介绍的TCP的keepalive选项可以用来检测半开连接,我们后面再学习
另外也可以在应用层维护心跳来检测半开连接
723937936@qq.com
帖子: 51
注册时间: 2023-02-26 9:59
系统: ubuntu

Re: 跟我一起学TCP/IP

#36

帖子 723937936@qq.com » 2023-03-29 12:35

Simultaneous Open

Simultaneous Open指的是两端同时执行active open
Screen Shot 2023-03-29 at 10.08.57 AM.png
如上图所示,建立连接需要交换4个segments,称为four-way handshake

显而易见的是要执行同时打开,双方都要知道对方bind的端口

作者给出了一个同时打开的例子,我们不再重复这个例子,但是这个例子里有一个有趣的事情是:同时打开的两个客户端也可以互相建立连接,下面我们来验证这种情况

我们改造一下之前开发的simpletcpclient程序,增加一个选项-d,表示在调用connect之前延迟一段时间

case 1

在linux(192.168.0.6)上执行simpletcpclient

代码: 全选

linux $ ./simpletcpclient -d 5 -p 7777 192.168.0.3 8888          # 5秒后调用connect
hello, world       # we type this line
and hi there       # this line was typed on other end
然后在macos(192.168.0.3)上执行simpletcpclient

代码: 全选

macos $ ./simpletcpclient -d 10 -p 8888 192.168.0.6 7777     # 10秒后调用connect
hello, world        # this is typed on the other end
and hi there        # we type this line
^D                      # and then type our EOF character
在linux上使用mytcpdump观察交换的segments

代码: 全选

linux $ sudo ./mytcpdump port 8888
 1    0.000 ( 0.000) IP 192.168.0.6.7777 > 192.168.0.3.8888: flags [S], seq 1734017499, win 64240, options [mss 1460], length 0
 2    1.013 ( 1.013) IP 192.168.0.6.7777 > 192.168.0.3.8888: flags [S], seq 1734017499, win 64240, options [mss 1460], length 0
 3    3.029 ( 2.016) IP 192.168.0.6.7777 > 192.168.0.3.8888: flags [S], seq 1734017499, win 64240, options [mss 1460], length 0
 4    7.102 ( 4.073) IP 192.168.0.3.8888 > 192.168.0.6.7777: flags [S], seq 50110790, win 65535, options [mss 1460], length 0
 5    7.102 ( 0.000) IP 192.168.0.6.7777 > 192.168.0.3.8888: flags [SA], seq 1734017499, ack 50110791, win 64240, options [mss 1460], length 0
 6    7.102 ( 0.000) IP 192.168.0.3.8888 > 192.168.0.6.7777: flags [A], seq 50110791, ack 1734017500, win 2058, options [], length 0

 7    30.971 (23.869) IP 192.168.0.6.7777 > 192.168.0.3.8888: flags [PA], seq 1734017500, ack 50110791, win 502, options [], length 12
 8    30.971 ( 0.000) IP 192.168.0.3.8888 > 192.168.0.6.7777: flags [A], seq 50110791, ack 1734017512, win 2058, options [], length 0

 9    40.180 ( 9.209) IP 192.168.0.3.8888 > 192.168.0.6.7777: flags [PA], seq 50110791, ack 1734017512, win 2058, options [], length 12
10    40.180 ( 0.000) IP 192.168.0.6.7777 > 192.168.0.3.8888: flags [A], seq 1734017512, ack 50110803, win 502, options [], length 0

11    52.267 (12.087) IP 192.168.0.3.8888 > 192.168.0.6.7777: flags [FA], seq 50110803, ack 1734017512, win 2058, options [], length 0
12    52.267 ( 0.000) IP 192.168.0.6.7777 > 192.168.0.3.8888: flags [FA], seq 1734017512, ack 50110804, win 502, options [], length 0
13    52.268 ( 0.001) IP 192.168.0.3.8888 > 192.168.0.6.7777: flags [A], seq 50110804, ack 1734017513, win 2058, options [], length 0
从上面的mytcpdump输出(我标了序号)看:
前3行是linux端发送的SYN segments(超时重传了2次),虽然macos端bind了8888端口,但是macos的TCP模块并没有自动响应SYN+ACK segment
第4行是macos端调用connect触发的SYN segment
第5行是linux端内核TCP模块自动响应的SYN+ACK segment
第6行是macos端内核TCP模块自动响应的ACK segment
至此连接已经建立,第3、4、5、6行交换4个segments,虽然第6个segment不是SYN+ACK,但这也是同时打开
第7、8行是对应hello, world的data segment和ack segment
第9、10行是对应and hi there的data segment和ack segment
第11、12、13行是正常的终止segment

case 2

上面的实现是linux主机先发起连接,macos主机后发起连接
下面我们反过来,让macos主机先发起连接

在macos(192.168.0.3)上执行simpletcpclient

代码: 全选

macos $ ./simpletcpclient -d 5 -p 8888 192.168.0.6 7777     # 5秒后调用connect
connect: Connection refused
然后在linux(192.168.0.6)上执行simpletcpclient

代码: 全选

linux $ ./simpletcpclient -d 10 -p 7777 192.168.0.3 8888          # 10秒后调用connect
connect: Connection refused
在linux上使用mytcpdump观察交换的segments

代码: 全选

linux $ sudo ./mytcpdump port 8888
 0.000 ( 0.000) IP 192.168.0.3.8888 > 192.168.0.6.7777: flags [S], seq 2502625888, win 65535, options [mss 1460], length 0
 0.000 ( 0.000) IP 192.168.0.6.7777 > 192.168.0.3.8888: flags [RA], seq 0, ack 2502625889, win 0, options [], length 0
 6.934 ( 6.934) IP 192.168.0.6.7777 > 192.168.0.3.8888: flags [S], seq 227747194, win 64240, options [mss 1460], length 0
 6.934 ( 0.000) IP 192.168.0.3.8888 > 192.168.0.6.7777: flags [RA], seq 0, ack 227747195, win 0, options [], length 0
从上述输出的前两行看:linux上如果只是调用bind指定端口(仍处于CLOSED状态,netstat命令不显示),并没有调用listen的话(处于LISTEN状态),那么当收到SYN segment时,内核会立即响应一个RST segment,对比上一个例子中,这种情况macos是什么都不响应

后两行的输出是因为macos的simpletcpclient进程已经退出了,所以macos收到SYN segment后也会响应一个RST segment

case 1的例子之所以能成功建立连接,是因为linux的socket处于SYN_SENT状态时再收到SYN segment,这时linux认为这是同时打开
在macos上如果只调用bind的话,socket也是处于CLOSED状态,netstat查看如下:

代码: 全选

macos $ netstat -f inet -p tcp -an  | grep 8888
tcp4       0      0  *.8888                 *.*                    CLOSED
然而很明显macos区分已调用bind的socket和未调用bind的socket


Simultaneous Close

Simultaneous Close指的是两端同时执行active close
Screen Shot 2023-03-29 at 12.26.36 PM.png
上图是同时关闭两端交换的4个segments
同时关闭,两端都会经过TIME_WAIT状态
723937936@qq.com
帖子: 51
注册时间: 2023-02-26 9:59
系统: ubuntu

Re: 跟我一起学TCP/IP

#37

帖子 723937936@qq.com » 2023-03-29 15:15

TCP Options

segment header里可以包含选项,选项格式如下图:
Screen Shot 2023-03-29 at 2.52.54 PM.png
kind=0和kind=1这两个选项只有1个字节
kind=0这个选项用于标识选项结束,但是它是一个充分非必要的选项,也就是说header里的最后一个选项可能是这个选项也可能不是这个选项
kind=1这个选项用于pad,目的是为了对齐选项到4字节
其余选项的格式是以1个字节的kind开头,随后是1个字节表示选项长度(包含kind、len、value),随后是选项value

本节只描述选项格式,相关选项待后续章节遇到时再进行学习,其中mss选项已经学过,用于通告对端自己最大可以接收的segment里data的大小

之前开发的mytcpdump程序只打印了mss选项,我们更新下mytcpdump程序,打印另外两个选项timestamp和window scale factor

示例:

代码: 全选

linux $ sudo ./mytcpdump port 8888
 0.000 ( 0.000) IP 192.168.0.3.54136 > 192.168.0.6.8888: flags [S], seq 1207282720, win 65535, options [mss 1460,ws 6,ts 952778593 0], length 0
 0.000 ( 0.000) IP 192.168.0.6.8888 > 192.168.0.3.54136: flags [SA], seq 2036517356, ack 1207282721, win 65160, options [mss 1460,ts 1753219082 952778593,ws 7], length 0
 0.000 ( 0.000) IP 192.168.0.3.54136 > 192.168.0.6.8888: flags [A], seq 1207282721, ack 2036517357, win 2058, options [ts 952778593 1753219082], length 0
 
 3.948 ( 3.948) IP 192.168.0.3.54136 > 192.168.0.6.8888: flags [PA], seq 1207282721, ack 2036517357, win 2058, options [ts 952782512 1753219082], length 6
 3.948 ( 0.000) IP 192.168.0.6.8888 > 192.168.0.3.54136: flags [A], seq 2036517357, ack 1207282727, win 510, options [ts 1753223030 952782512], length 0
 
 5.556 ( 1.608) IP 192.168.0.3.54136 > 192.168.0.6.8888: flags [FA], seq 1207282727, ack 2036517357, win 2058, options [ts 952784101 1753223030], length 0
 5.556 ( 0.000) IP 192.168.0.6.8888 > 192.168.0.3.54136: flags [FA], seq 2036517357, ack 1207282728, win 510, options [ts 1753224638 952784101], length 0
 5.556 ( 0.000) IP 192.168.0.3.54136 > 192.168.0.6.8888: flags [A], seq 1207282728, ack 2036517358, win 2058, options [ts 952784101 1753224638], length 0
前三行是连接建立的3个segments,中间两行是数据交互的segments,最后三行是终止连接的segments
其中前两个SYN segments里包含了mss、ws、ts三个选项
后续的segments里只包含ts选项

options后面的中括号里名字含义如下:
mss表示maximum segment size
ts表示timestamp
ws表示window scale factor
723937936@qq.com
帖子: 51
注册时间: 2023-02-26 9:59
系统: ubuntu

Re: 跟我一起学TCP/IP

#38

帖子 723937936@qq.com » 2023-03-29 22:12

TCP Server Design

TCP Server Port Numbers

观察server端维护的连接

在linux上启动一个tcp server

代码: 全选

linux $ nc -l 8888

在macos上启动几个tcp client

代码: 全选

macos $ nc linux 8888                 # 已在/etc/hosts里配置了192.168.0.6	linux
再开两个终端窗口,创建额外的2个tcpclient
...

然后在linux上使用netstat查看连接(无关行已删除)

代码: 全选

linux $ netstat -4tan
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
tcp        2      0 0.0.0.0:8888            0.0.0.0:*               LISTEN
tcp        0      0 192.168.0.6:8888        192.168.0.3:56736       ESTABLISHED
tcp        0      0 192.168.0.6:8888        192.168.0.3:56735       ESTABLISHED
tcp        0      0 192.168.0.6:8888        192.168.0.3:56734       ESTABLISHED
上述输出显示4个连接(处于LISTEN状态端点的一般不称为连接,而是称为listening socket或listening end point)
要强调的是:
1. 处于LISTEN状态的端点绑定的IP地址是0.0.0.0,这表示可以从主机的任意接口接受连接
2. 处于ESTABLISHED状态的连接的local port都是8888,标识连接的四元组里只有foreign port是不同的
3. 对于data segments,只能由处于ESTABLISHED状态的端点接收(TCP模块使用四元组来确定将该数据放到哪个连接的receive buffer里)
4. 对于syn segments,只能由处于LISTEN状态的端点接收(TCP模块使用segment里携带的目的IP地址和目的端口号来确定是哪个端点)

Restricting Local IP Address

tcp server在调用bind函数时,可以指定特定接口的IP地址,从而限制只能这个接口接受连接

Restricting Foreign IP Address

目的是限制服务端只接受指定的客户端发起的连接请求,linux上的socket API没有提供相关接口

Incoming Connection Request Queue

因为三次握手是内核中的TCP模块自动完成的,不需要应用进程参与,所以TCP模块需要维护一个队列来存放已完成三次握手的连接
实际上正在三次握手过程中的连接(处于SYN_RCVD状态的连接)也需要存放到一个队列中

这两个队列的总大小是由应用进程指定的(通过listen函数的第二个参数,称为backlog)

UNP第4.5小节的一张图很好的描述了这两个队列,如下图:
Screen Shot 2023-03-29 at 9.14.05 PM.png
几点说明:
1. 从应用的角度,应用进程可以认为只有一个队列
2. 当队列未满时,如果有连接进来,TCP模块自动完成三次握手,并将连接放到队列中
3. 当队列满时,如果再有连接进来,TCP模块什么都不会响应,促使客户端超时重传SYN segment
4. 当进程调用accept时,则从队列中取出一个连接(FIFO)

关于backlog:
实际上backlog指定的值并不是精确的队列长度,内核会根据backlog的值计算一个新的队列长度,不同的实现使用不同计算方法
UNP 图4.10给出了各种系统的backlog与实际的队列长度的对应值,如下图所示:
Screen Shot 2023-03-29 at 10.11.37 PM.png
实验:

准备工作:
1. 更新我们之前开发的simpletcpserver,添加一个-d选项,指定调用accept之前睡眠一定时间
2. 开发一个backlogtest程序,不停的发起连接请求

开始实验:
在linux上运行simpletcpserver

代码: 全选

linux $ ./simpletcpserver -d 60 -q 0 8888         # 指定睡眠60s再调用accept,-q 0指定backlog为0
在macos上运行backlogtest

代码: 全选

macos $ ./backlogtest linux 8888                 # 已在/etc/hosts里配置了192.168.0.6	linux
1                 # 只成功建立了一个连接
使用不同-q选项重新运行simpletcpserver,测得结果如下:

backlog queue len
0 1
1 2
2 3
3 4
4 5
5 6
6 7
7 8
8 9
9 10
10 11
11 12
12 13
13 14
14 15

看起来linux只是简单的把backlog+1的值作为队列的长度
723937936@qq.com
帖子: 51
注册时间: 2023-02-26 9:59
系统: ubuntu

Re: 跟我一起学TCP/IP

#39

帖子 723937936@qq.com » 2023-03-30 16:06

Connection Queue Review

上一个帖子说当服务端的连接队列满时,服务端什么都不响应,促使客户端重传SYN segment,实际上没这么简单

下面我们通过实验说明:

在linux上启动simpletcpserver

代码: 全选

linux $ ./simpletcpserver -d 100000 -q 3 8888       # -q 3表示指定backlog为3,那么全连接队列大小则为4,-d 100000表示延迟100000秒再调用accept
在macos os上启动backlogtest

代码: 全选

macos $ ./backlogtest -c 10 linux 8888          # 启动10个线程,同时发起连接
recv_timeout: Operation timed out
recv_timeout: Operation timed out
recv_timeout: Operation timed out
recv_timeout: Operation timed out
recv_timeout: Operation timed out
recv_timeout: Operation timed out
recv_timeout: Operation timed out
recv_timeout: Operation timed out
recv_timeout: Operation timed out
recv_timeout: Operation timed out
在Linux上使用mytcpdump观察segments

代码: 全选

linux $ sudo ./mytcpdump port 8888
 0.000 ( 0.000) IP macos.52551 > linux.8888: flags [S], seq 274874867, win 65535, options [mss 1460,ws 6,ts 990385264 0], length 0
 0.000 ( 0.000) IP macos.52552 > linux.8888: flags [S], seq 3641250079, win 65535, options [mss 1460,ws 6,ts 990385264 0], length 0
 0.000 ( 0.000) IP macos.52553 > linux.8888: flags [S], seq 2153714274, win 65535, options [mss 1460,ws 6,ts 990385264 0], length 0
 0.000 ( 0.000) IP macos.52554 > linux.8888: flags [S], seq 99659206, win 65535, options [mss 1460,ws 6,ts 990385264 0], length 0
 0.000 ( 0.000) IP macos.52555 > linux.8888: flags [S], seq 4198299130, win 65535, options [mss 1460,ws 6,ts 990385264 0], length 0
 0.000 ( 0.000) IP macos.52556 > linux.8888: flags [S], seq 2660334176, win 65535, options [mss 1460,ws 6,ts 990385264 0], length 0
 0.000 ( 0.000) IP macos.52557 > linux.8888: flags [S], seq 693903927, win 65535, options [mss 1460,ws 6,ts 990385264 0], length 0
 0.000 ( 0.000) IP macos.52558 > linux.8888: flags [S], seq 1809403647, win 65535, options [mss 1460,ws 6,ts 990385264 0], length 0
 0.000 ( 0.000) IP linux.8888 > macos.52551: flags [SA], seq 254823052, ack 274874868, win 65160, options [mss 1460,ts 1603279394 990385264,ws 7], length 0
 0.000 ( 0.000) IP linux.8888 > macos.52552: flags [SA], seq 433081043, ack 3641250080, win 65160, options [mss 1460,ts 1603279394 990385264,ws 7], length 0
 0.000 ( 0.000) IP linux.8888 > macos.52553: flags [SA], seq 2399418257, ack 2153714275, win 65160, options [mss 1460,ts 1603279394 990385264,ws 7], length 0
 0.000 ( 0.000) IP linux.8888 > macos.52554: flags [SA], seq 67195418, ack 99659207, win 65160, options [mss 1460,ts 1603279370 990385264,ws 7], length 0
 0.000 ( 0.000) IP linux.8888 > macos.52555: flags [SA], seq 3860588226, ack 4198299131, win 65160, options [mss 1460,ts 1603279370 990385264,ws 7], length 0
 0.000 ( 0.000) IP linux.8888 > macos.52556: flags [SA], seq 618064228, ack 2660334177, win 65160, options [mss 1460,ts 1603279370 990385264,ws 7], length 0
 0.000 ( 0.000) IP linux.8888 > macos.52557: flags [SA], seq 3620651171, ack 693903928, win 65160, options [mss 1460,ts 1603279370 990385264,ws 7], length 0
 0.000 ( 0.000) IP linux.8888 > macos.52558: flags [SA], seq 4249802168, ack 1809403648, win 65160, options [mss 1460,ts 1603279370 990385264,ws 7], length 0
 0.001 ( 0.001) IP macos.52559 > linux.8888: flags [S], seq 4212668517, win 65535, options [mss 1460,ws 6,ts 990385264 0], length 0
 0.001 ( 0.000) IP macos.52560 > linux.8888: flags [S], seq 2104951382, win 65535, options [mss 1460,ws 6,ts 990385264 0], length 0
 0.001 ( 0.000) IP macos.52551 > linux.8888: flags [A], seq 274874868, ack 254823053, win 2058, options [ts 990385264 1603279394], length 0
 0.001 ( 0.000) IP macos.52552 > linux.8888: flags [A], seq 3641250080, ack 433081044, win 2058, options [ts 990385264 1603279394], length 0
 0.001 ( 0.000) IP linux.8888 > macos.52559: flags [SA], seq 548771131, ack 4212668518, win 65160, options [mss 1460,ts 1603279370 990385264,ws 7], length 0
 0.001 ( 0.000) IP linux.8888 > macos.52560: flags [SA], seq 3472646427, ack 2104951383, win 65160, options [mss 1460,ts 1603279370 990385264,ws 7], length 0
 0.001 ( 0.000) IP macos.52553 > linux.8888: flags [A], seq 2153714275, ack 2399418258, win 2058, options [ts 990385264 1603279394], length 0
 0.001 ( 0.000) IP macos.52554 > linux.8888: flags [A], seq 99659207, ack 67195419, win 2058, options [ts 990385264 1603279370], length 0
 0.001 ( 0.000) IP macos.52555 > linux.8888: flags [A], seq 4198299131, ack 3860588227, win 2058, options [ts 990385264 1603279370], length 0
 0.001 ( 0.000) IP macos.52556 > linux.8888: flags [A], seq 2660334177, ack 618064229, win 2058, options [ts 990385264 1603279370], length 0
 0.001 ( 0.000) IP macos.52557 > linux.8888: flags [A], seq 693903928, ack 3620651172, win 2058, options [ts 990385264 1603279370], length 0
 0.001 ( 0.000) IP macos.52558 > linux.8888: flags [A], seq 1809403648, ack 4249802169, win 2058, options [ts 990385264 1603279370], length 0
 0.001 ( 0.000) IP macos.52559 > linux.8888: flags [A], seq 4212668518, ack 548771132, win 2058, options [ts 990385264 1603279370], length 0
 0.001 ( 0.000) IP macos.52560 > linux.8888: flags [A], seq 2104951383, ack 3472646428, win 2058, options [ts 990385264 1603279370], length 0
从上面的输出看:
1. macos向Linux发送了10个SYN segments
2. linux向macos发送了10个SYN+ACK segments
3. macos向Linux发送了10个ACK segments
由此可知,10个连接全部完成了三次握手

在macos上查看连接

代码: 全选

macos $ netstat -f inet -p tcp -an | grep 8888 | sort
tcp4       0      0  192.168.0.3.52551      192.168.0.6.8888       ESTABLISHED
tcp4       0      0  192.168.0.3.52552      192.168.0.6.8888       ESTABLISHED
tcp4       0      0  192.168.0.3.52553      192.168.0.6.8888       ESTABLISHED
tcp4       0      0  192.168.0.3.52554      192.168.0.6.8888       ESTABLISHED
tcp4       0      0  192.168.0.3.52555      192.168.0.6.8888       ESTABLISHED
tcp4       0      0  192.168.0.3.52556      192.168.0.6.8888       ESTABLISHED
tcp4       0      0  192.168.0.3.52557      192.168.0.6.8888       ESTABLISHED
tcp4       0      0  192.168.0.3.52558      192.168.0.6.8888       ESTABLISHED
tcp4       0      0  192.168.0.3.52559      192.168.0.6.8888       ESTABLISHED
tcp4       0      0  192.168.0.3.52560      192.168.0.6.8888       ESTABLISHED
从netstat的输出看,10个连接都处于ESTABLISHED状态

在Linux上查看连接

代码: 全选

linux $ netstat -4tan |  grep 8888 | sort
tcp        0      0 192.168.0.6:8888        192.168.0.3:52551       ESTABLISHED
tcp        0      0 192.168.0.6:8888        192.168.0.3:52552       ESTABLISHED
tcp        0      0 192.168.0.6:8888        192.168.0.3:52553       ESTABLISHED
tcp        0      0 192.168.0.6:8888        192.168.0.3:52554       ESTABLISHED
tcp        4      0 0.0.0.0:8888            0.0.0.0:*               LISTEN
从netstat的输出看,只有前4个连接处于ESTABLISHED状态
这是符合预期的,因为全连接队列的大小是4,其他6个连接还在半连接队列里

为什么会出现这个现象?
原因是:
1. 当收到SYN时,如果全连接队列和半连接队列都没满,则响应SYN+ACK
2. 当收到ACK时,如果全连接队列没满,则完成三次握手,然后将完成三次握手的连接从半连接队列移动到全连接队列,此时如果全连接队列已经满了,则继续留在半连接队列
3. 当收到ACK时,如果全连接队列满了,则直接丢弃ACK,随后会重传SYN+ACK给客户端(这是默认行为,可以通过/proc/sys/net/ipv4/tcp_abort_on_overflow文件修改)

使能tcp_abort_on_overflow

文件/proc/sys/net/ipv4/tcp_abort_on_overflow是一个布尔值,默认是0,如果设为1,则:
当服务端收到ACK时,如果全连接队列满了,则发送RST segment给客户端

实验:

先使能tcp_abort_on_overflow

代码: 全选

linux $ sudo bash -c 'echo 1 > /proc/sys/net/ipv4/tcp_abort_on_overflow'
在linux上启动simpletcpserver

代码: 全选

linux $ ./simpletcpserver -d 100000 -q 3 8888       # -q 3表示指定backlog为3,那么全连接队列大小则为4,-d 100000表示延迟100000秒再调用accept
在macos os上启动backlogtest

代码: 全选

macos $ ./backlogtest -c 10 linux 8888          # 启动10个线程,同时发起连接
recv_timeout: Connection reset by peer
recv_timeout: Connection reset by peer
recv_timeout: Operation timed out
recv_timeout: Operation timed out
recv_timeout: Operation timed out
recv_timeout: Operation timed out
recv_timeout: Operation timed out
recv_timeout: Operation timed out
recv_timeout: Operation timed out
recv_timeout: Operation timed out
上面的程序每次执行有一定的随机性,每次执行输出不一样
有几点说明:
1. 对于connect函数,只要收到对端发送的SYN+ACK就返回成功
2. connect返回成功,不代表完成了三次握手,因为服务端可能忽略客户端发送的ACK,然后发送一个RST segment给客户端,此时客户端只能通过recv函数来检测这个错误,上述输出的前两个就是这种情况
3. 还有一种可能是服务端收到SYN segment时,全连接队列或半连接队列已经满了,这时,服务端会丢弃SYN,什么都不响应,在客户端的表现是connect返回ETIMEDOUT



在Linux上使用mytcpdump观察segments

代码: 全选

linux $ sudo ./mytcpdump port 8888
 0.000 ( 0.000) IP macos.55756 > linux.8888: flags [S], seq 2861565099, win 65535, options [mss 1460,ws 6,ts 995360867 0], length 0
 0.000 ( 0.000) IP linux.8888 > macos.55756: flags [SA], seq 3413470649, ack 2861565100, win 65160, options [mss 1460,ts 1608300459 995360867,ws 7], length 0
 0.000 ( 0.000) IP macos.55757 > linux.8888: flags [S], seq 3088571366, win 65535, options [mss 1460,ws 6,ts 995360867 0], length 0
 0.000 ( 0.000) IP macos.55758 > linux.8888: flags [S], seq 3910276556, win 65535, options [mss 1460,ws 6,ts 995360867 0], length 0
 0.000 ( 0.000) IP macos.55756 > linux.8888: flags [A], seq 2861565100, ack 3413470650, win 2058, options [ts 995360867 1608300459], length 0
 0.000 ( 0.000) IP linux.8888 > macos.55757: flags [SA], seq 1016507251, ack 3088571367, win 65160, options [mss 1460,ts 1608300459 995360867,ws 7], length 0
 0.000 ( 0.000) IP linux.8888 > macos.55758: flags [SA], seq 1110096341, ack 3910276557, win 65160, options [mss 1460,ts 1608300459 995360867,ws 7], length 0
 0.000 ( 0.000) IP macos.55759 > linux.8888: flags [S], seq 3792309566, win 65535, options [mss 1460,ws 6,ts 995360867 0], length 0
 0.000 ( 0.000) IP macos.55760 > linux.8888: flags [S], seq 1721799658, win 65535, options [mss 1460,ws 6,ts 995360867 0], length 0
 0.000 ( 0.000) IP macos.55761 > linux.8888: flags [S], seq 4062300956, win 65535, options [mss 1460,ws 6,ts 995360867 0], length 0
 0.000 ( 0.000) IP macos.55762 > linux.8888: flags [S], seq 431299088, win 65535, options [mss 1460,ws 6,ts 995360867 0], length 0
 0.000 ( 0.000) IP macos.55763 > linux.8888: flags [S], seq 842395789, win 65535, options [mss 1460,ws 6,ts 995360867 0], length 0
 0.000 ( 0.000) IP macos.55757 > linux.8888: flags [A], seq 3088571367, ack 1016507252, win 2058, options [ts 995360867 1608300459], length 0
 0.000 ( 0.000) IP macos.55758 > linux.8888: flags [A], seq 3910276557, ack 1110096342, win 2058, options [ts 995360867 1608300459], length 0
 0.000 ( 0.000) IP macos.55764 > linux.8888: flags [S], seq 3893580623, win 65535, options [mss 1460,ws 6,ts 995360867 0], length 0
 0.000 ( 0.000) IP linux.8888 > macos.55759: flags [SA], seq 3977851215, ack 3792309567, win 65160, options [mss 1460,ts 1608300459 995360867,ws 7], length 0
 0.000 ( 0.000) IP linux.8888 > macos.55760: flags [SA], seq 3607323909, ack 1721799659, win 65160, options [mss 1460,ts 1608300426 995360867,ws 7], length 0
 0.000 ( 0.000) IP linux.8888 > macos.55761: flags [SA], seq 4253133500, ack 4062300957, win 65160, options [mss 1460,ts 1608300426 995360867,ws 7], length 0
 0.000 ( 0.000) IP linux.8888 > macos.55762: flags [SA], seq 139184709, ack 431299089, win 65160, options [mss 1460,ts 1608300426 995360867,ws 7], length 0
 0.000 ( 0.000) IP linux.8888 > macos.55763: flags [SA], seq 1964370174, ack 842395790, win 65160, options [mss 1460,ts 1608300426 995360867,ws 7], length 0
 0.000 ( 0.000) IP linux.8888 > macos.55764: flags [SA], seq 2372681031, ack 3893580624, win 65160, options [mss 1460,ts 1608300459 995360867,ws 7], length 0
 0.000 ( 0.000) IP macos.55765 > linux.8888: flags [S], seq 2037093638, win 65535, options [mss 1460,ws 6,ts 995360867 0], length 0
 0.000 ( 0.000) IP macos.55759 > linux.8888: flags [A], seq 3792309567, ack 3977851216, win 2058, options [ts 995360867 1608300459], length 0
 0.000 ( 0.000) IP macos.55760 > linux.8888: flags [A], seq 1721799659, ack 3607323910, win 2058, options [ts 995360867 1608300426], length 0
 0.000 ( 0.000) IP macos.55761 > linux.8888: flags [A], seq 4062300957, ack 4253133501, win 2058, options [ts 995360867 1608300426], length 0
 0.000 ( 0.000) IP macos.55762 > linux.8888: flags [A], seq 431299089, ack 139184710, win 2058, options [ts 995360867 1608300426], length 0
 0.000 ( 0.000) IP macos.55763 > linux.8888: flags [A], seq 842395790, ack 1964370175, win 2058, options [ts 995360868 1608300426], length 0
 0.001 ( 0.001) IP linux.8888 > macos.55765: flags [SA], seq 2492765422, ack 2037093639, win 65160, options [mss 1460,ts 1608300460 995360867,ws 7], length 0
 0.001 ( 0.000) IP macos.55764 > linux.8888: flags [A], seq 3893580624, ack 2372681032, win 2058, options [ts 995360868 1608300459], length 0
 0.001 ( 0.000) IP macos.55765 > linux.8888: flags [A], seq 2037093639, ack 2492765423, win 2058, options [ts 995360868 1608300460], length 0
 0.001 ( 0.000) IP linux.8888 > macos.55764: flags [R], seq 2372681032, win 0, options [], length 0
 0.001 ( 0.000) IP linux.8888 > macos.55765: flags [R], seq 2492765423, win 0, options [], length 0
从上面的输出看:
有8个连接完成了三次握手
有2个连接被reset了

在macos上查看连接

代码: 全选

macos $ netstat -f inet -p tcp -an | grep 8888 | sort
tcp4       0      0  192.168.0.3.55756      192.168.0.6.8888       ESTABLISHED
tcp4       0      0  192.168.0.3.55757      192.168.0.6.8888       ESTABLISHED
tcp4       0      0  192.168.0.3.55758      192.168.0.6.8888       ESTABLISHED
tcp4       0      0  192.168.0.3.55759      192.168.0.6.8888       ESTABLISHED
tcp4       0      0  192.168.0.3.55760      192.168.0.6.8888       ESTABLISHED
tcp4       0      0  192.168.0.3.55761      192.168.0.6.8888       ESTABLISHED
tcp4       0      0  192.168.0.3.55762      192.168.0.6.8888       ESTABLISHED
tcp4       0      0  192.168.0.3.55763      192.168.0.6.8888       ESTABLISHED
在Linux上查看连接

代码: 全选

linux $ netstat -4tan |  grep 8888 | sort
tcp        0      0 192.168.0.6:8888        192.168.0.3:55756       ESTABLISHED
tcp        0      0 192.168.0.6:8888        192.168.0.3:55757       ESTABLISHED
tcp        0      0 192.168.0.6:8888        192.168.0.3:55758       ESTABLISHED
tcp        0      0 192.168.0.6:8888        192.168.0.3:55759       ESTABLISHED
tcp        4      0 0.0.0.0:8888            0.0.0.0:*               LISTEN
从上面的输出看:
有4个连接被放到了全连接队列里,还有4个完成了三次握手的连接在半连接队列里

总结:
Screen Shot 2023-03-30 at 5.47.34 PM.png
723937936@qq.com
帖子: 51
注册时间: 2023-02-26 9:59
系统: ubuntu

Re: 跟我一起学TCP/IP

#40

帖子 723937936@qq.com » 2023-04-01 18:48

TCP Interactive Data Flow

安装rlogin

在macos上安装rlogin客户端

代码: 全选

macos $ brew install inetutils
在linux上安装rlogin服务端

代码: 全选

linux $ apt install rsh-redone-server
macos上安装的rlogin需要用root用户执行,为了方便,将其设置成set-uid程序

代码: 全选

macos $ realpath `which rlogin`               # macos原生没有realpath这个命令,需要安装coreutils(brew install coreutils)
/usr/local/Cellar/inetutils/2.4/bin/grlogin
macos $ sudo chown root /usr/local/Cellar/inetutils/2.4/bin/grlogin
macos $ sudo chmod u+s /usr/local/Cellar/inetutils/2.4/bin/grlogin
观察segments

执行这个实验前,我将我的linux的PS1环境变量设为了"\h $ "

在macos上使用rlogin登录linux

代码: 全选

macos $ rlogin fly@linux          # 已在/etc/hosts配置192.168.0.6	linux
Password:                      # 输入fly的密码(fly是我在Linux主机上的账户名)
linux $                           # 已登录到linux主机上
linux $ date                   # 下面的mytcpdump的输出对应这条命令
Sat Apr  1 10:23:23 CST 2023        # 共28个字符
下面是date命令产生的segments(我标了序号)

代码: 全选

linux $ sudo ./mytcpdump port 513
 1   0.000000 ( 0.000000) IP macos.998 > linux.513: flags [PA], seq 2798646469, ack 3195191363, win 2048, options [ts 1040137157 2614572408], length 1
 2   0.000023 ( 0.000023) IP linux.513 > macos.998: flags [A], seq 3195191363, ack 2798646470, win 510, options [ts 2614578501 1040137157], length 0
 3   0.000311 ( 0.000288) IP linux.513 > macos.998: flags [PA], seq 3195191363, ack 2798646470, win 510, options [ts 2614578501 1040137157], length 1
 4   0.000435 ( 0.000124) IP macos.998 > linux.513: flags [A], seq 2798646470, ack 3195191364, win 2047, options [ts 1040137157 2614578501], length 0
 
 5   0.091388 ( 0.090953) IP macos.998 > linux.513: flags [PA], seq 2798646470, ack 3195191364, win 2048, options [ts 1040137247 2614578501], length 1
 6   0.091411 ( 0.000023) IP linux.513 > macos.998: flags [A], seq 3195191364, ack 2798646471, win 510, options [ts 2614578592 1040137247], length 0
 7   0.091859 ( 0.000448) IP linux.513 > macos.998: flags [PA], seq 3195191364, ack 2798646471, win 510, options [ts 2614578593 1040137247], length 1
 8   0.092024 ( 0.000165) IP macos.998 > linux.513: flags [A], seq 2798646471, ack 3195191365, win 2047, options [ts 1040137247 2614578593], length 0
 
 9   0.252924 ( 0.160900) IP macos.998 > linux.513: flags [PA], seq 2798646471, ack 3195191365, win 2048, options [ts 1040137407 2614578593], length 1
10   0.253631 ( 0.000707) IP linux.513 > macos.998: flags [PA], seq 3195191365, ack 2798646472, win 510, options [ts 2614578755 1040137407], length 1
11   0.253767 ( 0.000136) IP macos.998 > linux.513: flags [A], seq 2798646472, ack 3195191366, win 2047, options [ts 1040137408 2614578755], length 0

12   0.313669 ( 0.059902) IP macos.998 > linux.513: flags [PA], seq 2798646472, ack 3195191366, win 2048, options [ts 1040137467 2614578755], length 1
13   0.313984 ( 0.000315) IP linux.513 > macos.998: flags [PA], seq 3195191366, ack 2798646473, win 510, options [ts 2614578815 1040137467], length 1
14   0.314099 ( 0.000115) IP macos.998 > linux.513: flags [A], seq 2798646473, ack 3195191367, win 2047, options [ts 1040137467 2614578815], length 0

15   0.438255 ( 0.124156) IP macos.998 > linux.513: flags [PA], seq 2798646473, ack 3195191367, win 2048, options [ts 1040137590 2614578815], length 1
16   0.438871 ( 0.000616) IP linux.513 > macos.998: flags [PA], seq 3195191367, ack 2798646474, win 510, options [ts 2614578940 1040137590], length 2
17   0.439106 ( 0.000235) IP macos.998 > linux.513: flags [A], seq 2798646474, ack 3195191369, win 2047, options [ts 1040137590 2614578940], length 0

18   0.440855 ( 0.001749) IP linux.513 > macos.998: flags [PA], seq 3195191369, ack 2798646474, win 510, options [ts 2614578942 1040137590], length 30
19   0.441111 ( 0.000256) IP macos.998 > linux.513: flags [A], seq 2798646474, ack 3195191399, win 2047, options [ts 1040137592 2614578942], length 0

20   0.441140 ( 0.000029) IP linux.513 > macos.998: flags [PA], seq 3195191399, ack 2798646474, win 510, options [ts 2614578942 1040137592], length 8
21   0.441350 ( 0.000210) IP macos.998 > linux.513: flags [A], seq 2798646474, ack 3195191407, win 2047, options [ts 1040137592 2614578942], length 0
输出分析:
第1行的segment对应字符d,第2行是第1行的确认segment,第3行的segment对应服务端echo的字符d,第4行的segment是第3行的确认
第5、6、7、8行对应字符a
第9行的segment对应字符t,第10行是echo+确认,第11行是第10行的的确认
第12、13、14行对应字符e
第15、16、17行对应字符'\n',注意'\n'的echo是'\r\n'
第18行的segment是"Sat Apr 1 10:23:23 CST 2023\r\n"共30个字符,第19行是18行的确认
第20行的segment是"linux $ "共8个字符,第21行是20行的确认

另外:
第2、4、6、8、11、14、17、19、21这行都是ack segment(专门跑了一趟)
第10、13、16这几行是echo+确认,在data segment里捎带确认flag的segment,称为piggyback(搭顺风车)
有的ack搭了顺风车,有的ack自己专门跑一趟

delayed acknowledgment

为了降低network load,我们希望ack都能尽可能的搭顺风车,而不是自己专门跑一趟,为了这个目的TCP实现了delayed acknowledgment

TCP为每个端点维护一个称为delayed ack timer的定时器,当TCP端点收到data segment时,它不会立刻发送ack segment,而是有一个延迟。
比如delayed ack timer每200ms超时一次,当它每次超时时,它检查是否有收到的data segment还没有确认,如果有则发送ack segment
因为收到data segment的时机是随机的,所以距离delayed ack timer下一次超时的时间间隔也是随机的
但是当TCP端点收到data segment时,如果正好tcp端点的send buffer里有数据要发送,那么会在发送data segment时捎带ack(ack搭了顺风车)

原理是上面的原理,但是我们观察上面的mytcpdump的输出,发现并非如此

查看第4、8、11、14、17、19、21行,这几行从收到数据到发送确认的时间间隔分别是:124us、165us、136us、115us、235us、256us、210us,都是微秒级,延迟的并不多(不像是因为delayed ack timer超时触发的ack发送),这可能是macos的优化
查看第2、6行,这两行从收到数据到发送确认的时间间隔分别是:23us、23us,就这两行而言,linux似乎是立刻发送了ack
查看第10、13、16行,这三行从收到数据到发送echo+确认的时间间隔分别是:707us、315us、616us,这三行的ack都是搭了data segment的顺风车,但是显然比第2、6行的时间间隔大,说明ack也是延迟了(他们等到了顺风车),这三个时间间隔是rlogind从收到字符到产生echo的时间间隔

Host Requirements RFC要求TCP必须实现delayed acknowledgment,我们可以肯定的是现代系统都实现了delayed acknowledgment,但是各个系统都不是单纯的只使用delayed ack timer,都进行了一些优化。
723937936@qq.com
帖子: 51
注册时间: 2023-02-26 9:59
系统: ubuntu

Re: 跟我一起学TCP/IP

#41

帖子 723937936@qq.com » 2023-04-02 1:07

Nagle Algorithm

与delayed acknowledgment的目的一样,Nagle Algorithm算法的目的也是为了降低network load
与delayed acknowledgment不同的是,它不是减少ack segments的数量,而是减少small data segments的数量
Nagle Algorithm的原理非常简单,它用的方法是:
如果还没有收到上一次发送的data segment的确认segment,则不会发送small data segment
其中:small的定义是:小于对方通告的MSS(注意不是小于对方通告的window size)

这个算法隐含的含义是,如果确认比较慢(可能是网络拥塞),那么发送也慢
在等待ack期间,端点的send buffer里可能会积累更多的数据,从而可以合并成一个segment发送,这就减少了small data segment的数量

在我们上一个帖子的mytcpdump输出里,第10、13、16行显示的RTT分别是707us、315us、616us,大概平均半毫秒,在这里个例子,用户产生输入的速度远低于确认的速度(用户需要每秒输入2000个字符,才能赶上确认的速度),因此在LAN的环境里,Nagle Algorithm基本派不上用场


在WAN环境测试

下面的mydev主机是一台在150公里之外的linux主机(Ubuntu 18.04.6)

代码: 全选

mydev $ date
Sat Apr  1 14:54:54 UTC 2023
mydev $ 
对应上面的date命令的mytcpdump的输出:

代码: 全选

linux $ sudo ./mytcpdump port 513
 1   0.000000 ( 0.000000) IP linux.1023 > mydev.513: flags [PA], seq 3331695544, ack 2341823669, win 501, options [ts 658397879 3771275900], length 1
 2   0.032117 ( 0.032117) IP mydev.513 > linux.1023: flags [PA], seq 2341823669, ack 3331695545, win 509, options [ts 3771280346 658397879], length 1
 3   0.032138 ( 0.000021) IP linux.1023 > mydev.513: flags [A], seq 3331695545, ack 2341823670, win 501, options [ts 658397912 3771280346], length 0

 4   0.093836 ( 0.061698) IP linux.1023 > mydev.513: flags [PA], seq 3331695545, ack 2341823670, win 501, options [ts 658397973 3771280346], length 1
 5   0.119968 ( 0.026132) IP mydev.513 > linux.1023: flags [PA], seq 2341823670, ack 3331695546, win 509, options [ts 3771280434 658397973], length 1
 6   0.120004 ( 0.000036) IP linux.1023 > mydev.513: flags [A], seq 3331695546, ack 2341823671, win 501, options [ts 658397999 3771280434], length 0

 7   0.240752 ( 0.120748) IP linux.1023 > mydev.513: flags [PA], seq 3331695546, ack 2341823671, win 501, options [ts 658398120 3771280434], length 1
 8   0.260028 ( 0.019276) IP mydev.513 > linux.1023: flags [PA], seq 2341823671, ack 3331695547, win 509, options [ts 3771280574 658398120], length 1
 9   0.260073 ( 0.000045) IP linux.1023 > mydev.513: flags [A], seq 3331695547, ack 2341823672, win 501, options [ts 658398140 3771280574], length 0

10   0.287436 ( 0.027363) IP linux.1023 > mydev.513: flags [PA], seq 3331695547, ack 2341823672, win 501, options [ts 658398167 3771280574], length 1
11   0.332464 ( 0.045028) IP mydev.513 > linux.1023: flags [PA], seq 2341823672, ack 3331695548, win 509, options [ts 3771280619 658398167], length 1
12   0.332485 ( 0.000021) IP linux.1023 > mydev.513: flags [A], seq 3331695548, ack 2341823673, win 501, options [ts 658398212 3771280619], length 0

13   0.389674 ( 0.057189) IP linux.1023 > mydev.513: flags [PA], seq 3331695548, ack 2341823673, win 501, options [ts 658398269 3771280619], length 1
14   0.409861 ( 0.020187) IP mydev.513 > linux.1023: flags [PA], seq 2341823673, ack 3331695549, win 509, options [ts 3771280724 658398269], length 2
15   0.409884 ( 0.000023) IP linux.1023 > mydev.513: flags [A], seq 3331695549, ack 2341823675, win 501, options [ts 658398289 3771280724], length 0

16   0.427918 ( 0.018034) IP mydev.513 > linux.1023: flags [PA], seq 2341823675, ack 3331695549, win 509, options [ts 3771280742 658398289], length 38
17   0.427938 ( 0.000020) IP linux.1023 > mydev.513: flags [A], seq 3331695549, ack 2341823713, win 501, options [ts 658398307 3771280742], length 0
第1-3行对应字符'd',4-6行对应字符'a',7-9行对应字符't',10-12行对应字符'e',13-15行对应字符'\n'
第16-17行对应的是28个字节的日期字符串+2个字节的"\r\n"+8个字节的提示符

发送字符到字符被echo回来的RTT分别如下:
'd':32.117ms
'a':26.132ms
't':19.276ms
'e':45.028ms
'\n':20.187ms

平均RTT大概28ms,按这个速度需要用户每秒至少输入35个字符,我输入的太慢了,还是没有看到Nagle Algorithm起作用
如果输入足够快,第3、6、9、12行的ACK应该能搭上顺风车

观察F1键对应的segments

F1产生的三个字符是:ESC O P,对应的ASCII码是0x1b 0x4f 0x50
服务器echo一个字符是:BEL,对应的ASCII码是0x07

代码: 全选

mydev $ sudo ./mytcpdump  port 513
1   0.000000 ( 0.000000) IP macos.992 > mydev.513: flags [PA], seq 2956551669, ack 3156396418, win 2048, options [ts 1071040108 3776760772], length 1
2   0.040168 ( 0.040168) IP mydev.513 > macos.992: flags [A], seq 3156396418, ack 2956551670, win 510, options [ts 3776767856 1071040108], length 0

3   0.061845 ( 0.021677) IP macos.992 > mydev.513: flags [PA], seq 2956551670, ack 3156396418, win 2048, options [ts 1071040174 3776767856], length 2
4   0.061875 ( 0.000030) IP mydev.513 > macos.992: flags [A], seq 3156396418, ack 2956551672, win 510, options [ts 3776767878 1071040174], length 0

5   0.062240 ( 0.000365) IP mydev.513 > macos.992: flags [PA], seq 3156396418, ack 2956551672, win 510, options [ts 3776767878 1071040174], length 1
6   0.080953 ( 0.018713) IP macos.992 > mydev.513: flags [A], seq 2956551672, ack 3156396419, win 2047, options [ts 1071040193 3776767878], length 0
输出分析:
第1行只发送一个字符,即ESC
第2行是第1行的确认,看起来是delayed ack,因为时间差了40.168ms
第3行发送了两个字符,即OP,这说明Nagle Algorithm算法起作用了,send buffer里积累了2个字符,合并成一个segment了
第4行是第3行的确认,看起来是立刻确认,时间只差了30us
第5行是echo的一个字符BEL
第6行是第5行的确认

Disable Nagle Algorithm

通过下面代码可以Disable Nagle Algorithm

代码: 全选

#include <netinet/tcp.h>
int one = 1;
setsockopt(rem, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(int));
macos上的rlogin的代码是gnu版本的,源代码地址:https://www.gnu.org/software/inetutils/

下面是F1键对应的segments:

代码: 全选

mydev $ sudo ./mytcpdump  port 513
1   0.000000 ( 0.000000) IP macos.991 > mydev.513: flags [PA], seq 3836750087, ack 1147015183, win 2048, options [ts 1072486208 3778210075], length 1
2   0.015574 ( 0.015574) IP macos.991 > mydev.513: flags [PA], seq 3836750088, ack 1147015183, win 2048, options [ts 1072486208 3778210075], length 1
3   0.015646 ( 0.000072) IP mydev.513 > macos.991: flags [A], seq 1147015183, ack 3836750089, win 510, options [ts 3778221759 1072486208], length 0
4   0.016323 ( 0.000677) IP macos.991 > mydev.513: flags [PA], seq 3836750089, ack 1147015183, win 2048, options [ts 1072486208 3778210075], length 1
5   0.016721 ( 0.000398) IP mydev.513 > macos.991: flags [PA], seq 1147015183, ack 3836750090, win 510, options [ts 3778221760 1072486208], length 1
6   0.039878 ( 0.023157) IP macos.991 > mydev.513: flags [A], seq 3836750090, ack 1147015184, win 2047, options [ts 1072486272 3778221760], length 0
输出分析:
第1行发送了一个字节,即ESC,第2行发送了一个字节,即O,第3行确认了这两个字节
第4行发送了一个字节,即P,第5行echo了一个字节,即BEL,第6行确认了BEL

Disable Nagle Algorithm后,字符确实是一个字符一个字符发送的

其他说明:

Ubuntu仓库包含的rlogin(rsh-redone-client),在按F1键时,会把3个字符一起发送过去,与gnu版本的rlogin行为不一致
723937936@qq.com
帖子: 51
注册时间: 2023-02-26 9:59
系统: ubuntu

Re: 跟我一起学TCP/IP

#42

帖子 723937936@qq.com » 2023-04-04 23:14

第二十一章:TCP Bulk Data Flow

Sliding Windows

滑动窗口是TCP用来实现流控(Flow Control)的方法,简单说,发送端可以发送多少数据是由窗口大小决定的
TCP发送端为每个连接维护一个窗口数据结构,但是窗口的值是接收端通告给发送端的
窗口数据结构的主要字段大概如下:

代码: 全选

struct window {
	int ack_seq;
	int usable_seq;
	int size;
};
滑动窗口的含义就好像有一个窗口覆盖在发送的数据流上,如下图:
Screen Shot 2023-04-04 at 9.50.42 PM.png
接收端发送的每个ack segment里都携带了ack sequence number和window size两个字段
发送端收到ack segment后,会更新窗口,类似

代码: 全选

win.ack_seq = ack_segment.ack_seq;
win.size = ack_segment.win_size;
win.usable_seq = 0;
发送端每发送一个data segment后,会更新win.usable_seq字段,类似

代码: 全选

win.usable_seq = data_segment.seq+data_segment.len;
每次更新窗口数据结构,都可能会改变窗口的位置及大小
当usable window的大小为0时,发送端就不会发送数据了,而是停下来等待对方确认

上图中的两个术语说明:
1. offered window:这个信息就是由ack_segment.ack_seq和ack_segment.win_size两个字段传达的
2. usable window:这个信息是发送端自己维护的,当发送端发送一个data segment后,会更新win.usable_seq字段

关于窗口移动(或滑动)的术语图示说明:
Screen Shot 2023-04-04 at 11.09.31 PM.png
close:窗口的left edge向右移动称为close操作,这个操作就是更新win.ack_seq这个字段,这个字段的值只能增大不能减小
open:窗口的right edge向右移动称为open操作,窗口的right edge移动后的具体位置是由win.ack_seq和win.size这两个字段共同决定
shrink:窗口的right edge向左移动称为shrink操作,窗口的right edge移动后的具体位置是由win.ack_seq和win.size这两个字段共同决定


Window Size

接收端通告的窗口大小是由接收端TCP维护的receive buffer的大小决定的,这个receive buffer的大小可以通过套接字选项SO_RCVBUF改变
另外,TCP维护的send buffer的大小可以通过SO_SNDBUF套接字选项改变

PUSH Flag

应该是已废弃了

Slow Start

下一章和congestion avoidance一起学习
723937936@qq.com
帖子: 51
注册时间: 2023-02-26 9:59
系统: ubuntu

Re: 跟我一起学TCP/IP

#43

帖子 723937936@qq.com » 2023-04-06 21:54

Bandwidth-Delay Product

对带宽和时延的理解举例:用公路来代表链路

有以下假设:
1. 一条1公里长的公路(从A点到B点)
2. 一辆车占5米长
3. 车每秒行驶5米

那么:
这条路的带宽是:1辆车/秒(即:每秒只能移动1辆车的距离)
从A点到B点的时延是:200秒(即:1000/5=200)
带宽*时延=1*200=200辆(即:200秒内,最多可以驶入公路的车辆数)


回到TCP,带宽时延乘积代表的是在RTT时间内,链路上可以传输的最大比特数

这个乘积可以用来设置接收端的receive buffer的大小,原因是:
发送端在一个RTT时间内向网络链路注入的数据量必须小于下面两个值:
1. RTT * 带宽(物理链路的限制)
2. 对端通告的窗口大小

我们不希望窗口大小成为瓶颈,所以应该将接收端的receive buffer设置为:RTT * 带宽

为什么时延是RTT?
因为只有经过RTT时间后,发送端才能收到ack,在这段时间内,发送端持续注入数据,下一个RTT周期内会使用ack通告的新的窗口大小

局域网举例:
比如局域网的带宽是100Mbps,RTT是20ms,那么
带宽时延乘积=100000000*0.02=250000 bytes
那么接收端的receive buffer应该设为250000,但是TCP header里的win size字段占16位,最大为65535,win scale option用来解决该问题,后面章节学习

Urgent Mode

关于Urgent Mode的几点说明:

1. TCP并没有实现在一个连接上传输带外数据的能力,而是通过一种称之为urgent mode的方法来实现紧急通知能力
2. 当进程发送urgent data时,只是将urgent data放入send buffer,然后紧急发送一个segment(URG flag置位,urgent pointer指定紧急数据的偏移)
3. urgent pointer实际是指向最后一个紧急数据字节的后一个位置

urgent mode类似,在排队的车辆后方突然出现一辆急救车(开启了警报),实际急救车还是在后面排队,但是前方车辆已经得知后面有急救车,这时候前方车辆可以驶出避让(对比:接收端的应用进程立即收到了紧急通知,可以迅速清空自己的receive buffer,腾出空间接收紧急数据)

urgent mode极少使用,可能一辈子都用不到

有兴趣的同学请参考:UNP第24章有详细描述
723937936@qq.com
帖子: 51
注册时间: 2023-02-26 9:59
系统: ubuntu

Re: 跟我一起学TCP/IP

#44

帖子 723937936@qq.com » 2023-04-09 13:32

第21章:TCP timeout and Retransmission

观察data segment的超时重传

前面的帖子(第31个回帖)观察过SYN segment的超时重传,现在来观察下data segment的超时重传

在linux主机上执行

代码: 全选

linux $ telnet macos 8888
Trying 192.168.0.3...
Connected to macos.
Escape character is '^]'.
hello, world
and hi                          // 输入这行前先把macos的网线断了
Connection closed by foreign host.
在macos主机上执行

代码: 全选

macos $ sock -i -s 8888
下面是在linux主机上执行mytcpdump的输出

代码: 全选

 1   0.000000 ( 0.000000) IP linux.37618 > macos.8888: flags [S], seq 1860515697, win 64240, options [mss 1460,ts 2698373960 0,ws 7], length 0
 2   0.000322 ( 0.000322) IP macos.8888 > linux.37618: flags [SA], seq 2050790691, ack 1860515698, win 65535, options [mss 1460,ws 6,ts 1157433930 2698373960], length 0
 3   0.000350 ( 0.000028) IP linux.37618 > macos.8888: flags [A], seq 1860515698, ack 2050790692, win 502, options [ts 2698373960 1157433930], length 0
 4   0.000515 ( 0.000165) IP macos.8888 > linux.37618: flags [A], seq 2050790692, ack 1860515698, win 2058, options [ts 1157433930 2698373960], length 0

 5   7.826830 ( 7.826315) IP linux.37618 > macos.8888: flags [PA], seq 1860515698, ack 2050790692, win 502, options [ts 2698381787 1157433930], length 14
 6   7.827011 ( 0.000181) IP macos.8888 > linux.37618: flags [A], seq 2050790692, ack 1860515712, win 2058, options [ts 1157441715 2698381787], length 0

 7  28.552742 (20.725731) IP linux.37618 > macos.8888: flags [PA], seq 1860515712, ack 2050790692, win 502, options [ts 2698402513 1157441715], length 8
 8  28.775215 ( 0.222237) IP linux.37618 > macos.8888: flags [PA], seq 1860515712, ack 2050790692, win 502, options [ts 2698402735 1157441715], length 8
 9  28.983564 ( 0.207536) IP linux.37618 > macos.8888: flags [PA], seq 1860515712, ack 2050790692, win 502, options [ts 2698402943 1157441715], length 8
10  29.402715 ( 0.418524) IP linux.37618 > macos.8888: flags [PA], seq 1860515712, ack 2050790692, win 502, options [ts 2698403363 1157441715], length 8
11  30.234422 ( 0.831349) IP linux.37618 > macos.8888: flags [PA], seq 1860515712, ack 2050790692, win 502, options [ts 2698404194 1157441715], length 8
12  31.950549 ( 1.715545) IP linux.37618 > macos.8888: flags [PA], seq 1860515712, ack 2050790692, win 502, options [ts 2698405910 1157441715], length 8
13  35.352377 ( 3.401422) IP linux.37618 > macos.8888: flags [PA], seq 1860515712, ack 2050790692, win 502, options [ts 2698409312 1157441715], length 8
14  42.017130 ( 6.664068) IP linux.37618 > macos.8888: flags [PA], seq 1860515712, ack 2050790692, win 502, options [ts 2698415977 1157441715], length 8
15  55.321203 (13.303909) IP linux.37618 > macos.8888: flags [PA], seq 1860515712, ack 2050790692, win 502, options [ts 2698429281 1157441715], length 8
16  82.458693 (27.136625) IP linux.37618 > macos.8888: flags [PA], seq 1860515712, ack 2050790692, win 502, options [ts 2698456419 1157441715], length 8
17  135.705138 (53.245892) IP linux.37618 > macos.8888: flags [PA], seq 1860515712, ack 2050790692, win 502, options [ts 2698509665 1157441715], length 8
18  242.201762 (106.496181) IP linux.37618 > macos.8888: flags [PA], seq 1860515712, ack 2050790692, win 502, options [ts 2698616162 1157441715], length 8
19  363.061170 (120.858954) IP linux.37618 > macos.8888: flags [PA], seq 1860515712, ack 2050790692, win 502, options [ts 2698737021 1157441715], length 8
20  483.865949 (120.804507) IP linux.37618 > macos.8888: flags [PA], seq 1860515712, ack 2050790692, win 502, options [ts 2698857826 1157441715], length 8
21  604.699201 (120.832843) IP linux.37618 > macos.8888: flags [PA], seq 1860515712, ack 2050790692, win 502, options [ts 2698978659 1157441715], length 8
22  725.529627 (120.830176) IP linux.37618 > macos.8888: flags [PA], seq 1860515712, ack 2050790692, win 502, options [ts 2699099490 1157441715], length 8
23  846.362242 (120.831809) IP linux.37618 > macos.8888: flags [PA], seq 1860515712, ack 2050790692, win 502, options [ts 2699220322 1157441715], length 8
输出分析:
第1-3行是正常的连接建立
第4行是macos发送的窗口更新
第5行对应hello, world(12个字符+"\r\n")
第6行是第5行的确认
第7行对应and hi(6个字符+"\r\n")
第8-23行是第7行的重传
第8行和第9行快速的重传了2个segment(间隔0.2ms)
第10-18行重传的时间间隔呈指数增长,这个称为exponential backoff算法
第19-23行重传的时间间隔固定为120.8秒
总共重传了16次
TCP放弃之前,没有发送RST segment
第7行到第23行的时间间隔大约13.6分钟

在Linux上重传次数可以通过下面文件修改

代码: 全选

linux $ cat /proc/sys/net/ipv4/tcp_retries2
15
但是这个文件并不是确切的指定重传次数,而是根据这个值来计算一个最大时间

RTO的计算公式

TCP每次重传的时间间隔称为RTO(retransmission timeout),TCP为每个连接维护一个RTO值,这个值是动态更新的

基本概念说明:
measured RTT:TCP发送端测量的RTT值(用M表示)
smoothed RTT:TCP发送端维护的平滑的RTT值(用R表示)
RTO:retransmission timeout

最初的TCP规范说明了计算R的公式如下:

R = 0.9R + 0.1M

当TCP发送端每次测量到一个M值时,就更新R值。从这个公式看,M值占比只有10%,因此当M波动比较大时,因为占比比较小,所以计算出的R的波动不会那么大,这就是low-pass filter的含义,不管M波动有多大,最终计算的R的波动都不大。

计算RTO的公式如下:

RTO = 2R

RTO的计算只是简单的乘上一个系数

上面计算的RTO不能快速的反应M的大幅度的波动,也就是说当M波动比较大时,计算出来的RTO可能变化不大,这会导致超时值设置的过小,导致不必要的重传,而M波动比较大的原因可能是因为网络负载过重,但是RTO过小却导致不必要的重传,进一步加重网络负载,简直就是火上浇油

Jacobson给出了一个新的算法,这个算法的基本思路是在计算RTO时,不是简单的乘上一个固定的系数(上面的公式中的2),而是要实时计算一个值(称为D),该值能反应M的波动(有点类似统计学中方差的概念)

计算D的公式为:

D = 0.75D + 0.25|M-R|

上述公式中的|M-R|表示measured RTT与smoothed RTT的差的绝对值,该值反应了波动大小,D称为smoothed mean deviation

计算R的公式为:

R = 0.875R + 0.125M

上述计算R的公式与最初的TCP规范给出的计算公式类似,但是使用了不同的系数

计算RTO的公式为:

RTO = R + 4D

因为计算RTO考虑了M的波动(D反应了M的波动),所以RTO可以快速跟上M的波动

书上给出的公式用A表示smoothed RTT,我为了与TCP规范最初给出的公式对比,仍使用R表示smoothed RTT


Karn's Algorithm

TCP在测量连接的RTT时有一个问题:
当TCP发送一个segment,然后在RTO时间内未收到ACK,则TCP会重传segment,此后如果TCP收到了ACK,那么这个ACK是与第一次发送的那个segment对应还是与重传的那个segment对应?这个问题称为:retransmission ambiguity problem
因为不知道接收到的ACK对应哪个segment,因此当发生重传时,在收到ACK后,TCP不会更新R的值(M、D都不更新)
但是因为发生重传,TCP会应用指数退避算法来更新RTO的值,即:当发送完第一个重传的segment后,立即更新RTO=2RTO

TCP只使用非重传的segment对应的ACK来测量RTT(即:M的值)

M的值的测量过程

TCP为每个连接维护一个用于测量RTT的timer,当发送一个data segment时,启动该timer,当收到对应的ACK时,停止该timer,这就测得了RTT
因为一个连接只有一个测量RTT的timer,当这个timer正在使用期间,其他发送的data segment就不会被用于测量RTT了

D和R的初始化

TCP连接的R和D分别初始化为0和3

前面给出了RTO的计算公式如下:

RTO = R + 4D

但是RTO的初始化并不是用这个公式计算的,而是下面的公式:

RTO = R + 2D

用这个公式计算出RTO = 0 + 2 * 3 = 6

这个RTO用于SYN的的第一次重传,随后使用公式RTO = R + 4D重新初始化RTO:

RTO = 0 + 4 * 3 = 12

随后应用指数退避算法更新RTO

RTO = 2RTO = 2 * 12 = 24

书上的图4.5验证了上述的RTO值


当TCP第一次测量到RTT后,会使用下面的公式重新初始化R和D,并计算RTO:

R = M + 0.5
D = R / 2
RTO = R + 4D

之后,TCP每次测量到RTT后,会按照Jacobson给出的公式更新R、D及RTO的值:

D = 0.75D + 0.25|M-R|
R = 0.875R + 0.125M
RTO = R + 4D

当发生超时重传时,应用指数退避算法更新RTO的值
723937936@qq.com
帖子: 51
注册时间: 2023-02-26 9:59
系统: ubuntu

Re: 跟我一起学TCP/IP

#45

帖子 723937936@qq.com » 2023-04-10 23:10

Slow Start

TCP发送端可以发送的数据量大小除了受接收端通告的窗口大小限制外,还有一个限制称为congestion window(简称cwnd)
较小的窗口决定了TCP发送端可以发送的数据量大小

当连接建立后,cwnd初始化为1个segment大小
TCP发送cwnd个segment后必须停下来等待ACK,每收到一个ack,cwn加1

观察慢启动

下面是linux主机向mydev主机发送5120字节的mytcpdump输出,两台主机相距大约150公里

代码: 全选

 0.000000 ( 0.000000) IP linux.38788 > mydev.8888: flags [S], seq 3826044325:3826044325, win 65500, options [mss 524,ts 768567145 0,ws 7], length 0
 0.026493 ( 0.026493) IP mydev.8888 > linux.38788: flags [SA], seq 4088726725:4088726725, ack 3826044326, win 65160, options [mss 1400,ts 2476130085 768567145,ws 7], length 0
 0.026538 ( 0.000045) IP linux.38788 > mydev.8888: flags [A], seq 3826044326:3826044326, ack 4088726726, win 512, options [ts 768567172 2476130085], length 0
 cwnd=1
 0.026606 ( 0.000068) IP linux.38788 > mydev.8888: flags [A], seq 3826044326:3826044838, ack 4088726726, win 512, options [ts 768567172 2476130085], length 512
 0.051558 ( 0.024952) IP mydev.8888 > linux.38788: flags [A], seq 4088726726:4088726726, ack 3826044838, win 506, options [ts 2476130112 768567172], length 0
 cwnd=2
 0.051585 ( 0.000027) IP linux.38788 > mydev.8888: flags [A], seq 3826044838:3826045350, ack 4088726726, win 512, options [ts 768567197 2476130112], length 512
 0.051600 ( 0.000015) IP linux.38788 > mydev.8888: flags [A], seq 3826045350:3826045862, ack 4088726726, win 512, options [ts 768567197 2476130112], length 512
 0.075255 ( 0.023655) IP mydev.8888 > linux.38788: flags [A], seq 4088726726:4088726726, ack 3826045350, win 502, options [ts 2476130136 768567197], length 0
 0.075255 ( 0.000000) IP mydev.8888 > linux.38788: flags [A], seq 4088726726:4088726726, ack 3826045862, win 501, options [ts 2476130137 768567197], length 0
 cwnd=4
 0.075286 ( 0.000031) IP linux.38788 > mydev.8888: flags [A], seq 3826045862:3826046374, ack 4088726726, win 512, options [ts 768567220 2476130136], length 512
 0.075301 ( 0.000015) IP linux.38788 > mydev.8888: flags [A], seq 3826046374:3826046886, ack 4088726726, win 512, options [ts 768567220 2476130136], length 512
 0.075310 ( 0.000009) IP linux.38788 > mydev.8888: flags [A], seq 3826046886:3826047398, ack 4088726726, win 512, options [ts 768567220 2476130137], length 512
 0.075311 ( 0.000001) IP linux.38788 > mydev.8888: flags [PA], seq 3826047398:3826047910, ack 4088726726, win 512, options [ts 768567220 2476130137], length 512
 0.099958 ( 0.024647) IP mydev.8888 > linux.38788: flags [A], seq 4088726726:4088726726, ack 3826046374, win 501, options [ts 2476130159 768567220], length 0
 0.099959 ( 0.000001) IP mydev.8888 > linux.38788: flags [A], seq 4088726726:4088726726, ack 3826047398, win 501, options [ts 2476130160 768567220], length 0
 0.099959 ( 0.000000) IP mydev.8888 > linux.38788: flags [A], seq 4088726726:4088726726, ack 3826047910, win 497, options [ts 2476130160 768567220], length 0
 cwnd=7
 0.099997 ( 0.000038) IP linux.38788 > mydev.8888: flags [A], seq 3826047910:3826048422, ack 4088726726, win 512, options [ts 768567245 2476130159], length 512
 0.099998 ( 0.000001) IP linux.38788 > mydev.8888: flags [PA], seq 3826048422:3826048934, ack 4088726726, win 512, options [ts 768567245 2476130159], length 512
 0.100152 ( 0.000154) IP linux.38788 > mydev.8888: flags [FPA], seq 3826048934:3826049446, ack 4088726726, win 512, options [ts 768567245 2476130160], length 512
 0.123960 ( 0.023808) IP mydev.8888 > linux.38788: flags [A], seq 4088726726:4088726726, ack 3826048934, win 501, options [ts 2476130185 768567245], length 0
 0.123960 ( 0.000000) IP mydev.8888 > linux.38788: flags [A], seq 4088726726:4088726726, ack 3826049447, win 497, options [ts 2476130185 768567245], length 0
 0.126946 ( 0.002986) IP mydev.8888 > linux.38788: flags [FA], seq 4088726726:4088726726, ack 3826049447, win 501, options [ts 2476130185 768567245], length 0
 0.126984 ( 0.000038) IP linux.38788 > mydev.8888: flags [A], seq 3826049447:3826049447, ack 4088726727, win 512, options [ts 768567272 2476130185], length 0
上述输出中标注了cwnd的值

为了实现与书上类似的输出,我对linux默认设置进行了调整:

1. linux默认的cwnd的初始值并不是1,为了更好的观察慢启动,使用下面命令将cwnd的默认值设为1

代码: 全选

$ linux sudo ip route change default via 192.168.0.1 dev enp0s3 proto static metric 100 initcwnd 1
2. 使用ethtool命令关闭tso

代码: 全选

$ linux sudo ethtool -K enp0s3 tso off
3. 修改MTU

代码: 全选

$ linux sudo ifconfig enp0s3 mtu 564
设置mtu为564,可以使tcp每个数据段的携带的数据大小为512字节
ip header占用20字节
tcpheader占用32字节,其中tcp选项占用了12个字节

其他说明:

我测试在同一个子网里的两台主机间发送数据,linux似乎并没有使用慢启动算法
回复