Rosetta Tcp Shutdown Fixed

TCP Socket关闭机制 - 罗塞塔石碑实验分析(修复版)

生成时间: 2026-01-05 目标系统: TCP Socket双工关闭机制 分析方法: 罗塞塔石碑实验法


第一步:定层级

层级名称数据形态为什么需要这层可观测性
层级1应用层socket.close() / shutdown()用户代码直接调用的接口,需要理解API行为✅ 源代码可见
层级2系统调用层close() / shutdown() syscallAPI到内核的桥梁,理解参数传递✅ strace追踪
层级3TCP状态机ESTABLISHED → CLOSE_WAIT → LAST_ACK理解连接状态的转换逻辑✅ ss命令查看
层级4网络传输层FIN/ACK/RST包理解数据包的实际内容✅ tcpdump抓包

为什么要分层?

每一层都是数据的一次"形态转换":

应用API → 系统调用 → TCP状态 → 网络包

只有追踪形态变化,才能回答:

  • close()和shutdown()有什么区别?
  • 为什么会停在CLOSE_WAIT?
  • 数据包里到底有什么?

第二步:定关卡

关卡表格

关卡数据转换对象关键问题主要观测工具
关卡1 API选择close() → FIN包完全关闭 vs 半关闭?strace
关卡2 内核处理FIN → tcp_close()如何发送FIN?ftrace
关卡3 状态转换ESTAB → FIN_WAIT_1下一个状态是什么?ss命令
关卡4 网络传输FIN → ACK → FIN → ACK握手顺序?tcpdump
关卡5 半关闭机制shutdown(SHUT_WR) → 可接收但不可发送如何优雅关闭?应用代码 + 抓包

关卡因果链路(横向展示)

正常关闭流程(使用shutdown)

应用层调用 shutdown(SHUT_WR) ━━━━━━━━━━━━━━━━━━━━━━━┓
                                                      ↓
                                               发送FIN,保留接收能力
                                                      ↓
                                               进入FIN_WAIT_1
                                                      ↓
                                               收到ACK?
        ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━┓
        ↓                                                            ↓
    收到ACK                                                    超时重传
        ↓                                                            ↓
  进入FIN_WAIT_2                                                 重试...
        ↓
  收到对端FIN?
        ↓
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━┓
↓                                                              ↓
收到FIN                                                        超时(2MSL)
↓                                                              ↓
发送ACK                                                    最终CLOSED
↓
进入TIME_WAIT
↓
等待60秒
↓
CLOSED

异常关闭流程(使用close且有未发送数据)

应用层调用 close() ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
                                                          ↓
                                                发送缓冲区有数据?
              ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━┓
              ↓                                                        ↓
          是(有数据)                                           否(干净)
              ↓                                                        ↓
      发送RST(丢弃数据)                                    发送FIN(优雅)
              ↓                                                        ↓
      立即CLOSED                                            进入FIN_WAIT_1

服务器端CLOSE_WAIT路径

收到客户端FIN ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
                                                          ↓
                                                   进入CLOSE_WAIT
                                               (等待应用层处理)
                                                          ↓
                                              应用层调用close?
  ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┻━━━━┓
  ↓                                                              ↓
  是(调用close)                                             否(忘记close)
  ↓                                                              ↓
发送FIN                                                    永远停在CLOSE_WAIT
  ↓                                                              ↓
进入LAST_ACK                                                ← 常见泄漏!
  ↓
等待ACK
  ↓
CLOSED

第三步:架工具

关卡1:应用层工具

工具解决痛点命令功能
strace追踪系统调用strace -e trace=close,shutdown python server.py看到API调用
ltrace追踪库函数ltrace -e close python server.py看到glibc调用

关卡2:TCP状态监控工具

工具解决痛点命令功能
ss查看socket状态ss -tan | grep :8080显示State/Recv-Q/Send-Q
watch持续监控watch -n 0.1 'ss -tan | grep :8080'每0.1秒刷新
/proc/net/tcp内核TCP表cat /proc/net/tcp查看内核连接

关卡3:网络抓包工具

工具解决痛点命令功能
tcpdump抓取TCP包tcpdump -i lo -n 'tcp port 8080' -vv -S完整抓包
WiresharkGUI分析wireshark图形化分析

🧪 实验1:close() vs shutdown() 对比

🎯 为什么要学这个?

