跳到内容

第 4 卷 / 第 3 章 / 第 1 课

3.1 TCP 连接建立、关闭与 TIME_WAIT ​

Socket 让程序联系另一端,常规握手会先交换初始序号并确认连接状态,再正常交付应用数据。连接结束时,两个方向又可能先后关闭。诊断工具里于是出现 SYN-SENT、CLOSE-WAIT、TIME-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。两端进入 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 等选项还会改变应用可见时机。

服务端:

  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,也可能是中间设备主动拒绝;单凭 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 表示“这个方向不会再有新字节”,不表示它拒绝接收:

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。

把序号代入第 2 章的请求和响应:客户端 ISN 为 1000,服务端为 7000,两边各传 12 字节。下面的 Python 3 计算只追踪序号,不发送报文:

python
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 7014

ACK-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:

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

参考 ​

Built with VitePress | Software Systems Atlas