当前位置: 首页 > news >正文

【动手实验】TCP 连接的建立与关闭抓包分析

本文是基于知识星球程序员踩坑案例分享中的作业进行的复现和总结,借此加深对 TCP 协议的理解, 原文参见TCP 连接的建立和关闭 —— 强烈建议新手看看。

实验环境

这里使用两台位于同一子网的腾讯云服务器,IP 分别是 node2(172.19.0.12)和 node3(172.19.0.15),内核版本均为 5.15.0-130-generic。

# node02
$ uname -a
Linux node2 5.15.0-130-generic #140-Ubuntu SMP Wed Dec 18 17:59:53 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux$ ip -4 addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000inet 127.0.0.1/8 scope host lovalid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 8500 qdisc mq state UP group default qlen 1000altname enp0s5altname ens5inet 172.19.0.12/20 metric 100 brd 172.19.15.255 scope global eth0valid_lft forever preferred_lft forever# node03
$ uname -a
Linux node3 5.15.0-130-generic #140-Ubuntu SMP Wed Dec 18 17:59:53 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux$ ip -4 addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000inet 127.0.0.1/8 scope host lovalid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 8500 qdisc mq state UP group default qlen 1000altname enp0s5altname ens5inet 172.19.0.15/20 metric 100 brd 172.19.15.255 scope global eth0valid_lft forever preferred_lft forever

启动服务

首先我们使用 nc(netcat) 作为服务端,在 node2 监听 9527 端口:

# ubuntu @ node2 in ~ [10:40:58]
$ nc -k -l 172.19.0.12  9527

该命令表示在 IP 地址 172.19.0.12 的 9527 端口上持续监听(等待连接并接收数据)。参数含义如下:

  • -k 保持连接(Keep Listening),在客户端断开后继续监听端口。
  • -l 监听模式(Listen Mode),启动服务器等待连接。

启动成功后用 netstat 命令查看 socket 的连接状态:

$ sudo netstat -anpo | grep Recv-Q; sudo netstat -anpo | grep 9527
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name     Timer
tcp        0      0 172.19.0.12:9527        0.0.0.0:*               LISTEN      13504/nc             off (0.00/0/0)

netstat 命令的各个参数含义如下:

  • -a 显示所有连接和监听的套接字。
  • -n 显示 IP 地址和端口号,不解析主机名。
  • -o 显示进程 ID(PID)和计时器信息。
  • -p 显示进程名称。

可以看到 9527 端口处于 LISTEN 状态,表示正在监听端口,等待连接请求。

连接建立

在客户端请求 node2 之前,我们先在 node2 开启抓包:

# ubuntu @ node2 in ~ [10:38:33]
$ sudo tcpdump -s0 -X -nn "tcp port 9527" -w tcp.pcap --print
tcpdump: listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes

命令各个参数含义为:

  • -s0 捕获完整数据包(默认 -s 只抓取前 68/96 字节),0 代表不截断。
  • -X 以十六进制(hex)+ ASCII 格式打印数据包内容。
  • -nn 不解析主机名和端口(-n 不解析 IP,-nn 也不解析端口)。
  • "tcp port 9527" 仅捕获 TCP 端口 9527 的流量。
  • -w tcp.pcap 将捕获的数据包写入 tcp.pcap 文件(可用 wireshark 或 tcpdump -r tcp.pcap 查看)。
  • --print 同时在终端打印数据包内容(类似 -X,但 --print 仅在 -w 选项启用时生效)。

接下来我们在 node3 上使用 nc 连接 node2 的 9527 端口:

# ubuntu @ node3 in ~ [10:41:48]
$ nc 172.19.0.12 9527

然后分别在 node2 和 node3 上使用 netstat 命令查看 socket 的连接状态:

# node2
$ sudo netstat -anpo | grep -E "Recv-Q|9527"
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name     Timer
tcp        0      0 172.19.0.12:9527        0.0.0.0:*               LISTEN      13504/nc             off (0.00/0/0)
tcp        0      0 172.19.0.12:9527        172.19.0.15:48868       ESTABLISHED 13504/nc             off (0.00/0/0)# node3
$ sudo netstat -anpo | grep -E "Recv-Q|9527"
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name     Timer
tcp        0      0 172.19.0.15:48868       172.19.0.12:9527        ESTABLISHED 17255/nc             off (0.00/0/0)

可以看到 node2 和 node3 中都有一条端口为 9527 处于 ESTABLISHED 状态的连接,表示连接已建立。 tcpdump 命令也会输出三次握手的数据包详情。

$ sudo tcpdump -s0 -X -nn "tcp port 9527" -w tcp.pcap --print
tcpdump: listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
10:54:13.960797 IP 172.19.0.15.48868 > 172.19.0.12.9527: Flags [S], seq 2713301685, win 59220, options [mss 8460,sackOK,TS val 2002430584 ecr 0,nop,wscale 7], length 00x0000:  4500 003c 3a31 4000 4006 a849 ac13 000f  E..<:1@.@..I....0x0010:  ac13 000c bee4 2537 a1b9 b2b5 0000 0000  ......%7........0x0020:  a002 e754 92b3 0000 0204 210c 0402 080a  ...T......!.....0x0030:  775a aa78 0000 0000 0103 0307            wZ.x........
10:54:13.960874 IP 172.19.0.12.9527 > 172.19.0.15.48868: Flags [S.], seq 3309498602, ack 2713301686, win 59136, options [mss 8460,sackOK,TS val 556655863 ecr 2002430584,nop,wscale 7], length 00x0000:  4500 003c 0000 4000 4006 e27a ac13 000c  E..<..@.@..z....0x0010:  ac13 000f 2537 bee4 c542 f0ea a1b9 b2b6  ....%7...B......0x0020:  a012 e700 5870 0000 0204 210c 0402 080a  ....Xp....!.....0x0030:  212d e4f7 775a aa78 0103 0307            !-..wZ.x....
10:54:13.961020 IP 172.19.0.15.48868 > 172.19.0.12.9527: Flags [.], ack 1, win 463, options [nop,nop,TS val 2002430584 ecr 556655863], length 00x0000:  4500 0034 3a32 4000 4006 a850 ac13 000f  E..4:2@.@..P....0x0010:  ac13 000c bee4 2537 a1b9 b2b6 c542 f0eb  ......%7.....B..0x0020:  8010 01cf 05fa 0000 0101 080a 775a aa78  ............wZ.x0x0030:  212d e4f7                                !-..

三次握手抓包 & TCP 协议头解析

我们将抓包文件拖入 Wireshark 中来分析三次握手的过程。

首先回顾下 TCP 协议头格式:

在这里插入图片描述

图片来自 TCP/IP Reference

像序列号、端口信息、FLAG 等字段都比较熟悉了,我们这里重点看下 Options 的各个字段,完整的 Option 字段可以参考 Transmission Control Protocol (TCP) Parameters,这里我们只关注包中出现的最常见的几个字段:

  • MSS(Maximum Segment Size) 该字段只能在 SYN 包中,用来告知对方自己可以接收的最大数据包,这里指的是 TCP 包中 data 的大小,不包含 TCP 头数据。RFC 6691 中规定了 MSS 的值为 MTU 减去 IP 固定头大小(20 字节)和 TCP 固定头大小(20字节),不包含任何 Option 字段。从 ip -4 addr 命令中可以看到网卡的 MTU 大小为 8500,因此 MSS 大小为 8500 - 20 - 20 = 8460,和抓包中显示的 MSS 大小一致。
# ubuntu @ node3 in ~ [10:41:48]
$ ip -4 addr
...
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 8500 qdisc mq state UP group default qlen 1000
...
  • SACK(Selective Acknowledgment) 选择性确认。在 RFC 2018 确定的机制,必须在握手时确认是否支持。TCP 最开始是按顺序响应的,比如有 1、2、3、4 共 4 个包,如果 2 没有收到,那即使 3、4 收到了也不会响应 ACK,这可能导致客户端不断重传 3、4 号包,对网络造成不必要的负载。SACK 解决了这一问题,可以让服务端响应 3、4 包,客户端只需要重传 2 号包就可以了。


图片来自 TCP/IP Guide

在 Linux 内核中,使用 net.ipv4.tcp_sack 参数来控制是否开启 SACK ,默认开启。

$ sysctl net.ipv4.tcp_sack
net.ipv4.tcp_sack = 1
  • TS(Timestamp) 时间戳标记。内核用来计算 RTT(Round-Trip Time),即数据包从发送端到接收端的时间。在内核中可以使用 net.ipv4.tcp_timestamps 参数来控制是否开启该选项。
$ sysctl net.ipv4.tcp_timestamps
net.ipv4.tcp_timestamps = 1
  • NOP(No Operation) NOP 一般用来占位对齐,因为 TCP 头大小必须是 4 字节的倍数。因此当 TCP 固定头 + Option 字段长度不为 4 字节的倍数时,一般会填充 NOP 字段。

  • WScale(Window Scale) 窗口缩放因子。TCP 的 window 窗口字段大小是 16bit,其最大值为 65536 ,也就是说 TCP 包能传输的最大数据为 65536 byte / 1024 = 64KB。在硬件设备和网络如此发达的今天,这个窗口大小显然有点太小了,为此 RFC 7323 中提出了 WScale 选项,用来扩展 window 字段的大小。

    WScale Option 中有 shift.count 值,顾名思义就是移位数,表示 2 的多少次方,虽然 shift.count 占了 1 个字节,但 RFC 规定只能使用后 4 位,其最大值为 1110,也就是 14。结合最大 window 值为 64KB,在 WScale 的帮助下,最大窗口大小可以达到 64KB * (2^14) = 1048576KB = 1GB。

