【动手实验】TCP半连接队列、全连接队列实战分析

本文是对 从一次线上问题说起,详解 TCP 半连接队列、全连接队列 这篇文章的实验复现和总结,借此加深对 TCP 半连接队列、全连接队列的理解。

实验环境

两台腾讯云服务器 node2(172.19.0.12) 和 node3(172.19.0.15)配置为 2C4G,Ubuntu 系统,内核版本 5.15.0-130-generic 。

全连接半连接队列简介

在 TCP 三次握手过程中,Linux 会维护两个队列分别是:

  • SYN Queue 半连接队列
  • Accept Queue 全连接队列

创建连接时,两个队列作用如下:

  • 客户端向服务端发送 SYN 包,客户端进入 SYN_SENT 状态
  • 服务端收到 SYN 包后,进入 SYN_RECV 状态,内核将连接信息放入 SYN Queue 队列,然后向客户端发送 SYN+ACK 包
  • 客户端收到 SYN+ACK 包后,发送 ACK 包,客户端进入 ESTABLISHED 状态
  • 服务端收到 ACK 包后,将连接从 SYN Queue 队列中取出移到 Accept Queue 队列,Server 端进入 ESTABLISHED。
  • 服务端应用程序调用 accept 函数处理数据,连接从 Accept Queue 队列移除。


图片来自:从一次线上问题说起,详解 TCP 半连接队列、全连接队列

图片来自Cloudflare Blog: SYN Packet Handling in the Wild

两个队列的长度都是有限的,当队列满了之后,新建连接时内核会将 SYN 包丢弃或者直接返回 RST 包。

全连接队列实战

全连接队列长度控制

TCP 全连接队列的长度计算公式为:

min(somaxconn, backlog)

  • somaxconn Linux 内核参数 net.core.somaxconn 的值,默认为 4096。可以通过修改该参数来控制全连接队列的长度。
  • backlog 是系统调用 listen 函数 int listen(int sockfd, int backlog) 的 backlog 参数, Golang 中默认使用系统 somaxconn 的值。

下面是 Linux 5.15.130 内核源码中计算全连接队列长度的代码:

源码地址:https://elixir.bootlin.com/linux/v5.15.130/source/net/socket.c#L1716

我们修改 somaxconn 的值,然后运行实验代码查看全连接队列的长度变化。

  • 服务端实验代码
package mainimport ("log""net""time"
)func main() {l, err := net.Listen("tcp", ":8888")if err != nil {log.Printf("failed to listen due to %v", err)}defer l.Close()log.Println("listen :8888 success")for {time.Sleep(time.Second * 100)}
}

首先我们修改 somaxconn 为 128:

sudo sysctl -w net.core.somaxconn=128

启动服务后查看全连接队列的长度:

$ go run server.go
2025/02/13 09:53:01 listen :8888 success$ ss -lnt
State             Recv-Q            Send-Q                         Local Address:Port                         Peer Address:Port            Process
LISTEN            0                 128                                        *:8888                                    *:*
...

这里简单解释下 ss 命令输出的含义:

  • 对于 Listen 状态的 socket,Recv-Q 表示当前全连接队列的长度,也就是已经完成三次握手,等待应用层调用 accept 的 TCP 连接数;Send-Q 表示全连接队列的最大长度。

  • 对于非 Listen 状态的 socket,Recv-Q 表示已经收到但尚未被应用读取的字节数;Send-Q 表示已发送但尚未收到确认的字节数。

再次修改 somaxconn 为 1024 重启服务后,查看全连接队列的长度已经变成了 1024。

$ sudo sysctl -w net.core.somaxconn=1024
$ go run server.go
2025/02/13 09:53:01 listen :8888 success$ ss -lnt
State             Recv-Q            Send-Q                         Local Address:Port                         Peer Address:Port            Process
LISTEN            0                 1024                                       *:8888                                    *:*
...

全连接队列溢出

下面我们让服务端只 Listen 端口但不执行 accept() 处理数据,模拟全连接队列溢出的情况。

  • 服务端代码
// server 端监听 8888 tcp 端口 
package main import ( "log" "net" "time" 
) func main() { l, err := net.Listen("tcp", ":8888") if err != nil { log.Printf("failed to listen due to %v", err) } defer l.Close() log.Println("listen :8888 success") for { time.Sleep(time.Second * 100) } 
}
  • 客户端代码

和原实验相比加了 time.Sleep(500 * time.Millisecond) 一行代码,让连接一个个建立,可以更精准的复现全连接队列已满的情况。

package main import ( "context" "log" "net" "os" "os/signal" "sync" "syscall" "time" 
) var wg sync.WaitGroup func establishConn(ctx context.Context, i int) { defer wg.Done() conn, err := net.DialTimeout("tcp", ":8888", time.Second*5) if err != nil { log.Printf("%d, dial error: %v", i, err) return } log.Printf("%d, dial success", i) _, err = conn.Write([]byte("hello world")) if err != nil { log.Printf("%d, send error: %v", i, err) return } select { case <-ctx.Done(): log.Printf("%d, dail close", i) } 
} func main() { ctx, cancel := context.WithCancel(context.Background()) // 并发请求 10 次服务端,连接建立成功后发送数据for i := 0; i < 10; i++ { wg.Add(1) time.Sleep(500 * time.Millisecond)go establishConn(ctx, i) } go func() { sc := make(chan os.Signal, 1) signal.Notify(sc, syscall.SIGINT) select { case <-sc: cancel() } }() wg.Wait() log.Printf("client exit") 
}

