侧边栏壁纸
博主头像
一笑痕

人生若只如初见,
是可喜亦或者是可悲?

  • 累计撰写 148 篇文章
  • 累计收到 7 条评论

服务监听 localhost,从 127.0.0.1 连不上

2026-9-29 / 0 评论 / 7 阅读

服务监听 localhost,从 127.0.0.1 连不上

本地起一个开发服务,日志里打印 Listening on http://localhost:3000,浏览器打开正常。同一台机器上用 127.0.0.1 去探活,连接被拒。这一步卡住过不少人的排查:服务明明在跑,端口也没被占。

把六种写法挨个跑一遍,每次都用 curl 从三个地址各探一次。下面所有输出是在本机跑出来的。

localhost 解析出来两个地址

解析结果决定了 bind 到哪里,先看这一步:

== 1. localhost 解析成什么 ==
  1. ::1   family=6
  2. 127.0.0.1   family=4
  不带 all 时返回第一个:::1  family=6
  /etc/hosts 里提到 localhost 的行:
    # localhost is used to configure the loopback interface
    127.0.0.1   localhost
    ::1             localhost

dns.lookup 返回的第一个是 ::1,第二个才是 127.0.0.1,不带 all 的时候只给第一个。/etc/hosts 里 localhost 有两条记录,一条 IPv4 一条 IPv6,两边的写法不同:IPv4 那条写在 127.0.0.1 后面,IPv6 那条写在 ::1 后面。

绑定地址决定谁能连上

六种写法的 server.address() 和三次探测的结果:

代码里的写法 server.address() curl localhost curl 127.0.0.1 curl [::1]
listen({ host: 'localhost' }) ::1 HTTP 200 连不上 HTTP 200
listen(0) :: HTTP 200 HTTP 200 HTTP 200
listen({ host: '127.0.0.1' }) 127.0.0.1 HTTP 200 HTTP 200 连不上
listen({ host: '::1' }) ::1 HTTP 200 连不上 HTTP 200
先 setDefaultResultOrder('ipv4first') 再绑 localhost 127.0.0.1 HTTP 200 HTTP 200 连不上

第一行就是开头那个现象。写 localhost 的代码,实际监听地址是 ::1,同一台机器的 IPv4 回环地址上没有进程在监听,curl 127.0.0.1 报的是连接失败而不是超时。原文这样:

== 2. 绑 localhost ==
  代码里写的是:listen({ host: 'localhost' })
  server.address() 报的是:::1  family=IPv6  port=63257
  curl localhost   -> HTTP 200
  curl 127.0.0.1   -> 连不上(退出码 7)(7) Failed to connect to 127.0.0.1 port 63257 after 0 ms: Couldn't connect to server
  curl [::1]       -> HTTP 200

不给 host 的时候监听的是通配地址

listen(0) 里 server.address().address 是 ::,这是同时接受 IPv4 和 IPv6 的通配地址,三个探测全通:

== 3. 不给 host ==
  代码里写的是:listen(0)
  server.address() 报的是:::  family=IPv6  port=63261
  curl localhost   -> HTTP 200
  curl 127.0.0.1   -> HTTP 200
  curl [::1]       -> HTTP 200

开发环境里这是最省事的一种写法:不写 host,本机所有回环地址都能连。代价是它也接受来自本机以外网卡的连接,放到生产上要配合防火墙或者反向代理限制来源。

写死一个地址就只服务那一半

把地址写死成 127.0.0.1,IPv6 那侧连不上;写死成 ::1,IPv4 那侧连不上。两个方向都试了:

== 4. 只绑 127.0.0.1 ==
  代码里写的是:listen({ host: '127.0.0.1' })
  server.address() 报的是:127.0.0.1  family=IPv4  port=63265
  curl localhost   -> HTTP 200
  curl 127.0.0.1   -> HTTP 200
  curl [::1]       -> 连不上(退出码 7)(7) Failed to connect to ::1 port 63265 after 0 ms: Couldn't connect to server

server.address() 报什么,就只有什么能连。这一条对写探活配置的人尤其重要:容器里的 healthcheck 常写成 curl 127.0.0.1:PORT,应用里绑的却是 localhost,两边各说各的地址,探活就一直失败,服务本身没问题。

想继续写 localhost,就改解析顺序

最后一组是先调用 dns.setDefaultResultOrder('ipv4first'),再照原样绑 localhost:

== 6. 先 setDefaultResultOrder(ipv4first) 再绑 localhost ==
  代码里写的是:listen({ host: 'localhost' })
  server.address() 报的是:127.0.0.1  family=IPv4  port=63274
  curl localhost   -> HTTP 200
  curl 127.0.0.1   -> HTTP 200
  curl [::1]       -> 连不上(退出码 7)(7) Failed to connect to ::1 port 63274 after 0 ms: Couldn't connect to server

server.address() 这时变成 127.0.0.1,curl 127.0.0.1 通了,[::1] 换成连不上。同一个函数,换了解析顺序,绑定的地址就翻了个面。

顺带说一句版本:dns.lookup 按解析器返回的原样顺序给结果,这件事在较新的 Node 上是默认行为,设置 ipv4first 属于把旧行为要回来。本机只装了 Node 26,旧版本上的表现我没有实测,不猜。

三条改法各自的代价

最省事的是不写 host,让服务监听通配地址,本机怎么连都通,适合开发机。

想收紧一点就写死 127.0.0.1,只服务本机 IPv4 回环,别人连不上,前提是探活脚本和调用方也用这个地址。

还要保留 localhost 这个写法,就在进程启动最前面设一次解析顺序,改动最小,但影响的是整个进程里所有 dns.lookup 的调用,其他依赖解析地址的代码会跟着变。

倒过来看这件事,日志里那句 Listening on http://localhost:3000 说的是「我绑在了 localhost 解析出来的那个地址」,不是「本机所有地址都能连」。把日志打成实际监听地址更有用:

srv.listen({ host: 'localhost', port: 3000 }, () => {
  const a = srv.address();
  console.log(`listening on ${a.address}:${a.port} (${a.family})`);
});
    🤞 分享