在我们的抓包中,可以看到 WScale 选项的值为 7,因此 window * (2^7) 才是真正的 window 大小。

需要注意的是,WScale 只会在携带这个选项的包之后生效,因此发送第一个 SYN 包时是没有生效的,在第三次握手时该选项才生效,可以看到 window 值为 463,而计算后的 window 值为 463 * (2^7) = 463 * 128 = 59264,和 Wireshark 中显示的 window 值一致。

在 Linux 内核中,可以通过 net.ipv4.tcp_window_scaling 参数来控制是否开启 WScale 选项。

$ sysctl net.ipv4.tcp_window_scaling
net.ipv4.tcp_window_scaling = 1

SYN-SENT 状态抓包

前文抓包我们看到的是 LISTEN 和 ESTABLISHED 状态的 socket,除了这两种状态,连接建立时客户端、服务端还会分别经历 SYN-SENT 和 SYN-RECV 状态。

图片来自 TCP/IP Guide

这里通过 iptables 拦截握手包来看下 SYN-SENT 和 SYN-RECV 状态的 socket,首先在 node2 上使用 iptables 规则,将访问 9527 的端口包丢弃掉,命令如下:

sudo iptables -A INPUT -p tcp --dport 9527 -j DROP

然后在 node3 再次执行 nc 命令连接服务,这次带上参数 -w 3600,表示连接超时时间为 3600 秒,命令如下:

nc -w 3600 172.19.0.12 9527

请求发出后,tcpdump 抓包会打印 SYN 包和后续的重传包,用 Wireshark 打开抓包文件:

可以看到 SYN 包一共有 6 次重传,共传了 7 个包。Linux 的 SYN 最大重传次数是由内核参数 net.ipv4.tcp_syn_retries 控制的,默认值为 6。

# ubuntu @ node3 in ~ [16:26:35] C:130
$ sysctl net.ipv4.tcp_syn_retries
net.ipv4.tcp_syn_retries = 6

重传的超时 RTO 时间初始值通常在 1s 左右,按照指数级增长,因此重传时间间隔大约为 1s、2s、4s、8s、16s、32s。从抓包中也可以看到,在 1.02,3.03,7.726s,15.15,31.58,65.11s 发生了重传,因此默认情况下,一个 TCP 连接建立的超时时间会大于 64s。

在重传期间,查看客户端的 netstat 信息可以看到 SYN-SENT 状态的 socket,表示连接正在等待 SYN 包的响应。

# ubuntu @ node3 in ~ [16:26:35] C:130
$ while true; do sudo netstat -anpo | grep 9527; sleep 1; done
tcp        0      1 172.19.0.15:44004       172.19.0.12:9527        SYN_SENT    3066236/nc           on (0.77/0/0)
tcp        0      1 172.19.0.15:44004       172.19.0.12:9527        SYN_SENT    3066236/nc           on (1.78/1/0)
tcp        0      1 172.19.0.15:44004       172.19.0.12:9527        SYN_SENT    3066236/nc           on (0.76/1/0)
tcp        0      1 172.19.0.15:44004       172.19.0.12:9527        SYN_SENT    3066236/nc           on (3.76/2/0)
tcp        0      1 172.19.0.15:44004       172.19.0.12:9527        SYN_SENT    3066236/nc           on (2.74/2/0)
tcp        0      1 172.19.0.15:44004       172.19.0.12:9527        SYN_SENT    3066236/nc           on (1.72/2/0)
tcp        0      1 172.19.0.15:44004       172.19.0.12:9527        SYN_SENT    3066236/nc           on (0.70/2/0)
tcp        0      1 172.19.0.15:44004       172.19.0.12:9527        SYN_SENT    3066236/nc           on (7.88/3/0)
tcp        0      1 172.19.0.15:44004       172.19.0.12:9527        SYN_SENT    3066236/nc           on (6.86/3/0)
tcp        0      1 172.19.0.15:44004       172.19.0.12:9527        SYN_SENT    3066236/nc           on (5.84/3/0)
tcp        0      1 172.19.0.15:44004       172.19.0.12:9527        SYN_SENT    3066236/nc           on (4.82/3/0)
tcp        0      1 172.19.0.15:44004       172.19.0.12:9527        SYN_SENT    3066236/nc           on (3.80/3/0)
tcp        0      1 172.19.0.15:44004       172.19.0.12:9527        SYN_SENT    3066236/nc           on (2.78/3/0)
tcp        0      1 172.19.0.15:44004       172.19.0.12:9527        SYN_SENT    3066236/nc           on (1.76/3/0)
tcp        0      1 172.19.0.15:44004       172.19.0.12:9527        SYN_SENT    3066236/nc           on (0.75/3/0)
tcp        0      1 172.19.0.15:44004       172.19.0.12:9527        SYN_SENT    3066236/nc           on (15.92/4/0)
tcp        0      1 172.19.0.15:44004       172.19.0.12:9527        SYN_SENT    3066236/nc           on (14.90/4/0)
tcp        0      1 172.19.0.15:44004       172.19.0.12:9527        SYN_SENT    3066236/nc           on (13.88/4/0)
tcp        0      1 172.19.0.15:44004       172.19.0.12:9527        SYN_SENT    3066236/nc           on (12.86/4/0)
tcp        0      1 172.19.0.15:44004       172.19.0.12:9527        SYN_SENT    3066236/nc           on (11.84/4/0)
tcp        0      1 172.19.0.15:44004       172.19.0.12:9527        SYN_SENT    3066236/nc           on (10.83/4/0)
tcp        0      1 172.19.0.15:44004       172.19.0.12:9527        SYN_SENT    3066236/nc           on (9.81/4/0)
tcp        0      1 172.19.0.15:44004       172.19.0.12:9527        SYN_SENT    3066236/nc           on (8.79/4/0)
tcp        0      1 172.19.0.15:44004       172.19.0.12:9527        SYN_SENT    3066236/nc           on (7.77/4/0)
tcp        0      1 172.19.0.15:44004       172.19.0.12:9527        SYN_SENT    3066236/nc           on (6.75/4/0)
tcp        0      1 172.19.0.15:44004       172.19.0.12:9527        SYN_SENT    3066236/nc           on (5.73/4/0)
tcp        0      1 172.19.0.15:44004       172.19.0.12:9527        SYN_SENT    3066236/nc           on (4.71/4/0)
tcp        0      1 172.19.0.15:44004       172.19.0.12:9527        SYN_SENT    3066236/nc           on (3.70/4/0)
tcp        0      1 172.19.0.15:44004       172.19.0.12:9527        SYN_SENT    3066236/nc           on (2.68/4/0)
tcp        0      1 172.19.0.15:44004       172.19.0.12:9527        SYN_SENT    3066236/nc           on (1.65/4/0)
tcp        0      1 172.19.0.15:44004       172.19.0.12:9527        SYN_SENT    3066236/nc           on (0.64/4/0)
tcp        0      1 172.19.0.15:44004       172.19.0.12:9527        SYN_SENT    3066236/nc           on (31.74/5/0)

最后一列是 Timer 计时器,格式为 timer(a/b/c),timer取值有四种

  • on: 超时计时器
  • off: 没有计时器
  • keepalive: keepalive 计时器
  • timewait: TIME_WAIT 计时器

对于超时计时器,a 表示当前计时器剩余时间,b 表示当前计时器重传次数,c 表示已发送的保活探测次数,比如命令中一行时 (1.72/2/0),1.72 表示在等 1.72 秒进行重传,2 表示已经重传了两次。

node2 使用 iptables 屏蔽了所有 9527 端口的包,因此不会有任何 socket 信息。

SYN-RECV 状态抓包

我们在修改下 node3 的 iptables 规则,将源端口为 9527 的包丢弃掉,命令如下:

# --sport 9527 表示源端口为 9527 的包被匹配,也就是 node2 发来的 ACK 包会被拦截
sudo iptables -A INPUT -p tcp --sport 9527 -j DROP

为了避免 SYN 重传,这里使用 namp 命令执行访问,命令如下:

sudo nmap -sS 172.19.0.12 -p 9527

node02 抓包如下:

在这里插入图片描述

可以看到 SYN-ACK 重传了 5 次,这是由内核参数 net.ipv4.tcp_synack_retries 控制的,默认值为 5,重传时间也是从 1s 开始逐渐翻倍,成指数级增长。

$ sysctl net.ipv4.tcp_synack_retries
net.ipv4.tcp_synack_retries = 5

重传过程中,查看 node2 的 netstat 信息可以看到 SYN-RECV 状态的 socket,表示连接正在等待 SYN-ACK 包的响应。

