A 不再发送,S 还能回复吗?
A 向 S 发出 PING,然后调用 shutdown(SHUT_WR),表示自己的发送方向结束。S 读完请求,接着读到 EOF,却仍能发回 PONG;A 也仍能收到这份回复。
TCP 的两个发送方向可以分别结束:FIN 表示本方向不再有新字节,不要求对方立刻停止发送。 因此“收到 FIN”和“整条连接已经完全关闭”不能画等号。
上一章用 EOF 让短实验退出;本章把它变成一次完整的半关闭(Half-Close)交流,再用报文和状态说明协议如何善后。实验验证 API 行为;图中的序列号和报文划分是教学设定,没有抓包测量。
先跑通半关闭,再看关闭过程
本地实验使用 Python 3.10+ 标准库、IPv4 回环地址与系统随机端口。实际验证环境为 Python 3.13.3 / macOS:
python3 examples/network-interview/user_code/half_close.pyA 与 S 两个已连接 socket 由 core/transport.py 的 tcp_pair() 建立,每次 socket 等待最多 3 秒,退出上下文时关闭端点。公开入口的核心交流如下:
with tcp_pair() as (a, s):
a.sendall(b"PING")
a.shutdown(socket.SHUT_WR)
request = read_to_eof(s)
assert request == b"PING"
s.sendall(b"PONG")
s.shutdown(socket.SHUT_WR)
response = read_to_eof(a)
assert response == b"PONG"read_to_eof(endpoint) 在正数读取上限下反复接收,保留返回的数据,直到 recv 返回 b''。S 先拿到完整的 PING,再观察到 A 结束发送;它没有因此失去自己的发送能力。实验最终结果为:
PASS S read PING then EOF from A
PASS S can send PONG after peer EOF
PASS A read PONG then EOF from S
PASS A cannot reopen its write direction
PASS half-close exchange and endpoint cleanup这里 shutdown(SHUT_WR) 保留读取能力;普通 socket close() 则释放这个 socket 对象的使用入口,不能再拿该对象接收剩余回复。具体 socket API 与 TCP 规范中的抽象 CLOSE 操作要分开读。Python 3.13 socket.shutdown / close
实验还验证 A 无法在已关闭的写方向上继续写。没有模拟 FIN 丢失、RST、内核状态定时,也没有测量 TIME_WAIT 的持续时间;这些机制以下按规范解释。RFC 9293 §3.6.1
FIN 怎样排在最后一个字节之后?
设 A 的 PING 四字节从 Seq=1001 开始,占 [1001, 1005),已被 S 确认。S 此时下一发送序列号为 7001,尚未发送 PONG。
FIN 也占一个序列位置。因此 A 的 FIN 使用 Seq=1005,S 确认它时 Ack=1006。FIN 排在前面的数据之后;若前面还有缺口,不能跳过缺口把这个方向当作完整结束。
下面固定 A 先结束,S 先单独确认、稍后回复,再结束自己的方向:
查看 Mermaid 源码
sequenceDiagram
participant A as A(先结束发送)
participant S as S(继续回复)
Note over A,S: PING 已确认:A 下一 Seq=1005,S 下一 Seq=7001
Note over A: FIN_WAIT_1
A->>S: FIN,ACK Seq=1005 Ack=7001
Note over S: CLOSE_WAIT
S->>A: ACK Seq=7001 Ack=1006
Note over A: FIN_WAIT_2
S->>A: PONG:Seq=7001,4 字节,Ack=1006
Note over S: 应用结束发送,进入 LAST_ACK
S->>A: FIN,ACK Seq=7005 Ack=1006
Note over A: TIME_WAIT
A->>S: ACK Seq=1006 Ack=7006
Note over S: CLOSED
Note over A: TIME_WAIT 等待结束后 CLOSED文字回退:A 的 FIN 结束 A → S;S 确认后仍发送 PONG。S 的 FIN 结束 S → A,A 确认后保留 TIME_WAIT 状态。本图省略 PONG 的单独确认,最后 Ack=7006 同时累计确认其四字节与 FIN;双方序列空间独立。
A 的典型状态路线是 ESTABLISHED → FIN_WAIT_1 → FIN_WAIT_2 → TIME_WAIT → CLOSED。S 则为 ESTABLISHED → CLOSE_WAIT → LAST_ACK → CLOSED。下划线是常见工具显示写法,规范中对应 FIN-WAIT-1 等名称;本图不包含所有关闭分支。RFC 9293 §3.3.2、§3.6
为什么说“通常四次”,不是“一定四个包”?
图里有四份关闭控制报文,中间另有一份 PONG 数据报文。四个逻辑动作分别是 A 声明结束、S 确认、S 声明结束、A 确认。
收到 A 的 FIN 时,S 可以立即确认,但应用可能还要发送响应。确认对方结束与结束自己发送是两件事,所以它们常常分开发生。 这也是不能把关闭过程照搬成握手流程的原因。
如果 S 已经没有待发数据且决定关闭,它对 A 的确认与自己的 FIN 可以同处一份报文;若双方同时结束,则还可能经过 CLOSING,最终双方进入 TIME_WAIT。再加上重传、与数据合并,实际抓到的报文数不固定。应复述两个方向各自结束并被确认,避免死记“四个包”。
关闭数据通道也不会自动移除服务器原来的监听入口。图中的状态属于这一条已连接对象,S 的 listener 仍可以服务其他连接。
最后一份 ACK 丢了,谁来补这个善后?
继续本例:S 发出 FIN 后处于 LAST_ACK。如果 A 最后的 ACK 丢失,S 的 FIN 仍未确认,于是可以按重传机制再发 FIN。
A 已不再收发新业务数据,但保留 TIME_WAIT 时还能回答重复 FIN,再给出 Ack=7006,并重新开始相关等待。若 A 立刻删除所有旧连接状态,处理这份迟到 FIN 就失去了正常关闭所需的上下文。RFC 9293 §3.10.7.4
这里重传的是未被确认的 FIN。纯 ACK 不因“等自己的确认超时”而独立重传,恢复由对方重传 FIN 后再次触发 ACK。TIME_WAIT 为这种善后保留机会,不承诺在永久断网下对方也一定成功关闭。
另一项任务是让旧连接的迟到或重复报文失效,减少相同地址/端口组合很快再次使用时对新连接的干扰。新旧报文仍有序列号等验证规则,TIME_WAIT 是保护的一部分,不是“否则任何旧包都必然被当作新包”。RFC 1337 专门讨论了过早结束 TIME_WAIT 带来的旧报文风险;它属于 Informational,不作为现代操作系统默认参数表。RFC 1337
动手追踪半关闭与最后确认
先观察 A 的 FIN 之后 S 如何发送 PONG,再切换最后 ACK 丢失:谁重传、谁等待、TIME_WAIT 怎样参与善后?
PING已经送达。逐步看两个方向的FIN、PONG和双方状态,再切换最后确认丢失。
ESTABLISHED
A → S:仍打开ESTABLISHED
S → A:仍打开PING:[1001,1005),S 下一 Seq=7001
两个发送方向仍打开。下面固定A先结束,S先确认再回复,最后结束自己的发送方向。
报文与状态为普通顺序关闭教学轨迹,不是抓包;省略PONG单独确认、丢包计时与重试上限。FIN可与其他动作合并,同时关闭有不同分支。真实半关闭API实验验证PONG与EOF,不测TIME_WAIT秒数,也不证明业务完成。
2MSL 是什么,为什么不能回答成固定 60 秒?
MSL(Maximum Segment Lifetime)表示报文段最大生存时间这一协议假设。正常主动关闭需要保留 TIME_WAIT,规范以 2 × MSL 描述等待期:既照顾关闭末尾的往返善后,也让旧报文有机会消失。RFC 9293 §3.6.1
它不是当前测得的 RTT,也不能直接把 IP 的 TTL 数值乘二得到。具体系统的等待时间、符合条件的连接重新打开和时间戳优化,要核对实现版本及配置;不能把教程里的 60 秒或 2 分钟当作所有平台常量。
假设某环境的等待期为 60 秒,稳定每秒有 100 条连接进入 TIME_WAIT,且没有其他提前退出或定时重启变化,那么其数量量级约为 100 × 60 = 6000。这是给定假设下的存量估算,不是本机测量或默认值。
大量 TIME_WAIT 可能来自连接周转快,并不直接证明应用忘记 close。应用对象/描述符释放后,内核仍可能保留这些状态。若观察到资源压力,应先核对连接创建率、关闭方、临时端口与目标分布,再评估合理复用连接;不要只为减少一个计数就抹掉关闭保护。
CLOSE_WAIT 与 FIN_WAIT_2:分别在等谁?
相同的“等”,可能等待完全不同的动作:RFC 9293 §3.3.2
| 本端状态 | 本端已经完成的事 | 还在等什么 |
|---|---|---|
| FIN_WAIT_1 | 已发出本端 FIN | 本端 FIN 的确认,或对方结束;后续分支取决于到达顺序 |
| FIN_WAIT_2 | 本端 FIN 已被确认 | 对方的 FIN,本端仍可收剩余数据 |
| CLOSE_WAIT | 已收到对方 FIN 并确认 | 本地应用结束自己的发送方向 |
| LAST_ACK | 被动关闭侧已发出自己的 FIN | 对方确认这个 FIN |
| TIME_WAIT | 已确认对方 FIN,正常关闭进入善后 | 等待期结束,期间可响应重复 FIN |
如果服务进程的 CLOSE_WAIT 持续增加,先追踪本地应用为何没有结束:是否仍在合法地生成响应、读到 EOF 后是否遗漏退出和释放、异常分支是否遗留 socket。正常业务处理也会短暂处于此状态,单次快照不是泄漏证明。
一条可验证的处理路径是:按连接定位拥有它的进程与对象 → 追踪 EOF、写完响应、异常与取消的生命周期 → 补齐结束动作 → 用同样的连接负载验证状态能够退出、对象和描述符能够回收。仅调整 TIME_WAIT 时间不会替应用发出缺失的 FIN。
反过来,本例 A 如果已经进入 FIN_WAIT_2,而 S 一直没有结束,A 等的是远端 FIN。此时应检查 S 的应用状态和双方协议是否都在等对方动作。系统可能有适用条件不同的清理策略,业务也要规定等待期限;不能把这类等待一律诊断为 A 忘记 close。
RST 与 FIN:中止不等于正常收尾
FIN 表达本方向数据结束,正常流程保留前面数据的交付和确认;RST 用于复位或中止,一般不走上面那条完整的 FIN 路线。未建立连接时收到不符合状态的报文、或者本地明确中止,都可能触发复位。接收端需要按状态和序列等规则检查 RST,不是任意带 RST 的报文都有效。RFC 9293 §3.5.2、§3.5.3
对应用而言,正常读取可以先返回剩余字节,再看到 EOF;复位则可能表现为连接错误。具体错误在何时、哪次调用暴露取决于系统与缓存状态,不能断言每次都立即得到同一个 errno。OpenBSD recv(2)
“调用 close 一定发送 FIN”和“进程崩溃一定发送 RST”也过于绝对。未读数据、关闭选项、系统清理路径,以及整机掉电还是只有进程结束,都会影响结果。对端突然消失时更可能暂时什么都收不到,需要后续数据、重传或探测才发现异常。
不论 FIN 还是 RST,都不能仅据此判断 PING 对应业务是否已经执行。连接状态解决传输生命周期,业务结果需要应用协议表达。
没有数据、也没有 FIN,Keepalive 在看什么?
A 长时间空闲,S 所在主机断电,网络没有送来 FIN 或 RST。A 的连接状态可能仍显示 ESTABLISHED;这个名字不等于实时证明对方可用。
TCP Keepalive 可在空闲连接上发探测,引出对端 TCP 的响应,并按失败策略判断失效。规范要求它可按连接开启/关闭,默认关闭;若提供,默认空闲间隔不小于两小时。实际应用可以配置更短参数,须核对目标栈,不能把两小时写成任意部署的现状。RFC 9293 §3.8.4
一份探测没有响应,也可能只是丢包;不能据单次缺失就断言对端已死。即使 TCP 及时回应,也不证明服务应用正在消费消息、数据库正常或业务线程没有卡住。
因此三种机制职责不同:零窗口探测寻找接收窗口重新打开;TCP Keepalive 检查空闲传输连接;应用心跳与请求超时按业务需要判断服务进展。若 S 的业务线程卡住而 TCP 栈仍工作,Keepalive 通过也不能替代应用层检查。
30—60 秒参考回答
TCP 是双向通道,两边可以分别结束发送。FIN 表示本方向不再有新数据,确认对方 FIN 与自己发 FIN 是不同动作,所以常见关闭有四份控制报文,但可以合并,也有同时关闭等分支。正常先关闭的一方通常进入 TIME_WAIT,既能再次确认重传 FIN,也减少旧报文干扰新连接;2MSL 不是所有系统的固定秒数。CLOSE_WAIT 在等本地应用结束,长期累积要查应用生命周期。RST 是中止,Keepalive 则检查空闲连接,都不能直接证明业务完成。
四个追问及解析
1. 收到 FIN 后,recv 一定马上返回零吗?
不一定。FIN 前的已接收数据仍可先交给应用,应用读完后才观察到 EOF。本例 S 先读 PING 再读 EOF;不能丢掉缓冲数据,也不能用一次 EOF 反推请求是否被业务成功处理。
2. TIME_WAIT 一定在客户端吗?
不是。本例由 A 先结束,A 进入 TIME_WAIT;服务器也可以先结束。双方同时关闭还可能都进入 TIME_WAIT。主动建立连接和主动关闭是两种角色,不能用“客户端/服务器”名称直接决定关闭状态。
3. 最后一份 ACK 丢失,A 的 ACK 超时器会重发吗?
这里没有等待 ACK 自身被确认的独立重传。S 的未确认 FIN 重传后,TIME_WAIT 中的 A 再次确认,相关等待重启。如果网络一直不可用,仍可能无法正常完成善后。
4. Keepalive 成功,是不是应用心跳就不需要了?
不一定。对端内核回应探测时,应用可能仍卡在处理请求。若业务需要秒级进展判断,应该明确应用心跳、请求完成期限或健康语义;根据所需判断选择机制,而不是只问 TCP 有没有连着。
两个易错辨析
“CLOSE_WAIT 多,把 TIME_WAIT 调短就能解决。” 两者等待对象不同。CLOSE_WAIT 是本地应用尚未完成结束动作,应先查 EOF、写完响应与异常清理;TIME_WAIT 的善后定时不替代这些动作。
“半关闭就是半开连接。” 半关闭是一个方向已正常结束、另一个还能发送;半开(Half-Open)通常指双方对连接是否存在的状态不一致,例如一端崩溃后丢失状态。它们不是同一种正常状态。RFC 9293 §3.5.1
到这里,建立、传输、分帧和结束都能按事件解释了。但如果请求发出后只看到超时或断开,是否可以放心重试?下一章将 TCP 的交付范围与业务结果分开,解释超时、幂等和连接复用。
本章核验资料
核验日期:2026-10-09。实验只测半关闭 API 交流;报文编号、等待期与连接数算例是教学设定。
- RFC 9293:关闭状态、FIN/RST、TIME_WAIT、Keepalive 与半开连接。
- RFC 1337:TIME_WAIT 提前结束的旧报文风险,Informational。
- Python 3.13 socket:shutdown、close、recv 与超时。
- OpenBSD shutdown(2)、recv(2):该系统的方向关闭和读取错误语义。