【Java 开发日记】TCP 三次握手与四次挥手详解
在 Java 开发中,我们经常使用 Socket、Netty、Spring WebFlux、Dubbo、Kafka、Redis 等网络通信框架,这些底层都依赖 TCP 协议。所以理解 TCP 的三次握手和四次挥手 是网络编程的基础,也是面试中出现频率极高的知识点。
下面用最清晰的方式,把过程、状态、目的、常见问题全部讲透。
1. TCP 三次握手(建立连接)
目的:
双方互相确认对方的发送能力和接收能力都正常,确保连接是可靠的、双向的。
三次握手的本质:
- 确认双方的初始序列号(ISN)
- 确认双方的接收窗口大小(可选)
- 确认对方处于可接收数据的状态
流程图解(经典三步):
客户端 服务端
| |
| SYN (seq=x) | ← 第一次握手
|───────────────────►|
| |
| SYN+ACK (seq=y, ack=x+1) ← 第二次握手
|◄───────────────────|
| |
| ACK (ack=y+1) | ← 第三次握手
|───────────────────►|
| |
▼ ▼
已建立连接 已建立连接
每一步详细说明:
- 第一次握手(客户端 → 服务端)
客户端发送 SYN 包(SYN=1),同时附带一个初始序列号 seq = x
状态变化:客户端进入 SYN_SENT 状态 - 第二次握手(服务端 → 客户端)
服务端收到 SYN 后,回复 SYN + ACK 包
- SYN=1(我也想建立连接)
- ACK=1(确认收到你的 SYN)
- ack = x + 1(确认收到你发的 seq=x)
- seq = y(服务端自己的初始序列号)
状态变化:服务端进入 SYN_RCVD 状态
- 第三次握手(客户端 → 服务端)
客户端收到 SYN+ACK 后,回复 ACK 包
- ACK=1
- ack = y + 1
- seq = x + 1
状态变化:客户端进入 ESTABLISHED 状态
服务端收到 ACK 后也进入 ESTABLISHED 状态
为什么是三次而不是两次?
如果只有两次:
- 客户端发 SYN → 服务端回 SYN+ACK
- 如果第二次的 SYN+ACK 丢包了,客户端会重传 SYN,但服务端已经认为连接建立了(浪费资源)
- 三次握手可以让服务端确认客户端的接收能力正常
常见面试追问:
- 为什么客户端最后还要发一次 ACK?
→ 为了让服务端确认客户端确实收到了服务端的 SYN(防止已失效的连接请求)
2. TCP 四次挥手(断开连接)
目的:
双方都确认对方不再发送数据,安全关闭连接,释放资源。
四次挥手流程(经典四步):
主动关闭方(通常客户端) 被动关闭方(通常服务端)
| |
| FIN (seq=u) | ← 第一次挥手
|─────────────────────────►|
| |
| ACK (ack=u+1) | ← 第二次挥手
|◄─────────────────────────|
| |
| | (服务端可能还有数据要发)
| |
| FIN (seq=v) | ← 第三次挥手
|◄─────────────────────────|
| |
| ACK (ack=v+1) | ← 第四次挥手
|─────────────────────────►|
| |
▼ ▼
TIME_WAIT CLOSED
每一步详细说明:
- 第一次挥手(主动方 → 被动方)
主动方发送 FIN(FIN=1,seq=u),表示“我没有数据要发了”
主动方进入 FIN_WAIT_1 状态 - 第二次挥手(被动方 → 主动方)
被动方回复 ACK(ack = u+1),表示“我收到你的 FIN 了”
被动方进入 CLOSE_WAIT 状态
主动方收到 ACK 后进入 FIN_WAIT_2 状态
→ 此时连接处于半关闭状态(被动方还能继续发数据) - 第三次挥手(被动方 → 主动方)
被动方发完剩余数据后,发送 FIN(seq=v),表示“我也没数据要发了”
被动方进入 LAST_ACK 状态 - 第四次挥手(主动方 → 被动方)
主动方回复 ACK(ack = v+1),表示“我收到你的 FIN 了”
主动方进入 TIME_WAIT 状态
被动方收到 ACK 后直接进入 CLOSED 状态
为什么是四次而不是三次?
因为 TCP 是全双工通信,双方都可能有数据要发。
当一方说“我发完了”(FIN)后,另一方可能还有数据没发完,所以需要先回复 ACK(确认收到 FIN),等自己数据发完后再发 FIN。
3. 重要状态与时间
| 状态 | 含义 | 常见持续时间 / 注意事项 |
|---|---|---|
| SYN_SENT | 已发送 SYN,正在等待 SYN-ACK | — |
| SYN_RCVD | 已收到 SYN,已发送 SYN-ACK | — |
| ESTABLISHED | 连接正常建立,可传输数据 | — |
| FIN_WAIT_1 | 已发送 FIN,等待对方 ACK | — |
| FIN_WAIT_2 | 已收到对方 ACK,等待对方 FIN | — |
| CLOSE_WAIT | 收到 FIN,已发送 ACK(半关闭) | 应用程序未关闭 socket 时会长时间停留 |
| LAST_ACK | 已发送 FIN,等待最后 ACK | — |
| TIME_WAIT | 收到最后 ACK,等待 2MSL | 最重要,持续时间通常 30s–4min(视系统) |
| CLOSED | 连接完全关闭 | — |
TIME_WAIT 存在的两大原因(面试必问):
- 确保最后一次 ACK 能到达对方(防止被动方没收到 ACK 而一直重发 FIN)
- 防止旧连接的延迟包干扰新连接(2MSL 时间内同一端口的旧包都会失效)
4. Java 开发中常见相关问题
- TIME_WAIT 过多 → 导致端口耗尽
解决:调大net.ipv4.ip_local_port_range、开启tcp_tw_reuse/tcp_tw_recycle(慎用)、使用连接池 - CLOSE_WAIT 堆积 → 说明服务端应用层没有及时 close socket
常见于:读取到 -1(EOF)后没有关闭流、异常处理不当 - Netty / Spring 中长连接管理 → 心跳 + 空闲检测 + 主动关闭
小结口诀
三次握手:我要、你也要、我确认你也要
四次挥手:我好了、知道了(你继续发)、我也好了、知道了
希望这篇把 TCP 连接建立与断开的本质讲清楚了。
如果你想继续深入某个点,比如:
- TIME_WAIT 过多如何解决(参数调优)
- 半连接队列 / 全连接队列 / backlog
- TCP 粘包/拆包问题(Netty 如何解决)
- TCP 与 UDP 对比
- 抓包实战分析三次握手
随时告诉我,我继续展开。