$ $ while true; do sudo netstat -anpo | grep SYN_RECV; sleep 1; done
tcp        0      0 172.19.0.12:9527        172.19.0.15:48803       SYN_RECV    -                    on (1.24/1/0)
tcp        0      0 172.19.0.12:9527        172.19.0.15:48803       SYN_RECV    -                    on (0.22/1/0)
tcp        0      0 172.19.0.12:9527        172.19.0.15:48803       SYN_RECV    -                    on (3.22/2/0)
tcp        0      0 172.19.0.12:9527        172.19.0.15:48803       SYN_RECV    -                    on (2.20/2/0)
tcp        0      0 172.19.0.12:9527        172.19.0.15:48803       SYN_RECV    -                    on (1.18/2/0)
tcp        0      0 172.19.0.12:9527        172.19.0.15:48803       SYN_RECV    -                    on (0.16/2/0)
tcp        0      0 172.19.0.12:9527        172.19.0.15:48803       SYN_RECV    -                    on (7.34/3/0)
tcp        0      0 172.19.0.12:9527        172.19.0.15:48803       SYN_RECV    -                    on (6.32/3/0)

SYN Flood 攻击

上面实验可以看到在 SYN-ACK 包重传期间,始终有 socket 在占用服务器的资源。如果有恶意攻击者不断发送 SYN 包,同时拒绝接收 SYN-ACK 或者故意不响应 ACK,服务器就会有大量处于 SYN-RECV 状态的连接消耗资源。这里的原理是在三次握手时,Linux 内核维护了半连接队列(SYN Queue)和全连接队列(Accept Queue),大量 SYN-RECV 状态的 socket 会占满 SYN Queue 队列,导致服务器无法正常处理新的 SYN 包,这就是 SYN Flood 攻击。

Linux 内核提供了 net.ipv4.tcp_syncookies 参数来应对 SYN Flood 攻击,当该参数开启时,如果队列已满,内核会计算一个 Cookies 值作为 SYN-ACK 包的序列号返回,客户端收到后会在 ACK 中使用 Cookie+1 作为序列号进行响应,服务端只有在检查 ACK 包的序列号正确后才会建立连接。这样如果有 SYN Flood 攻击,服务端每次都只计算 cookie 进行响应,不会真的占用半连接队列,从而达到服务拒绝的目的。

关于半连接队列全连接队列的更详细介绍可以以参考笔者的另一篇文章 【动手实验】TCP半连接队列、全连接队列实战分析,这里不在赘述。

PS:原实验用了 nc 验证 SYN Queue 的队列长度,但笔者在做实验时发现 nc 的 SYN-Queue 默认长度为 1,无法复现实验中的效果。

在 ChatGPT 帮助下了解到,对于网络 socket 来说,nc 在调用 listen 时,默认的 backlog 长度为 1,因此无法复现实验中的效果。查看 nc 的源码也可以验证这一点。因此如果要做类似的实验,最好用其他工具,比如 Python、Go 等语言做服务端。

// 源码地址
// https://github.com/openbsd/src/blob/d800967ee04b1c92ceefa78494d0ff66606a806d/usr.bin/nc/netcat.c#L1072/** local_listen()* Returns a socket listening on a local port, binds to specified source* address. Returns -1 on failure.*/
int
local_listen(const char *host, const char *port, struct addrinfo hints)
{// 代码省略if (!uflag && s != -1) {// 调用 listen 时,默认的 backlog 长度为 1if (listen(s, 1) == -1)err(1, "listen");}// 代码省略return s;
}

为什么需要三次握手?

实验完成了这里多扯一句三次握手的目的,网络上的资料大部分都会提到三次握手的目的是客户端、服务端同步序列号、窗口大小、MSS、SACK 等信息,其实这部分在前两次握手就已经完成了。三次握手最重要的原因在 RFC 里写的很清楚,主要是为了是防止历史的重复连接初始化造成的混乱问题,防止使用 TCP 协议通信的双方建立了错误的连接。

The principal reason for the three-way handshake is to prevent old duplicate connection initiations from causing confusion.

RFC9293

TCP 是半开、全双工通信的,通信双方要互相建立连接才行,所谓的三次握手本质上是完成四步操作:

  • 客户端请求建立连接;服务端响应确认。
  • 服务端请求建立连接,客户端响应确认。

我们将主动请求建立连接的一方称为发起方,被动建立连接的一方称为接收方。连接建立时,发起方的 SYN 请求和接收方的 ACK 响应是必不可少的,但除此之外,发起方必须要确认接收方的的响应是否正确,因此 TCP 引入了三次握手和 RST 机制来完成这一确认操作:

  • 如果接收方响应正确。则接收方发送 ACK 消息,完成正常的第三次握手。
  • 如果接收方响应错误,则接收方发送 RST 消息中断连接。

无论发送方返回 ACK 还是 RST 消息,都至少需要一次发起方到接收方的通信,这才是三次握手最重要的目的。

下面是 RFC9293 中的例子:

在这里插入图片描述

图片来自:为什么 TCP 建立连接需要三次握手

客户端发送第一次 SYN 后响应超时,又发送了一次 SYN,然而服务端响应了首次的 SYN,客户端收到 ACK 后检查到序列号不对,此时返回 RST 包中断连接,然后重新执行三次握手过程。

连接关闭

分析完了 TCP 连接建立的过程,我们再来分析下 TCP 连接关闭的过程。我们继续使用 nc 作为工具,首先启动服务端和客户端。

# node2 使用 nc 启动服务端
$ nc -k -l 172.19.0.12  9527# node3 使用 nc 启动客户端
$ nc 172.19.0.12 9527

完成后查看服务端和客户端的状态信息:

# node2 服务端
$ ss -atnp | grep -E "Recv-Q|9527"
State  Recv-Q Send-Q Local Address:Port    Peer Address:Port Process
LISTEN 0      1        172.19.0.12:9527         0.0.0.0:*     users:(("nc",pid=147133,fd=3))
ESTAB  0      0        172.19.0.12:9527     172.19.0.15:42526 users:(("nc",pid=147133,fd=4))# node3 客户端
$ ss -atnp | grep -E "Recv-Q|9527"
State  Recv-Q Send-Q Local Address:Port    Peer Address:Port Process
ESTAB  0      0        172.19.0.15:42526    172.19.0.12:9527  users:(("nc",pid=149072,fd=3))

正常关闭

我们首先在 node2 执行抓包,然后在客户端按照 ctrl+c 关闭连接,然后执行 netstat 命令查看服务端的状态信息:

# node2 抓包
$ sudo tcpdump -s0 -X -nn "tcp port 9527" -w tcp-handshake-03.pcap --print
tcpdump: listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes# node2 服务端
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name     Timer
tcp        0      0 172.19.0.12:9527        0.0.0.0:*               LISTEN      147133/nc            off (0.00/0/0)
tcp        0      0 172.19.0.12:9527        172.19.0.15:41492       ESTABLISHED 147133/nc            off (0.00/0/0)# node3 客户端
$ sudo netstat -anpo | grep Recv-Q; sudo netstat -anpo | grep 9527
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name     Timer
tcp        0      0 172.19.0.15:41492       172.19.0.12:9527        TIME_WAIT   -                    timewait (58.92/0/0)$ sudo netstat -anpo | grep Recv-Q; sudo netstat -anpo | grep 9527
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name     Timer
tcp        0      0 172.19.0.15:41492       172.19.0.12:9527        TIME_WAIT   -                    timewait (47.42/0/0)$ sudo netstat -anpo | grep Recv-Q; sudo netstat -anpo | grep 9527
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name     Timer
tcp        0      0 172.19.0.15:41492       172.19.0.12:9527        TIME_WAIT   -                    timewait (36.56/0/0)$ sudo netstat -anpo | grep Recv-Q; sudo netstat -anpo | grep 9527
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name     Timer
tcp        0      0 172.19.0.15:41492       172.19.0.12:9527        TIME_WAIT   -                    timewait (24.22/0/0)

抓包结果如图:

我们来简要分析下上述过程:

  1. 连接断开的很快,从抓包结果可以看出耗时大约 0.0019s,因此服务端执行 netstat 已经查不到连接了。

  2. 四次握手只有 3 个包,因为服务端没有数据需要处理,所以在对客户端的 FIN 进行 ACK 时,把 FIN 也捎带上了。

  3. 客户端收到了服务端的 FIN 并发送了 ACK 后进入 TIME_WAIT 状态,从 netstat 输出结果看有一个 60s 的timewait 定时器正在执行。

关于 TIME_WAIT 我们来做进一步的分析。

TIME_WAIT 状态处理

TIME_WAIT 主要是为了解决两个问题:

  1. 防止前一个连接的延迟发送的 Segment 被使用相同四元组的连接接收。

我们看下面图中的例子,第一个连接服务端发送的 SEQ=3 因为某些原因丢失,服务端执行了重传后客户端接收并断开了连接进入 TIME_WAIT 状态。此时如果 TIME_WAIT 时间过短,很快又和服务端建立了另一个使用相同四元组的连接,而此时之前丢失的 SEQ=3 包又发送来了,造成 TCP 状态的紊乱。


图片来源:Coping with the TCP TIME-WAIT state on busy Linux servers

  1. 确保远端已经关闭连接

当被动关闭的一方发送 FIN 后会进入 LAST_ACK 状态等待对端的 ACK。如果没有 TIME_WAIT 状态,服务端处于 LAST_ACK 状态时,客户端可能会使用相同的四元组来新建连接,因为新的连接会使用新的序列号,与之前的不匹配,服务端会认为新连接错误,从而返回 RST 包中止连接。


