前言
这是以前找工作时候面试官问我的,但是记不清具体怎么问了。
后来回去研究了一下,但又忙其他事情,没结果,再后来才想起,觉得这么问肯定有道理,又找了些资料。
答案是可以的,一个端口确实可以让多个程序绑定,只不过是在Linux 3.9 上引入了一个特性,称为 SO_REUSEPORT。
我们知道如果一个程序已经在8080上绑定监听了,那么其他程序就不能对8080绑定,否则出异常,但是有个情况特殊,那就是对socket设置过SO_REUSEPORT
,这个选项是怎么描述的:允许同一主机上的多个套接字绑定到同一端口,只要第一个服务器在绑定之前设置了这个选项,那么其他任意数量的socket都可以绑定相同的端口,前提是它们也设置了这个选项。
但是如果第一个socket的uid是A,那么其他非A运行的就无法绑定。
Nginx在1.9.1上也引用了这个功能,只需要在listen 后面加上reuseport可以了。
1 | c复制代码server{ |
之后会创建worker_processes个进程,监听相同的端口。
1 | java复制代码root@meet:/etc/nginx/nginx_configs# netstat -an |grep 6060 |
在java中貌似还没有直接办法,没有提供设置这个的选项,虽然内部有个SocketOptions类,但里面没有关于SO_REUSEPORT字段的,更何况也无法调用,但是更高级别的jdk不知道有没有办法,但也不是绝对的,如果非要在java中使用这个特性,除了使用jni,还可以通过反射。
首先要了解sun.nio.ch.Net,这是个很重要的类,ServerSocketChannel内部就是使用他,下面是创建一个socket的流程。
1 | java复制代码 |
如果要用最老的ServerSocket,就是稍微有些麻烦,其中原理是替换掉内部的FileDescriptor,但是这里有个细节,如果使用ServerSocket的有参构造方法,那么创建、绑定、监听socket在一起,没有办法替换,只有使用无参构造,并且在之后调用一次createImpl才行。
但问题是不能直接访问createImpl,他只有一处调用,就是getImpl(),getImpl中判断了如果不为空,才调用createImpl()创建,但问题是getImpl()也不能调用,只能在向上一层找,所以我找了getLocalPort方法,但是TM的getLocalPort()也有限制,就是isBound()返回True才行,所以要通过反射修改一下bound值,之后要修改回来。
1 | java复制代码fun createNewFd(): FileDescriptor { |
但其实使用ServerSocketChannel更方便,ServerSocketChannel很松,原理还是替换内部fd。
1 | java复制代码var serverSocketChannel = ServerSocketChannel.open() |
open方法调用后,ServerSocketChannelImpl只会创建一个socket,替换掉即可。
1 | java复制代码ServerSocketChannelImpl(SelectorProvider var1) throws IOException { |
这里提到的fd,在Windows下可以理解成句柄,就是描述具体东西的一个id。
下面拿c演示
socket的创建流程就不说了,里面通过setsockopt设置SO_REUSEPORT选项,那么成功后其他程序还是可以绑定8080端口,前提是他们也设置这个选项,JVM最后也会调用下面使用到的函数创建。
1 | c复制代码#include <unistd.h> |
编译运行,可以看到两个程序都绑定成功了。
通过netstat查看,确实有两个程序在8080上监听。
1 | shell复制代码root@hxl-PC:/home/hxl# netstat -anp |grep 8080 |
那么问题就是,客户端连接的时候,那个会做出响应?
我找了很多资料,没有找到具体的说明,但是根据实际情况,大概是随机的,也就是唤醒是不公平的。
哦,对了,我找到了一篇关于SO_REUSEPORT非常好的文章
tech.flipkart.com/linux-tcp-s…
但是最后还有个问题,ServerSocketChannel如果在启用IPV6的情况下,那么最后会调用下面函数创建,但如果其他端不是AF_INET6,是AF_INET,虽然可以进行绑定,但使用AF_INET6的一端将永远收不到请求,全部由AF_INET的负责。
1 | c复制代码int server_fd = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP); |
JVM禁用IPV6。
-Djava.net.preferIPv4Stack=true
这个没有找到明确的说明, 但我测试了很多次,确实是这样的。
本文转载自: 掘金