现实痛点

  1. 客户端close()后,服务器数据接收不到
  2. 不知道为什么CLOSE_WAIT堆积
  3. 想实现优雅关闭但不知道怎么做

📝 源代码

client_close.py - 使用close()(错误示例):

import socket
import time

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('127.0.0.1', 8080))

sock.send(b"[TRACER] Hello")
time.sleep(0.5)

sock.close()  # 完全关闭,销毁socket
print("[Client] 已调用close()")

client_shutdown.py - 使用shutdown()(正确示例):

import socket
import time

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('127.0.0.1', 8080))

sock.send(b"[TRACER] Hello")
time.sleep(0.5)

sock.shutdown(socket.SHUT_WR)  # 半关闭
print("[Client] 已调用shutdown(SHUT_WR)")

# 关键:仍可接收服务器数据
data = sock.recv(1024)
print(f"[Client] 收到: {data.decode()}")

sock.close()

💉 埋示踪

# Terminal 1: 抓包
tcpdump -i lo -n 'tcp port 8080' -vv -S > tcpdump.log 2>&1 &

# Terminal 2: 查看状态
watch -n 0.1 'ss -tan | grep :8080'

# Terminal 3: 运行server
python server.py

# Terminal 4: 运行client
python client_close.py

👀 观察分析


🔷 观察1:应用层行为差异

👁️ 观测结果

client_close.py输出

[Client] 已调用close()
(程序结束,看不到服务器响应)

client_shutdown.py输出

[Client] 已调用shutdown(SHUT_WR)
[Client] 收到: [Server] 我收到了你的FIN

怎么解读

  • close()后程序立即结束,说明socket被销毁
  • shutdown()后程序继续运行,能recv服务器数据

排查SOP

问题:close()后收不到服务器数据
  ↓
步骤1:确认是否使用了close()
  │ $ strace -e close python client.py
  │ close(3) = 0  ← 确认调用了close
  ↓
步骤2:改用shutdown(SHUT_WR)
  │ sock.shutdown(socket.SHUT_WR)
  ↓
步骤3:验证仍可recv
  │ data = sock.recv(1024)
  │ print(data)  ← 应该能收到

📊 冰山下的知识

概念是什么为什么需要能解决什么问题
close()完全关闭socket释放资源,销毁文件描述符简单场景快速断开
shutdown(SHUT_WR)半关闭(关闭写但保留读)实现优雅关闭,确保数据完整解决"close后数据丢失"
双工特性TCP可同时收发通信更高效理解为什么可以单向关闭

🔗 认知映射

表层现象                    底层原理
调用close()          →   发送FIN + 销毁socket
程序无法recv         →   socket已失效,文件描述符不存在

调用shutdown()       →   发送FIN + 保留socket
程序仍可recv         →   socket有效,接收缓冲区仍工作

🔷 观察2:TCP抓包对比

👁️ 观测结果

使用close()的抓包

13:45:22.123456 IP 127.0.0.1.52341 > 127.0.0.1.8080: Flags [P.], seq 1:29, ack 1, win 512
13:45:22.789012 IP 127.0.0.1.52341 > 127.0.0.1.8080: Flags [F.], seq 29, ack 1, win 512
13:45:22.789345 IP 127.0.0.1.8080 > 127.0.0.1.52341: Flags [.], ack 30, win 512
13:45:22.890123 IP 127.0.0.1.8080 > 127.0.0.1.52341: Flags [F.], seq 1, ack 30, win 512
13:45:22.890456 IP 127.0.0.1.52341 > 127.0.0.1.8080: Flags [.], ack 2, win 512

使用shutdown()的抓包

13:45:22.123456 IP 127.0.0.1.52341 > 127.0.0.1.8080: Flags [P.], seq 1:29, ack 1, win 512
13:45:22.789012 IP 127.0.0.1.52341 > 127.0.0.1.8080: Flags [F.], seq 29, ack 1, win 512
13:45:22.789345 IP 127.0.0.1.8080 > 127.0.0.1.52341: Flags [.], ack 30, win 512
13:45:22.800123 IP 127.0.0.1.8080 > 127.0.0.1.52341: Flags [P.], seq 1:75, ack 30, win 512
    ↑
    关键!服务器在Client FIN_WAIT_2状态下仍可发送
