知识点编号:002
难度等级:⭐⭐⭐(必须掌握)
面试频率:🔥🔥🔥🔥🔥(超高频!)
🎯 一句话总结
TCP四次挥手就像情侣分手,需要双方都确认清楚,避免还有未完成的事情!
🤔 什么是TCP四次挥手?
如果说三次握手是谈恋爱,那四次挥手就是分手。但分手可不是一句"我们分手吧"就能解决的,必须确保双方都把话说完,没有遗留问题!
📖 正经版定义
TCP四次挥手(Four-Way Handshake)是TCP连接终止的过程。由于TCP是全双工通信(双方都可以同时收发数据),所以需要双方分别关闭各自的发送通道,总共需要四次挥手。
🎭 四次挥手的完整过程
第一次挥手:主动提分手 💔
客户端 → 服务器
客户端:"我没什么要说的了,我们分手吧。"
(发送 FIN=1, seq=u 的数据包)
- FIN(Finish):结束标志位,表示"我要关闭连接了"
- seq=u:序列号
生活例子:
就像你对对象说:"我觉得我们不合适,分手吧。" 😢
此时客户端进入: FIN_WAIT_1 状态(发起分手请求,等待对方回应)
注意:客户端不再发送数据,但还可以接收数据!
第二次挥手:知道了,但我还有话说 💬
服务器 → 客户端
服务器:"我知道了,但我还有些话想说完。"
(发送 ACK=1, seq=v, ack=u+1 的数据包)
- ACK=1:确认标志位
- seq=v:服务器的序列号
- ack=u+1:确认号
生活例子:
对方说:"好吧,我知道了。但是我还有些东西要跟你说清楚..." 😔
此时服务器进入: CLOSE_WAIT 状态(知道对方要分手,但还有事要处理)
此时客户端进入: FIN_WAIT_2 状态(对方已经知道我要分手,等待对方也说分手)
关键:此时服务器还可以继续发送数据!客户端还要继续接收!
第三次挥手:我也没话说了 💔
服务器 → 客户端
服务器:"好了,我也说完了,我们分手吧。"
(发送 FIN=1, ACK=1, seq=w, ack=u+1 的数据包)
- FIN=1:服务器也要关闭连接
- ACK=1:确认
- seq=w:服务器的序列号
- ack=u+1:确认号
生活例子:
对方说:"好吧,我也想通了,我们真的分手吧。" 😭
此时服务器进入: LAST_ACK 状态(发送了分手请求,等待最后的确认)
此时客户端进入: TIME_WAIT 状态(收到了对方的分手请求,但还要等一会儿)
第四次挥手:最后的告别 👋
客户端 → 服务器
客户端:"好的,拜拜了。"
(发送 ACK=1, seq=u+1, ack=w+1 的数据包)
- ACK=1:确认
- seq=u+1:序列号
- ack=w+1:确认号
生活例子:
你说:"嗯,拜拜。" 😥
此时服务器进入: CLOSED 状态(彻底关闭)
此时客户端进入: TIME_WAIT 状态(还要等待2MSL时间)
等待2MSL后,客户端才进入: CLOSED 状态(彻底关闭)
🎨 图解四次挥手
客户端 服务器
| |
| ① FIN=1, seq=100 |
| "我要关闭连接了" |
|--------------------------------------->|
| (FIN_WAIT_1) | (CLOSE_WAIT)
| |
| ② ACK=1, seq=200, ack=101 |
| "知道了,等我说完" |
|<---------------------------------------|
| (FIN_WAIT_2) |
| |
| ... 服务器继续发送数据 ... |
|<---------------------------------------|
| |
| ③ FIN=1, ACK=1, seq=250, ack=101 |
| "我也说完了,分手吧" |
|<---------------------------------------|
| (TIME_WAIT) | (LAST_ACK)
| |
| ④ ACK=1, seq=101, ack=251 |
| "拜拜" |
|--------------------------------------->|
| | (CLOSED)
| (等待 2MSL...) |
| |
| (CLOSED) |
| |
💡 为什么要四次挥手?三次不行吗?
🤨 为什么不能像握手一样,把第二和第三次合并?
原因:TCP是全双工通信!
握手时(建立连接):
- 双方都还没开始传输数据
- 可以同时准备好
- 所以第二次握手可以把 SYN 和 ACK 一起发
挥手时(断开连接):
- 一方想关闭,但另一方可能还有数据要发!
- 需要等数据发完才能关闭
- 所以必须分两步:
1. 先回复 ACK(知道了)
2. 等数据发完再发 FIN(我也要关了)
🎭 生活场景比喻
打电话的例子:
你:"我要挂了啊。"(第一次挥手)
对方:"等等!我还有话要说!"(第二次挥手)
对方:"巴拉巴拉...说完了,我也挂了。"(第三次挥手)
你:"好的,拜拜。"(第四次挥手)
如果合并第二、三次挥手:
你:"我要挂了啊。"
对方:"好,我也挂了!"
你心想:"卧槽,我还没说完呢!" 😱
🔍 关键状态详解
客户端状态变化
- ESTABLISHED:正常连接状态
- FIN_WAIT_1:发送FIN后,等待ACK
- FIN_WAIT_2:收到ACK,等待对方的FIN
- TIME_WAIT:收到对方的FIN,发送最后的ACK,等待2MSL
- CLOSED:连接彻底关闭
服务器状态变化
- ESTABLISHED:正常连接状态
- CLOSE_WAIT:收到FIN,发送ACK,应用层还没关闭
- LAST_ACK:应用层关闭,发送FIN,等待最后的ACK
- CLOSED:收到ACK,连接彻底关闭
⏰ TIME_WAIT 状态:最麻烦的状态!
什么是TIME_WAIT?
主动关闭连接的一方,在收到对方的FIN并发送最后的ACK后,会进入TIME_WAIT状态,等待2MSL(Maximum Segment Lifetime,最大报文生存时间)。
MSL:一个数据包在网络中的最大生存时间,通常为30秒、1分钟或2分钟。
2MSL:通常是1-4分钟。
为什么要等待2MSL?🤔
原因1:确保最后的ACK被收到
场景:如果最后的ACK丢失了
客户端 服务器
| |
| ④ ACK(丢失!) |
|------------------------X | (LAST_ACK)
| |
| | (超时重传FIN)
| ③ FIN(重传) |
|<-------------------------|
| |
| ④ ACK(重发) |
|------------------------->|
| | (CLOSED)
如果客户端立即关闭:
- 服务器重传的FIN无人响应
- 服务器会一直重传,浪费资源
- 可能影响后续相同端口的连接
原因2:确保旧连接的数据包消失
场景:防止旧连接的数据包干扰新连接
旧连接:客户端:8888 -> 服务器:80
- 发送了数据包A,但在网络中延迟了
旧连接关闭,新连接立即建立:
新连接:客户端:8888 -> 服务器:80(相同的四元组!)
- 这时数据包A到达了
- 服务器误以为是新连接的数据
- 数据混乱!😱
等待2MSL:
- 旧连接的所有数据包都会在这期间消失(MSL)
- 对方重传的数据包也会消失(MSL)
- 总共2MSL,确保网络中没有旧数据包
TIME_WAIT过多的问题 😱
问题:大量TIME_WAIT状态占用资源
场景:高并发Web服务器
- 客户端发起大量短连接
- 每次连接用完就关闭
- 主动关闭方会进入TIME_WAIT
- 每个TIME_WAIT要等待1-4分钟
- 大量端口被占用,无法建立新连接!
示例:
客户端发起10000个连接/秒
每个连接TIME_WAIT = 60秒
同时存在的TIME_WAIT = 10000 * 60 = 600000个!
端口总数才65535个!💥
TIME_WAIT过多的解决方案 ✅
方案1:使用长连接(HTTP Keep-Alive)
// 使用HTTP长连接,减少频繁建立/关闭连接
HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_1_1) // 支持Keep-Alive
.build();
方案2:让服务器主动关闭连接
原理:
- TIME_WAIT发生在主动关闭方
- 让服务器主动关闭,客户端被动关闭
- 服务器端口固定(如80、443),不怕占用
- 客户端端口动态分配,可以复用
实现:
- 服务器发送 Connection: close 头
- 服务器先发送FIN
方案3:调整系统参数(谨慎使用!)
# Linux系统
# 1. 启用TIME_WAIT快速回收(存在风险!)
net.ipv4.tcp_tw_recycle = 1
# 2. 允许TIME_WAIT状态的socket被重用
net.ipv4.tcp_tw_reuse = 1
# 3. 减少TIME_WAIT超时时间
net.ipv4.tcp_fin_timeout = 30
⚠️ 警告:tcp_tw_recycle 在NAT环境下可能导致问题!
建议只使用 tcp_tw_reuse
方案4:使用连接池
// 使用连接池复用连接,减少创建/销毁
HikariConfig config = new HikariConfig();
config.setMaximumPoolSize(100);
config.setMinimumIdle(10);
HikariDataSource ds = new HikariDataSource(config);
💔 CLOSE_WAIT 状态:另一个大坑!
什么是CLOSE_WAIT?
被动关闭方收到FIN后,发送ACK,进入CLOSE_WAIT状态。这时应用程序应该关闭socket,发送FIN。
CLOSE_WAIT过多的问题 😱
现象:服务器出现大量CLOSE_WAIT状态
原因:应用程序没有正确关闭socket!
场景:
客户端:"我要关闭了"(FIN)
服务器:"知道了"(ACK,进入CLOSE_WAIT)
... 时间流逝 ...
服务器应用程序:忘记调用 socket.close() 了!
服务器一直停留在CLOSE_WAIT状态!😱
CLOSE_WAIT过多的解决方案 ✅
原因1:代码没有正确关闭socket
// ❌ 错误写法
Socket socket = new Socket("localhost", 8080);
InputStream is = socket.getInputStream();
// ... 使用socket ...
// 忘记关闭了!
// ✅ 正确写法1:手动关闭
Socket socket = null;
try {
socket = new Socket("localhost", 8080);
// ... 使用socket ...
} finally {
if (socket != null) {
socket.close(); // 确保关闭
}
}
// ✅ 正确写法2:try-with-resources(推荐!)
try (Socket socket = new Socket("localhost", 8080)) {
// ... 使用socket ...
} // 自动关闭
// ✅ 正确写法3:使用Apache HttpClient等库
CloseableHttpClient httpClient = HttpClients.createDefault();
try (CloseableHttpResponse response = httpClient.execute(request)) {
// ... 处理响应 ...
} // 自动关闭
原因2:代码异常没有处理好
// ❌ 错误写法
Socket socket = new Socket("localhost", 8080);
InputStream is = socket.getInputStream();
int data = is.read(); // 可能抛出异常
// 如果异常,下面的close()不会执行!
socket.close();
// ✅ 正确写法
try (Socket socket = new Socket("localhost", 8080)) {
InputStream is = socket.getInputStream();
int data = is.read();
// 即使异常,也会自动关闭
} catch (IOException e) {
// 处理异常
}
原因3:应用程序处理太慢
// 问题:处理时间太长,客户端等不及关闭了
try (Socket socket = serverSocket.accept()) {
// 处理请求
processRequest(socket); // 这个方法执行了10分钟!
// 客户端早就发送FIN关闭了
// 服务器进入CLOSE_WAIT
// 但是代码还在执行processRequest
// 直到processRequest结束,才会调用socket.close()
}
// 解决方案:
// 1. 优化处理逻辑,减少处理时间
// 2. 使用异步处理
// 3. 设置超时时间
socket.setSoTimeout(30000); // 30秒超时
排查方法
# 1. 查看CLOSE_WAIT数量
netstat -an | grep CLOSE_WAIT | wc -l
# 2. 查看具体的CLOSE_WAIT连接
netstat -anp | grep CLOSE_WAIT
# 3. 查看进程打开的文件描述符
lsof -p <pid> | grep TCP
# 4. 使用jstack查看Java线程堆栈
jstack <pid> > thread.dump
# 5. 检查代码中是否有socket泄漏
# 搜索所有new Socket的地方,确保都有对应的close()
🐛 常见面试题
Q1:为什么TIME_WAIT状态要等待2MSL?
答案:
-
确保最后的ACK被对方收到
- 如果ACK丢失,对方会重传FIN
- 需要等待1个MSL(FIN重传)+ 1个MSL(ACK到达)
- 总共2MSL
-
确保旧连接的数据包消失
- 等待网络中所有旧数据包(1 MSL)消失
- 等待对方可能重传的数据包(1 MSL)消失
- 总共2MSL,确保新连接不会收到旧数据
-
避免端口复用时的数据混乱
- 如果新连接使用相同的端口
- 旧连接的延迟数据包可能被误认为是新连接的数据
Q2:为什么建立连接是三次握手,关闭连接是四次挥手?
答案:
建立连接(三次握手):
- 双方都没有数据要发送
- 只需要同步初始序列号
- 第二次握手可以同时发送SYN和ACK
- 所以只需要3次
第二次握手:
服务器:SYN(我也要建立连接) + ACK(确认你的请求)
关闭连接(四次挥手):
- TCP是全双工通信,双方都可以发送数据
- 一方想关闭,但另一方可能还有数据要发
- 必须等数据发完才能关闭
- 所以不能合并第二次和第三次挥手
- 需要4次
第二次挥手:ACK(知道了,等我发完数据)
... 继续发送数据 ...
第三次挥手:FIN(我的数据发完了,可以关闭了)
核心区别:关闭时可能有未发完的数据!
Q3:服务器出现大量CLOSE_WAIT状态,怎么办?
答案:
问题根源:应用程序没有正确关闭socket
排查步骤:
- 定位问题进程
netstat -anp | grep CLOSE_WAIT
# 找到对应的进程PID
- 查看进程详情
lsof -p <pid> | grep TCP
# 查看哪些socket没有关闭
- 分析代码
// 检查是否有以下问题:
- 忘记调用socket.close()
- 异常处理不当
- 使用了连接池但没有归还连接
- 处理时间过长
- 解决方案
// 使用try-with-resources确保关闭
try (Socket socket = new Socket(...)) {
// 使用socket
} // 自动关闭
// 或使用finally块
Socket socket = null;
try {
socket = new Socket(...);
} finally {
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
// 记录日志
}
}
}
Q4:客户端和服务器可以同时发起关闭吗?
答案:
可以!这叫做同时关闭(Simultaneous Close)。
场景:双方同时发送FIN
客户端 服务器
| |
| FIN, seq=100 | FIN, seq=200
|------► ◄---------------|
| |
| (收到对方的FIN) | (收到对方的FIN)
| |
| ACK, ack=201 | ACK, ack=101
|------------------------->|
|<-------------------------|
| |
| (TIME_WAIT) | (TIME_WAIT)
| |
| (等待2MSL) | (等待2MSL)
| |
| (CLOSED) | (CLOSED)
结果:
- 只需要2次挥手!(双方各发送FIN和ACK)
- 双方都进入TIME_WAIT状态
- 双方都等待2MSL后关闭
💻 Java代码示例
客户端主动关闭连接
import java.io.*;
import java.net.*;
public class TCPClientClose {
public static void main(String[] args) {
Socket socket = null;
try {
// 建立连接(三次握手)
socket = new Socket("127.0.0.1", 8888);
System.out.println("✅ 连接建立成功");
// 发送数据
OutputStream os = socket.getOutputStream();
os.write("Hello Server!".getBytes());
System.out.println("📤 数据发送完毕");
// 主动关闭连接(四次挥手开始)
System.out.println("👋 客户端主动关闭连接...");
socket.close(); // 这会发起四次挥手
System.out.println("💔 连接已关闭");
// 此时客户端会进入TIME_WAIT状态
} catch (IOException e) {
e.printStackTrace();
} finally {
// 确保资源释放
if (socket != null && !socket.isClosed()) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
服务器端处理关闭
import java.io.*;
import java.net.*;
public class TCPServerClose {
public static void main(String[] args) {
ServerSocket serverSocket = null;
Socket clientSocket = null;
try {
serverSocket = new ServerSocket(8888);
System.out.println("🎧 服务器启动,等待连接...");
// 接受连接(三次握手)
clientSocket = serverSocket.accept();
System.out.println("✅ 客户端连接成功");
// 接收数据
InputStream is = clientSocket.getInputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) != -1) {
String message = new String(buffer, 0, len);
System.out.println("📥 收到消息:" + message);
// 检测客户端是否关闭
if (len == -1) {
System.out.println("💔 客户端已关闭连接");
break;
}
}
// 服务器也关闭连接
System.out.println("👋 服务器关闭连接...");
} catch (IOException e) {
e.printStackTrace();
} finally {
// 确保资源释放
try {
if (clientSocket != null) {
clientSocket.close(); // 重要!必须关闭
}
if (serverSocket != null) {
serverSocket.close();
}
System.out.println("✅ 资源已释放");
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
正确的try-with-resources写法
import java.io.*;
import java.net.*;
public class TCPClientProper {
public static void main(String[] args) {
// ✅ 推荐写法:使用try-with-resources
try (Socket socket = new Socket("127.0.0.1", 8888);
OutputStream os = socket.getOutputStream();
InputStream is = socket.getInputStream()) {
// 发送数据
os.write("Hello".getBytes());
// 接收数据
byte[] buffer = new byte[1024];
int len = is.read(buffer);
System.out.println("收到:" + new String(buffer, 0, len));
} catch (IOException e) {
e.printStackTrace();
}
// socket会自动调用close(),触发四次挥手
System.out.println("✅ 连接已自动关闭");
}
}
检测CLOSE_WAIT的工具类
import java.io.*;
public class NetworkStateChecker {
/**
* 检查当前系统的CLOSE_WAIT数量
*/
public static int checkCloseWaitCount() {
try {
Process process = Runtime.getRuntime()
.exec("netstat -an | grep CLOSE_WAIT | wc -l");
BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream())
);
String line = reader.readLine();
int count = Integer.parseInt(line.trim());
if (count > 100) {
System.err.println("⚠️ 警告:CLOSE_WAIT数量过多!" + count);
System.err.println("可能存在socket泄漏问题!");
} else {
System.out.println("✅ CLOSE_WAIT数量正常:" + count);
}
return count;
} catch (Exception e) {
e.printStackTrace();
return -1;
}
}
/**
* 检查当前系统的TIME_WAIT数量
*/
public static int checkTimeWaitCount() {
try {
Process process = Runtime.getRuntime()
.exec("netstat -an | grep TIME_WAIT | wc -l");
BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream())
);
String line = reader.readLine();
int count = Integer.parseInt(line.trim());
if (count > 1000) {
System.err.println("⚠️ 警告:TIME_WAIT数量过多!" + count);
System.err.println("建议:使用长连接或连接池");
} else {
System.out.println("✅ TIME_WAIT数量正常:" + count);
}
return count;
} catch (Exception e) {
e.printStackTrace();
return -1;
}
}
public static void main(String[] args) {
System.out.println("=== 网络状态检查 ===");
checkCloseWaitCount();
checkTimeWaitCount();
}
}
🔧 实战技巧
1. 使用Wireshark抓包观察四次挥手
# 过滤TCP挥手包
tcp.flags.fin == 1 || tcp.flags.reset == 1
# 观察关键字段
- [FIN, ACK] 第一次挥手
- [ACK] 第二次挥手
- [FIN, ACK] 第三次挥手
- [ACK] 第四次挥手
2. 使用netstat查看连接状态
# 查看所有TCP连接状态
netstat -an | grep tcp
# 统计各状态的数量
netstat -an | awk '/tcp/ {print $6}' | sort | uniq -c
# 查看CLOSE_WAIT详情
netstat -anp | grep CLOSE_WAIT
# 查看TIME_WAIT详情
netstat -an | grep TIME_WAIT
# 状态说明:
# FIN_WAIT_1 - 发送FIN,等待ACK
# FIN_WAIT_2 - 收到ACK,等待对方FIN
# TIME_WAIT - 收到FIN,发送ACK,等待2MSL
# CLOSE_WAIT - 收到FIN,发送ACK,应用层未关闭
# LAST_ACK - 发送FIN,等待ACK
# CLOSED - 连接关闭
3. 使用ss命令(更高效)
# ss比netstat更快
ss -tan | grep CLOSE_WAIT | wc -l
ss -tan | grep TIME_WAIT | wc -l
# 查看定时器信息
ss -tano
4. Java程序监控
// 使用JMX监控连接状态
import java.lang.management.*;
public class ConnectionMonitor {
public static void main(String[] args) {
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
// 定期检查线程状态
while (true) {
long[] threadIds = threadMXBean.getAllThreadIds();
System.out.println("活动线程数:" + threadIds.length);
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
break;
}
}
}
}
⚠️ 常见陷阱和注意事项
陷阱1:忘记关闭socket导致CLOSE_WAIT堆积
// ❌ 危险代码
public void handleRequest() {
Socket socket = new Socket("localhost", 8080);
// ... 处理请求 ...
// 忘记close()!
}
// 每次调用都会泄漏一个socket
// 客户端关闭后,服务器进入CLOSE_WAIT
// 但是应用程序没有关闭socket
// CLOSE_WAIT越积越多!😱
陷阱2:异常时没有关闭socket
// ❌ 危险代码
Socket socket = new Socket("localhost", 8080);
socket.getInputStream().read(); // 可能抛异常
socket.close(); // 如果抛异常,这行不会执行!
// ✅ 正确做法
try (Socket socket = new Socket("localhost", 8080)) {
socket.getInputStream().read();
} // 无论是否异常,都会关闭
陷阱3:连接池没有归还连接
// ❌ 危险代码
Connection conn = dataSource.getConnection();
// ... 使用连接 ...
// 忘记归还!
// ✅ 正确做法
try (Connection conn = dataSource.getConnection()) {
// ... 使用连接 ...
} // 自动归还连接池
陷阱4:主动关闭方TIME_WAIT过多
// 问题场景:客户端频繁建立短连接
for (int i = 0; i < 10000; i++) {
Socket socket = new Socket("localhost", 8080);
// ... 发送请求 ...
socket.close(); // 客户端主动关闭,进入TIME_WAIT
}
// 10000个TIME_WAIT!端口耗尽!😱
// ✅ 解决方案:使用连接池或长连接
HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_1_1) // Keep-Alive
.build();
for (int i = 0; i < 10000; i++) {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:8080"))
.build();
client.send(request, HttpResponse.BodyHandlers.ofString());
// 复用连接,不会产生大量TIME_WAIT
}
📊 性能影响
四次挥手的开销
时间开销:
- 最少需要 2个RTT(往返时间)
* 第一、二次挥手:1 RTT
* 第三、四次挥手:1 RTT
- TIME_WAIT等待:2MSL(1-4分钟)
资源开销:
- 端口占用(TIME_WAIT期间)
- 内存占用(连接状态信息)
- CPU开销(定时器、状态管理)
示例:
- 如果RTT = 50ms,MSL = 60秒
- 关闭连接需要:100ms(挥手) + 120秒(TIME_WAIT)
- 如果每秒关闭100个连接
- 同时存在的TIME_WAIT = 100 * 120 = 12000个!
🎓 知识点总结
⭐ 必须记住的要点
-
四次挥手的过程
- 第一次:客户端FIN(我要关闭了)
- 第二次:服务器ACK(知道了)
- 第三次:服务器FIN(我也要关闭了)
- 第四次:客户端ACK(好的)
-
为什么是四次
- TCP是全双工通信
- 一方关闭,另一方可能还有数据要发
- 不能合并第二、三次挥手
-
TIME_WAIT状态
- 主动关闭方会进入
- 等待2MSL时间
- 确保最后的ACK被收到
- 确保旧数据包消失
-
CLOSE_WAIT状态
- 被动关闭方会进入
- 应用程序应该关闭socket
- 如果不关闭,会一直停留
- 导致socket泄漏
📝 面试高分回答模板
面试官:请详细说明TCP四次挥手的过程,以及为什么是四次而不是三次?
你的回答:
"TCP四次挥手是连接终止的过程。
第一次挥手:客户端发送FIN包,表示不再发送数据。客户端进入FIN_WAIT_1状态。
第二次挥手:服务器收到FIN后,回复ACK,表示知道了。服务器进入CLOSE_WAIT
状态,客户端进入FIN_WAIT_2状态。此时服务器可能还有数据要发送。
第三次挥手:服务器数据发送完毕后,发送FIN包,表示也要关闭。服务器进入
LAST_ACK状态,客户端进入TIME_WAIT状态。
第四次挥手:客户端收到FIN后,发送ACK确认。服务器收到后进入CLOSED状态,
客户端等待2MSL后也进入CLOSED状态。
之所以是四次而不是三次,是因为TCP是全双工通信。当一方想关闭连接时,
只是表示不再发送数据,但仍可以接收数据。另一方可能还有数据要发送,
所以不能立即发送FIN,必须等数据发送完毕后再发送FIN。因此第二次和第三次
挥手不能合并,需要四次挥手。
在实际应用中,还需要注意TIME_WAIT和CLOSE_WAIT状态的问题。TIME_WAIT过多
说明短连接太多,应该使用长连接或连接池。CLOSE_WAIT过多说明应用程序没有
正确关闭socket,需要检查代码,确保使用try-with-resources或finally块
关闭资源。"
💯 完美!面试官满意地点头~
🔗 相关知识点
- 上一个知识点:001-TCP三次握手详细过程
- 下一个知识点:003-TCP和UDP的区别及使用场景
- 相关知识:TCP状态机、Socket编程、连接池技术
📚 推荐阅读
- 《图解TCP/IP》- 四次挥手章节
- 《TCP/IP详解 卷1》- 第18章
- 《UNIX网络编程 卷1》- Socket关闭
💪 加油!掌握了四次挥手,你已经理解了TCP连接的完整生命周期!
记住:四次挥手就像好聚好散,要确保双方都把话说完,才能分手!👋