我们先将全连接队列的最大长度设置为 5:

$ sudo sysctl -w net.core.somaxconn=5$ cat /proc/sys/net/core/somaxconn
5

运行服务端和客户端后,查看全连接队列情况:

  • 服务端 socket 情况
$ ss -ant | grep -E "Recv|8888"
State      Recv-Q Send-Q        Local Address:Port            Peer Address:Port Process
LISTEN     6      5                         *:8888                       *:*
ESTAB      11     0      [::ffff:172.19.0.12]:8888    [::ffff:172.19.0.15]:40148
ESTAB      11     0      [::ffff:172.19.0.12]:8888    [::ffff:172.19.0.15]:40162
ESTAB      11     0      [::ffff:172.19.0.12]:8888    [::ffff:172.19.0.15]:40128
ESTAB      11     0      [::ffff:172.19.0.12]:8888    [::ffff:172.19.0.15]:40132
ESTAB      11     0      [::ffff:172.19.0.12]:8888    [::ffff:172.19.0.15]:40110
ESTAB      11     0      [::ffff:172.19.0.12]:8888    [::ffff:172.19.0.15]:40112
  • 客户端 socket 情况
$ ss -ant | grep -E "Recv|8888"
State      Recv-Q Send-Q Local Address:Port    Peer Address:Port Process
ESTAB      0      0        172.19.0.15:40132    172.19.0.12:8888
ESTAB      0      0        172.19.0.15:40162    172.19.0.12:8888
ESTAB      0      0        172.19.0.15:40148    172.19.0.12:8888
SYN-SENT   0      1        172.19.0.15:51906    172.19.0.12:8888
ESTAB      0      0        172.19.0.15:40112    172.19.0.12:8888
ESTAB      0      0        172.19.0.15:40128    172.19.0.12:8888
SYN-SENT   0      1        172.19.0.15:51912    172.19.0.12:8888
SYN-SENT   0      1        172.19.0.15:40176    172.19.0.12:8888
ESTAB      0      0        172.19.0.15:40110    172.19.0.12:8888
SYN-SENT   0      1        172.19.0.15:51926    172.19.0.12:8888
  • 客户端日志输出
$ go run client.go
2025/02/19 11:14:22 0, dial success
2025/02/19 11:14:22 1, dial success
2025/02/19 11:14:23 2, dial success
2025/02/19 11:14:23 3, dial success
2025/02/19 11:14:24 4, dial success
2025/02/19 11:14:24 5, dial success
2025/02/19 11:14:30 6, dial error: dial tcp 172.19.0.12:8888: i/o timeout
2025/02/19 11:14:30 7, dial error: dial tcp 172.19.0.12:8888: i/o timeout
2025/02/19 11:14:31 8, dial error: dial tcp 172.19.0.12:8888: i/o timeout
2025/02/19 11:14:31 9, dial error: dial tcp 172.19.0.12:8888: i/o timeout

我们来分析下上述结果:

1. 全连接队列是否已满

服务端 Listen 状态的 socket 显示 Send-Q 为 5,表示该 socket 的全连接队列最大值为 5;Recv-Q 为 6,表示当前 Accept queue 中数量为 6,我们看有 6 条 ESTAB 状态的连接,符合观察结果。Linux 内核的判断依据是 > 而不是 >=,所以实际的连接数为比队列的最大值多 1 个。5.15.0-130-generic 内核代码如下:

// 源码地址
// https://elixir.bootlin.com/linux/v5.15.130/source/include/net/sock.h#L980
/* Note: If you think the test should be:*	return READ_ONCE(sk->sk_ack_backlog) >= READ_ONCE(sk->sk_max_ack_backlog);* Then please take a look at commit 64a146513f8f ("[NET]: Revert incorrect accept queue backlog changes.")*/
static inline bool sk_acceptq_is_full(const struct sock *sk)
{return READ_ONCE(sk->sk_ack_backlog) > READ_ONCE(sk->sk_max_ack_backlog);
}

之所以这样做,是为了保证在 backlog 设置为 0 时,依然可以有一个连接进入全连接队列,具体可以查看以下 commit 信息:

https://github.com/torvalds/linux/commit/64a146513f8f12ba204b7bf5cb7e9505594ead42[NET]: Revert incorrect accept queue backlog changes.
This reverts two changes:8488df8
248f067A backlog value of N really does mean allow "N + 1" connections
to queue to a listening socket.  This allows one to specify
"0" as the backlog and still get 1 connection.Noticed by Gerrit Renker and Rick Jones.Signed-off-by: David S. Miller <davem@davemloft.net>
2. 内核 drop 包处理逻辑