图片来源:Coping with the TCP TIME-WAIT state on busy Linux servers

TIME_WAIT 状态的 socket 本身也会带来问题,主要是端口占用,可能导致服务器无法建立新的连接。TIME_WAIT 状态只会在主动断开连接的一方出现,在收到对方的 FIN 包后进入该状态。Linux 默认的 MSL(Maximum Segment Lifetime, 最大报文生存时间) 为 30s,因此默认的 TIME_WAIT 时间为 2* MSL = 60s。在这期间端口是一直被占用的,服务器是根据四元组来识别 socket 的,因此在这 1 分钟内,服务器不能在建立相同的连接。

Linux 开放给应用使用的端口大约在 3 万个左右,受 sysctl net.ipv4.ip_local_port_range 的影响。假设我们可以使用全部的可用接口,在大量连接执行正常断开的流程下,我们只能支持每秒 500 条连接建立。但实际情况是服务端程序往往只会监听若干固定端口,并且收到的流量可能是通过几台 LoadBalancer 转发过来的,因此实际能支持的四元组数量是有限的。

$ sysctl net.ipv4.ip_local_port_range
net.ipv4.ip_local_port_range = 32768	60999

Linux 内核提供了以下参数来影响 TIME_WAIT 状态的处理:

  1. net.ipv4.ip_local_port_range:可以通过该参数来扩大可用端口范围,让服务器可以创建更多的连接。

  2. net.ipv4.tcp_timestamps:RFC 7323 引入的时间戳机制。其定义了一个 Timestamp 的 option,包含两个值:value,发送方当前的时间戳;echo,对端响应的最新时间戳。

  1. net.ipv4.tcp_tw_recycle:开启后,如果某个远端发来的包的时间戳,小于上次发过来的时间戳,会将这些包丢掉。开启后理论上是可以解决上面提到的第一个问题的,旧的包发来时被发现其 timestamp 小于新连接发来的 timestamp,会被丢掉。但理想很丰满,现实很骨感,该参数要求 timestamp 必须是单调递增的。这在 LB/NAT 环境下是无法得到保证的,因为无法共享时间戳时钟。在 4.12 版本之后该配置已经被移除,因此在生产环境中,任何情况下都不在建议开启这个选项。

  2. net.ipv4.tcp_tw_reuse: 将处于 TIME_WAIT 状态的 socket 用于主动建立新的 socket 连接,其允许内核复用超过 1s 的 TIME_WAT socket 被复用(仅适用于主动建立连接,被动建立连接的一方这个选项没啥用)。

  3. net.ipv4.tcp_max_tw_buckets:内核允许的状态为 TIME_WAIT 的最大连接数。超过该数字后,新的 TIME_WAIT 会被立即销毁。

$ sysctl net.ipv4.tcp_timestamps net.ipv4.tcp_tw_reuse net.ipv4.tcp_max_tw_buckets net.ipv4.tcp_tw_recycle
net.ipv4.tcp_timestamps = 1
net.ipv4.tcp_tw_reuse = 2 # 0 - disable, 1 - global enable, 2 - enable for loopback traffic only
net.ipv4.tcp_max_tw_buckets = 4096
sysctl: cannot stat /proc/sys/net/ipv4/tcp_tw_recycle: No such file or directory

下面做实验来看下上述参数的效果:

# 服务端 启动服务
$ nc -k -l 172.19.0.12  9527# 客户端程序
# 不断打开并关闭连接
import socketdef connect_and_immediately_disconnect(host, port, count):try:for i in range(count):cli = socket.socket(socket.AF_INET, socket.SOCK_STREAM)cli.connect((host, port))cli.close()except Exception as e:print(f"Failed to connect: {e}")if __name__ == '__main__':connect_and_immediately_disconnect('172.19.0.12', 9527, 70000)

先将 tcp_max_tw_buckets 调到 100 万,执行程序,结果如下,最终客户端报了 Cannot assign requested address 表示没有地址可用,客户端有
28232 个 TIME_WAIT 状态的 socket,与 ip_local_port_range 的计算范围一致。

$ sysctl net.ipv4.ip_local_port_range
net.ipv4.ip_local_port_range = 32768	60999$ sudo netstat -anpo | grep 9527 | grep timewait | wc -l
28232$ python3 client.py
Failed to connect: [Errno 99] Cannot assign requested address

现在我们将 net.ipv4.tcp_tw_reuse 设置为全局有效后再次执行程序,可以看到 TIME_WAIT 状态的数量会稳定在一万多条,客户端没有报错,执行完成后正常退出。

# 修改客户端 tcp_tw_reuse
sudo sysctl -w net.ipv4.tcp_tw_reuse=1$ sudo netstat -anpo | grep 9527 | grep timewait | wc -l
10967$ sudo netstat -anpo | grep 9527 | grep timewait | wc -l
13921$ sudo netstat -anpo | grep 9527 | grep timewait | wc -l
14070$ sudo netstat -anpo | grep 9527 | grep timewait | wc -l
14070

我们将 tcp_tw_reuse 关闭并调整 tcp_max_tw_buckets 为 5000 重复实验:

$ sudo sysctl -w net.ipv4.tcp_tw_reuse=2 net.ipv4.tcp_max_tw_buckets=5000

再次执行客户端程序后会发现 TIME_WAIT 状态的 socket 不会超过 5000,超过阈值后的 socket 会被清理并统计,可以通过 netstat -s 命令查看:

$ sudo netstat -anpo | grep 9527 | grep timewait | wc -l
4996$ netstat -s | grep TCPTimeWaitOverflowTCPTimeWaitOverflow: 65008

这里我们总结下 TIME_WAIT 的处理:

  1. TIME_WAIT 是为了保证通信的可靠性而存在的,这也是为什么 Linux 内核不支持修改 60s 限制的原因。

The TIME_WAIT state is our friend and is there to help us (i.e., to let old duplicate segments expire in the network). Instead of trying to avoid the state, we should understand it. – 《Unix programming》

  1. 在服务端,永远不要开启 net.ipv4.tcp_tw_recycle,新的内核版本已废弃;旧的版本在 LB/NAT 环境下会将正常包丢弃,导致问题。

  2. net.ipv4.tcp_tw_reuse 仅对主动断开和发起的一方有效,可以理解为只对客户端有效,服务端大部分都是被动建立连接,因此对其意义不大。

  3. 客户端还可以设置 0 延迟关闭的方式,此时会发送 RST 直接终止连接,不走正常的断开流程,也就不会进入 TIME_WAIT 状态,对于探活类应用非常拥有。但服务端永远不要设置,否则客户端会收到 connnection reset by peer 的错误。

  4. 服务端尽量不要主动断开连接,将 TIME_WAIT 留在客户端,不然会耗费更多的资源,并且调优方式有限。

  5. 如果可以尽量使用长连接的方式。

上面分析了正常关闭的流程,下面我们再来看下各个状态的情况。

FIN_WAIT_1 状态

我们使用 iptalbes 拦截第一个 FIN 包,然后看下服务端和客户端的状态信息:

# node2
$ nc -k -l 172.19.0.12  9527# node3
$ nc 172.19.0.12 9527# 在 node2设置规则,将目标端口为 9527 的 FIN 包丢弃
sudo iptables -A INPUT -p tcp --dport 9527 --tcp-flags FIN FIN -j DROP# 在 node2 开启抓包
$ sudo tcpdump -s0 -X -nn "tcp port 9527" -w tcp-handshake-FIN1-01.pcap --print# 在两台服务器执行命令,查看 socket 状态
$ while true; do sudo netstat -anpo | grep 9527; sleep 1; done

命令都执行后,我们在 node3 按下 ctrl+c 关闭连接,查看 node3 的链接可以看到进入了 FIN_WAIT1 状态。node2 因为 FIN 包被丢弃,所以还是 ESTABLISHED 状态。

$ while true; do sudo netstat -anpo | grep 9527; sleep 1; done
tcp        0      0 172.19.0.15:53072       172.19.0.12:9527        ESTABLISHED 546891/nc            off (0.00/0/0)tcp        0      0 172.19.0.15:53072       172.19.0.12:9527        ESTABLISHED 546891/nc            off (0.00/0/0)
tcp        0      1 172.19.0.15:53072       172.19.0.12:9527        FIN_WAIT1   -                    on (0.36/1/0)
tcp        0      1 172.19.0.15:53072       172.19.0.12:9527        FIN_WAIT1   -                    on (0.17/2/0)
tcp        0      1 172.19.0.15:53072       172.19.0.12:9527        FIN_WAIT1   -                    on (0.80/3/0)
tcp        0      1 172.19.0.15:53072       172.19.0.12:9527        FIN_WAIT1   -                    on (3.08/4/0)
...
tcp        0      1 172.19.0.15:53072       172.19.0.12:9527        FIN_WAIT1   -                    on (0.35/7/0)
tcp        0      1 172.19.0.15:53072       172.19.0.12:9527        FIN_WAIT1   -                    on (0.00/7/0)
tcp        0      1 172.19.0.15:53072       172.19.0.12:9527        FIN_WAIT1   -                    on (51.56/8/0)
tcp        0      1 172.19.0.15:53072       172.19.0.12:9527        FIN_WAIT1   -                    on (50.54/8/0)
tcp        0      1 172.19.0.15:53072       172.19.0.12:9527        FIN_WAIT1   -                    on (49.52/8/0)
...
tcp        0      1 172.19.0.15:53072       172.19.0.12:9527        FIN_WAIT1   -                    on (0.61/8/0)
tcp        0      1 172.19.0.15:53072       172.19.0.12:9527        FIN_WAIT1   -                    on (0.00/8/0)

