先给三句话的答案。
默认不行:第二个程序去 bind 同一个地址端口,会拿到 WinError 10048「每个套接字地址(协议/网络地址/端口)只允许使用一次」。
但只要两边都把 SO_REUSEADDR 打开,不行就变成了行。
两个独立进程都 bind 成功、都 listen 成功,netstat -ano 里并排两行 LISTENING,挂着两个不同的 PID(实测 9916 和 31928)。
代价藏在第三句:后绑的那个程序是个哑巴。
我发了 10 条连接,10 条全被先绑的接走,后绑的 accept 次数是 0。
所以「端口被占用」不一定是残留进程在捣鬼。
更可能是某个程序或它依赖的库打开了 SO_REUSEADDR,把端口借了出去。
为什么值得看:这个报错的锅,一半不在残留进程
SO_REUSEADDR 这个名字太容易被误读成「允许复用端口」。
于是很多人把它当万能开关:服务起不来就顺手加一行。
它的本意是让服务在重启时不被 TIME_WAIT 卡住,不是为了两个进程共享端口。
Windows 在这条路上比 BSD 和 Linux 走得更远。
它允许两个打开了 SO_REUSEADDR 的套接字绑同一个地址端口,而且两个 listen 都会返回成功。
系统一声不吭,问题被推迟到连接进来的那一刻。
这次想钉死四个问题:什么情况下第二个 bind 会失败、失败给的是哪个错误码、两边都开复用之后连接到底归谁、想要真·多进程共用端口在 Windows 上有没有解。
怎么测的:11 组绑定,逐组记录返回码和连接归属
环境是 Windows 10.0.26200、CPython 3.11.15,全部走本机回环。
端口取 47811~47851 这类高位端口,避免撞上正在跑的服务。
每一组都真跑:先 bind 再 listen,异常按原样打印 winerror 和错误文字。
凡是两边都绑成功的,再补一次真实连接,用 accept 判断连接落在哪一把套接字上。
为了区分「一时运气」和「稳定行为」,统计组每组连 10 次。
矩阵是同一进程内两把套接字,地址 127.0.0.1:
| 组 | 场景 | 第一个 bind/listen | 第二个 bind/listen | 连接归谁 |
|---|---|---|---|---|
| 1 | 两边都不设 SO_REUSEADDR | OK | FAIL 10048 | --- |
| 2 | 两边都设 SO_REUSEADDR=1 | OK | 都 OK | 先绑的(10/10) |
| 3 | 只有先绑的设 SO_REUSEADDR | OK | FAIL 10048 | --- |
| 4 | 只有后绑的设 SO_REUSEADDR | OK | FAIL 10013 | --- |
| 5 | 先 0.0.0.0、后 127.0.0.1,都不设复用 | OK | 都 OK | 127.0.0.1 那把(10/10) |
| 6 | 先 127.0.0.1、后 0.0.0.0,都不设复用 | OK | 都 OK | 127.0.0.1 那把(10/10) |
| 7 | 先 :: 双栈(IPV6_V6ONLY=0)、后 0.0.0.0 | OK | 都 OK | 走 IPv4 的那把 |
| 8 | UDP 两边都不设复用 | OK | FAIL 10048 | --- |
| 9 | UDP 两边都设复用 | OK | 都 OK | 先绑的(后绑的收不到数据) |
| 10 | 端口处于 TIME_WAIT 时重绑 | 不设复用 OK / 设复用 OK | --- | --- |
| 11 | 独占旗标与复用旗标互抢 | 见下表 | 10048 / 10013 | --- |
拿两个真正的独立进程复跑,结论一致。
两个都不设复用:第二个进程 10048。
两个都设复用:两个进程都进 LISTENING,10 条连接全给了先绑的那个。
复现方式(同一台机器上,脚本和原始输出都在下面的证据目录里):
bash
python evidence/bind_matrix.py # 11 组矩阵,含错误码原文
python evidence/bind_matrix2.py # 逐个追「谁收到了连接」
python evidence/bind_matrix3.py # 每组 10 次连接的统计
python evidence/two_proc.py # 跨进程版,并打印 netstat 的 PID
原始输出在 evidence/ 下:bind-matrix-win.txt、bind-matrix-win-2.txt、bind-matrix-win-3.txt、bind-two-process-win.txt。
验证与踩坑:四个和直觉相反的结果
第一个坑是错误码会把你带偏。
只有后绑的那个打开 SO_REUSEADDR 时,失败码不是熟悉的 10048。
它给的是 WinError 10013「以一种访问权限不允许的方式做了一个访问套接字的尝试」。
同一类问题两种码,网上搜到的答案经常答不到点上。
第二个坑是「先绑的赢」这件事很稳。
两边都开复用时,我连了 10 次,10 次都被先绑的接走。
共享状态下不存在负载均衡,后绑的那个只是占着位置等饿死。
它的健康检查、启动日志、端口占用检查全部正常,业务却一条请求都收不到。
第三个坑是通配地址与具体地址抢端口时,赢的是地址更具体的那把,和谁先绑无关。
先绑 0.0.0.0:47841、后绑 127.0.0.1:47841 都不报错,10 条来自 127.0.0.1 的连接全部落到后者。
IPv6 双栈同理:先绑 :: 再绑 0.0.0.0 都能成功,而 127.0.0.1 上的请求被 IPv4 那把接走。
这类端口被悄悄分流的故障,日志里看不出任何异常。
第四个坑是 TIME_WAIT 背了太久的锅。
服务端侧先关闭后,netstat 里确实留着一排该端口的 TIME_WAIT,还有一条 FIN_WAIT_2。
但在这种状态下重绑同一个端口,设不设 SO_REUSEADDR 都直接成功。
重启失败先别怪 TIME_WAIT,先看有没有另一个进程正趴在那个端口上。
UDP 比 TCP 更狠一点。
两边都开复用后,两个套接字都能绑定成功。
但一条数据发给 127.0.0.1:47812,只有先绑的收到,后绑的那把一个字节都没拿到。
套接字存在、绑定成功、却永远收不到数据,比直接报错难查得多。
结果与经验:什么时候该用哪个旗标
先把结论落成一张表,按你要解决的问题选:
| 你要解决的问题 | 正确做法 | 实测依据 |
|---|---|---|
| 服务重启时报 10048 / 10013 | 先查是不是真有另一个进程在监听 | 第 1、3、4 组 |
| 不想让别的进程分享自己的端口 | 显式给监听套接字设 SO_EXCLUSIVEADDRUSE | 第 11 组:独占先占住,带复用的一方来抢会拿到 10013;反过来独占的一方后进来抢已被复用的口,也抢不到(10048) |
| 两个进程真想一起分担同一个端口 | Windows 上没有这个能力,改用单进程内多线程或异步 IO | 本机 socket.SO_REUSEPORT 不存在(hasattr 为 False) |
| 想让连接均匀分摊到多进程 | 只有 Linux 3.9+ 的 SO_REUSEPORT 能做,分摊由内核决定 | 本机没有 Linux 环境,这一条是文档语义,未实测 |
第一条经验:排查端口占用时别只看有没有残留进程。
如果 netstat -ano 里同一个端口出现两行 LISTENING、PID 却不一样,说明已经有两个程序在共享它。
此时杀哪个都不一定对,因为连接实际归先绑的那个。
先看这两行出现的时间顺序,再决定动谁。
第二条经验:自己写服务时,Windows 上不要顺手给监听套接字加 SO_REUSEADDR。
它的语义在这里等于「欢迎别人来分享我的端口」,还不会给你任何回报。
共享状态下你先绑的那把能拿到全部连接,看起来一切正常。
直到哪天你的进程成了后绑的那个,才会以「服务在跑但没流量」的形式爆出来。
想保护自己就用 SO_EXCLUSIVEADDRUSE。
下一步打算把这套矩阵搬到容器里再跑一遍,用 Linux 上的 SO_REUSEPORT 做同样的 A/B 对照。
两个进程共用一个端口到底怎么分摊,这部分在这台 Windows 上测不了,就不硬写了。