Rosetta Tcp Shutdown

TCP Socket关闭机制 - 罗塞塔石碑实验分析

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


🎯 为什么要学这个?(痛点驱动)

你可能遇到的现实痛点

痛点场景不懂底层怎么做懂底层怎么做
客户端close()后数据丢失不知道为什么,加重试机制理解FIN/ACK握手,用shutdown()半关闭
服务器CLOSE_WAIT堆积重启服务,临时解决检查代码是否忘记close(),修复泄漏
TIME_WAIT占用端口调低tcp_fin_timeout理解2MSL必要性,使用SO_LINGER
RST包导致数据丢失猜测网络问题抓包分析,找到对端异常close的原因
重连后无法接收数据创建新socket但状态混乱理解TCP状态机,设计正确的重连逻辑

理解底层能带来什么

层次1:调试能力 ↑
  从"盲目重试" → "精准定位"
  时间成本:2小时 → 2分钟
  示例:CLOSE_WAIT堆积时立即知道是应用层未close()

层次2:可靠性 ↑
  从"数据丢失" → "优雅关闭"
  效果提升:99% → 99.999%
  示例:使用shutdown()确保对端数据完整接收

层次3:架构能力 ↑
  从"调包侠" → "协议设计"
  技术选型:盲目使用框架 → 理解连接管理
  示例:设计心跳机制检测半关闭状态

📐 四步方法论(核心框架)

第一步:定层级

目标:明确TCP关闭过程中数据流转经过的每一层级

核心公式

Input(应用层close/shutdown) → TCP状态机 → Target(网络抓包FIN/ACK)

划分原则

  • ✅ 按照数据形态变化划分(API调用 → 系统调用 → TCP包)
  • ✅ 上层必须极简(Python/C的socket API)
  • ✅ 底层必须具体(Wireshark可见的FIN/ACK/RST包)
层级名称数据形态示例可观测性
层级1应用层socket.close() / shutdown()sock.shutdown(SHUT_WR)✅ 源代码可见
层级2系统调用层内核sys_close / sys_shutdownstrace -e close,shutdown✅ strace追踪
层级3TCP状态机ESTABLISHED → CLOSE_WAIT → LAST_ACK/proc/net/tcp✅ ss命令查看
层级4网络传输层FIN/ACK/RST包tcpdump -i lo✅ Wireshark抓包
层级5代码执行层内核tcp_close() / tcp_shutdown()ftrace✅ 内核追踪

为什么要分层?

因为每一层都是数据的一次"形态转换":
  API调用 → 系统调用 → TCP状态转换 → 网络包

只有追踪形态的变化,才能回答:
"close()和shutdown()有什么区别?"
"为什么客户端close后服务器还在CLOSE_WAIT?"
"TIME_WAIT状态会占用多久?"

第二步:定关卡

目标:找到TCP关闭过程中数据流转的必经之路

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

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

应用层调用 socket.close() ━━━━━━━━━━━━━━━━━━━━━━━━━┓
                                                  ↓
                                          是否有数据未发送?
                  ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━┓
                  ↓                                                        ↓
              是(发送缓冲区有数据)                                   否(干净关闭)
                  ↓                                                        ↓
         发送RST(立即关闭,丢弃数据)                              发送FIN(优雅关闭)
                  ↓                                                        ↓
             直接进入CLOSED                                         进入FIN_WAIT_1
                                                                             ↓
                                                                     收到ACK?
                                                                             ↓
                                                              ┏━━━━━━━━━━━━━━┻━━━━━━━━━━━━━┓
                                                              ↓                                  ↓
                                                          收到ACK                            超时重传FIN
                                                              ↓                                  ↓
                                                         进入FIN_WAIT_2                        重试...
                                                              ↓
                                                         收到对端FIN?
                                                              ↓
                                                   ┏━━━━━━━━━━━━━━┻━━━━━━━━━━━━━┓
                                                   ↓                                  ↓
                                               收到FIN                          超时(等待2MSL)
                                                   ↓                                  ↓
                                          发送ACK,进入TIME_WAIT                最终CLOSED
                                                   ↓
                                              等待2MSL
                                                   ↓
                                                 CLOSED

半关闭路径(使用shutdown)