13:45:22.800456 IP 127.0.0.1.52341 > 127.0.0.1.8080: Flags [.], ack 75, win 512
13:45:22.890123 IP 127.0.0.1.8080 > 127.0.0.1.52341: Flags [F.], seq 75, ack 30, win 512

怎么解读

  • [F.] = FIN包(结束连接)
  • [P.] = 数据包(携带数据)
  • [.] = ACK包(确认)
  • seq = 序列号,ack = 确认号
  • 关键差异:shutdown()后服务器还能发[P.]数据包

排查SOP

问题:close()后数据丢失
  ↓
步骤1:tcpdump抓包
  │ $ tcpdump -i lo -n 'tcp port 8080' -vv -S
  ↓
步骤2:查找服务器发送的数据包
  │ $ grep "P." tcpdump.log
  │ 看到有数据包 → 但Client已close
  ↓
步骤3:确认Client是否能接收
  │ ss命令查看:Client在TIME_WAIT,已无法接收
  ↓
步骤4:改用shutdown()
  │ 重新测试,应该能收到数据包

📊 冰山下的知识

表层现象底层原因为什么会这样
close()后收不到数据socket被销毁,接收缓冲区失效close()会释放文件描述符
shutdown()后能收数据socket保留,接收缓冲区仍工作shutdown只关闭写方向
服务器仍可发送TCP双工,单向关闭不影响另一方向双工特性,收发独立

🔗 认知映射

表层现象                    底层原理
close()后看不到服务器数据  →  socket销毁,接收缓冲区失效
shutdown()后能看到数据     →  socket保留,接收缓冲区有效
服务器仍可发送数据         →  TCP双工,单向关闭不影响另一方向

🔷 观察3:socket状态转换

👁️ 观测结果

使用shutdown()时ss输出

State      Recv-Q Send-Q  Local Address:Port
ESTAB      0      0       127.0.0.1:8080
FIN-W2     73     0       127.0.0.1:52341
                           ↑
                       Recv-Q=73表示有73字节未读数据!

怎么解读

  • State = TCP状态(ESTAB=已建立,FIN-W2=FIN_WAIT_2)
  • Recv-Q = 接收缓冲区未读字节数
  • Send-Q = 发送缓冲区未发送字节数
  • Recv-Q=73说明服务器发送了数据,Client还没读取

排查SOP

问题:CLOSE_WAIT状态堆积
  ↓
步骤1:查看TCP状态
  │ $ ss -tan | grep :8080
  │ 看到大量CLOSE-W
  ↓
步骤2:确认Recv-Q
  │ Recv-Q = 0 → 说明没数据,但应用没close
  ↓
步骤3:检查应用代码
  │ 查找所有conn.recv()=0的分支
  │ 是否每个分支都调用了close()
  ↓
步骤4:添加close()
  │ 在finally块中添加conn.close()

📊 冰山下的知识

状态位置含义为什么停留
CLOSE_WAIT被动关闭方收到FIN,等待应用close应用忘记调用close()
FIN_WAIT_2主动关闭方已发送FIN,等待对端FIN等待服务器处理完并发送FIN
Recv-Q > 0任意方有数据未读应用没有及时recv

🔗 认知映射

表层现象                    底层原理
CLOSE_WAIT堆积         →  应用层忘记close()
Recv-Q > 0             →  有数据未读,应用没有recv
FIN_WAIT_2停留         →  等待对端发送FIN

🎯 这个实验能解决什么问题?

问题现象根本原因解决方案
客户端close后数据丢失服务器发送的数据Client收不到close()销毁socket改用shutdown(SHUT_WR)
CLOSE_WAIT堆积大量连接停在CLOSE_WAIT应用忘记close()检查代码,确保close
TIME_WAIT耗尽端口大量TIME_WAIT占用端口频繁创建/关闭连接开启tcp_tw_reuse

总结

核心发现

  1. close() vs shutdown()

    • close() = 发送FIN + 销毁socket
    • shutdown() = 发送FIN + 保留socket
  2. TCP状态机

    • 主动关闭:ESTAB → FIN-W1 → FIN-W2 → TIME-W → CLOSED
    • 被动关闭:ESTAB → CLOSE-W → LAST-A → CLOSED
  3. 实际应用

    • 优雅关闭:shutdown() → recv数据 → close()
    • 防止CLOSE_WAIT泄漏:确保所有分支都close()
    • 防止TIME_WAIT堆积:开启tcp_tw_reuse