node3 使用 ss -s 命令统计,可以看到有 1 个 orphaned 状态的 socket。

$ ss -s
Total: 192
TCP:   10 (estab 6, closed 0, orphaned 1, timewait 0)Transport Total     IP        IPv6
RAW	  1         0         1
UDP	  6         4         2
TCP	  10        9         1
INET	  17        13        4
FRAG	  0         0         0

下面是抓包结果

可以看到 FIN 包重传了 8 次,一共发了 9 个包。这个的重传次数是由内核参数 net.ipv4.tcp_orphan_retries 控制的,该参数会控制连接关闭时所有的超时重传次数,默认为 0。其计算逻辑是:

  • 如果为 0,则重传 8 次
  • 如果不为 0,则重传次数为该参数的值。可以将该值调小来减少重传次数,提高性能。

当超时重传次数达到上限后,内核将连接关闭并清除定时器。

//源码地址:https://elixir.bootlin.com/linux/v5.15.130/source/net/ipv4/tcp_timer.c#L139
// 如果 net.ipv4.tcp_orphan_retries 是 0,则重传次数为 8。
/***  tcp_orphan_retries() - Returns maximal number of retries on an orphaned socket*  @sk:    Pointer to the current socket.*  @alive: bool, socket alive state*/
static int tcp_orphan_retries(struct sock *sk, bool alive)
{int retries = READ_ONCE(sock_net(sk)->ipv4.sysctl_tcp_orphan_retries); /* May be zero. *//* We know from an ICMP that something is wrong. */if (sk->sk_err_soft && !alive)retries = 0;/* However, if socket sent something recently, select some safe* number of retries. 8 corresponds to >100 seconds with minimal* RTO of 200msec. */if (retries == 0 && alive)retries = 8;return retries;
}// 源码地址: https://elixir.bootlin.com/linux/v5.15.130/source/net/ipv4/tcp.c#L4450
// 将 state 设置为 TCP_CLOSE 状态,并清除发送定时器
void tcp_done(struct sock *sk)
{struct request_sock *req;/* We might be called with a new socket, after* inet_csk_prepare_forced_close() has been called* so we can not use lockdep_sock_is_held(sk)*/req = rcu_dereference_protected(tcp_sk(sk)->fastopen_rsk, 1);if (sk->sk_state == TCP_SYN_SENT || sk->sk_state == TCP_SYN_RECV)TCP_INC_STATS(sock_net(sk), TCP_MIB_ATTEMPTFAILS);tcp_set_state(sk, TCP_CLOSE);tcp_clear_xmit_timers(sk);if (req)reqsk_fastopen_remove(sk, req, false);WRITE_ONCE(sk->sk_shutdown, SHUTDOWN_MASK);if (!sock_flag(sk, SOCK_DEAD))sk->sk_state_change(sk);elseinet_csk_destroy_sock(sk);
}

这里的控制参数是 tcp_orphan_retries,使用 orphan 代表而不是像连接建立时的参数tcp_syn_retriestcp_synack_retries,明确按包类型区分。原因是 Linux 将执行关闭 的 socket 视为 orphan(孤儿)socket,处于 FIN-WAIT-1、FIN-WAIT-2、LAST-ACK、CLOSING 状态的 socket 都可能属于 orphan socket。

内核还有一个参数 net.ipv4.tcp_max_orphans 用来控制 orphan socket 的最大数量。当该状态的 socket 数量超过阈值后,Linux 内核将不会走正常的四次挥手流程,而是直接发送 RST 信息终止连接。

关于 orphan socket 的详细讨论可以参考笔者另一篇实验 TCP orphan socket 的产生与消亡,这里不再赘述。

FIN_WAIT_2 状态

我们将 node2 的iptables 清理后,在重启服务端和客户端,然后在 node3 添加 iptables 拦截 node2 发来的 FIN 包。

# node2 清理 iptables
sudo iptables -F# node2 重启服务端和客户端
$ nc -k -l 172.19.0.12  9527# node3
$ nc 172.19.0.12 9527# 在 node3 设置规则,将源端口为 9527 的 FIN 包丢弃
sudo iptables -A INPUT -p tcp --sport 9527 --tcp-flags FIN FIN -j DROP# node2 开启抓包
$ sudo tcpdump -s0 -X -nn "tcp port 9527" -w tcp-handshake-FIN2-01.pcap --print# 在两台服务器执行命令,查看 socket 状态
$ while true; do sudo netstat -anpo | grep 9527; sleep 1; done

执行上述命令后,在 node3 按下 ctrl+c 关闭连接,可以看到 node2 服务端进入 LAST_ACK 状态,node3 客户端进入 FIN_WAIT2 状态。