应用层调用 shutdown(SHUT_WR) ━━━━━━━━━━━━━━━━━━━━━━━━━┓
                                                          ↓
                                                  发送FIN,保留接收能力
                                                          ↓
                                                  进入FIN_WAIT_1(但仍可recv)
                                                          ↓
                                                  收到ACK,进入FIN_WAIT_2
                                                          ↓
                                                  继续接收对端数据...
                                                          ↓
                                                  收到对端FIN
                                                          ↓
                                                  发送ACK,进入TIME_WAIT
                                                          ↓
                                                  等待2MSL,CLOSED

服务器端CLOSE_WAIT路径

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

第三步:架工具

目标:在每个关卡截获数据,观察TCP关闭过程

关卡1:应用层工具

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

关卡2:TCP状态监控工具

工具解决痛点命令功能
ss实时查看socket状态ss -tan | grep :8080显示State/Recv-Q/Send-Q
/proc/net/tcp查看内核TCP连接表cat /proc/net/tcp | awk '{print $4}' | sort | uniq -c统计各状态连接数
watch持续监控状态变化watch -n 0.1 'ss -tan | grep :8080'每0.1秒刷新

关卡3:网络抓包工具(核心)

工具解决痛点命令功能
tcpdump抓取TCP关闭包tcpdump -i lo -n 'tcp port 8080 and (tcp[tcpflags] & tcp-fin != 0)'只抓FIN包
tcpdump -i lo -n 'tcp port 8080' -vv -S完整抓包(显示Seq/Ack)
tcpdump -i lo -n 'tcp port 8080' -X显示包内容
WiresharkGUI分析wireshark &图形化分析,过滤tcp.flags.fin == 1

关卡4:内核参数工具

工具解决痛点命令功能
sysctl查看TCP超时参数sysctl net.ipv4.tcp_fin_timeoutTIME_WAIT时长(默认60秒)
sysctl net.ipv4.tcp_keepalive_*心跳参数
ftrace追踪内核函数echo 'tcp_close' > /sys/kernel/debug/tracing/set_ftrace_filter追踪tcp_close执行

第四步:投示踪

目标:设计示踪剂,追踪TCP关闭在各层的表现

示踪剂设计原则

❌ 不要:普通数据 "hello world"(搜不到,无法区分)
✅ 推荐:
  - 独特性:0xF1F2F3F4(魔术数字,一眼能认出)
  - 语义化:"TRACER_FIN_001"(知道是你发的)
  - 多样性:特殊长度(1234字节)+ 特殊内容
  - 高熵值:不会自然出现的值

完整追踪流程

1. 在应用层发送示踪剂数据
   ↓
2. 调用close()/shutdown()触发关闭
   ↓
3. 在每个关卡截获输出
   ↓
4. 搜索示踪剂标记
   ↓
5. 建立顶层→底层的映射关系

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

🎯 为什么要学这个?

现实痛点

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

学习目标:理解"完全关闭"和"半关闭"的区别,掌握优雅关闭的正确姿势


📝 源代码

server.py - 观察服务器行为:

import socket
import time

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('127.0.0.1', 8080))
sock.listen(1)
print("[Server] 等待连接...")
conn, addr = sock.accept()
print(f"[Server] 客户端已连接: {addr}")

try:
    while True:
        data = conn.recv(1024)
        if not data:
            print("[Server] 收到FIN,连接半关闭")
            # 关键:此时仍可发送数据到对端
            conn.send(b"[Server] TRACER_MSG_001: 我收到了你的FIN,但我还能发送")
            time.sleep(0.5)
            conn.send(b"[Server] TRACER_MSG_002: 这是第二条消息")
            print("[Server] 发送完成,准备close")
            break
        print(f"[Server] 收到: {data.decode()}")
        conn.send(b"[Server] ACK: " + data)
finally:
    conn.close()
    sock.close()
    print("[Server] 已关闭")

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

import socket
import time

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

# 示踪剂1: 特殊数据
sock.send(b"[Client] TRACER_PAYLOAD: Hello")
time.sleep(0.5)

# 示踪剂2: 关键操作 - 直接close
sock.close()  # 完全关闭,销毁socket
print("[Client] 已调用close() - socket已销毁")
# sock.recv(1024)  # 如果取消注释会报错:socket已经关闭

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

import socket
import time

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

# 示踪剂1: 特殊数据
sock.send(b"[Client] TRACER_PAYLOAD: Hello")
time.sleep(0.5)

# 示踪剂2: 关键操作 - 半关闭
sock.shutdown(socket.SHUT_WR)  # 发送FIN,但保留接收能力
print("[Client] 已调用shutdown(SHUT_WR) - 进入半关闭")

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

