前两天在调试一个本地应用的时候,偶然发现一个奇怪的问题:nginx和spring boot应用竟然同时监听了8080端口,且能正常工作!
这实在是太震惊了,我一向认为只有父子进程才可以共用端口,而nginx和调试中的spring boot应用很明显不是父子关系。
为了研究这个问题,我做了如下的几个测试:
- 先启动nginx,此时访问 http://127.0.0.1:8080 和 http://localhost:8080 均可以正常访问到nginx。
- 再启动 spring boot应用,此时 http://127.0.0.1:8080 能访问到nginx,http://localhost:8080能访问到spring boot.
- 先启动spring boot应用,此时访问 http://127.0.0.1:8080 和 http://localhost:8080 均可以正常访问到spring boot。
- 再启动nginx,报端口占用,启动失败
这里我使用的是serve,一个基于node.js的本地静态文件http服务,启动命令:npx serve -p 8080
- 先启动nginx,再启动serve,能正常启动。127.0.0.1指向nginx, localhost指向serve.
- 先启动serve,再启动nginx,无法正常启动。
- 先启动serve,再启动spring boot,无法正常启动。
- 先启动spring boot,再启动nginx,无法正常启动。
通过spring boot启动后nginx无法启动等现象,可以证明不同进程之间端口冲突确实是存在的。
那么如何解释nginx启动后 spring boot 可以正常启动的现象呢?
最初我猜想spring boot可能魔改了nginx的配置文件并重启,通过proxy_pass做了反向代理,但是这个过于天方夜潭了,且后面serve也能伴随nginx启动说明不是spring boot的问题。
最后还是从sudo lsof -i:8080
的命令返回结果中看出了端倪:
nginx 71366 root 7u IPv4 0x613d5a88e3eee6cd 0t0 TCP *:http-alt (LISTEN)
nginx 71367 nobody 7u IPv4 0x613d5a88e3eee6cd 0t0 TCP *:http-alt (LISTEN)
java 78340 yudu 196u IPv6 0x613d5a88dddfdc05 0t0 TCP *:http-alt (LISTEN)
从图中可以看出,有两个Nginx进程,带有IPv4标识,还有一个Java进程,还有IPv6标识。
从这里开始,问题总算有了眉目。
为了验证是否真的由ipv4、ipv6导致了端口的重复监听,我写了如下的两个Node.js脚本:
ipv4.js
// 监听 ipv4 端口,并在收到请求时返回 Hello ipv4!
require('http').createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.write('Hello ipv4!\n');
res.end();
}).listen({
port: 9999,
host: '127.0.0.1'
});
ipv6.js
// 监听 ipv6 端口,并在收到请求时返回 Hello ipv6!
require('http').createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.write('Hello ipv6!\n');
res.end();
}).listen({
port: 9999,
host: '::1',
ipv6Only: true
});
先启动ipv4:node ipv4.js
,再启动 ipv6:node ipv6.js
,没有出现报错。此时访问各种本地url,得到结果如下:
- http://127.0.0.1:9999 => Hello ipv4!
- http://localhost:9999 => Hello ipv6!
- http://[::1]:9999 => Hello ipv6!
这里面 [::1]就是ipv6形式的本地地址,类似于ipv4中的127.0.0.1。
再看/etc/hosts
中localhost的配置:
# localhost is used to configure the loopback interface
127.0.0.1 localhost
::1 localhost
可以看出系统默认将localhost绑定到了 127.0.0.1
和 ::1
上,所以如果我们加下hosts配置
127.0.0.1 test1.localhost # ipv4 only
::1 test2.localhost # ipv6 only
127.0.0.1 test3.localhost # ipv4 and ipv6
::1 test3.localhost
::1 test4.localhost # ipv6 and ipv4
127.0.0.1 test4.localhost
再做一次上面的测试,会得到如下的结果:
- http://test1.localhost:9999 => Hello ipv6!
- http://test2.localhost:9999 => Hello ipv4!
- http://test3.localhost:9999 => Hello ipv6!
- http://test4.localhost:9999 => Hello ipv6!
这就说明确实可以给ipv4或ipv6单独加hosts配置,且在我测试的机器中(macOS) ipv6是优先使用的,与定义的顺序无关(这一点未找到明确的理论支持,在不同的系统中可能会表现不一致)。
在研究问题的过程中,陆续地学习到了一些不同的知识点,也在这里记录一下:
-
IPv4、IPv6双协议栈:有些系统会在进程监听ipv6端口的同时,也自动为其监听ipv4端口,而有些系统则可能不会
-
nginx中启用ipv6的方法:
将
listen 8080;
换成listen [::]:8080;
它会自动监听 ipv6和ipv4端口。
ipv4和ipv6的端口号是两套不同的集合,尽管系统层面上往往会同时监听ipv4和ipv6的端口,但是实际上可能人为地控制只监听ipv4或只监听ipv6(参考上方的Node.js脚本)。
通过这种方式,两个不相关的进程监听“同一个端口”变成了可能。