言成言成啊 | Kit Chen's Blog

客户端连接已建立,但服务端却说没连上?一次TCP三次握手背后的真相

发布于2025-05-31 00:18:09,更新于2025-07-14 01:12:33,标签:java devops tcp  文章会持续修订,转载请注明来源地址:https://meethigher.top/blog

一、原因定位

该问题是偶然发现的,起源于内网穿透在高并发时,DataProxyServer返回给User数据时,偶现数据丢失。 · Issue #8 · meethigher/tcp-reverse-proxy

复现步骤

  1. 开启TunnelServer
  2. 开启TunnelClient
  3. 开启自定义BackendServer。简单实现一个长连接TcpServer,连接进来之后,返回一段字符串即可。

BackendServer源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.net.NetServer;

public class BackendServer {
public static void main(String[] args) {
Vertx vertx = Vertx.vertx();
NetServer netServer = vertx.createNetServer();
netServer.connectHandler(socket -> {
socket.remoteAddress();
socket.write(Buffer.buffer("SSH-2.0-OpenSSH_8.7")).onComplete(ar -> {
System.out.println(socket.remoteAddress().toString() + " write " + ar.succeeded());
});
}).listen(23).onComplete(ar -> {
if (ar.succeeded()) {
System.out.println("Server started on port " + ar.result().actualPort());
} else {
System.err.println("Server failed to start");
ar.cause().printStackTrace();
System.exit(1);
}
});
}
}

客户端与TunnelServer建立2000个长连接,会出现一种情况:客户端显示连接已经处于ESTABLISHED,但是服务端并没有该连接。

那简单,tcp抓包。发现客户端和服务端的三次握手均成功了。

那么,就得去了解三次握手建立连接的这个过程了。

对于客户端来说,当三次握手后,连接就会进入ESTABLISHED。

对于服务端来说,当三次握手后,操作系统内核会将连接放入到全连接队列AcceptQueue,此时连接会进入ESTABLISHED。若放入失败,则丢弃该连接或者发送RST。如果accept()过慢、连接建立过快。就会出现连接丢失的问题。而客户端的连接却仍然处于ESTABLISHED。

解决办法如下

  1. 优化程序,提升accept()速度。
  2. 调大全连接队列AcceptQueue的参数值。临时立即生效sysctl -w net.core.somaxconn=5000,重启会失效。不过切记在Linux中,全连接队列的大小为min(backlog,net.core.somaxconn)
  3. 设置若超过AcceptQueue最大值,则向客户端发送RST中止连接。临时立即生效sysctl -w net.ipv4.tcp_abort_on_overflow=1,重启会失效。

但是第三个方法有点鸡肋,即使AcceptQueue满了,也不一定会触发。因此保险起见,还是调大AcceptQueue比较好。

二、简单复现

上面的复现步骤较为麻烦,我这边直接写一个更简单的示例。

源码meethigher/bug-test at close-wait-server

服务端10.0.0.10代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.util.concurrent.locks.LockSupport;

public class AcceptQueueTest {
public static void main(String[] args) throws Exception {
/**
* Windows与Linux的机制不同,使用Linux进行复现
*/
ServerSocket serverSocket = new ServerSocket();
// net.core.somaxconn=128,这时候全连接队列为min(5,net.core.somaxconn)=5,也就是说,同时只允许5个连接ESTAB
// 实际由于Linux的延迟判定,可能会多于5个
serverSocket.bind(new InetSocketAddress("0.0.0.0", 6666), 5);


LockSupport.park();
}
}

客户端10.0.0.20代码

batch_telnet.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/env bash

# 连接数
connNum=20

# host:port 列表
target="10.0.0.10:6666"

for ((i = 0; i < $connNum; i++)); do
host="${target%%:*}"
port="${target##*:}"
echo "telnet $host $port..."
nohup ./telnet_keepalive.expect "$host" "$port" >/dev/null 2>&1 &
echo "$!"
done


echo "all started"

telnet_keepalive.expect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/env expect

# 获取命令行参数
set timeout -1
set host [lindex $argv 0]
set port [lindex $argv 1]

# 开始 telnet 连接
spawn telnet $host $port

# 保持连接:每隔一段时间发送一个空命令或心跳
while {1} {
sleep 30
# send "\r"
}

服务端启动服务,客户端执行./batch_telnet.sh

上图,就出现了“客户端连接已建立,但服务端却说没连上。”

我们通过ss -nplt命令查看,可知全连接队列数5,等待accpet()的连接数6。这个命令的使用可以参考TCP与UDP的端口连接 - 言成言成啊

已经超过了允许的最大全连接队列容量,因此后续的连接内核直接丢弃了,这就出现服务端不存在这个连接的现象。

发布:2025-05-31 00:18:09
修改:2025-07-14 01:12:33
链接:https://meethigher.top/blog/2025/tcp-established-but-not-visible/
标签:java devops tcp 
付款码 打赏 分享
Shift+Ctrl+1 可控制工具栏