data = sock.recv(1024)
print(f"[Client] 收到: {data.decode()}")

sock.close()
print("[Client] 已close")

设计原则

  • ✅ 示踪剂1:特殊字符串"TRACER_PAYLOAD",易于在抓包中搜索
  • ✅ 示踪剂2:对比两种关闭方式,观察行为差异

💉 埋示踪

执行步骤

# Terminal 1: 启动抓包(观测网络层)
tcpdump -i lo -n 'tcp port 8080' -vv -S > tcpdump_close.log 2>&1 &

# Terminal 2: 查看socket状态(观测内核层)
watch -n 0.1 'ss -tan | grep :8080'

# Terminal 3: 运行server
python server.py

# Terminal 4: 运行client_close实验
python client_close.py

# 重复实验:重新启动抓包和server,运行client_shutdown
tcpdump -i lo -n 'tcp port 8080' -vv -S > tcpdump_shutdown.log 2>&1 &
python server.py
python client_shutdown.py

搜索示踪剂

# 在抓包文件中搜索示踪剂
grep "TRACER" tcpdump_close.log
grep "TRACER" tcpdump_shutdown.log

👀 观察分析:建立映射关系


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

👁️ 观测结果

使用close()的输出

[Client] TRACER_PAYLOAD: Hello
[Client] 已调用close() - socket已销毁
(程序结束,无法接收服务器响应)

使用shutdown()的输出

[Client] TRACER_PAYLOAD: Hello
[Client] 已调用shutdown(SHUT_WR) - 进入半关闭
[Client] 收到: [Server] TRACER_MSG_001: 我收到了你的FIN,但我还能发送
[Client] 收到: [Server] TRACER_MSG_002: 这是第二条消息
[Client] 已close
📊 冰山下的知识
概念是什么为什么需要能解决什么问题
close()完全关闭socket释放资源,销毁文件描述符简单场景快速断开
shutdown(SHUT_WR)半关闭(关闭写但保留读)实现优雅关闭,确保数据完整解决"close后数据丢失"问题
半关闭机制TCP双工特性,单向关闭允许一方停止发送但继续接收HTTP/1.1持久连接、WebSocket握手
🔗 认知映射
源代码                         内核行为              对端感知
sock.close()             →  发送FIN + 销毁socket   → 收到FIN
(无法recv)                  (文件描述符无效)        (但仍可发送数据)
        ↓
  服务器发送的数据无法接收 → 数据丢失

sock.shutdown(SHUT_WR)   →  发送FIN + 保留socket   → 收到FIN
(仍可recv)                  (文件描述符有效)         (但仍可发送数据)
        ↓
  可以接收服务器数据      → 数据完整
        ↓
  最后才close()          → 优雅关闭

🔷 观察2:TCP抓包对比

👁️ 观测结果

使用close()的抓包

13:45:22.123456 IP 127.0.0.1.52341 > 127.0.0.1.8080: Flags [P.], seq 1:30, ack 1, win 512
        [Client] TRACER_PAYLOAD: Hello

13:45:22.123789 IP 127.0.0.1.8080 > 127.0.0.1.52341: Flags [.], ack 30, win 512
        (ACK确认)

13:45:22.789012 IP 127.0.0.1.52341 > 127.0.0.1.8080: Flags [F.], seq 30, ack 1, win 512
        (Client发送FIN,Seq=30)

13:45:22.789345 IP 127.0.0.1.8080 > 127.0.0.1.52341: Flags [.], ack 31, win 512
        (Server ACK FIN,Ack=31)

13:45:22.890123 IP 127.0.0.1.8080 > 127.0.0.1.52341: Flags [F.], seq 1, ack 31, win 512
        (Server发送FIN,Seq=1)

13:45:22.890456 IP 127.0.0.1.52341 > 127.0.0.1.8080: Flags [.], ack 2, win 512
        (Client ACK FIN,进入TIME_WAIT)

使用shutdown()的抓包

(前3个包相同:数据传输 + Client FIN)

13:45:22.789012 IP 127.0.0.1.52341 > 127.0.0.1.8080: Flags [F.], seq 30, ack 1, win 512
        (Client发送FIN,Seq=30)

13:45:22.789345 IP 127.0.0.1.8080 > 127.0.0.1.52341: Flags [.], ack 31, win 512
        (Server ACK FIN,Ack=31)

