3.1 TCP 连接建立、关闭与 TIME_WAIT
Socket 让程序联系另一端,常规握手会先交换初始序号并确认连接状态,再正常交付应用数据。连接结束时,两个方向又可能先后关闭。诊断工具里于是出现 SYN-SENT、CLOSE-WAIT、TIME-WAIT 等状态;只背“四次挥手”解释不了它们为什么停在那里。
“一根连接管道”仍是有用的直觉,但不够精确。TCP 的两个 endpoint 各自维护序号空间、收发窗口、重传计时、已确认范围和关闭进度。三次握手让双方的初始序号与连接状态达成一致,FIN 则只关闭一个发送方向。
这一课沿着连接建立到释放的时间线前进。下一课看序号、ACK 与流量控制怎样把会丢、会乱序的 IP packet 还原成有序字节流。
1. 一条连接由什么标识
常见 TCP flow 用四元组标识:
source IP, source port, destination IP, destination port协议号 TCP 常一起构成网络设备里的五元组。服务器可以让大量连接共享同一个 local port,因为每条连接的 remote address/port 不同。
NAT、代理和负载均衡会改写或终止 flow,所以客户端、边缘代理和后端看到的四元组可能不同。应用 request ID 比 socket 四元组更适合跨代理追踪业务请求。
2. 三次握手交换两套序号空间
客户端主动打开:
Client Server
| |
| SYN, seq = x |
|---------------------------------------->|
| |
| SYN + ACK, seq = y, ack = x + 1 |
|<----------------------------------------|
| |
| ACK, ack = y + 1 |
|---------------------------------------->|
| ESTABLISHED |每个方向独立选择 initial sequence number(ISN)。SYN 本身占用一个 sequence number,所以确认值是 ISN + 1。
第三个 ACK 让服务器确认客户端收到了服务器的 SYN 和 ISN。两端进入 ESTABLISHED 的时刻并不相同:客户端处理有效 SYN-ACK 后即可进入,服务器通常要等有效最终 ACK。这个交换建立序号和连接状态,也能排除部分旧连接报文;它不保证路径以后一直可用,更不提供身份认证。
最后一个 ACK 可以携带应用数据。TCP Fast Open 又允许在满足额外条件时更早携带数据,但会引入重放和中间盒兼容边界,应用不能假设首次数据天然只执行一次。
3. connect 和 accept 何时返回
blocking connect 通常等待握手成功或错误;nonblocking connect 常先返回 in-progress,之后由 writable/error readiness 提示应用检查最终 SO_ERROR。
因此客户端 connect 成功不证明服务器应用已 accept,甚至不能证明第三个 ACK 已到达服务器。下面的队列路径描述普通 TCP;Fast Open、deferred accept 等选项还会改变应用可见时机。
服务端:
- listening endpoint 收到 SYN;
- 内核创建或编码半连接状态并回复 SYN-ACK;
- 收到有效 ACK 后,连接进入可供
accept的队列; - 应用调用
accept取得 connected socket。
实现可能分别管理未完成握手与已完成待 accept 状态,backlog 又受内核配置约束。看到 SYN-ACK 不代表应用已经 accept;看到握手完成也不代表业务线程已开始读取。
4. 握手失败怎样出现在包里
SYN 重传后超时。 常见于请求或回包被丢弃、路由黑洞、防火墙静默丢包。仅凭客户端 timeout 不知道丢在哪一方向。
立即 RST。 可能是目标 endpoint 无 listener,也可能是中间设备主动拒绝;单凭 RST 不能证明目标应用主机收到了 SYN。应用通常看到 connection refused。
SYN-ACK 到达但最后 ACK 丢失。 服务器会重传 SYN-ACK;客户端再次确认。协议依靠重传容忍这种丢包。
地址选错。 DNS 返回多地址,第一候选不可达而应用没有及时尝试其他地址族。连接策略应与 Happy Eyeballs 和总 deadline 配合。
抓包要在两端对照:客户端发出 SYN 不代表服务器收到,服务器发出 SYN-ACK 也不代表客户端或其内核接受。
5. 连接状态不是一条简单直线
常见状态:
| 状态 | 含义 |
|---|---|
LISTEN | 等待入站连接 |
SYN-SENT | 主动打开后等待 SYN/ACK |
SYN-RECEIVED | 已收 SYN 并发送 SYN-ACK |
ESTABLISHED | 双方握手完成,可传数据 |
FIN-WAIT-1 / FIN-WAIT-2 | 已发 FIN;前者尚待本端 FIN 的 ACK,后者已获 ACK、等待对端 FIN |
CLOSE-WAIT | 已收到对端 FIN,本地应用尚未 close 发送方向 |
LAST-ACK | 被动关闭方已发 FIN,等待最终 ACK |
CLOSING | 本端 FIN 尚未被确认时已收到对端 FIN |
TIME-WAIT | 主动关闭路径保留状态,吸收旧 segment 并处理末尾重传 |
状态名属于 TCP endpoint,不是整个应用的唯一状态。一个进程可同时拥有数千条不同状态连接。
6. FIN 只关闭一个方向
客户端发送 FIN 表示“这个方向不会再有新字节”,不表示它拒绝接收:
Client Server
| FIN ----------------------->| client send direction ends
|<----------------------- ACK |
|<----------- response data |
|<----------------------- FIN | server send direction ends
| ACK ----------------------->|这就是 half-close。协议可以让客户端发完请求后 shutdown(SHUT_WR),继续读服务器响应。
所谓“四次挥手”是常见时间线,不是固定必须四个独立 packet。ACK 可以与数据或 FIN 合并,双方也可能同时关闭。
FIN 也占用一个 sequence number,并且只有此前所有字节按序到达后,接收方才向应用呈现 EOF。
把序号代入第 2 章的请求和响应:客户端 ISN 为 1000,服务端为 7000,两边各传 12 字节。下面的 Python 3 计算只追踪序号,不发送报文:
MOD = 1 << 32
client_isn, server_isn = 1000, 7000
client_next = (client_isn + 1) % MOD
server_next = (server_isn + 1) % MOD
assert (client_next, server_next) == (1001, 7001)
client_next = (client_next + len(b"hello socket")) % MOD
client_fin = client_next
ack_client_fin = (client_fin + 1) % MOD
server_next = (server_next + len(b"HELLO SOCKET")) % MOD
server_fin = server_next
ack_server_fin = (server_fin + 1) % MOD
print(client_fin, ack_client_fin) # 1013 1014
print(server_fin, ack_server_fin) # 7013 7014ACK-only segment 不消耗序号,所以服务端确认请求 FIN 后,响应的首字节仍从 7001 开始;客户端确认服务端 FIN 时,自己的序号已到 1014。抓包工具若显示相对序号,数值会减去各方向选定的基准,不能直接和这里的绝对数比较。
7. CLOSE_WAIT 多通常是应用生命周期问题
进入 CLOSE-WAIT 表示:
- 对端已结束发送;
- 本端内核已处理对端 FIN,应用读完已缓存字节后会得到 EOF;
- 本端应用尚未关闭自己的方向。
少量短暂 CLOSE_WAIT 正常。数量持续增长常见原因是:
- 读到 EOF 后漏掉 close;
- exception/cancel 路径没有释放 socket;
- worker 卡在下游操作,连接清理排队;
- fd 被其他对象或线程继续引用;
- TLS/protocol 状态机等待一个永远不会来的应用事件。
调整 TCP timeout 不能替应用补 close。应从 fd 所有权与调用栈查起。
CLOSE_WAIT 期间本端仍可发送响应;协议若有意允许长时间半关闭,这个状态本身不是错误。要看数量、停留时间和应用本来应当结束会话的时机。
8. TIME_WAIT 为什么保留状态
连接已经关闭,最后一个确认仍可能丢失,旧 segment 也可能迟到。主动关闭的一侧不能立刻丢弃状态,否则无法回应重传的 FIN,相同四元组的新连接还可能误收旧 segment。
因此,通常发送最终 ACK 的主动关闭一侧进入 TIME_WAIT,保留约 2×MSL 的协议状态。主要作用包括:
- 若最终 ACK 丢失,对端会重传 FIN,本端仍能再次 ACK;
- 让同一四元组的旧 duplicate segment 在网络中过期,避免污染后来复用的连接。
具体持续时间是实现策略,不能把某个 Linux 数字写成 TCP 永久常量。TIME_WAIT 不表示应用进程仍持有 connected fd,也不等于“连接泄漏”。
同时关闭时,两端都可能进入 TIME_WAIT;异常 reset 则可能走另一条释放路径。这里的 2×MSL 是协议等待规则,MSL 是最大 segment 生存时间模型,不是测得的 RTT。
大量短连接会产生大量 TIME_WAIT,并可能和 client ephemeral port、NAT table 或负载均衡状态容量共同成为限制。优先使用连接复用、合理的源地址/端口容量和代理设计,不要先用危险 sysctl 强行缩短协议保护。
9. RST 是异常终止
已建立连接收到并接受有效 RST 后会中止,未完成的读写可能报错,应用消息也可能不完整。RST 仍须通过与状态、序号等有关的检查;不是抓到任意 RST 都能断定它已被内核接受。常见来源:
- 连接到无 listener 的 port;
- endpoint 不存在或状态不匹配;
- 应用以特定
SO_LINGER配置 abortive close; - 进程关闭时仍有未读入站数据,某些栈选择 reset;
- 中间盒伪造/生成 reset。
本端 send 成功后再 close,并不确认对端应用已处理数据;本端尚未获知对端关闭时,某次发送也可能先成功。可靠业务协议需要 response 或 application acknowledgment。
SO_LINGER 的语义跨平台且容易导致阻塞 close 或 RST,不应作为“加速释放端口”的通用技巧。
10. Keepalive 与应用心跳
TCP keepalive 是内核层空闲连接探测,默认是否启用、空闲多久开始、重试间隔与次数都依系统和 socket 配置。它适合回收长期静默的失效 peer,但时间尺度可能不符合业务故障检测。
应用 heartbeat 可以携带协议状态并使用业务 deadline,却会增加流量和误判风险。若 event loop 卡住、网络短暂抖动或 GC 暂停,心跳超时不一定表示节点永久失效。
两者都不能替代请求级 timeout。连接“仍活着”不表示某个请求会按时完成。
11. SYN flood 与状态资源
攻击者可以发送大量 SYN 而不完成握手,消耗半连接状态与回复带宽。防护可能包括:
- SYN cookies 或类似无状态编码;
- 合理队列与速率限制;
- 边缘清洗/负载均衡;
- 源验证和网络侧策略;
- 监控 SYN、SYN-ACK、完成握手与 accept 的比例。
SYN cookies 以编码状态换取部分连接选项/能力约束,具体实现随系统演进。不要把“打开 cookies”当作容量规划和 DDoS 防护的全部。
12. 把网络路径诊断工具对准内核
Linux 诊断先从 ss 和 packet capture 入手。前者读本机 socket 状态,后者记录某个 capture point 经过的 segment;两种证据各缺一半,必须按时间对齐。
Linux 可先查看本机 socket:
ss -lnt
ss -ntipo
ss -s字段与权限依版本变化。关键是把:
- local/peer endpoint;
- TCP state;
- process/fd;
- queue;
- timer/retransmission;
和抓包时间线对应起来。
ss 只能看本机内核状态。经代理后,前端连接和后端连接是两条不同 TCP flow,必须分别观察。
13. 常见误解
“三次握手是验证用户名。” 它建立 TCP 序号与连接状态,不提供应用身份认证。
“收到 ACK 表示应用处理了数据。” TCP ACK 表示接收协议栈确认字节范围,不是业务提交。
“close 会同时关闭双向传输。” 普通 close 最终释放本地引用;协议层两个方向可以分别 FIN。
“TIME_WAIT 都在服务器。” 哪一侧执行主动关闭路径更关键,架构和协议会改变分布。
“只要开 keepalive 就没有僵尸连接。” keepalive 参数、网络状态和应用 deadline 都影响检测时机。
14. 小结
TCP 生命周期是一台双向状态机:
- 三次握手交换并确认两套初始序号;
- listen、半连接、待 accept 与应用处理是不同阶段;
- FIN 只结束一个发送方向,四个 packet 不是固定公式;
- CLOSE_WAIT 常指向本地应用未完成清理;
- TIME_WAIT 保护最终 ACK 重传与旧 segment 隔离;
- RST 是异常终止,不能保证应用消息完成;
- keepalive、heartbeat 与 request deadline 解决不同层次的问题。
连接已经完成建立、数据交换和释放。下一节进入3.2 序号、ACK 与流量控制,看 ESTABLISHED 连接如何把会丢、会乱序的 IP packet 还原成可靠字节流。