logo

鱼肚的博客

Don't Repeat Yourself

浅谈ipv4和ipv6中的端口占用

前两天在调试一个本地应用的时候,偶然发现一个奇怪的问题:nginx和spring boot应用竟然同时监听了8080端口,且能正常工作!

这实在是太震惊了,我一向认为只有父子进程才可以共用端口,而nginx和调试中的spring boot应用很明显不是父子关系。

问题表现

为了研究这个问题,我做了如下的几个测试:

测试一,场景复原

  1. 先启动nginx,此时访问 http://127.0.0.1:8080http://localhost:8080 均可以正常访问到nginx。
  2. 再启动 spring boot应用,此时 http://127.0.0.1:8080 能访问到nginx,http://localhost:8080能访问到spring boot.

测试2:调整顺序

  1. 先启动spring boot应用,此时访问 http://127.0.0.1:8080http://localhost:8080 均可以正常访问到spring boot。
  2. 再启动nginx,报端口占用,启动失败

测试3:启动其它服务占用8080

这里我使用的是serve,一个基于node.js的本地静态文件http服务,启动命令:npx serve -p 8080

  1. 先启动nginx,再启动serve,能正常启动。127.0.0.1指向nginx, localhost指向serve.
  2. 先启动serve,再启动nginx,无法正常启动。
  3. 先启动serve,再启动spring boot,无法正常启动。
  4. 先启动spring boot,再启动nginx,无法正常启动。

问题分析

通过spring boot启动后nginx无法启动等现象,可以证明不同进程之间端口冲突确实是存在的。

那么如何解释nginx启动后 spring boot 可以正常启动的现象呢?

最初我猜想spring boot可能魔改了nginx的配置文件并重启,通过proxy_pass做了反向代理,但是这个过于天方夜潭了,且后面serve也能伴随nginx启动说明不是spring boot的问题。

最后还是从sudo lsof -i:8080的命令返回结果中看出了端倪:

image-20210714205213423

1nginx     71366   root    7u  IPv4 0x613d5a88e3eee6cd      0t0  TCP *:http-alt (LISTEN)
2nginx     71367 nobody    7u  IPv4 0x613d5a88e3eee6cd      0t0  TCP *:http-alt (LISTEN)
3java      78340   yudu  196u  IPv6 0x613d5a88dddfdc05      0t0  TCP *:http-alt (LISTEN)

从图中可以看出,有两个Nginx进程,带有IPv4标识,还有一个Java进程,还有IPv6标识。

从这里开始,问题总算有了眉目。

结果验证

为了验证是否真的由ipv4、ipv6导致了端口的重复监听,我写了如下的两个Node.js脚本:

ipv4.js

1// 监听 ipv4 端口,并在收到请求时返回 Hello ipv4!
2require('http').createServer(function (req, res) {
3  res.writeHead(200, {'Content-Type': 'text/plain'});
4  res.write('Hello ipv4!\n');
5  res.end();
6}).listen({
7  port: 9999,
8  host: '127.0.0.1'
9});

ipv6.js

1// 监听 ipv6 端口,并在收到请求时返回 Hello ipv6!
2require('http').createServer(function (req, res) {
3  res.writeHead(200, {'Content-Type': 'text/plain'});
4  res.write('Hello ipv6!\n');
5  res.end();
6}).listen({
7  port: 9999, 
8  host: '::1',
9  ipv6Only: true
10});

先启动ipv4:node ipv4.js,再启动 ipv6:node ipv6.js,没有出现报错。此时访问各种本地url,得到结果如下:

这里面 [::1]就是ipv6形式的本地地址,类似于ipv4中的127.0.0.1。

再看/etc/hosts中localhost的配置:

1# localhost is used to configure the loopback interface
2127.0.0.1 localhost
3::1       localhost

可以看出系统默认将localhost绑定到了 127.0.0.1::1上,所以如果我们加下hosts配置

1127.0.0.1 test1.localhost # ipv4 only
2::1       test2.localhost # ipv6 only
3
4127.0.0.1 test3.localhost # ipv4 and ipv6
5::1       test3.localhost
6
7::1       test4.localhost # ipv6 and ipv4
8127.0.0.1 test4.localhost

再做一次上面的测试,会得到如下的结果:

这就说明确实可以给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脚本)。

通过这种方式,两个不相关的进程监听“同一个端口”变成了可能。