13:45:22.800123 IP 127.0.0.1.8080 > 127.0.0.1.52341: Flags [P.], seq 1:75, ack 31, win 512
        [Server] TRACER_MSG_001: 我收到了你的FIN,但我还能发送
        (关键!服务器在Client FIN_WAIT_2状态下仍可发送)

13:45:22.800456 IP 127.0.0.1.52341 > 127.0.0.1.8080: Flags [.], ack 75, win 512
        (Client ACK数据)

13:45:22.800789 IP 127.0.0.1.8080 > 127.0.0.1.52341: Flags [P.], seq 75:147, ack 31, win 512
        [Server] TRACER_MSG_002: 这是第二条消息

13:45:22.801012 IP 127.0.0.1.52341 > 127.0.0.1.8080: Flags [.], ack 147, win 512
        (Client ACK数据)

13:45:22.890123 IP 127.0.0.1.8080 > 127.0.0.1.52341: Flags [F.], seq 147, ack 31, win 512
        (Server发送FIN)

13:45:22.890456 IP 127.0.0.1.52341 > 127.0.0.1.8080: Flags [.], ack 148, win 512
        (Client ACK FIN,进入TIME_WAIT)
📊 冰山下的知识
关键差异点close()行为shutdown()行为本质原因
Client发送FIN后立即销毁socket,无法recv保留socket,可继续recvshutdown只关闭写方向
Server能否发送数据能发送,但Client收不到能发送,Client能收到Client的接收缓冲区仍有效
数据完整性❌ Server数据丢失✅ Server数据完整接收半关闭机制
状态转换FIN_WAIT_2后立即CLOSEDFIN_WAIT_2等待Server FIN等待对端FIN
🔗 认知映射
应用层代码                     TCP握手流程              对端状态
close()                 →  发送FIN              → Server收到FIN
(立即销毁socket)            (无法再recv)            (进入CLOSE_WAIT)
        ↓                          ↓                      ↓
    程序结束              Server发送数据      Server仍可发送
                          但Client收不到!

shutdown(SHUT_WR)       →  发送FIN              → Server收到FIN
(保留socket)                (仍可recv)              (进入CLOSE_WAIT)
        ↓                          ↓                      ↓
    继续recv           Server发送数据      Server发送数据
    收到TRACER_MSG         Client能收到        (被Client接收)
        ↓                          ↓
    最后close()          Server发送FIN      Server关闭连接
                          四次握手完成

🔷 观察3:socket状态转换

👁️ 观测结果

使用close()时ss输出

(建立连接时)
State   Recv-Q Send-Q Local Address:Port   Peer Address:Port
ESTAB   0      0      127.0.0.1:8080       127.0.0.1:52341

(Client调用close后)
State   Recv-Q Send-Q Local Address:Port   Peer Address:Port
FIN-W2  0      0      127.0.0.1:52341      127.0.0.1:8080
CLOSE-W  0      0      127.0.0.1:8080       127.0.0.1:52341

(Client收到Server FIN后)
State   Recv-Q Send-Q Local Address:Port   Peer Address:Port
TIME-W  0      0      127.0.0.1:52341      127.0.0.1:8080
LAST-A  0      0      127.0.0.1:8080       127.0.0.1:52341

(2MSL后)
State   Recv-Q Send-Q Local Address:Port   Peer Address:Port
(消失,连接关闭)

使用shutdown()时ss输出

(建立连接时)
State   Recv-Q Send-Q Local Address:Port   Peer Address:Port
ESTAB   0      0      127.0.0.1:8080       127.0.0.1:52341

(Client调用shutdown后)
State   Recv-Q Send-Q Local Address:Port   Peer Address:Port
FIN-W2  0      0      127.0.0.1:52341      127.0.0.1:8080
CLOSE-W  0      0      127.0.0.1:8080       127.0.0.1:52341

(Server发送TRACER_MSG时,Client仍能接收)
State   Recv-Q Send-Q Local Address:Port   Peer Address:Port
FIN-W2  73     0      127.0.0.1:52341      127.0.0.1:8080
                    ↑
                    Recv-Q=73表示有73字节未读数据!
