💔 TCP四次挥手:比分手还复杂的断开过程

120 阅读18分钟

知识点编号: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(我也要关了)

🎭 生活场景比喻

打电话的例子:

你:"我要挂了啊。"(第一次挥手)
对方:"等等!我还有话要说!"(第二次挥手)
对方:"巴拉巴拉...说完了,我也挂了。"(第三次挥手)
你:"好的,拜拜。"(第四次挥手)

如果合并第二、三次挥手:
你:"我要挂了啊。"
对方:"好,我也挂了!"
你心想:"卧槽,我还没说完呢!" 😱

🔍 关键状态详解

客户端状态变化

  1. ESTABLISHED:正常连接状态
  2. FIN_WAIT_1:发送FIN后,等待ACK
  3. FIN_WAIT_2:收到ACK,等待对方的FIN
  4. TIME_WAIT:收到对方的FIN,发送最后的ACK,等待2MSL
  5. CLOSED:连接彻底关闭

服务器状态变化

  1. ESTABLISHED:正常连接状态
  2. CLOSE_WAIT:收到FIN,发送ACK,应用层还没关闭
  3. LAST_ACK:应用层关闭,发送FIN,等待最后的ACK
  4. 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?

答案:

  1. 确保最后的ACK被对方收到

    • 如果ACK丢失,对方会重传FIN
    • 需要等待1个MSL(FIN重传)+ 1个MSL(ACK到达)
    • 总共2MSL
  2. 确保旧连接的数据包消失

    • 等待网络中所有旧数据包(1 MSL)消失
    • 等待对方可能重传的数据包(1 MSL)消失
    • 总共2MSL,确保新连接不会收到旧数据
  3. 避免端口复用时的数据混乱

    • 如果新连接使用相同的端口
    • 旧连接的延迟数据包可能被误认为是新连接的数据

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

答案:

建立连接(三次握手)

- 双方都没有数据要发送
- 只需要同步初始序列号
- 第二次握手可以同时发送SYN和ACK
- 所以只需要3次

第二次握手:
服务器:SYN(我也要建立连接) + ACK(确认你的请求)

关闭连接(四次挥手)

- TCP是全双工通信,双方都可以发送数据
- 一方想关闭,但另一方可能还有数据要发
- 必须等数据发完才能关闭
- 所以不能合并第二次和第三次挥手
- 需要4次

第二次挥手:ACK(知道了,等我发完数据)
... 继续发送数据 ...
第三次挥手:FIN(我的数据发完了,可以关闭了)

核心区别:关闭时可能有未发完的数据!


Q3:服务器出现大量CLOSE_WAIT状态,怎么办?

答案:

问题根源:应用程序没有正确关闭socket

排查步骤

  1. 定位问题进程
netstat -anp | grep CLOSE_WAIT
# 找到对应的进程PID
  1. 查看进程详情
lsof -p <pid> | grep TCP
# 查看哪些socket没有关闭
  1. 分析代码
// 检查是否有以下问题:
- 忘记调用socket.close()
- 异常处理不当
- 使用了连接池但没有归还连接
- 处理时间过长
  1. 解决方案
// 使用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个!

🎓 知识点总结

⭐ 必须记住的要点

  1. 四次挥手的过程

    • 第一次:客户端FIN(我要关闭了)
    • 第二次:服务器ACK(知道了)
    • 第三次:服务器FIN(我也要关闭了)
    • 第四次:客户端ACK(好的)
  2. 为什么是四次

    • TCP是全双工通信
    • 一方关闭,另一方可能还有数据要发
    • 不能合并第二、三次挥手
  3. TIME_WAIT状态

    • 主动关闭方会进入
    • 等待2MSL时间
    • 确保最后的ACK被收到
    • 确保旧数据包消失
  4. 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块
关闭资源。"

💯 完美!面试官满意地点头~

🔗 相关知识点


📚 推荐阅读

  1. 《图解TCP/IP》- 四次挥手章节
  2. 《TCP/IP详解 卷1》- 第18章
  3. 《UNIX网络编程 卷1》- Socket关闭

💪 加油!掌握了四次挥手,你已经理解了TCP连接的完整生命周期!

记住:四次挥手就像好聚好散,要确保双方都把话说完,才能分手!👋