你用 Java 的 Socket 连接服务器,一行 new Socket(host, port) 就搞定了。
但在这行代码背后,操作系统内核为你悄悄完成了三次握手:SYN、SYN+ACK、ACK。
当你关闭连接时,又是四次挥手:FIN、ACK、FIN、ACK。
每一个状态转变,都对应着你代码里的 connect()accept()close()
而 TIME_WAIT 状态,更是让无数后端工程师抓狂的“端口被占用”元凶。

大家好,我是 Evan,一个用 netstat -an | grep TIME_WAIT 排查过线上服务端口耗尽的 Java+AI 学生。
今天,我从 TCP 的三次握手四次挥手讲起,带你看看 Java 的 SocketServerSocket 和 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.3 connect() 超时与 accept() 阻塞

Socket socket = new Socket();
socket.connect(new InetSocketAddress("host", 8080), 2000); // 超时 2 秒

如果在规定时间内没完成握手,抛出 SocketTimeoutException
而 ServerSocket.accept() 会一直阻塞,直到有客户端完成三次握手。

二、四次挥手:优雅地告别

2.1 为什么需要四次?因为 TCP 是全双工的

每一方都需要单独关闭自己的发送通道。
主动关闭方发送 FIN,被动方回复 ACK,然后被动方也发送 FIN,主动方回复 ACK

2.2 close() 与 TIME_WAIT

主动关闭方(谁先调用 close())会进入 TIME_WAIT 状态,持续 2 倍 MSL(Maximum Segment Lifetime,通常 30 秒到 2 分钟)。
为什么要有 TIME_WAIT

  • 确保最后一个 ACK 能被对方收到(如果对方没收到会重发 FIN)。

  • 让网络上残留的旧数据包消失,避免影响新连接。

问题:高并发短连接场景(如压测),大量 TIME_WAIT 会占用本地端口,导致 Address already in use

Java 中的对应

socket.close();  // 触发 TCP 四次挥手的主动关闭

2.3 SO_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_WAIT"
  • ESTABLISHED:正常通信的连接。

  • TIME_WAIT:主动关闭方等待 2MSL,过多会影响端口。

  • CLOSE_WAIT:被动关闭方收到 FIN 后未调用 close()代码 bug 常见:服务端忘记关闭连接)。

CLOSE_WAIT 泄漏:如果你的 Java 服务不断出现 CLOSE_WAIT,说明你没有在 finally 块里关闭 socket 或 channel。

3.2 调整系统参数缓解 TIME_WAIT

Linux 下可以修改:

# 开启快速回收(不推荐新内核)
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 0   # 已废弃
# 减少 TIME_WAIT 持续时间(不建议低于 30 秒)
net.ipv4.tcp_fin_timeout = 30

更推荐:使用连接池(如 HikariCP、HttpClient 连接池)复用连接,避免频繁创建和关闭。

3.3 ServerSocket.accept() 与多线程

每个新连接 accept() 后通常交给线程池处理,别忘了在 finally 中关闭 Socket

Socket 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.3 connect 超时 vs read 超时

  • connect 超时:三次握手未完成。

  • read 超时:连接已建立但对方迟迟不发送数据(setSoTimeout)。

4.4 Socket 的 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?”

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