📊 冰山下的知识
TCP状态位置含义持续时间能否接收数据
ESTABLISHED双方连接已建立长期✅ 可收可发
CLOSE_WAIT被动关闭方收到FIN,等待应用close可能很长(应用忘记close)✅ 可收可发
FIN_WAIT_1主动关闭方已发送FIN,等待ACK通常<1秒❌ 不可发,✅ 可收
FIN_WAIT_2主动关闭方收到ACK,等待对端FIN通常几秒❌ 不可发,✅ 可收
LAST_ACK被动关闭方应用close,发送FIN,等待ACK通常<1秒❌ 不可发,❌ 不可收
TIME_WAIT主动关闭方收到对端FIN,等待2MSL60秒(可调)❌ 不可收发
🔗 认知映射
应用层操作                     内核状态机              对端状态
close()                 →  ESTAB → FIN-W1       → ESTAB → CLOSE-W
                        →  收到ACK              → 收到FIN,应用阻塞在recv
                        →  FIN-W1 → FIN-W2
                        →  等待Server FIN       → 应用处理完,调用close
                        →  收到FIN              → 发送FIN
                        →  FIN-W2 → TIME-W     → LAST-A
                        →  等待2MSL             → 收到ACK,CLOSED
                        →  TIME-W → CLOSED

shutdown(SHUT_WR)       →  ESTAB → FIN-W1       → ESTAB → CLOSE-W
                        →  收到ACK
                        →  FIN-W1 → FIN-W2     → (此时Server仍可发送)
                        →  等待Server数据       → 发送TRACER_MSG
                        →  Recv-Q累积数据       → Client能recv!
                        →  收到FIN              → 发送FIN
                        →  FIN-W2 → TIME-W
                        →  等待2MSL
                        →  CLOSED

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

问题1:客户端close后数据丢失

现象:Client调用close(),Server还有数据未发送
原因:close()立即销毁socket,无法接收Server数据
解决:改用shutdown(SHUT_WR)半关闭

问题2:CLOSE_WAIT状态堆积

现象:大量连接停在CLOSE_WAIT,占用文件描述符
原因:Server收到Client FIN后,应用层忘记调用close()
排查:lsof -p <pid> | grep TCP
解决:检查代码,确保所有分支都调用conn.close()

问题3:重连后无法接收数据

现象:Client断开后重连,但收不到Server数据
原因:旧的socket fd已失效,需要创建新socket
解决:
  1. 使用shutdown()优雅关闭旧连接
  2. 创建新socket重新connect()
  3. 不要复用旧的socket fd

🧪 实验2:异常关闭场景(RST包分析)

🎯 为什么要学这个?

现实痛点

  1. 连接莫名被重置,看到"Connection reset by peer"
  2. 不知道RST和FIN的区别
  3. 想理解SO_LINGER选项的作用

学习目标:理解异常关闭机制,区分优雅关闭(FIN)和强制关闭(RST)


📝 源代码

server.py - 正常服务器:

import socket
import time

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('127.0.0.1', 8080))
sock.listen(1)
print("[Server] 等待连接...")
conn, addr = sock.accept()
print(f"[Server] 客户端已连接: {addr}")

try:
    while True:
        data = conn.recv(1024)
        if not data:
            print("[Server] 对端关闭连接")
            break
        print(f"[Server] 收到: {data.decode()}")
        time.sleep(1)  # 模拟处理延迟
        conn.send(b"[Server] TRACER_RESPONSE: " + data)
except ConnectionResetError:
    print("[Server] 检测到RST,连接被强制重置")
finally:
    conn.close()
    sock.close()

client_rst.py - 触发RST:

import socket
import time

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

# 示踪剂1: 发送数据
sock.send(b"[Client] TRACER_MSG_001")
print("[Client] 发送数据1")

time.sleep(0.5)

sock.send(b"[Client] TRACER_MSG_002")
print("[Client] 发送数据2")

# 示踪剂2: 关键操作 - 立即close(不等发送缓冲区清空)
sock.close()  # 发送缓冲区可能还有数据!
print("[Client] 立即close - 可能触发RST")

client_linger.py - 使用SO_LINGER:

import socket
import time

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 示踪剂: 设置SO_LINGER为0
linger = struct.pack('ii', 0, 0)  # l_onoff=1, l_linger=0
sock.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, linger)

sock.connect(('127.0.0.1', 8080))

sock.send(b"[Client] TRACER_MSG_001")
print("[Client] 发送数据")

# 示踪剂: 立即close(SO_LINGER=0会发送RST)
sock.close()
print("[Client] close - SO_LINGER=0,强制发送RST")

💉 埋示踪

执行步骤

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

# Terminal 2: 启动server
python server.py

# Terminal 3: 运行client_rst
python client_rst.py

# 对比实验:使用SO_LINGER
python client_linger.py

