客户端连接已建立,但服务端却说没连上?一次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。
复现步骤
- 开启TunnelServer
- 开启TunnelClient
- 开启自定义BackendServer。简单实现一个长连接TcpServer,连接进来之后,返回一段字符串即可。
BackendServer源码如下
1 | import io.vertx.core.Vertx; |
客户端与TunnelServer建立2000个长连接,会出现一种情况:客户端显示连接已经处于ESTABLISHED,但是服务端并没有该连接。
那简单,tcp抓包。发现客户端和服务端的三次握手均成功了。
那么,就得去了解三次握手建立连接的这个过程了。
对于客户端来说,当三次握手后,连接就会进入ESTABLISHED。
对于服务端来说,当三次握手后,操作系统内核会将连接放入到全连接队列AcceptQueue,此时连接会进入ESTABLISHED。若放入失败,则丢弃该连接或者发送RST。如果accept()
过慢、连接建立过快。就会出现连接丢失的问题。而客户端的连接却仍然处于ESTABLISHED。
解决办法如下
- 优化程序,提升accept()速度。
- 调大全连接队列AcceptQueue的参数值。临时立即生效
sysctl -w net.core.somaxconn=5000
,重启会失效。不过切记在Linux中,全连接队列的大小为min(backlog,net.core.somaxconn)
- 设置若超过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 |
|
客户端10.0.0.20
代码
batch_telnet.sh
1 |
|
telnet_keepalive.expect
1 | #!/usr/bin/env expect |
服务端启动服务,客户端执行./batch_telnet.sh
上图,就出现了“客户端连接已建立,但服务端却说没连上。”
我们通过ss -nplt
命令查看,可知全连接队列数5,等待accpet()的连接数6。这个命令的使用可以参考TCP与UDP的端口连接 - 言成言成啊
已经超过了允许的最大全连接队列容量,因此后续的连接内核直接丢弃了,这就出现服务端不存在这个连接的现象。