客户端有 6 个 ESTAB 状态的 socket,另外还有 4 个 SYN-SENT 状态的 socket,对应着 4 条 timeout 报错信息。我们只改了全连接队列大小为 5,半连接队列大小依然为默认的 net.ipv4.tcp_max_syn_backlog=256,所以第 6 个请连接建立后 Accept Queue 满了但 SYN Queue 还没有满。按理说从第 7 个请求开始服务端可以接收 SYN 但不能在处理客户端的 ACK 进入 Accept Queue,服务端会有 4 条 SYN-RECV 状态的连接,而实际情况是服务端不存在 SYN_RECV 状态的连接,这是因为当 Accept Queue 被占满时,即使 SYN Queue 没有满,Linux 内核也会将新来的 SYN 请求丢弃掉。 5.15.0-130-generic 内核处理这部分逻辑的代码如下::

// 源码地址:https://elixir.bootlin.com/linux/v5.15.130/source/net/ipv4/tcp_input.c#L6848int tcp_conn_request(struct request_sock_ops *rsk_ops,const struct tcp_request_sock_ops *af_ops,struct sock *sk, struct sk_buff *skb)
{// ... 代码省略syncookies = READ_ONCE(net->ipv4.sysctl_tcp_syncookies);/* TW buckets are converted to open requests without* limitations, they conserve resources and peer is* evidently real one.*/// 强制启用 SYN cookie 或者半连接队列已满// !isn 表示是一个新的请求连接建立的 SYNif ((syncookies == 2 || inet_csk_reqsk_queue_is_full(sk)) && !isn) {// 这里表示是否启用 SYN cookie 机制;如果不开启,则直接 drop,如果开启,则继续执行。want_cookie = tcp_syn_flood_action(sk, rsk_ops->slab_name);if (!want_cookie)goto drop;}// 如果 accept queue 满了则 dropif (sk_acceptq_is_full(sk)) {NET_INC_STATS(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);goto drop;}static bool tcp_syn_flood_action(const struct sock *sk, const char *proto)
{struct request_sock_queue *queue = &inet_csk(sk)->icsk_accept_queue;const char *msg = "Dropping request";struct net *net = sock_net(sk);bool want_cookie = false;u8 syncookies;syncookies = READ_ONCE(net->ipv4.sysctl_tcp_syncookies);// 开启 SYN Cookie 机制
#ifdef CONFIG_SYN_COOKIESif (syncookies) {msg = "Sending cookies";want_cookie = true;__NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPREQQFULLDOCOOKIES);} else
#endif// 没有启用 syncookies,统计丢弃包的数量__NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPREQQFULLDROP);// 如果启用了 SYN cookie 机制,发送警告if (!queue->synflood_warned && syncookies != 2 &&xchg(&queue->synflood_warned, 1) == 0)net_info_ratelimited("%s: Possible SYN flooding on port %d. %s.  Check SNMP counters.\n",proto, sk->sk_num, msg);return want_cookie;
}// 判断半连接队列是否满,用的是半连接队列的长度是否大于等于全连接队列的最大长度
static inline int inet_csk_reqsk_queue_is_full(const struct sock *sk)
{return inet_csk_reqsk_queue_len(sk) >= sk->sk_max_ack_backlog;
}

从代码中可以推测出 net.ipv4.tcp_syncookies 参数值的含义和 Linux 的处理机制:

  • 2:强制开启 SYN Cookie 机制,发送警告
  • 1:当半连接队列满时,开启 SYN Cookie 机制,发送警告
  • 0:不开启 SYN Cookie 机制,并统计丢弃包的数量

这里判断半连接队列是否满的依据是 inet_csk_reqsk_queue_len(sk) >= sk->sk_max_ack_backlog,也就是说当半连接队列长度不小于全连接队列的最大长度时,如果不开启 SYN Cookie 机制,就会将 SYN 包丢弃。

回到我们的实验环境,net.ipv4.tcp_syncookies 设置为 1 并且半连接队列没满,因此不会开启 SYN Cookie 机制,继续往后执行时会因为 Accept Queue 满了将包丢弃。可以通过 netstat -s 命令查看丢弃包的数量。

$ date;netstat -s | grep -i "SYNs to LISTEN"
Wed Feb 19 12:05:51 PM CST 20251289 SYNs to LISTEN sockets dropped$ date;netstat -s | grep -i "SYNs to LISTEN"
Wed Feb 19 12:06:05 PM CST 20251301 SYNs to LISTEN sockets dropped

可以看到有 12 个 SYN 包被 DROP 了,查看抓包情况可以看到,我们有 4 个请求连接超时,每个请求传了 3 次 SYN(一次发起 + 两次重传)。

查看客户端 socket 状态能够看到重传计时器在工作,这里重传了两次和默认的 net.ipv4.tcp_syn_retries = 6 有出入,是因为代码 conn, err := net.DialTimeout("tcp", "172.19.0.12:8888", time.Second*5)设置了 5s 超时,操作系统的默认重传间隔大约为 1s、2s、4s、8s、16s、32s,第 3 次重传会发生在 7s 以后,客户端已经主动断开连接了。

$ 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.15:57384       172.19.0.12:8888        ESTABLISHED 3123924/client       keepalive (7.57/0/0)
tcp        0      0 172.19.0.15:57388       172.19.0.12:8888        ESTABLISHED 3123924/client       keepalive (8.07/0/0)
tcp        0      0 172.19.0.15:60276       172.19.0.12:8888        ESTABLISHED 3123924/client       keepalive (9.58/0/0)
tcp        0      1 172.19.0.15:60304       172.19.0.12:8888        SYN_SENT    3123924/client       on (0.08/1/0)
tcp        0      1 172.19.0.15:60286       172.19.0.12:8888        SYN_SENT    3123924/client       on (2.60/2/0)
tcp        0      0 172.19.0.15:60270       172.19.0.12:8888        ESTABLISHED 3123924/client       keepalive (9.08/0/0)
tcp        0      0 172.19.0.15:60280       172.19.0.12:8888        ESTABLISHED 3123924/client       keepalive (10.08/0/0)
tcp        0      1 172.19.0.15:60292       172.19.0.12:8888        SYN_SENT    3123924/client       on (3.11/2/0)
tcp        0      0 172.19.0.15:57398       172.19.0.12:8888        ESTABLISHED 3123924/client       keepalive (8.57/0/0)
tcp        0      1 172.19.0.15:60294       172.19.0.12:8888        SYN_SENT    3123924/client       on (3.62/2/0)
3. overflow 参数控制

当全连接队列满时,Linux 默认会 drop 掉包,这个受 net.ipv4.tcp_abort_on_overflow 参数控制,默认为 0 表示直接 drop,为 1 则表示中断连接,服务端会返回 RST 包。可以通过如下方式修改

$ sudo sysctl -w net.ipv4.tcp_abort_on_overflow=1或者echo 1 > /proc/sys/net/ipv4/tcp_abort_on_overflow

我们修改参数后再次执行客户端请求,会出现 connection reset by peer 错误,抓包能看到 RST 包。(在实验时,如果客户端不加时间间隔,会出现返回 RST 包的情况,如果加了则不会出现这种情况,应该是和两者的生效机制有关,SYN Cookie 和全连接队列满 drop 发生在 tcp_conn_request 函数,而 abort_on_overflow 发生在 tcp_check_req 函数, 先挖个坑,等后续梳理整个网络传输流程时在做进一步分析)。

$ go run client.go
2025/03/01 13:36:55 2, dial success
2025/03/01 13:36:55 5, dial success
2025/03/01 13:36:55 4, dial success
2025/03/01 13:36:55 1, dial success
2025/03/01 13:36:55 3, dial success
2025/03/01 13:36:55 0, dial success
2025/03/01 13:36:55 7, dial error: dial tcp 172.19.0.12:8888: connect: connection reset by peer
2025/03/01 13:36:55 6, dial error: dial tcp 172.19.0.12:8888: connect: connection reset by peer

4. ss 命令展示含义

服务端有 6 条 ESTAB 状态的 socket,RECV_Q 的值为 11,与客户端发送的数据 []byte("hello world") 数据长度一致,因为我们的没有执行 accept 接收数据,所以 RECV_Q 会展示这部分数据的大小;

客户端 6 条 ESTAB 状态的 socket,其 RECV_Q 和 SEND_Q 均为 0;而 4 条 SYN-SENT 状态的 SEND-Q 为 1,这是因为 6 条已建立连接的 socket 包可以被正常 ACK,而 4 条建立连接失败的 socket,其 SYN 包没有收到 ACK 包,因为 SEND-Q 显示为 1。由此我们可以再次总结下 ss 的展示含义:

对于 LISTEN 状态的 socket

  • Recv-Q:表示当前全连接队列的大小,即已完成三次握手等待应用程序 accept() 的 TCP 连接数。
  • Send-Q:全连接队列的最大长度,即全连接队列所能容纳的 socket 数量。

对于非 LISTEN 状态的 socket

  • Recv-Q:表示已被接收但尚未执行 accept 被应用程序读取的数据字节数,通常在服务端能观察到。
  • Send-Q:表示已经发送但尚未收到 ACK 确认的字节数。

内核代码如下:

// https://elixir.bootlin.com/linux/v5.15.130/source/net/ipv4/tcp_diag.c#L18
static void tcp_diag_get_info(struct sock *sk, struct inet_diag_msg *r,void *_info)
{struct tcp_info *info = _info;if (inet_sk_state_load(sk) == TCP_LISTEN) { // LISTEN 状态的连接// 当前已完成三次握手但未被 accept 的连接数r->idiag_rqueue = READ_ONCE(sk->sk_ack_backlog); // 最大队列长度r->idiag_wqueue = READ_ONCE(sk->sk_max_ack_backlog);} else if (sk->sk_type == SOCK_STREAM) { // 非 LISTEN 状态的普通连接const struct tcp_sock *tp = tcp_sk(sk);// TCP 读队列,即接收缓冲区中未被应用层读取的数据量,单位是字节r->idiag_rqueue = max_t(int, READ_ONCE(tp->rcv_nxt) -READ_ONCE(tp->copied_seq), 0);// TCP 写队列,即已经发送但尚未被对方 ACK 确认的数据量,单位是字节r->idiag_wqueue = READ_ONCE(tp->write_seq) - tp->snd_una;}if (info)tcp_get_info(sk, info);
}
5. SYN+ACK 重传

原实验有三种情况:

  • 三次握手成功,数据正常发送
  • 客户端认为连接建立成功,但服务端一直处于 SYN-RECV 状态,不断重传 SYN + ACK
  • 客户端发送 SYN 未得到响应一直在重传

我们复现了第 1 中和第 3 种,之所以没有第二种情况是因为每次请求加了 500ms 的间隔,这样下一个请求发起 SYN 时,上一个请求已经完成三次握手,服务端的 socket 已经进入全连接队列了。如果我们去掉时间间隔,请求可能会一下子发出去全部进入半连接队列,等到服务端在接收到客户端的 ACK 包时,全连接队列已经满了,从而导致服务端的 socket 无法进入全连接队列,从而 DROP 掉 ACK 包出现第二种情况。这里我们去掉时间间隔尝试复现,此时可以看到服务端有 SYN-RECV 状态的连接,

$ ss -ant | grep -E "Recv|8888"
State      Recv-Q Send-Q        Local Address:Port            Peer Address:Port Process
LISTEN     6      5                         *:8888                       *:*
ESTAB      11     0      [::ffff:172.19.0.12]:8888    [::ffff:172.19.0.15]:33430
ESTAB      11     0      [::ffff:172.19.0.12]:8888    [::ffff:172.19.0.15]:33458
ESTAB      11     0      [::ffff:172.19.0.12]:8888    [::ffff:172.19.0.15]:33482
SYN-RECV   0      0      [::ffff:172.19.0.12]:8888    [::ffff:172.19.0.15]:33512
ESTAB      11     0      [::ffff:172.19.0.12]:8888    [::ffff:172.19.0.15]:33442
ESTAB      11     0      [::ffff:172.19.0.12]:8888    [::ffff:172.19.0.15]:33428
ESTAB      11     0      [::ffff:172.19.0.12]:8888    [::ffff:172.19.0.15]:33472
SYN-RECV   0      0      [::ffff:172.19.0.12]:8888    [::ffff:172.19.0.15]:33496

查看抓包结果可以看到 SYN-ACK 包重传。

全连接队列的实验就到这里,下面我们来看半连接队列的实验。

半连接队列实战

半连接队列的最大长度计算有些麻烦,网络上资料也很繁杂,本着 talk is cheap, show me the code 的原则,这里还是直接看 Linux 的源码来分析,还是 tcp_conn_request 函数。

// 源码地址:https://elixir.bootlin.com/linux/v5.15.130/source/net/ipv4/tcp_input.c#L6848int tcp_conn_request(struct request_sock_ops *rsk_ops,const struct tcp_request_sock_ops *af_ops,struct sock *sk, struct sk_buff *skb)
{// ... 代码省略u8 syncookies;// 第一部分,基于 syncookies 和半连接队列是否超过全连接队列长度、半连接队列是否已满来判断是否 dropsyncookies = READ_ONCE(net->ipv4.sysctl_tcp_syncookies);if ((syncookies == 2 || inet_csk_reqsk_queue_is_full(sk)) && !isn) {want_cookie = tcp_syn_flood_action(sk, rsk_ops->slab_name);if (!want_cookie)goto drop;}// 第二部分,判断全连接队列是否已满if (sk_acceptq_is_full(sk)) {NET_INC_STATS(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);goto drop;}req = inet_reqsk_alloc(rsk_ops, sk, !want_cookie);if (!req)goto drop;// ... 代码省略if (!want_cookie && !isn) {// 获取系统参数 ``net.ipv4.tcp_max_syn_backlog`` 的值int max_syn_backlog = READ_ONCE(net->ipv4.sysctl_max_syn_backlog);/* Kill the following clause, if you dislike this way. */// 第三部分:判断半连接队列是否超过长度限制if (!syncookies &&(max_syn_backlog - inet_csk_reqsk_queue_len(sk) <(max_syn_backlog >> 2)) &&!tcp_peer_is_proven(req, dst)) {/* Without syncookies last quarter of* backlog is filled with destinations,* proven to be alive.* It means that we continue to communicate* to destinations, already remembered* to the moment of synflood.*/pr_drop_req(req, ntohs(tcp_hdr(skb)->source),rsk_ops->family);goto drop_and_release;}isn = af_ops->init_seq(skb);}tcp_ecn_create_request(req, skb, sk, dst);if (want_cookie) {isn = cookie_init_sequence(af_ops, sk, skb, &req->mss);if (!tmp_opt.tstamp_ok)inet_rsk(req)->ecn_ok = 0;}return 0;}

核心计算逻辑是 (max_syn_backlog - inet_csk_reqsk_queue_len(sk) < (max_syn_backlog >> 2)),即 max_syn_backlog 的值减去当前半连接队列的长度的值小于 max_syn_backlog 的 1/4 时,就会将 SYN 包丢弃。简单来说就是半连接队列长度不能超过 max_syn_backlog 的 3/4。因为比较条件是 > 而不是 >=,所以在不开启 syncookies 的情况下,实际的半连接队列长度应该是 max_syn_backlog 的 3/4 + 1。大致计算如下:

  • max_syn_backlog 为 128,则半连接队列长度最大为 97
  • max_syn_backlog 为 256,则半连接队列长度最大为 193
  • max_syn_backlog 为 512,则半连接队列长度最大为 385
  • max_syn_backlog 为 1024,则半连接队列长度最大为 769

结合上面全连接实验中的代码分析,我们可以总结下 Linux 5.15.30 内核下 SYN 包的 Drop 机制:

我们修改参数验证下上述三种情况。

实验一:关闭 syncookies,半连接长度超过全连接最大长度

客户端我们使用 iptables 将服务端的包拦截,模拟 SYN Flood 攻击,这样服务端不会收到 ACK 包,也就不会进入全连接队列。系统参数 syn_cookies=0,max_syn_backlog=128,somaxconn=64,理论上会有 64 个 SYN-RECV 状态连接,其余的包被丢弃。

# 拦截服务端 8888 端口的包
$ sudo iptables -A INPUT -p tcp --sport 8888 -j DROP# 发送 SYN 包
$ sudo hping3 -S 172.19.0.12 -p 8888 --flood

查看服务端情况

$ ss -ant | grep -E "Recv|8888"
State     Recv-Q Send-Q        Local Address:Port            Peer Address:Port Process
LISTEN    0      64                        *:8888                       *:*# ubuntu @ node2 in ~ [11:58:11]
$ sudo netstat -nat | grep :8888 | grep SYN_RECV  | wc -l
64

结果符合预期。这里可以用 go 客户端做更精确的验证,我们使用 Go 程序发送 100 个请求,然后查看服务端连接数和 DROP 数

$ date;netstat -s | grep -i "SYNs to LISTEN"
Fri Feb 21 12:01:58 PM CST 20253030591019 SYNs to LISTEN sockets dropped$ sudo netstat -nat | grep :8888 | grep SYN_RECV  | wc -l
64$ date;netstat -s | grep -i "SYNs to LISTEN"
Fri Feb 21 12:02:14 PM CST 20253030591127 SYNs to LISTEN sockets dropped

可以看到服务端只有 64 个 SYN-RECV 状态连接,程序执行有有 3030591127-3030591019=108 个 SYN 包被丢弃。上面我们分析过,因为客户端设置了超时时间为 5s,所以 SYN 只会重传 2 次,也就是每个被 DROP 的连接都会发送 3 次 SYN。100 - 64 = 36,36 * 3 = 108,符合我们预期。

实验二:关闭 syncookies,全连接队列已满

修改服务端系统参数 syn_cookies=0,max_syn_backlog=128,somaxconn=64,这样全连接队列最大长度为 64,当有 65 个连接建立时,全连接队列就会满,此时再有 SYN 包建立连接时就会被丢弃。

首先我们清理掉客户端机器的 iptables 规则,是的三次握手能够正常进程。

$ sudo iptables -F

设置系统参数

$ sudo sysctl -w net.ipv4.tcp_syncookies=0
$ sudo sysctl -w net.ipv4.tcp_max_syn_backlog=128
$ sudo sysctl -w net.core.somaxconn=64

我们再次用 Go 客户端发送 100 个请求,然后查看服务端状态,可以看到有 65 个 ESTAB 状态连接,没有 SYN-RECV 状态连接,因为全连接队列已满,所有 SYN 包都会被丢弃。

$ ss -ant | grep -E "Recv|8888"
State     Recv-Q Send-Q        Local Address:Port            Peer Address:Port Process
LISTEN    65     64                        *:8888$ sudo netstat -nat | grep :8888 | grep ESTAB  | wc -l
65# ubuntu @ node2 in ~ [12:18:27] C:130
$ sudo netstat -nat | grep :8888 | grep SYN_RECV  | wc -l
0

按照以上逻辑,会有 35 个连接被拒绝,一共有 35 * 3 = 105 个 SYN 包被丢弃。我们查看统计信息可以验证,3030591766 - 3030591661 = 105,符合预期。

$ date;netstat -s | grep -i "SYNs to LISTEN"
Fri Feb 21 12:18:19 PM CST 20253030591661 SYNs to LISTEN sockets dropped# ubuntu @ node2 in ~ [12:18:19]
$ date;netstat -s | grep -i "SYNs to LISTEN"
Fri Feb 21 12:18:34 PM CST 20253030591766 SYNs to LISTEN sockets dropped

实验三:关闭 syncookies,半连接队列长度超过 max_syn_backlog 的 3/4

现在我们将全连接队列长度调大 net.core.somaxconn 设置为 4096,使用 iptables 拦截服务端 8888 端口的包,这样全连接队列始终不会填满,然后 max_syn_backlog 分别设置为:

  • 128,预期有 97 个 SYN-RECV 状态连接
  • 256,预期有 193 个 SYN-RECV 状态连接
  • 512,预期有 385 个 SYN-RECV 状态连接
  • 1024,预期有 769 个 SYN-RECV 状态连接

分别设置并发送请求后,服务端显示结果如下,基本符合预期。

# 客户端设置 iptables 拦截服务端
sudo iptables -A INPUT -p tcp --sport 8888 -j DROP# 服务端查看 SYN-RECV 状态连接数
$ sudo sysctl -w net.ipv4.tcp_max_syn_backlog=128
$ ss -ant | grep -E "Recv|:8888" | grep SYN-RECV | wc -l
97$ sudo sysctl -w net.ipv4.tcp_max_syn_backlog=256
$ ss -ant | grep -E "Recv|:8888" | grep SYN-RECV | wc -l
193$ sudo sysctl -w net.ipv4.tcp_max_syn_backlog=512
$ ss -ant | grep -E "Recv|:8888" | grep SYN-RECV | wc -l
385$ sudo sysctl -w net.ipv4.tcp_max_syn_backlog=1024
$ ss -ant | grep -E "Recv|:8888" | grep SYN-RECV | wc -l
769

执行过程数值会有变化,但最大半连接队列长度符合预期。

实验四:开启 syncookies,半连接队列长度取决于 max(somaxconn, backlog)

当开启 syncookies 时,半连接队列不在保留 1/4 的限制,而是取决于 max(somaxconn, backlog)。这里源码判断是 >=,因此最大长度应该会等于 max(somaxconn, backlog)

// 源码地址:https://elixir.bootlin.com/linux/v5.15.130/source/include/net/inet_connection_sock.h#L280
static inline int inet_csk_reqsk_queue_is_full(const struct sock *sk)
{return inet_csk_reqsk_queue_len(sk) >= sk->sk_max_ack_backlog;
}

我们分别设置 net.core.somaxconn 为 512,1024,4096并设置 net.ipv4.tcp_syncookies=1 开启 syncookies,每次设置完重启服务端,然后在发起请求,理论上会有 512,1024,4096 个 SYN-RECV 状态连接。

修改服务端 somaxconn 并重启后,使用 watch 命令查看 SYN-RECV 状态连接数,结果如下,符合预期。

$ watch -n 1 "netstat -nat | grep :8888 | grep SYN_RECV | wc -l"
Every 1.0s: netstat -nat | grep :8888 | grep SYN_RECV | wc -l              node2: Sat Mar  1 15:07:22 2025512$ watch -n 1 "netstat -nat | grep :8888 | grep SYN_RECV | wc -l"
Every 1.0s: netstat -nat | grep :8888 | grep SYN_RECV | wc -l              node2: Sat Mar  1 15:08:15 20251024$ watch -n 1 "netstat -nat | grep :8888 | grep SYN_RECV | wc -l"
Every 1.0s: netstat -nat | grep :8888 | grep SYN_RECV | wc -l              node2: Sat Mar  1 15:09:11 20254096

简要总结

  • 半连接队列受限于全连接队列长度,而全连接队列会受应用的影响,尽量不要将 somaxconn 设置的过小,否则会影响服务器的性能。
  • 尽量开启 syncookies,可以有效防止 SYN Flood 攻击,同时可以避免半连接队列被大量占用。
  • ss、netstat 的熟练使用对探查网络状态非常重要,要熟练掌握。
  • 代码之下无秘密,一定要结合源码去理解 Linux 的网络工作机制,不要只是死记硬背协议。
  • 动手!动手!动手!实践出真知。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/72526.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Springboot整合WebSocket+Redis以及微信小程序如何调用

一、 Springboot整合WebSocket 1. 引入socket依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId> </dependency>引入依赖后需要刷新maven,Websocket的版本默认跟随S…

影刀RPA开发拓展--SQL常用语句全攻略

前言 SQL&#xff08;结构化查询语言&#xff09;是数据库管理和操作的核心工具&#xff0c;无论是初学者还是经验丰富的数据库管理员&#xff0c;掌握常用的 SQL 语句对于高效管理和查询数据都至关重要。本文将系统性地介绍最常用的 SQL 语句&#xff0c;并为每个语句提供详细…

大模型训练微调技术介绍

大模型训练微调技术是人工智能领域中的一项重要技术&#xff0c;旨在通过少量特定领域的数据对预训练模型进行进一步训练&#xff0c;使其更好地适应具体任务或应用场景。以下是关于大模型训练微调技术的详细介绍&#xff1a; 1. 微调技术的定义与意义 微调&#xff08;Fine-…

生态安全相关

概念&#xff1a;生态安全指一个国家具有支撑国家生存发展的较为完整、不受威胁的生态系统&#xff0c;以及应对内外重大生态问题的能力。 &#xff08;1&#xff09;国外生态安全的研究进展 国际上对生态安全的研究是从“环境”与“安全”之间的关系展开的。开始的阶段&#x…

2024年第十五届蓝桥杯大赛软件赛省赛Python大学A组真题解析《更新中》

文章目录 试题A: 拼正方形(本题总分:5 分)解析答案试题B: 召唤数学精灵(本题总分:5 分)解析答案试题C: 数字诗意解析答案试题D:回文数组试题A: 拼正方形(本题总分:5 分) 【问题描述】 小蓝正在玩拼图游戏,他有7385137888721 个2 2 的方块和10470245 个1 1 的方块,他需…

《张一鸣,创业心路与算法思维》

张一鸣&#xff0c;多年如一日的阅读习惯。 爱读人物传记&#xff0c;称教科书式人类知识最浓缩的书&#xff0c;也爱看心理学&#xff0c;创业以及商业管理类的书。 冯仑&#xff0c;王石&#xff0c;联想&#xff0c;杰克韦尔奇&#xff0c;思科。 《乔布斯传》《埃隆马斯…

策略模式处理

首先&#xff0c;定义接口或抽象父类。例如&#xff0c;定义一个Strategy接口&#xff0c;里面有execute方法。然后不同的策略类实现这个接口&#xff0c;比如ConcreteStrategyA、ConcreteStrategyB等&#xff0c;每个类用Component注解&#xff0c;可能指定Bean的名字&#xf…

go程序中使用pprof增加程序监控功能

1.什么是 pprof&#xff1f; pprof 是 Go 内置的性能分析工具&#xff0c;用于生成程序运行时的性能分析数据。它可以帮助开发者分析&#xff1a; CPU 使用情况 内存分配情况 Goroutine 状况 系统调用分析等 2. 如何使用 pprof&#xff1f; 要使用 pprof&#xff0c;首先需要在…

javaweb自用笔记:Vue

Vue 什么是vue vue案例 1、引入vue.js文件 2、定义vue对象 3、定义vue接管的区域el 4、定义数据模型data 5、定义视图div 6、通过标签v-model来绑定数据模型 7、{{message}}直接将数据模型message展示出来 8、由于vue的双向数据绑定&#xff0c;当视图层标签input里的…

376_C++_云透传,板端负责处理透传数据的API函数,用于实现客户端对设备内部接口的访问(VMS把数据直接传给板端内部)

RsApi_PassThrough 云透传,板端负责处理透传数据的API函数,用于实现客户端对设备内部接口的访问(VMS把数据直接传给板端内部) 我来分析一下 RsApi_PassThrough 函数的作用和实现逻辑: 1. 功能概述 RsApi_PassThrough 是一个透传接口,用于处理 /API/Http/PassThrough 的…

基于eRDMA实测DeepSeek开源的3FS

DeepSeek昨天开源了3FS分布式文件系统, 通过180个存储节点提供了 6.6TiB/s的存储性能, 全面支持大模型的训练和推理的KVCache转存以及向量数据库等能力, 每个客户端节点支持40GB/s峰值吞吐用于KVCache查找. 发布后, 我们在阿里云ECS上进行了快速的复现, 并进行了性能测试, ECS…

计算机毕业设计SpringBoot+Vue.js医院挂号就诊系统(源码+文档+PPT+讲解)

温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 作者简介&#xff1a;Java领…

Linux的用户与权限--第二天

认知root用户&#xff08;超级管理员&#xff09; root用户用于最大的系统操作权限 普通用户的权限&#xff0c;一般在HOME目录内部不受限制 su与exit命令 su命令&#xff1a; su [-] 用户名 -符号是可选的&#xff0c;表示切换用户后加载环境变量 参数为用户名&#xff0c…

计算机网络软考

1.物理层 1.两个主机之间发送数据的过程 自上而下的封装数据&#xff0c;自下而上的解封装数据&#xff0c;实现数据的传输 2.数据、信号、码元 码元就是数字通信里用来表示信息的基本信号单元。比如在二进制中&#xff0c;用高电平代表 “1”、低电平代表 “0”&#xff0c…

第四十一:Axios 模型的 get ,post请求

Axios 的 get 请求方式 9.双向数据绑定 v-model - 邓瑞编程 Axios 的 post 请求方式&#xff1a;

【JQuery—前端快速入门】JQuery 操作元素

JQuery 操作元素 1. 获取/修改元素内容 三个简单的获取元素的方法&#xff1a; 这三个方法即可以获取元素的内容&#xff0c;又可以设置元素的内容. 有参数时&#xff0c;就进行元素的值设置&#xff0c;没有参数时&#xff0c;就进行元素内容的获取. 接下来&#xff0c;我们需…

2025年4月1日-2日AutoCable 中国汽车线束线缆及连接技术创新峰会即将开幕

正如人体的心脏与四肢之间需要靠神经和血管连接&#xff0c;汽车的各个部件&#xff0c;也要靠各种电线、管道连接。线束&#xff0c;就是汽车的神经和血管&#xff0c;车主向汽车下达的每一个功能指令&#xff0c;都通过线束来传递&#xff0c;看似不起眼的线束&#xff0c;却…

编程题 - 汽水瓶【JavaScript/Node.js解法】

目录 题目描述 解题思路 代码实现 复杂度分析 代码解释 输入输出处理 题目描述 有这样一道经典的编程题&#xff1a;某商店规定&#xff1a;三个空汽水瓶可以换一瓶汽水。小张手上有 n 个空汽水瓶&#xff0c;他最多可以换多少瓶汽水喝&#xff1f; 解题思路 这是一个…

深度学习神经网络分类原理

每一个神经元做的是一个类似回归的操作 最后一层是softmax函数&#xff0c;每一个输出就会变成一个0到1之间的数&#xff0c;也就是概率&#xff0c;然后他们之间的和加起来等于1&#xff0c;到底是哪一个分类就是看哪个神经元的这个值最大。 那么如何算损失呢&#xff1a; 加…

硬核技术组合!用 DeepSeek R1、Ollama、Docker、RAGFlow 打造专属本地知识库

文章目录 一、引言二、安装Ollama部署DeepSeekR1三、安装Docker四、安装使用RAGFlow4.1 系统架构4.2 部署流程4.3 使用RAGFlow4.4 在RAGFlow中新增模型4.5 创建知识库4.6 创建私人助理使用RGA 一、引言 本地部署DeepSeek R1 Ollama RAGFlow构建个人知识库&#xff0c;通过将…