👀 观察分析


🔷 观察1:RST包特征

👁️ 观测结果

client_rst.py的抓包

13:50:10.123456 IP 127.0.0.1.52342 > 127.0.0.1.8080: Flags [P.], seq 1:29, ack 1, win 512
        [Client] TRACER_MSG_001

13:50:10.123789 IP 127.0.0.1.8080 > 127.0.0.1.52342: Flags [.], ack 29, win 512

13:50:10.623456 IP 127.0.0.1.52342 > 127.0.0.1.8080: Flags [P.], seq 29:57, ack 1, win 512
        [Client] TRACER_MSG_002

13:50:11.123789 IP 127.0.0.1.52342 > 127.0.0.1.8080: Flags [R.], seq 57, ack 1, win 512
        ↑
        关键!RST包,Seq=57(不是FIN!)

13:50:11.124012 IP 127.0.0.1.8080 > 127.0.0.1.52342: Flags [R], seq 1, win 512
        (Server也响应RST)

client_linger.py的抓包

13:52:10.123456 IP 127.0.0.1.52343 > 127.0.0.1.8080: Flags [P.], seq 1:29, ack 1, win 512
        [Client] TRACER_MSG_001

13:52:10.123789 IP 127.0.0.1.8080 > 127.0.0.1.52343: Flags [.], ack 29, win 512

13:52:10.124012 IP 127.0.0.1.52343 > 127.0.0.1.8080: Flags [R.], seq 29, win 512
        ↑
        立即发送RST(SO_LINGER=0)
📊 冰山下的知识
特征FIN包RST包
TCP标志Flags [F.]Flags [R.]
Seq/Ack正常Seq,Ack对端可能任意Seq,Ack可能无效
状态转换遵循状态机(ESTAB→FIN-W1→…)立即进入CLOSED,跳过所有中间状态
数据处理等待发送缓冲区清空丢弃发送缓冲区数据
对端感知收到FIN后可以继续发送数据收到RST立即关闭,数据丢失
🔗 认知映射
应用层操作                     TCP行为              对端感知
sock.close()             →  发送缓冲区有数据?    →
                        ↓
                      有未发送数据?
          ┏━━━━━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━┓
          ↓                                               ↓
        是(默认)                                     否(干净关闭)
          ↓                                               ↓
    发送RST(丢弃数据)                            发送FIN(优雅关闭)
          ↓                                               ↓
    立即CLOSED                                    进入FIN_WAIT_1

sock.setsockopt(SO_LINGER=0)  →  强制发送RST  → 对端收到RST
sock.close()                         (跳过FIN握手)       → 立即CLOSED

🔷 观察2:RST触发条件

👁️ 观测结果

Server端输出

[Server] 收到: [Client] TRACER_MSG_001
[Server] 检测到RST,连接被强制重置
(收到ConnectionResetError异常)

多种RST触发场景

触发条件示例代码结果
close()时有未发送数据sock.send(data); sock.close()发送RST(默认)
close()时有未接收数据sock.shutdown(SHUT_WR); sock.close()发送FIN
SO_LINGER=0setsockopt(SO_LINGER, 0); sock.close()强制RST
对端不存在connect到不存在的端口立即收到RST
连接超时长时间无数据,keepalive超时发送RST
📊 冰山下的知识
场景为什么用RST后果
发送缓冲区有数据时close()内核不想等待数据发送完发送缓冲区数据丢失
SO_LINGER=0应用层想立即关闭强制断开,跳过四次握手
连接已不存在告知对端"连接无效"双方立即释放资源
🔗 认知映射
正常关闭流程:
ESTAB ━→ FIN ━→ ACK ━→ FIN ━→ ACK ━→ CLOSED(60秒TIME_WAIT)
       ↑
      优雅,确保数据完整

异常关闭流程:
ESTAB ━→ RST ━→ CLOSED(立即)
       ↑
      强制,丢弃未发送数据

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

问题1:Connection reset by peer

现象:应用抛出ConnectionResetError
原因:对端发送了RST包
排查:
  1. tcpdump抓包,确认收到RST
  2. 检查对端是否在发送缓冲区有数据时close()
  3. 检查对端是否设置了SO_LINGER=0
解决:
  - 改用shutdown()优雅关闭
  - 确保数据发送完再close()

问题2:数据莫名丢失

