bigywater.tech
网络协议 · 58

同时打开与自连接:两端同时 connect 会怎样

TCP 握手不一定是“一个主动、一个被动”。两端同时 connect 对方,会走一条对称的路径;极端情形下,连自己都能连上。这篇把这两种少见的情形在 macOS 上实际跑一遍,看线上长什么样、内核怎么处理。

4 / 60000
向临时端口连 60000 次,内核挑到源端口等于目标端口、被 EINVAL 拒绝的次数
0 个包
自连接 connect 返回 EINVAL 时线上的 TCP 包数
20 / 20
两个套接字互相 connect 同时打开成功的次数(有无 150 ms 延迟都是)

自连接被拒

实测 · 本机执行输出
  bind 127.0.0.1:P 再 connect 自己     :connect_ex = EINVAL;线上抓到的 TCP 包 0 个
  加 SO_REUSEADDR                    :connect_ex = EINVAL;线上抓到的 TCP 包 0 个
  加 SO_REUSEPORT                    :connect_ex = EINVAL;线上抓到的 TCP 包 0 个
  bind 0.0.0.0:P 再 connect 127.0.0.1:P:connect_ex = EINVAL;线上抓到的 TCP 包 0 个
  四种写法 macOS 都直接返回 EINVAL,没有发出任何 SYN:内核在 connect 里就拒绝了“源地址端口 = 目的地址端口”的自连接

规范允许“连自己”,macOS 在 connect 里直接返回 EINVAL,线上一个包也没有。

同时打开的线上形态

实测 · 本机执行输出
  两个 connect 的结果:a → 0(300 ms),b → 0(300 ms)(0 = 成功)
  中途(约 180 ms 时)netstat 里的状态:[('9406', '9405', 'SYN_RCVD'), ('9405', '9406', 'SYN_RCVD')]
  线上的包(去重,端口 A 与 B):
    +    0.0 ms  B→A  SYN      载荷 0 字节
    +    0.0 ms  A→B  SYN      载荷 0 字节
    +  150.1 ms  A→B  SYN+ACK  载荷 0 字节
    +  150.1 ms  B→A  SYN+ACK  载荷 0 字节
    +  300.4 ms  B→A  ACK      载荷 0 字节
    +  300.4 ms  A→B  ACK      载荷 0 字节
  连上之后 a 发的数据,b 收到:b'from a'

两个 SYN 交叉,两个 SYN+ACK 交叉,两个 ACK;中途两边都是 SYN_RCVD,没有谁是客户端谁是服务端。

成功率

实测 · 本机执行输出
  延迟 0 ms:20 次里两端都连上 20 次;失败时的 (a, b) 错误码:{}(61 = ECONNREFUSED)
  延迟 150 ms:20 次里两端都连上 20 次;失败时的 (a, b) 错误码:{}(61 = ECONNREFUSED)

在回环上 20 次全部成功,有无延迟都一样;错开时间的情形没有测。

意外的自连接

实测 · 本机执行输出
  net.inet.ip.portrange.first: 49152;net.inet.ip.portrange.last: 65535
  目标端口 57343(临时端口范围 49152–65535,共 16384 个端口):60000 次 connect,结果 {'ECONNREFUSED': 59996, 'EINVAL': 4},成功(自连接)0 次;用时 12.2 秒
  EINVAL 的那几次,内核挑的源端口分别是 [57343, 57343, 57343, 57343](目标端口 57343);每次的理论概率约 1/16384,60000 次的期望值 3.7 次

临时端口范围内随机挑到与目标相同的概率约 1/16384;实测 60000 次里 4 次被内核拒绝。

分步演示

同时打开与自连接
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64(Apple 芯片)/ Python 3.14.8 / tcpdump(libpcap)/ scapy 2.8 / dpkt / dnspython / h2 / aioquic / OpenSSL / curl 8.7.1;抓包在 lo0(环回)上做,少数实验用真实网卡 en0(这台机器上 en0 是 Wi-Fi,tcpdump 看到的是系统转成以太网格式的帧,所以文中的“以太网头”都指这个 14 字节的头)或访问公网(会在文章里注明);需要丢包 / 延迟 / 限速时,用一个只作用于 lo0 端口 9400-9499 的 dummynet 辅助脚本(netlab);所有服务端都是本机自己起的进程;时间戳取自 tcpdump 的 -tt,数字每次运行会有小幅抖动。第一、四节在 lo0 上;第二节用 netlab 加每方向 150 ms 延迟,抓包经过去重(dummynet 会让同一个包被抓多次);第三节只测了两个线程同时开始 connect 的情形
    • 没有验证:允许自连接的系统(Linux)上的实际行为;两个 connect 错开时间后同时打开失败的边界;同时打开时的序号细节(只看了标志位);netstat 里除 SYN_RCVD 之外的其它中间状态(只取了一个时间点);自连接在应用层造成问题的例子
    京ICP备2026067502号-1京公网安备11010502063487号