# node2 服务端
tcp        0      0 172.19.0.12:9527        172.19.0.15:55942       ESTABLISHED 579852/nc            off (0.00/0/0)
tcp        0      0 172.19.0.12:9527        0.0.0.0:*               LISTEN      579852/nc            off (0.00/0/0)
tcp        0      0 172.19.0.12:9527        172.19.0.15:55942       ESTABLISHED 579852/nc            off (0.00/0/0)
tcp        0      0 172.19.0.12:9527        0.0.0.0:*               LISTEN      579852/nc            off (0.00/0/0)
tcp        0      1 172.19.0.12:9527        172.19.0.15:55942       LAST_ACK    -                    on (0.18/1/0)
tcp        0      1 172.19.0.12:9527        172.19.0.15:55942       LAST_ACK    -                    on (0.00/2/0)
tcp        0      1 172.19.0.12:9527        172.19.0.15:55942       LAST_ACK    -                    on (0.62/3/0)
tcp        0      1 172.19.0.12:9527        172.19.0.15:55942       LAST_ACK    -                    on (0.27/6/0)
tcp        0      1 172.19.0.12:9527        172.19.0.15:55942       LAST_ACK    -                    on (25.62/7/0)tcp        0      1 172.19.0.12:9527        172.19.0.15:55942       LAST_ACK    -                    on (1.32/8/0)
tcp        0      1 172.19.0.12:9527        172.19.0.15:55942       LAST_ACK    -                    on (0.30/8/0)tcp        0      1 172.19.0.12:9527        172.19.0.15:55942       LAST_ACK    -                    on (0.00/8/0)
tcp        0      0 172.19.0.12:9527        0.0.0.0:*               LISTEN      579852/nc            off (0.00/0/0)
tcp        0      0 172.19.0.12:9527        0.0.0.0:*               LISTEN      579852/nc            off (0.00/0/0)
tcp        0      0 172.19.0.12:9527        0.0.0.0:*               LISTEN      579852/nc            off (0.00/0/0)
tcp        0      0 172.19.0.12:9527        0.0.0.0:*               LISTEN      579852/nc            off (0.00/0/0)
tcp        0      0 172.19.0.12:9527        0.0.0.0:*               LISTEN      579852/nc            off (0.00/0/0)# node3 客户端
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        ESTABLISHED 582121/nc            off (0.00/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        ESTABLISHED 582121/nc            off (0.00/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        ESTABLISHED 582121/nc            off (0.00/0/0)
tcp        0      1 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT1   -                    on (0.14/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (59.12/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (58.11/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (57.09/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (56.07/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (55.05/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (54.03/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (53.01/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (52.00/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (50.98/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (49.96/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (48.94/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (47.92/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (46.90/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (45.88/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (44.86/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (43.84/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (42.82/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (41.81/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (40.79/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (39.77/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (38.75/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (37.73/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (36.71/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (35.69/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (34.67/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (33.65/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (32.63/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (31.61/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (30.59/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (29.57/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (28.56/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (27.54/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (26.52/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (25.50/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (24.48/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (23.46/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (22.44/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (21.42/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (20.40/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (19.38/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (18.36/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (17.34/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (16.33/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (15.31/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (14.29/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (13.27/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (12.25/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (11.23/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (10.21/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (9.19/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (8.18/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (7.16/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (6.14/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (5.12/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (4.10/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (3.08/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (2.06/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (1.04/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (0.02/0/0)
tcp        0      0 172.19.0.15:55942       172.19.0.12:9527        FIN_WAIT2   -                    timewait (0.00/0/0)

下面是抓包结果:

tcp-handshake-fin2-01

我们来简单分析下:

  1. 服务端一直处于 LAST_ACK 状态,说明 FIN 包已发送,但一直没有收到客户端的 ACK 包。
  2. 客户端一直处于 FIN_WAIT2 状态,说明客户端已经收到了服务端的 ACK 包,但迟迟没收到服务端的 FIN 包。说明我们的 iptables 拦截生效了。
  3. 客户端进入 FIN_WAIT2 状态后,有一个 60s 的 timewait 计时器在运行。这是由内核参数 net.ipv4.tcp_fin_timeout 控制的,默认是 60s。超过后会自动关闭连接,不会进入 TIME_WAIT 状态。

连接保活

TCP 通信需要建立连接,这里的连接并不是真的在通信双方之间有一个通路,而是双方各自维护一个 TCB 来管理状态数据。在这种情况下,如果一方挂了并且没有数据传输,那另一方是感知不到的,其连接可能会一直存在,造成不必要的资源浪费。

为了解决这个问题 TCP 也设计了保活机制,内核有三个参数与该机制有关:

$ sysctl net.ipv4.tcp_keepalive_time net.ipv4.tcp_keepalive_probes net.ipv4.tcp_keepalive_intvl
net.ipv4.tcp_keepalive_time = 7200
net.ipv4.tcp_keepalive_probes = 9
net.ipv4.tcp_keepalive_intvl = 75
  • net.ipv4.tcp_keepalive_time:最后一次数据发送到发送第一个探活包的间隔时长,默认为 7200s。也就是说如果超过了 7200s 没有发送数据,TCP 就会发送一个探活包。
  • net.ipv4.tcp_keepalive_probes:允许探活包没有回应的最大次数,默认为 9。也就是说如果发送了 9 次探活包后依然没有得到响应,那么 TCP 就会考虑连接已经失效,会通知应用层中断连接。
  • net.ipv4.tcp_keepalive_intvl:在第一个探测包发送后,在没有数据传输的情况下,每个探测包的发送频率,默认 75s。即每 75s 发送一个探测包。

我们来做实验验证一下,这里先将上述参数的值调小一些,方便我们观察实验结果。我们将首个探测包的发送时间改为最后一次发送数据 10s 后,并且探测包的时间间隔为 5s,超过 5 次后就断开连接。

客户端先发送一次数据,然后休眠 30 s,在发送一次数据,然后休眠 200s。

$ sudo sysctl -w net.ipv4.tcp_keepalive_time=10 net.ipv4.tcp_keepalive_intvl=5 net.ipv4.tcp_keepalive_probes=5
net.ipv4.tcp_keepalive_time = 10
net.ipv4.tcp_keepalive_intvl = 5
net.ipv4.tcp_keepalive_probes = 5
  • 服务端代码
import socket
import time
import os# 创建服务器端用于测试
def start_server():server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)server_sock.bind(('172.19.0.15', 8888))server_sock.listen(1)print("Server listening on 172.19.0.15:8888...")conn, addr = server_sock.accept()print(f"Connection from {addr}")while True:data = conn.recv(1024)if not data:breakprint(f"Received: {data.decode('utf-8')}")if __name__ == "__main__":start_server()
  • 客户端代码
import socket
import time
import os# 客户端代码,启用 TCP 保活
def start_client():# 创建 socketclient_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)# 启用 SO_KEEPALIVEclient_sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)# 连接到服务器client_sock.connect(('172.19.0.15', 8888))print("Connected to server")# 发送第一次 "hello world"client_sock.send("hello world".encode('utf-8'))print("Sent first 'hello world'")# 休眠 28 秒time.sleep(28)# 发送第二次 "hello world"client_sock.send("hello world".encode('utf-8'))print("Sent second 'hello world'")# 休眠 200 秒print("Sleeping for 200 seconds...")time.sleep(200)# 关闭连接client_sock.close()print("Connection closed")if __name__ == "__main__":# 启动客户端start_client()

启动程序,完成第二次数据传输中用 iptables 将 ACK 包连接来伪造探活失败的场景,

# 服务端执行
$ sudo iptables -A INPUT -p tcp --dport 8888  -j DROP

socket 状态抓包结果如下:

$ sudo netstat -anpo | grep -E "Recv-Q|8888"
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name     Timer
tcp        0      0 172.19.0.12:38224       172.19.0.15:8888        ESTABLISHED 3276657/python3      keepalive (0.12/0/4)# ubuntu @ node2 in ~/labs/syn-queue-lab [12:38:09]
$ sudo netstat -anpo | grep -E "Recv-Q|8888"
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name     Timer
tcp        0      0 172.19.0.12:38224       172.19.0.15:8888        ESTABLISHED 3276657/python3      keepalive (4.13/0/5)

  • 在第 10s 客户端发送了探活包,其数据长度为 0,这是第一次休眠时的探活检测。
  • 在第 28s 客户端发送了第二次数据,之后从第 38s 开始,每 10s 发送一次探活包。由此可以知道探活包的 ACK 也被视作正常的数据收发,探活检测会根据 net.ipv4.tcp_keepalive_time的值来确定。
  • 从第 68s 开始,我们在服务端设置了 iptables 规则拦截探活包,之后开始每隔 5s 发送一次探活包,这里开始受 net.ipv4.tcp_keepalive_intvl 参数的控制。
  • 连续 5 个探活包没有收到响应后,客户端发送了 RST 包中断连接,说明 net.ipv4.tcp_keepalive_intvl = 5 的改动已经生效。

PS:最开始使用的是 Golang 程序,但发现修改系统设置并不生效,探活包的发送时间间隔一直是 15s。

$ sudo netstat -anpo | grep -E "Recv-Q|8888"
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name     Timer
tcp        0      0 172.19.0.12:46706       172.19.0.15:8888        ESTABLISHED 3270763/client       keepalive (0.12/0/0)# ubuntu @ node2 in ~/labs/syn-queue-lab [12:25:07]
$ sudo netstat -anpo | grep -E "Recv-Q|8888"
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name     Timer
tcp        0      0 172.19.0.12:46706       172.19.0.15:8888        ESTABLISHED 3270763/client       keepalive (14.77/0/0)

经过调查后发现原因是 Golang 的 net net.DialTimeout 创建连接时默认设置了 15s,如果想修改必须获取到 TCPConn 对象后自行修改。

// 源码地址:https://github.com/golang/go/blob/bc5f4a555e933e6861d12edba4c2d87ef6caf8e6/src/net/dial.go#L19const (// defaultTCPKeepAliveIdle is a default constant value for TCP_KEEPIDLE.// See go.dev/issue/31510 for details.defaultTCPKeepAliveIdle = 15 * time.Second// defaultTCPKeepAliveInterval is a default constant value for TCP_KEEPINTVL.// It is the same as defaultTCPKeepAliveIdle, see go.dev/issue/31510 for details.defaultTCPKeepAliveInterval = 15 * time.Second// defaultTCPKeepAliveCount is a default constant value for TCP_KEEPCNT.defaultTCPKeepAliveCount = 9// For the moment, MultiPath TCP is used by default with listeners, if// available, but not with dialers.// See go.dev/issue/56539defaultMPTCPEnabledListen = truedefaultMPTCPEnabledDial   = false
)

总结

作为程序员,虽然接触到的网络知识基本逃不过 RFC1180: A TCP/IP Tutorial 的范畴,但这确实是最让人头大的知识点之一。作为将《TCP/IP Guide》、《TCP/IP 详解(英文版)》以及主要 RFC 都读过的踩坑者,只能无奈的感慨,光读这些是资料顶多可以让我们勉强了解,但要想在实际工作中对遇到的问题手到擒来,还远远不够。

网络知识的学习至少涉及到三方面内容:RFC 定义的协议原理、操作系统的具体实现、命令工具的使用。而每一部分学习起来都不容易,RFC 理论的枯燥,操作系统不同版本实现机制的繁杂,命令工具各种参数的琐碎,都让人望而却步。最好的方式就是做实验,将三者统一起来,通过动手实验,尤其是做生产级别的故障排查类实验,可以帮助我们熟悉工具的使用,验证系统的实现,并通过实验结果加深对理论的理解,做到全面而深刻的学习。

相关文章:

【动手实验】TCP 连接的建立与关闭抓包分析

本文是基于知识星球程序员踩坑案例分享中的作业进行的复现和总结&#xff0c;借此加深对 TCP 协议的理解&#xff0c; 原文参见TCP 连接的建立和关闭 —— 强烈建议新手看看。 实验环境 这里使用两台位于同一子网的腾讯云服务器&#xff0c;IP 分别是 node2&#xff08;172.1…...

语音识别踩坑记录

本来想在原来的语音识别的基础上增加本地扩展本地词典&#xff0c; 采用的语音识别是Vosk识别器&#xff0c;模型是 vosk-model-small-cn-0.22 // 初始化Vosk识别器 if (recognizer null) {using (Model model new Model(modelPath)){string grammar "{""…...

Conda常用命令汇总

Conda 是一个流行的包管理器和环境管理工具&#xff0c;广泛应用于数据科学、机器学习等领域。它可以帮助我们管理 Python 包以及不同版本的环境&#xff0c;避免包冲突&#xff0c;提升项目的可复现性。以下是一些常用的 Conda 命令&#xff0c;涵盖环境创建、管理、包安装等常…...

消息队列MQ使用场景有哪些?

MQ 在实际项目中的应用场景主要围绕异步处理、系统解耦、流量控制三大核心能力展开&#xff0c;结合具体业务需求可细分为以下场景&#xff1a; 1. 异步处理 典型场景&#xff1a;用户注册成功后发送短信/邮件、支付成功后通知物流系统发货、商品上架后同步至搜索引擎。优势&…...

5. 前后端实现文件上传与解析

1. 说明 在实际开发中&#xff0c;比较常见的一个功能是需要在前端页面中选择系统中的某个文件上传到服务器中进行解析&#xff0c;解析后的文件内容可以用来在服务器中当作参数&#xff0c;或者传递给其它组件使用&#xff0c;或者需要存储到数据库中。所以本文就提供一种方式…...

基于腾讯云高性能HAI-CPU的跨境电商客服助手全链路解析

跨境电商的背景以及痛点 根据Statista数据&#xff0c;2025年全球跨境电商市场规模预计达6.57万亿美元&#xff0c;年增长率保持在12.5% 。随着平台规则趋严&#xff08;如亚马逊封店潮&#xff09;&#xff0c;更多卖家选择自建独立站&#xff0c;2024年独立站占比已达35%。A…...

python中time模块的常用方法及应用

Python 的 time 模块是自带的标准模块&#xff0c;不需要额外安装&#xff0c;可以直接通过import time的方式导入并使用其中的函数和类。该模块提供了与时间相关的各种功能&#xff0c;以下是一些常用方法及其应用场景和示例&#xff1a; ### 1. time.time() - **功能**&…...

JavaScript性能优化

JavaScript性能优化指南 一&#xff1a;性能分析与指标确立 使用性能分析工具 • 使用Lighthouse、Chrome DevTools的Performance面板和WebPageTest进行基准测试&#xff0c;识别加载时间、脚本执行时长等瓶颈。 • 关注核心Web指标&#xff1a;LCP&#xff08;最大内容绘制&a…...

《React 属性与状态江湖:从验证到表单受控的实战探险》

属性初识 属性能解决两个大问题&#xff1a;通信和复用 props.js: import React, { Component } from react import Navbar from ./Navbarexport default class App extends Component {state {a:100}render() {return (<div><div><h2>首页</h2>&l…...

Android Retrofit 框架注解定义与解析模块深度剖析(一)

一、引言 在现代 Android 和 Java 开发中&#xff0c;网络请求是不可或缺的一部分。Retrofit 作为 Square 公司开源的一款强大的类型安全的 HTTP 客户端&#xff0c;凭借其简洁易用的 API 和高效的性能&#xff0c;在开发者社区中广受欢迎。Retrofit 的核心特性之一便是通过注…...

嵌入式学习L6网络编程D3TCP

TCP编程 写代码 第一步socket 绑定 先填充 点分十进制转换成32位整数 client 然后就连接成功了就可以读写数据了 client #include "net.h"int main (void) {int fd -1;struct sockaddr_in sin;/* 1. 创建socket fd */if ((fd socket (AF_INET, SOCK_S…...

【玩转23种Java设计模式】结构型模式篇:享元模式

软件设计模式&#xff08;Design pattern&#xff09;&#xff0c;又称设计模式&#xff0c;是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性、程序的重用性。 汇总目录链接&…...

超分之DeSRA

Desra: detect and delete the artifacts of gan-based real-world super-resolution models.DeSRA&#xff1a;检测并消除基于GAN的真实世界超分辨率模型中的伪影Xie L, Wang X, Chen X, et al.arXiv preprint arXiv:2307.02457, 2023. 摘要 背景&#xff1a; GAN-SR模型虽然…...

产城融合典范:树莓科技如何助力宜宾数字经济腾飞​

宜宾在推动数字经济发展的征程中&#xff0c;树莓科技扮演着至关重要的角色&#xff0c;堪称产城融合的典范。 树莓科技入驻宜宾后&#xff0c;积极与当地政府合作&#xff0c;以产业发展带动城市建设&#xff0c;以城市功能完善促进产业升级。在产业布局上&#xff0c;树莓科…...

Java数据结构第二十二期:Map与Set的高效应用之道(一)

专栏&#xff1a;Java数据结构秘籍 个人主页&#xff1a;手握风云 目录 一、Map和Set 1.1. 概念 二、搜索树 2.1. 概念 2.2. 查找操作 2.2. 插入操作 2.3. 删除操作 2.4. 性能分析 三、搜索 3.1. 概念及场景 3.2. 模型 四、Map 4.1. Map的说明 3.2. Map的使用 五…...

焊接安全的新纪元:智能监管系统的力量

在现代制造业中&#xff0c;焊接作为一项关键工艺&#xff0c;其安全性直接关系到生产质量和人员安全。为了应对这一挑战&#xff0c;一款创新的焊接联网智能化监管系统应运而生&#xff0c;为焊接行业带来了新的安全保障。 智能监管&#xff0c;安全升级 这款系统通过“一机…...

OpenGL中绘制图形元素的实现(使用visual studio(C++)绘制一个矩形)

目标&#xff1a;使用OpenGL提供的函数绘制矩形、线段、三角形等基本图形元素 所需效果 实验步骤 1、配置OpenGL&#xff08;详情参见OpenGL的配置&#xff09; 2、头文件引入 #include <gl/glut.h> 3、编写方法体 1>矩形实现 //绘制矩形 void DisplayRectangl…...

政安晨【零基础玩转各类开源AI项目】Wan 2.1 本地部署,基于ComfyUI运行,最强文生视频 图生视频,一键生成高质量影片

政安晨的个人主页&#xff1a;政安晨 欢迎 &#x1f44d;点赞✍评论⭐收藏 希望政安晨的博客能够对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff01; 目录 下载项目 创建虚拟环境 安装项目依赖 尝试运行 依次下载模型 完成 我们今天要使…...

DeepLabv3+改进8:在主干网络中添加SIM注意力机制|助力涨点

🔥【DeepLabv3+改进专栏!探索语义分割新高度】 🌟 你是否在为图像分割的精度与效率发愁? 📢 本专栏重磅推出: ✅ 独家改进策略:融合注意力机制、轻量化设计与多尺度优化 ✅ 即插即用模块:ASPP+升级、解码器 PS:订阅专栏提供完整代码 论文简介 在本文中,我们提出了…...

卷积神经网络(笔记01)

视觉处理三大任务&#xff1a;分类、目标检测、图像分割 CNN网络主要有三部分构成&#xff1a;卷积层&#xff08;Convolutional Layer&#xff09;、池化层&#xff08;Pooling Layer&#xff09;和激活函数 一、解释卷积层中的偏置项是什么&#xff0c;并讨论在神经网络中引…...

从自己电脑的浏览器访问阿里云主机中运行的LLaMA-Factory webui

阿里云主机上LLaMA-Factory的webui在0.0.0.0:7860侦听&#xff0c;无法直接打开&#xff0c;需要通过代理的方法访问。 在LLaMA-Factory目录下创建一个脚本文件run.sh&#xff0c;并加上执行权限&#xff0c;内容如下&#xff1a; #!/bin/shexport GRADIO_SERVER_PORT7860 ex…...

大数据面试之路 (一) 数据倾斜

记录大数据面试历程 数据倾斜 大数据岗位 &#xff0c;数据倾斜面试必问的一个问题。 一、数据倾斜的表现与原因 表现 某个或某几个Task执行时间过长&#xff0c;其他Task快速完成。 Spark/MapReduce作业卡在某个阶段&#xff08;如reduce阶段&#xff09;&#xff0c;日志显…...

文件上传漏洞 upload-labs靶场

&#xff08;这个没删就是还没做完 ; ω ; &#xff09; 目录 Pass-01 前端绕过 关卡分析 绕过&#xff1a;Burpsuite抓包修改或页面禁用js Pass-02 服务器端检测–IMME类型 关卡分析 Content-type 绕过&#xff1a;抓包修改文件的content-type Pass-03 黑名单绕过 关…...

「 DelegateUI 」Ant-d 风格的 Qt Qml UI 套件

写在前面&#xff1a;关于为什么要写一套新的UI框架 一方面&#xff0c;Qt Qml 生态中缺乏一套既遵循现代设计规范(自带的功能少且丑,懂得都懂)&#xff0c;又能深度整合 Qt 生态的开源组件库。 另一方面&#xff0c;Qt Qml 中也有一些其他方案&#xff0c;例如 FluentUI Qml…...

数字人分身开发指南:从概念到实战

一、什么是数字人分身&#xff1f; 想象一下&#xff0c;在电脑或手机屏幕里&#xff0c;一个能跟你聊天、回答问题&#xff0c;甚至还能做表情的虚拟角色。这就是数字人分身&#xff0c;它用上了人工智能技术&#xff0c;让机器也能像人一样交流。无论是在线客服、网络主播还…...

Java小白-管理项目工具Maven(2)初识Maven

一、Maven安装 ①安装jdk1.8或以上版本 ②下载Maven&#xff08;此为3.6.0&#xff09;&#xff1a;地址&#xff1a;Download Apache Maven – Maven 下载地址&#xff1a;Index of /dist/maven/maven-3/3.6.0/binaries ③安装Maven到无中文路径即可 bin&#xff1a;含…...

【附JS、Python、C++题解】Leetcode 面试150题(8)

一、题目 11. 盛最多水的容器 给定一个长度为 n 的整数数组 height 。有 n 条垂线&#xff0c;第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。 找出其中的两条线&#xff0c;使得它们与 x 轴共同构成的容器可以容纳最多的水。 返回容器可以储存的最大水量。你不能倾斜…...

什么是向量数据库向量搜索?

向量数据库 专为高效存储与检索高维向量设计&#xff0c;支持语义搜索、推荐系统等AI场景&#xff0c;如文本/图像嵌入的相似性匹配。 ChromaDB 轻量级开源向量数据库&#xff0c;优势在于易用性&#xff08;快速部署、简洁API&#xff09;和小规模场景&#xff08;本地开发、…...

【WRF-Urban】使用 LCZ 替换 WRF 运行中的 LUCC 数据

使用 LCZ 替换 WRF 运行中的 LUCC 数据 WRF-UCM中的城市类型LCZ的背景介绍完整步骤总结1. 获取 LCZ 数据2. 获取 WRF 运行所需的 LUCC 数据3. 使用 w2w 替换 WRF 的 LUCC 数据4. 运行 WRF 预处理(WPS & REAL)5. 运行 WRF 并优化城市参数化Q1:使用 LCZ 替换 WRF 运行中的…...

centos 7 安装apache服务

四步骤 解包 使用tar -zxvf 对.tar.gz 进行解压 使用tar -jxvf 对.tar.bz2 进行解压 rpm命令使用集合 rpm -qa 查询系统已安装的软件包 rpm -ql查看指定软件包存放的位置 rpm -qi查看指定软件包的属性 rpm -qf查看指定文件或目录是由哪个软件包所安装的 rpm -qpi查看指…...

2025各省市建筑产业和工程建设计划安排

1. 前言 十四届全国人大三次会议3月5日上午9时在人民大会堂开幕&#xff0c;国务院总理李强作政府工作报告。 《2025年政府工作报告》&#xff08;以下简称 “报告”&#xff09;作为统筹国家经济、战略布局与社会发展的蓝图&#xff0c;与建筑业息息相关&#xff0c;为今后的…...

广告营销,会被AI重构吗?

DeepSeek设计&#xff0c;即梦AI绘图&#xff0c;剪映成片。 DeepSeek的热度还在高开疯走。 用户对于各个场景下DS应用的探索也还在持续&#xff0c;各种DS的模式被挖掘出来&#xff0c;超级个体们开始给手下的大模型团队进行分工&#xff0c;实践出各种场景下最佳的排列组合方…...

01 音视频知识学习(视频)

图像基础概念 ◼像素&#xff1a;像素是一个图片的基本单位&#xff0c;pix是英语单词picture的简写&#xff0c;加上英 语单词“元素element”&#xff0c;就得到了“pixel”&#xff0c;简称px&#xff0c;所以“像素”有“图像元素” 之意。 ◼ 分辨率&#xff1a;是指图像…...

深入探究 Ryu REST API

Ryu 4.34 REST API 详细接口说明与示例 Ryu 4.34 的 REST API 提供了对 SDN 网络的核心管理功能&#xff0c;涵盖交换机、流表、端口、拓扑和 QoS 等操作。以下是详细的接口分类、功能说明及 Python 示例代码。 1. 交换机管理 1.1 获取所有交换机 DPID 端点: GET /stats/swi…...

不同AI生成的PHP版雪花算法

OpenAI <?php /*** Snowflake 雪花算法生成器* 生成的 64 位 ID 结构&#xff1a;* 1 位 保留位&#xff08;始终为0&#xff0c;防止负数&#xff09;* 41 位 时间戳&#xff08;毫秒级&#xff0c;当前时间减去自定义纪元&#xff09;* 5 位 数据中心ID* 5 …...

texstudio: 编辑器显示行号+给PDF增加行号

texstudio在编辑器部分增加行号&#xff1a; texstudio默认在编辑器部分不显示行号&#xff0c;如下图&#xff1a; 要实现以下的在编辑部分增加行号&#xff1a; 执行如下操作&#xff1a; 选项-->设置TexStudio-->编辑器-->显示行号-->所有行号选择好后&…...

强化学习基础-马尔可夫决策过程与贝尔曼方程

马尔可夫决策过程 在老虎机问题中&#xff0c;无论智能代理采取什么行动&#xff0c;之后要解决的问题都是一样的。也就是寻找最好的老虎机。但现实生活中的问题是不同的。例如&#xff0c;在围棋游戏中&#xff0c;智能代理落子后&#xff0c;棋盘上的棋子排列会发生变化&…...

爬虫的精准识别:基于 User-Agent 的正则实现

&#x1f9d1; 博主简介&#xff1a;CSDN博客专家&#xff0c;历代文学网&#xff08;PC端可以访问&#xff1a;https://literature.sinhy.com/#/?__c1000&#xff0c;移动端可微信小程序搜索“历代文学”&#xff09;总架构师&#xff0c;15年工作经验&#xff0c;精通Java编…...

Scala的初步使用

目录 1. Scala简介2. Scala编写的Hello World2.1 pom.xml中依赖和插件的配置2.2 安装Scala2.12.172.3 安装code-server插件2.4 helloworld.scala2.5 helloworld2.scala2.6 java调用scala object 3. Scala调用Java3.1 例子13.2 例子2 参考 1. Scala简介 Scala是一门多范式的编程…...

【Json RPC框架】框架介绍与环境搭建(Ubuntu 22.04)

&#x1f381;个人主页&#xff1a;我们的五年 &#x1f50d;系列专栏&#xff1a;Json RPC框架 &#x1f337;追光的人&#xff0c;终会万丈光芒 &#x1f389;欢迎大家点赞&#x1f44d;评论&#x1f4dd;收藏⭐文章 ​ JSon RPC框架系列文章Json RPC框架_我们的五年的博…...

python读取word文档 | AI应用开发

python读取word文档 | AI应用开发 RAG中python读取word文档 RAG系统中构建知识库流程中重要的一个步骤是读取外挂的知识文档&#xff0c;为word是其中比较常见的文件。 另一个值得注意的是&#xff0c;RAG在读取文档后需要对文档进行分割&#xff0c;而良好的分割需要有一定结…...

20、组件懒加载

组件懒加载&#xff0c;也被称为异步组件加载&#xff0c;是一种在 Vue 项目中提升性能的重要技术手段。下面从概念、实现原理、使用场景、实现方式几个方面详细介绍&#xff1a; 概念 在传统的 Vue 项目里&#xff0c;当应用启动时&#xff0c;所有的组件代码都会被一次性加…...

打造智能钉钉机器人:借助智谱GLM-4-Flash实现高效智能回复(文末附源码)

文章目录 前言一、准备工作&#xff08;一&#xff09;钉钉机器人&#xff08;二&#xff09;智谱 GLM-4-Flash&#xff08;三&#xff09;内网穿透工具 cpolar&#xff08;四&#xff09;需要准备的工具和环境 二、钉钉机器人的创建与配置步骤1&#xff1a;创建钉钉机器人步骤…...

【故障处理系列--docker卷的挂载】

一位伙伴需求是把容器的目录映射到宿主机且容器目录的内容不被宿主机的空白目录覆盖。我的第一反应是-v 卷的映射&#xff0c;参数是对的&#xff0c;但是用法是错的 1、容器卷的挂载方式 容器把目录映射到宿主机创建volume卷&#xff0c;然后把容器的目录和volume卷绑定 区别…...

兴达易控modbusTCP转profinet接防撞雷达测试

modbusTCP转profinet接防撞雷达测试 随着工业自动化程度的不断提高&#xff0c;现场设备之间的通信需求日益增长。ModbusTCP作为一种广泛应用的工业通信协议&#xff0c;因其简单、可靠的特点&#xff0c;被广泛应用于各种自动化设备中。而Profinet作为工业以太网的一种&#…...

Acknowledgment.nack方法重试消费kafka消息异常

文章目录 问题示例异常 原因nack方法Acknowledgment接口实现类&#xff1a;ConsumerAcknowledgment实现类&#xff1a;ConsumerBatchAcknowledgment 解决方案1 批量消费指定index示例 2 单条消费示例 问题 使用BatchAcknowledgingMessageListener 批量消费Kafka消息&#xff0…...

通过动态获取后端数据判断输入的值打小

eval() 函数在 JavaScript 中是一个非常强大的函数 【1】计算简单公式 很多时候如果需要动态的提供计算的公式&#xff0c;需要写一大段的公式计算逻辑去兼容&#xff0c;可能耗费大量的开发成本。为了快速了解 eval 的用法&#xff0c;直接 ① 打开浏览器&#xff1b;② F1…...

乐维网管平台核心功能解析(一)——告警关联知识

在数字化转型浪潮中&#xff0c;企业IT系统规模呈指数级增长&#xff0c;传统的"人工经验"运维模式已难以应对海量告警处理需求。某银行数据中心曾统计&#xff0c;其日均告警量突破10万条&#xff0c;关键故障的平均定位时间长达3.5小时&#xff0c;直接导致年损失超…...

数据结构_单链表

今天我们要开启链表的学习 &#x1f58b;️&#x1f58b;️&#x1f58b;️ 学了顺序表我们可以知道&#xff1a; &#x1f388;链表其实就是争对顺序表的缺点来设计的&#xff0c;补足的就是顺序表的缺点 &#x1f388;链表在物理上是上一个节点存放的下一个节点的地址 链表 …...

b站视频下载工具软件怎么下载

自行配置FFMPEG环境 请优先选择批量下载&#xff0c;会自处理视频和音频文件。 如果要下载更高质量请登陆。 没有配置FFMPEG下载后会有报错提示&#xff0c;视频音频文件无法合并生成mp4文件 更新批量下载标题&#xff0c;只取视频原标题&#xff0c;B站反爬机制登陆后下载多了…...