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。握手因此确认双向路径与双方序号状态;把它简化成“第三次只为防历史连接”会漏掉协议状态建立。
最后一个 ACK 可以携带应用数据。TCP Fast Open 又允许在满足额外条件时更早携带数据,但会引入重放和中间盒兼容边界,应用不能假设首次数据天然只执行一次。
3. connect 和 accept 何时返回
blocking connect 通常等待握手成功或错误;nonblocking connect 常先返回 in-progress,之后由 writable/error readiness 提示应用检查最终 SO_ERROR。
服务端:
- listening endpoint 收到 SYN;
- 内核创建或编码半连接状态并回复 SYN-ACK;
- 收到有效 ACK 后,连接进入可供
accept的队列; - 应用调用
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 表示“这个方向不会再有新字节”,不表示它拒绝接收:
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:
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 还原成可靠字节流。