👋 TCP四次挥手:客户端和服务器的"告别仪式"

63 阅读4分钟

知识点编号:011
难度等级:⭐⭐(必掌握)
面试频率:🔥🔥🔥🔥🔥


🎯 一句话总结

TCP四次挥手就像打电话结束前的礼貌告别,双方都要确认挂断!


🎭 四次挥手的四个步骤

第一步:FIN(我要挂了)

客户端发送:FIN=1, seq=u "我没话说了,要关闭连接了"

第二步:ACK(知道了,等等)

服务器回复:ACK=1, ack=u+1 "我知道了,但我还有话要说"

第三步:FIN(我也要挂了)

服务器发送:FIN=1, ACK=1, seq=w, ack=u+1 "我也说完了,可以关闭了"

第四步:ACK(好的,拜拜)

客户端确认:ACK=1, seq=u+1, ack=w+1 "好的,拜拜"


📊 图解

客户端                            服务器
   |                                |
   |  ①FIN seq=100                  |
   |------------------------------->|
   | (FIN_WAIT_1)                   | (CLOSE_WAIT)
   |                                |
   |  ②ACK ack=101                  |
   |<-------------------------------|
   | (FIN_WAIT_2)                   |
   |                                |
   |  ... 服务器继续发送数据 ...     |
   |<-------------------------------|
   |                                |
   |  ③FIN seq=200,ack=101          |
   |<-------------------------------|
   | (TIME_WAIT)                    | (LAST_ACK)
   |                                |
   |  ④ACK seq=101,ack=201          |
   |------------------------------->|
   |                                | (CLOSED)
   | (等待2MSL...)                  |
   |                                |
   | (CLOSED)                       |

💡 为什么是四次?三次不行吗?

原因:TCP是全双工通信!

三次握手时:双方都还没开始发数据,可以同时准备
  第二次握手合并了:SYN(我也要连接)+ ACK(收到你的SYN)

四次挥手时:一方想关闭,但另一方可能还有数据要发
  第二次挥手:ACK(我知道了)
  ... 继续发送未完成的数据 ...
  第三次挥手:FIN(我的数据发完了)
  
不能合并第二、三次挥手!

生活比喻

打电话结束:
你:"我要挂了"(第一次)
朋友:"等等!我还有话说!"(第二次)
朋友:"好了,我说完了,挂吧"(第三次)
你:"好的,拜拜"(第四次)

如果合并第二、三次:
你:"我要挂了"
朋友:"好,我也挂了"
你心想:"我话还没说完呢!"😱

⏰ TIME_WAIT状态:最关键的状态

什么是TIME_WAIT?

主动关闭方在发送最后的ACK后,进入TIME_WAIT状态,等待2MSL(1-4分钟)。

为什么要等2MSL?

原因1:确保最后的ACK被收到

如果最后的ACK丢失:
- 服务器会重传FIN
- 客户端要能接收到并重新回复ACK
- 需要等待足够的时间确保网络中的数据包都消失

原因2:防止旧连接的数据包干扰新连接

如果不等待:
- 旧连接的延迟数据包可能还在网络中
- 新连接可能使用相同的端口
- 旧数据包会被误认为是新连接的数据 ❌

等待2MSL后:
- 旧数据包肯定已经消失 ✅

💔 CLOSE_WAIT状态:开发者的噩梦

什么是CLOSE_WAIT?

被动关闭方收到FIN后,发送ACK,进入CLOSE_WAIT状态。

CLOSE_WAIT过多的原因

❌ 根本原因:应用程序没有调用socket.close()!

常见错误代码:
Socket socket = new Socket("localhost", 8080);
// ... 使用socket ...
// 忘记调用socket.close()了!

客户端关闭连接(发送FIN)
服务器收到FIN,进入CLOSE_WAIT
但是应用程序没有关闭socket
导致一直停留在CLOSE_WAIT状态!😱

解决方案

// ✅ 正确做法:使用try-with-resources
try (Socket socket = new Socket("localhost", 8080)) {
    // 使用socket
} // 自动调用close()

// ✅ 或者使用finally
Socket socket = null;
try {
    socket = new Socket("localhost", 8080);
    // 使用socket
} finally {
    if (socket != null) {
        socket.close();
    }
}

🐛 常见面试题

Q1:为什么建立连接是三次握手,关闭连接是四次挥手?

答案:

建立连接时:双方都没有数据要发
- 服务器可以同时发送SYN和ACK(第二次握手)
- 所以只需要3次

关闭连接时:一方想关闭,但另一方可能还有数据要发
- 第二次挥手:ACK(知道了)
- ... 发送剩余数据 ...
- 第三次挥手:FIN(发完了)
- 不能合并,所以需要4次

Q2:TIME_WAIT过多怎么办?

答案:

原因:短连接太多,每个连接关闭后都要等待1-4分钟

解决方案:
1. ✅ 使用长连接(HTTP Keep-Alive)
2. ✅ 使用连接池
3. ✅ 让服务器主动关闭(服务器端口固定,不怕占用)
4. ⚠️ 调整系统参数(谨慎使用):
   net.ipv4.tcp_tw_reuse = 1
   net.ipv4.tcp_tw_recycle = 1(NAT环境慎用!)

Q3:CLOSE_WAIT过多怎么办?

答案:

原因:应用程序没有正确关闭socket

排查步骤:
1. 查看CLOSE_WAIT数量:
   netstat -an | grep CLOSE_WAIT | wc -l
   
2. 检查代码:
   - 是否忘记调用socket.close()
   - 异常处理是否正确
   - 是否使用了try-with-resources

3. 修复代码:
   确保在finally块或try-with-resources中关闭socket

🎓 总结

四次挥手是TCP连接终止的过程,记住:

  1. 第一次:FIN(客户端→服务器)"我要关闭了"
  2. 第二次:ACK(服务器→客户端)"知道了"
  3. 第三次:FIN(服务器→客户端)"我也要关闭了"
  4. 第四次:ACK(客户端→服务器)"好的"

关键状态

  • TIME_WAIT:主动关闭方,等待2MSL
  • CLOSE_WAIT:被动关闭方,应用程序应该关闭socket

生活比喻

打电话结束:
你:"我要挂了"
朋友:"知道了,等等"
朋友:"好,我也挂了"
你:"拜拜"

文档创建时间:2025-10-31