三次握手,四次挥手:你的 connect() 和 close() 在 TCP 栈里经历了什么?
你用 Java 的Socket连接服务器一行new Socket(host, port)就搞定了。但在这行代码背后操作系统内核为你悄悄完成了三次握手SYN、SYNACK、ACK。当你关闭连接时又是四次挥手FIN、ACK、FIN、ACK。每一个状态转变都对应着你代码里的connect()、accept()、close()。而TIME_WAIT状态更是让无数后端工程师抓狂的“端口被占用”元凶。大家好我是Evan一个用netstat -an | grep TIME_WAIT排查过线上服务端口耗尽的 JavaAI 学生。今天我从 TCP 的三次握手和四次挥手讲起带你看看 Java 的Socket、ServerSocket和close()背后到底发生了什么。读完这篇你不仅能画出 TCP 状态机还能理解为什么高并发下会出现Address already in use。 写在前面大二学计网我死记硬背了三次握手和四次挥手的图但总觉得那是路由器和交换机的事跟我的 Java 代码无关。直到我在知识汇教育平台做一个长连接服务压测时频繁报BindException: Address already in use查了半天发现是TIME_WAIT堆积。那一刻我才明白每一次你调用close()内核都帮你维护了一个定时器。这篇博客我就用 Java 开发者的语言把 TCP 握手挥手的状态机讲透。一、三次握手建立连接的“礼尚往来”1.1 为什么需要三次两次不行吗三次握手的核心目的让双方确认自己的发送和接收能力正常并且协商初始序列号ISN。两次握手无法防止已过期的连接请求突然到达造成的混乱。1.2 握手过程与状态转移Java 视角new Socket(localhost, 8080)会阻塞直到三次握手完成或超时。ServerSocket.accept()在三次握手完成后才会返回一个新的Socket对象。1.3connect()超时与accept()阻塞Socket socket new Socket(); socket.connect(new InetSocketAddress(host, 8080), 2000); // 超时 2 秒如果在规定时间内没完成握手抛出SocketTimeoutException。而ServerSocket.accept()会一直阻塞直到有客户端完成三次握手。二、四次挥手优雅地告别2.1 为什么需要四次因为 TCP 是全双工的每一方都需要单独关闭自己的发送通道。主动关闭方发送FIN被动方回复ACK然后被动方也发送FIN主动方回复ACK。2.2close()与TIME_WAIT主动关闭方谁先调用close()会进入TIME_WAIT状态持续2 倍 MSLMaximum Segment Lifetime通常 30 秒到 2 分钟。为什么要有TIME_WAIT确保最后一个ACK能被对方收到如果对方没收到会重发FIN。让网络上残留的旧数据包消失避免影响新连接。问题高并发短连接场景如压测大量TIME_WAIT会占用本地端口导致Address already in use。Java 中的对应socket.close(); // 触发 TCP 四次挥手的主动关闭2.3SO_REUSEADDR选项可以通过设置 socket 选项允许重用TIME_WAIT状态的端口ServerSocket server new ServerSocket(); server.setReuseAddress(true); server.bind(new InetSocketAddress(port));但要注意风险可能收到老连接的残留数据。三、常见开发场景与排查命令3.1 查看 TCP 连接状态netstat -an | grep -E ESTABLISHED|TIME_WAIT|CLOSE_WAITESTABLISHED正常通信的连接。TIME_WAIT主动关闭方等待 2MSL过多会影响端口。CLOSE_WAIT被动关闭方收到 FIN 后未调用close()代码 bug 常见服务端忘记关闭连接。CLOSE_WAIT 泄漏如果你的 Java 服务不断出现CLOSE_WAIT说明你没有在 finally 块里关闭 socket 或 channel。3.2 调整系统参数缓解TIME_WAITLinux 下可以修改# 开启快速回收不推荐新内核 net.ipv4.tcp_tw_reuse 1 net.ipv4.tcp_tw_recycle 0 # 已废弃 # 减少 TIME_WAIT 持续时间不建议低于 30 秒 net.ipv4.tcp_fin_timeout 30更推荐使用连接池如 HikariCP、HttpClient 连接池复用连接避免频繁创建和关闭。3.3ServerSocket.accept()与多线程每个新连接accept()后通常交给线程池处理别忘了在 finally 中关闭SocketSocket client server.accept(); executor.submit(() - { try { // 处理请求 } finally { client.close(); // 触发四次挥手 } });四、常见问题与陷阱4.1 服务端大量CLOSE_WAIT原因服务端收到客户端 FIN 后没有调用close()。典型错误线程池处理请求时只关闭了输入输出流但没关闭 socket 本身。解决使用 try-with-resources 或确保 finally 中关闭 socket。4.2 客户端大量TIME_WAIT原因短连接场景客户端主动关闭。解决改用长连接HTTP Keep-Alive或连接池。4.3connect超时 vsread超时connect超时三次握手未完成。read超时连接已建立但对方迟迟不发送数据setSoTimeout。4.4Socket的close()与shutdownOutput()close()完全关闭发送 FIN。shutdownOutput()半关闭只关闭输出方向仍可读。常用于通知对端“我不再写数据”。 总结核心结论TCP 握手/挥手是可靠传输的基础每个状态都对应内核行为。Java 开发者要特别注意CLOSE_WAIT忘记关闭和TIME_WAIT短连接过多。用连接池、复用连接、合理设置SO_REUSEADDR可以缓解端口耗尽问题。思考题你写了一个 HTTP 服务器用ServerSocket.accept()接收请求每次处理完请求后调用socket.close()。压测 10000 个短连接后发现客户端报BindException: Address already in use。你用netstat -an | grep TIME_WAIT看到大量TIME_WAIT连接都是客户端端口。问题为什么客户端会大量TIME_WAIT如何在不修改内核参数的情况下减少这种现象欢迎在评论区留下你的方案 —— 下一篇我会聊聊“UDP 的无连接特性DNS 查询为什么喜欢用 UDP”