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