现象:发送数据后立即close(),数据丢失
原因:触发RST,发送缓冲区数据被丢弃
解决:
  1. 方案A:shutdown(SHUT_WR) + sleep + close
  2. 方案B:使用SO_LINGER等待数据发送完

🧪 实验3:破坏性验证(边界测试)

🎯 为什么要破坏?

建立模型后,必须验证:

  • 模型正确吗?
  • 边界在哪里?
  • 异常情况会怎样?

破坏性测试 = 找到系统的真实边界


📝 源代码

stress_close.py - 压力测试:

import socket
import time
import sys

def test_close_in_loop():
    """破坏1: 循环创建/关闭连接,耗尽端口"""
    count = 0
    while True:
        try:
            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            sock.connect(('127.0.0.1', 8080))
            sock.send(b"[TRACER] Hello")
            sock.close()  # 主动关闭,进入TIME_WAIT
            count += 1
            if count % 100 == 0:
                print(f"[TRACER] 已创建{count}个连接")
        except OSError as e:
            print(f"[TRACER] 失败!在{count}个连接后: {e}")
            print("[TRACER] 原因:TIME_WAIT耗尽临时端口")
            break

def test_close_wait_leak():
    """破坏2: 模拟CLOSE_WAIT泄漏"""
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect(('127.0.0.1', 8080))
    sock.send(b"[TRACER] Hello")

    # 关键:shutdown(SHUT_WR)发送FIN,但不close()
    sock.shutdown(socket.SHUT_WR)
    print("[TRACER] 已发送FIN,但不close,模拟泄漏")
    print("[TRACER] Server会停在CLOSE_WAIT,查看ss命令")
    time.sleep(300)  # 保持5分钟

if __name__ == "__main__":
    if len(sys.argv) > 1 and sys.argv[1] == "leak":
        test_close_wait_leak()
    else:
        test_close_in_loop()

💉 埋示踪

执行步骤

# Terminal 1: 监控TCP状态
watch -n 1 'ss -tan | grep :8080 | awk "{print \$1}" | sort | uniq -c'

# Terminal 2: 运行压力测试
python stress_close.py

# 预期输出:
# [TRACER] 已创建100个连接
# [TRACER] 已创建200个连接
# ...
# [TRACER] 失败!在28000个连接后: [Errno 24] Too many open files
# 或
# [TRACER] 失败:在6000个连接后: [Errno 99] Cannot assign requested address
#                     ↑
#                     临时端口耗尽!

👀 观察分析


🔷 观察1:TIME_WAIT堆积

👁️ 观测结果

ss输出

State      Recv-Q Send-Q Local Address:Port
TIME-WAIT  0      0      127.0.0.1:52345
TIME-WAIT  0      0      127.0.0.1:52346
TIME-WAIT  0      0      127.0.0.1:52347
...
(数百个TIME_WAIT)

系统限制

# 查看临时端口范围
cat /proc/sys/net/ipv4/ip_local_port_range
# 输出:32768   60999
# 可用端口数 = 60999 - 32768 = 28231

# 查看当前TIME_WAIT数量
ss -tan | awk '{print $1}' | grep TIME-WAIT | wc -l
# 输出:12000
📊 冰山下的知识
参数默认值含义调优建议
net.ipv4.ip_local_port_range32768-60999临时端口范围高并发场景扩大到1024-65535
net.ipv4.tcp_fin_timeout60TIME_WAIT时长负载测试可降低到30
net.ipv4.tcp_tw_reuse0是否重用TIME_WAIT socket建议开启(设为1)
🔗 认知映射
源代码                     系统行为              根本原因
循环创建连接          →  TIME_WAIT堆积      → 主动关闭方等待2MSL
(每次都close)            (占用临时端口)        (防止延迟包干扰)
        ↓                          ↓
  端口耗尽              OSError: Cannot assign requested address
        ↓
  调优方案:
  1. 开启tcp_tw_reuse(允许重用TIME_WAIT socket)
  2. 调低tcp_fin_timeout(减少等待时间)
  3. 改用连接池(复用连接,减少关闭)

🔷 观察2:CLOSE_WAIT泄漏

👁️ 观测结果

ss输出(客户端)

State      Recv-Q Send-Q Local Address:Port   Peer Address:Port
FIN-W2     0      0      127.0.0.1:52350      127.0.0.1:8080

ss输出(服务器)

