跳到内容

3.1 TCP 连接建立、关闭与 TIME_WAIT

Socket 让程序能敲响另一座信标塔的门,但第一个字节送出前,两端还要确认彼此正在场,并交换各自的初始序号。连接结束时,两个方向又可能先后关闭。驿道观测镜里于是出现 SYN-SENTCLOSE-WAITTIME-WAIT 等状态;只背“四次挥手”解释不了它们为什么停在那里。

“一根连接管道”仍是有用的直觉,但不够精确。TCP 的两个 endpoint 各自维护序号空间、收发窗口、重传计时、已确认范围和关闭进度。三次握手让双方的初始序号与连接状态达成一致,FIN 则只关闭一个发送方向。

这一课沿着连接从敲门到撤场的时间线前进。下一课再打开一封正在传输的信,看序号、ACK 与流量控制怎样把会丢、会乱序的 IP packet 还原成有序字节流。

1. 一条连接由什么标识

常见 TCP flow 用四元组标识:

text
source IP, source port, destination IP, destination port

协议号 TCP 常一起构成网络设备里的五元组。服务器可以让大量连接共享同一个 local port,因为每条连接的 remote address/port 不同。

NAT、代理和负载均衡会改写或终止 flow,所以客户端、边缘代理和后端看到的四元组可能不同。应用 request ID 比 socket 四元组更适合跨代理追踪业务请求。

2. 三次握手交换两套序号空间

客户端主动打开:

text
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。握手因此确认双向路径与双方序号状态;把它简化成“第三次只为防历史连接”会漏掉协议状态建立。

最后一个 ACK 可以携带应用数据。TCP Fast Open 又允许在满足额外条件时更早携带数据,但会引入重放和中间盒兼容边界,应用不能假设首次数据天然只执行一次。

3. connect 和 accept 何时返回

blocking connect 通常等待握手成功或错误;nonblocking connect 常先返回 in-progress,之后由 writable/error readiness 提示应用检查最终 SO_ERROR

服务端:

  1. listening endpoint 收到 SYN;
  2. 内核创建或编码半连接状态并回复 SYN-ACK;
  3. 收到有效 ACK 后,连接进入可供 accept 的队列;
  4. 应用调用 accept 取得 connected socket。

实现可能分别管理未完成握手与已完成待 accept 状态,backlog 又受内核配置约束。看到 SYN-ACK 不代表应用已经 accept;看到握手完成也不代表业务线程已开始读取。

4. 握手失败怎样出现在包里

SYN 重传后超时。 常见于请求或回包被丢弃、路由黑洞、防火墙静默丢包。仅凭客户端 timeout 不知道丢在哪一方向。

立即 RST。 目标主机到达,但该 endpoint 无 listener,或中间设备主动拒绝。应用通常看到 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/2本端主动结束发送,等待对端确认/结束
CLOSE-WAIT已收到对端 FIN,本地应用尚未 close 发送方向
LAST-ACK被动关闭方已发 FIN,等待最终 ACK
CLOSING双方 FIN 交叉的关闭状态
TIME-WAIT主动关闭路径保留状态,吸收旧 segment 并处理末尾重传

状态名属于 TCP endpoint,不是整个应用的唯一状态。一个进程可同时拥有数千条不同状态连接。

6. FIN 只关闭一个方向

两座塔的对话快结束时,先说完的一方可以封住自己的发送口,同时继续听完对面的回信。这个驿道画面对应 half-close,不是 TCP 额外创造的“第四种关闭”。

客户端发送 FIN 表示“这个方向不会再有新字节”,不表示它拒绝接收:

text
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。

7. CLOSE_WAIT 多通常是应用生命周期问题

进入 CLOSE-WAIT 表示:

  • 对端已结束发送;
  • 本端内核已经把 EOF 提供给应用;
  • 本端应用尚未关闭自己的方向。

少量短暂 CLOSE_WAIT 正常。数量持续增长常见原因是:

  • 读到 EOF 后漏掉 close;
  • exception/cancel 路径没有释放 socket;
  • worker 卡在下游操作,连接清理排队;
  • fd 被其他对象或线程继续引用;
  • TLS/protocol 状态机等待一个永远不会来的应用事件。

调整 TCP timeout 不能替应用补 close。应从 fd 所有权与调用栈查起。

8. TIME_WAIT 是撤场后的守候

门已经关了,最后确认仍可能丢在路上,旧信函也可能迟到。主动关闭的一侧不能立刻忘掉这次对话,否则重传的 FIN 无人回应,相同四元组的新连接还可能误收旧 segment。

因此,通常发送最终 ACK 的主动关闭一侧进入 TIME_WAIT,保留约 2×MSL 的协议状态。主要作用包括:

  • 若最终 ACK 丢失,对端会重传 FIN,本端仍能再次 ACK;
  • 让同一四元组的旧 duplicate segment 在网络中过期,避免污染后来复用的连接。

具体持续时间是实现策略,不能把某个 Linux 数字写成 TCP 永久常量。TIME_WAIT 不表示应用进程仍持有 connected fd,也不等于“连接泄漏”。

大量短连接会产生大量 TIME_WAIT,并可能和 client ephemeral port、NAT table 或负载均衡状态容量共同成为限制。优先使用连接复用、合理的源地址/端口容量和代理设计,不要先用危险 sysctl 强行缩短协议保护。

9. RST 是异常终止

RST 会让对端立即放弃连接状态,未读数据和应用边界可能丢失。常见来源:

  • 连接到无 listener 的 port;
  • endpoint 不存在或状态不匹配;
  • 应用以特定 SO_LINGER 配置 abortive close;
  • 进程关闭时仍有未读入站数据,某些栈选择 reset;
  • 中间盒伪造/生成 reset。

“调用 close 后 send 成功返回”仍不代表对端应用处理;可靠业务协议需要 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:

bash
ss -lnt
ss -ntp
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 还原成可靠字节流。

参考

Built with VitePress | Software Systems Atlas