可写却连接失败:非阻塞连接建立
一句话总结
写就绪并不是“连接成功”的印章。在连接进行中的状态下观察到完成就绪之后,只读取一次 SO_ERROR,并把最初的结果保存到应用状态中。
为什么需要它
如果只测试服务器接受连接的示例,那么一收到 EVENT_WRITE 就标记为 connected 的代码也能通过。把同样的代码用在已关闭的端口上,即使连接被拒绝,也会亮起绿灯。这是因为把“不必再等待了”与“成功了”当成了同一个意思。只有具备“失败也会产生完成事件”这一观点,才能相信诊断工具给出的成功率。
这里要构建的是一个汇总多个目标的 TCP 连接建立结果的诊断工具。连接成功后会立即关闭,不检查 HTTP 请求或 TLS 证书。因此 connected 并不意味着可以登录、API 正常或数据保存成功。如果呈现给用户的结果名称超出了实际验证的范围,那么即使代码本身正确,它也会变成不准确的运维工具。
工作原理
根据 connect(2),Linux 的非阻塞 TCP 连接可能立即完成,也可能处于进行中。用 Python connect_ex 的整数结果来做状态转换。本实验仅限于 AF_INET/SOCK_STREAM。不把 AF_UNIX 的进行中 EAGAIN 规则原样搬到数字 IPv4 TCP 上。
| 当前状态 | 观察 | 下一个状态 |
|---|---|---|
| new | connect_ex 结果为 0 或 EISCONN | connected,错误 0 |
| new | EINPROGRESS 或 EALREADY | pending,结果未定 |
| new | 其他错误 | failed,对应的 errno |
| pending | WRITE 之后 SO_ERROR 为 0 | connected |
| pending | WRITE 之后 SO_ERROR 为错误 | failed |
| new/pending | 绝对截止时间或取消 | timed_out 或 cancelled |
即使是 pending,也不会每次都重新调用 connect_ex。这是在等待已经开始的那一次尝试的结果。不把复用失败的套接字当作通用的重试方法,而是关闭之后重新创建一次新的尝试。这次的诊断工具本身不做自动重试,因此一个结果对应一次连接尝试。
socket(7) 中的 SO_ERROR 在获取错误的同时会把它清除。如果记录日志的函数先读取,状态判定函数再读第二次,两者的结果就可能不同。请设置 Dial.error,保存最初完成判定时的值。每次读取时都去请求“最新错误”,并不能提高准确度。
python3 /opt/fixtures/reactor/connect_probe.py
实验中,一个套接字调用 listen,另一个套接字只做 bind。两者都占着端口,所以不存在“找一个空闲端口、关闭后再去连接”这样的竞争。在 Linux 容器中观察到的进行中连接,两者都是 writable=true,但 SO_ERROR 分别是 0 和 ECONNREFUSED。再次读取被拒绝的结果时,得到的是 0。像 111 这样的 errno 常量值在不同操作系统上可能不同,所以代码中要使用 errno.ECONNREFUSED。立即完成的情况,则按 connect_ex 最初的返回值来分类。
不要偷偷插入名称解析
如果传入域名而不是数字地址,在连接之前就会多出名称解析这一步。不能仅因为把套接字改成了非阻塞,就说名称解析也是非阻塞的。本实验中的 host 按数字 IPv4 验证,localhost 也会被拒绝。IPv6 候选地址竞争、DNS 缓存和 Happy Eyeballs 是需要额外设计的独立主题。标明范围不是为了掩盖错误,而是为了说清楚所测量的是哪个阶段。
截止时间不是在尝试开始时确定,而是在创建 Dial 时确定。因为在队列中停留很久的连接,同样消耗总时间。如果在调用 connect_ex 之前就已经到了截止时间,就不开始网络尝试,直接以 timed_out 结束。终止状态不会被之后的 cancel 或 expire 覆盖。即使在成功之后马上收到全部取消,也不会把已经确认的成功改成取消。
在现场相遇的样子
2026-09-13 确认的 Cloudflare Software Engineer, Spectrum 招聘启事 涉及 Linux 套接字、TCP 连接状态和生命周期问题的调试。这里练习其中的连接建立和失败原因的保存。这并不是该公司的官方培训或录用保证,Go、Rust 以及大规模运维能力需要另行学习。
下一项检查要做什么
在接下来的测验中确认观察与判定的区别。在之后的综合实验中,要构建 Dial 的 new → pending → 终止的状态转换,并测试真实 loopback TCP 的成功与拒绝。请先用自己的话解释,为什么看似正确的“只要 WRITE 就成功”的实现,以及“不断重新查询 SO_ERROR”的实现是错的,然后再用测验来确认。