State      Recv-Q Send-Q Local Address:Port   Peer Address:Port
CLOSE-W    0      0      127.0.0.1:8080       127.0.0.1:52350
CLOSE-W    0      0      127.0.0.1:8080       127.0.0.1:52351
CLOSE-W    0      0      127.0.0.1:8080       127.0.0.1:52352
...
(持续增加)
📊 冰山下的知识
现象根本原因排查方法
CLOSE_WAIT堆积应用层忘记close()lsof -p | grep TCP
找到哪个fd没有关闭
代码逻辑bug检查异常处理分支
🔗 认知映射
源代码                     TCP状态机            应用层
客户端shutdown         →  发送FIN             → Server收到FIN
但不close                  (进入FIN-W2)          (进入CLOSE-W)
        ↓                      ↓                      ↓
  程序继续运行          等待Server FIN      Server等待应用close
    但不close                (无超时)          (永久等待!)
        ↓                      ↓                      ↓
  Server CLOSE_WAIT     一直停在FIN-W2      泄漏!占用fd
  堆积
        ↓
  排查:代码review
  找到所有conn.recv()=0的分支
  确保每个分支都调用close()

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

问题1:高并发下端口耗尽

现象:新连接报错"Cannot assign requested address"
原因:TIME_WAIT占用大量临时端口
解决:
  1. 开启tcp_tw_reuse
  2. 使用连接池复用连接
  3. 让服务器主动关闭(客户端在LAST_ACK不占端口)

问题2:CLOSE_WAIT堆积

现象:ss显示大量CLOSE_WAIT
排查:
  1. lsof -p <pid> | grep TCP  找到泄漏的fd
  2. 代码review,检查所有recv()返回0的分支
  3. 确保finally块中调用close()

📚 总结与实践建议

核心认知映射

应用层API                内核行为                对端感知                网络抓包
sock.close()        →  tcp_close()        →  收到FIN           →  [FIN]
(立即返回)            (发送FIN包)          (进入CLOSE_WAIT)      [ACK]
                                                                         [FIN]
                                                                         [ACK]

sock.shutdown(SHUT_WR) →  tcp_shutdown()    →  收到FIN           →  [FIN]
(立即返回)            (发送FIN,保留socket) (进入CLOSE_WAIT)    [ACK]
(仍可recv)            (可继续recv)         (仍可发送数据)       [P.] 数据
                                                                     [FIN]
                                                                     [ACK]

最佳实践清单

✅ 推荐做法

# 1. 优雅关闭流程
def graceful_close(sock):
    sock.shutdown(socket.SHUT_WR)  # 发送FIN
    while True:
        data = sock.recv(4096)
        if not data:
            break  # 对端也发送FIN了
    sock.close()  # 现在安全关闭

# 2. 异常处理确保close
try:
    # ... 业务逻辑
finally:
    if sock:
        sock.close()

# 3. 心跳检测半关闭
def send_with_heartbeat(sock, data, interval=30):
    sock.send(data)
    while True:
        time.sleep(interval)
        try:
            sock.send(b"[HEARTBEAT]")
        except:
            print("连接已断开")
            break

❌ 避免陷阱

# 陷阱1: 直接close丢失数据
sock.send(data)
sock.close()  # 发送缓冲区数据可能丢失

# 陷阱2: 忘记close导致CLOSE_WAIT
data = sock.recv(1024)
if not data:
    return  # 忘记close()!
# 应该:sock.close()

# 陷阱3: 双方同时close导致RST
# Client:
sock.close()
# Server同时:
sock.close()  # 可能收到RST

🔄 可迁移性评估

罗塞塔石碑实验法可应用到任何网络协议学习:

案例1:HTTP/1.1持久连接

关卡数据转换对象关键问题主要观测工具
关卡1 HTTP层Connection: keep-alive如何复用连接?curl -v
关卡2 TCP层多个请求共用一个TCP如何复用socket?ss -tan
关卡3 抓包多个HTTP请求/响应没有FIN包?Wireshark

案例2:WebSocket升级握手

关卡数据转换对象关键问题主要观测工具
关卡1 HTTP层HTTP Upgrade请求如何升级到WebSocket?浏览器DevTools
关卡2 TCP层TCP连接保持连接不断开?ss -tan
关卡3 抓包HTTP → WebSocket帧101 Switching ProtocolsWireshark

记住:罗塞塔石碑方法的核心是对照。通过已知的代码和未知的底层输出,建立映射关系,就能破译任何黑盒系统。

现在,你已经理解了TCP Socket关闭机制的底层原理,开始设计你的优雅关闭方案吧!