localhost 访问失败:服务只监听 IPv4,客户端却解析到 IPv6 ::1(地址族不匹配)Connection refused
日期:2026-09-09 | 分支:
2026-09-09-1600| 题号:第42题 技术栈:PythonsocketservercurlgetaddrinfoIPv6IPv4localhost::1127.0.0.1Connection refused
一、问题背景
服务明明在监听端口、ss -lntp 也看到端口处于 LISTEN,但 curl http://localhost:<port> 却报:
curl: (7) Failed to connect to localhost port 8080: Connection refused
可是 telnet 127.0.0.1 8080 又是通的。同一个 localhost,为什么一个能连一个不能连?
这类「服务活着、端口 LISTEN、却 Connection refused / 超时」的现象,绝大多数是 IPv4 / IPv6 地址族不匹配:
- 服务端只绑定了 IPv4 地址(
0.0.0.0或127.0.0.1); - 客户端(curl / 程序 / nginx upstream / 连接池)把
localhost解析成了 IPv6 的::1; - 客户端去连
::1:8080,服务端根本没监听::1,于是被拒绝。
二、复现步骤
环境:Linux 容器,已启用 IPv6 loopback(net.ipv6.conf.lo.disable_ipv6=0)。
# 1) 启动只监听 IPv4 的服务(server.py 绑定 0.0.0.0:8080)
python3 server.py 8080 &
# 2) 确认只监听在 IPv4
ss -tlnp | grep 8080
# LISTEN 0 5 0.0.0.0:8080 0.0.0.0:* users:(("python3",...))
# 3) 查询 localhost 解析顺序(第一个就是 ::1)
python3 -c "import socket; [print(r[0].name, r[4][0]) for r in socket.getaddrinfo('localhost', 8080)]"
# AF_INET6 ::1
# AF_INET6 ::1
# AF_INET 127.0.0.1
# 4) 核心对比
curl -4 -s -m 3 http://localhost:8080 # 通
curl -6 -s -m 3 http://localhost:8080 # Connection refused —— 服务没监听 ::1
更真实地触发「默认也失败」
很多程序只取 getaddrinfo 返回的第一个地址(而第一个就是 ::1),于是必然连不上:
import socket
infos = socket.getaddrinfo("localhost", 8080)
s = socket.socket(infos[0][0], infos[0][1], infos[0][2])
s.settimeout(3)
s.connect(infos[0][4]) # 连 ('::1', 8080)
# ConnectionRefusedError: [Errno 111] Connection refused
三、根本原因
localhost 并非单一 IP,而是一个会同时解析到 127.0.0.1 和 ::1 的主机名。
客户端到底用哪个地址,由 getaddrinfo(3) 的返回顺序决定(受 /etc/gai.conf、/etc/hosts、
系统 IPv6 是否启用等影响)。
- 服务端只
bind 0.0.0.0→ 只监听 IPv4; - 客户端拿到第一个地址
::1→ 去连 IPv6 loopback → 服务端无此监听 →Connection refused。
这不是「服务没起来」,而是两端地址族没对上。curl -4(强制 IPv4)通,curl -6(强制 IPv6)拒绝,
就是最直接的证据。
四、解决办法
三种可选修复,按推荐排序:
方案 A(推荐):服务端监听双栈 ::
让服务同时接受 IPv4 与 IPv6。Python socketserver 用 AF_INET6 绑定 :: 即双栈
(IPv4 以 ::ffff:127.0.0.1 映射形式进入):
import socket, socketserver
class DualStackServer(socketserver.TCPServer):
address_family = socket.AF_INET6
allow_reuse_address = True
server = DualStackServer(("::", 8080), Handler) # 双栈
方案 B:客户端显式指定地址族
curl -4、程序强制 AF_INET,或用 127.0.0.1 / 0.0.0.0 代替 localhost,绕开解析歧义。
方案 C:让客户端优先解析 IPv4
通过 /etc/gai.conf 调 precedence / 应用层配置,让 getaddrinfo 优先返回 127.0.0.1。
五、验证
用同一「只取第一个地址」的客户端,对比修复前后:
| 服务端监听 | 客户端取 first=getaddrinfo(...) | 结果 |
|---|---|---|
0.0.0.0:8080(IPv4 only) | ('::1', 8080) | ConnectionRefusedError [Errno 111] 拒绝 |
:::8083(双栈) | ('::1', 8083) | 成功,收到 HTTP/1.0 200 OK |
同时 curl -6 / curl -4 / 默认 curl 三种方式都通:
curl -6 -> hello from dualstack server, client=::1
curl -4 -> hello from dualstack server, client=::ffff:127.0.0.1
结论:服务端改监听 :: 后,IPv6 与 IPv4 均可访问,故障消除。
六、经验与避坑
- 先确认地址族,再怀疑服务:遇到
Connection refused,先ss -lntp看服务到底监听在0.0.0.0、127.0.0.1、还是::,再用curl -4和curl -6分别测,能瞬间定位是否地址族不匹配。 localhost是「两个地址」,不是「一个 IP」:它同时映射127.0.0.1与::1。很多 coder 以为localhost一定是 IPv4,实际在 IPv6 启用环境下getaddrinfo可能优先返回::1。- 「只取第一个地址」的程序最容易踩坑:nginx
proxy_pass http://localhost、连接池、老式 socket 代码, 常只连getaddrinfo的首个地址。若首地址是::1而服务只监听 IPv4,就会「服务在跑却连不上」。 - 复用绑定地址时记得 SO_REUSEADDR:
socketserver.TCPServer默认allow_reuse_address=False, 客户端断开留下的TIME_WAIT会让下一次bind报OSError: [Errno 98] Address already in use。在 Server 子类设allow_reuse_address = True即可。 - 改
/etc/hosts或/etc/gai.conf不一定能强制 IPv6 优先:glibc 的地址选择还受 nsswitch 与 系统兜底影响。更可靠的做法是直接用socket.getaddrinfo打印返回顺序,并让客户端「只取第一个地址」 来复现真实故障。 - 双栈监听是通用解法:容器 / 服务端统一监听
::(双栈、IPv4-mapped),能同时兼容127.0.0.1与::1两类客户端,是最省心的方案。
相关命令速查
ss -tlnp # 看监听地址族
curl -4 -s -m 3 http://localhost:PORT # 强制 IPv4
curl -6 -s -m 3 http://localhost:PORT # 强制 IPv6
python3 -c "import socket; print(socket.getaddrinfo('localhost', 8080))"
