朕喜技术(ZhenxiJS)
2026.09.09
1 分钟阅读

localhost 访问失败:服务只监听 IPv4,客户端却解析到 IPv6 ::1(地址族不匹配)Connection refused

日期:2026-09-09 | 分支:2026-09-09-1600 | 题号:第42题 技术栈:Python socketserver curl getaddrinfo IPv6 IPv4 localhost ::1 127.0.0.1 Connection 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 均可访问,故障消除。

六、经验与避坑

  1. 先确认地址族,再怀疑服务:遇到 Connection refused,先 ss -lntp 看服务到底监听在 0.0.0.0、127.0.0.1、还是 ::,再用 curl -4 和 curl -6 分别测,能瞬间定位是否地址族不匹配。
  2. localhost 是「两个地址」,不是「一个 IP」:它同时映射 127.0.0.1 与 ::1。很多 coder 以为 localhost 一定是 IPv4,实际在 IPv6 启用环境下 getaddrinfo 可能优先返回 ::1。
  3. 「只取第一个地址」的程序最容易踩坑:nginx proxy_pass http://localhost、连接池、老式 socket 代码, 常只连 getaddrinfo 的首个地址。若首地址是 ::1 而服务只监听 IPv4,就会「服务在跑却连不上」。
  4. 复用绑定地址时记得 SO_REUSEADDR:socketserver.TCPServer 默认 allow_reuse_address=False, 客户端断开留下的 TIME_WAIT 会让下一次 bind 报 OSError: [Errno 98] Address already in use。在 Server 子类设 allow_reuse_address = True 即可。
  5. 改 /etc/hosts 或 /etc/gai.conf 不一定能强制 IPv6 优先:glibc 的地址选择还受 nsswitch 与 系统兜底影响。更可靠的做法是直接用 socket.getaddrinfo 打印返回顺序,并让客户端「只取第一个地址」 来复现真实故障。
  6. 双栈监听是通用解法:容器 / 服务端统一监听 ::(双栈、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))"

版权声明

本文为博主原创文章,遵循 CC BY-NC-SA 4.0 许可协议。 转载请注明出处:http://localhost/blogs/2026-09-09-1610-localhost-ipv4-ipv6-address-family-mismatch

分享这篇文章