内容来源于StackOverflow的精彩回答,StackOverflow.
以BSD系统为例。
首先,一个TCP/UDP连接(Connection)的id,就是由下面五个值组成元组。
{<protocol>, <src addr>, <src port>, <dest addr>, <dest port>}
任何合法的五个值的组合都可以定义一个连接,同时,没有任何两个连接具有完全相同的元组。
第一个值protocol
是在socket()
设定的,src addr
和src port
是在bind()
的时候设定的,dest addr
和dest port
是在connect()
的时候设定的。虽然UDP是一个无连接协议,并不需要connect()
,但是在第一次发送数据的时候,UDP connection还是被系统非显式地绑定到了dest addr / port
上。
当绑定ip的时候,可以通过绑定到0.0.0.0: port
来绑定到所有本地网络地址的对应端口上,也可以绑定到192.168.0.100: port
来绑定到特定本地网络地址(回环)的特定端口。
在默认设置下,没有socket能够绑定到同一地址的同一端口。比如在Socket A已经绑定了0.0.0.0:8000
以后,Socket B若是想要绑定192.168.0.100:8000
,那就会报EADDRINUSE
。因为Socket A已经绑定了所有ip地址的8000端口,包括192.168.0.100:8000
。
SO_REUSEADDR
作用一
在为Socket B设置了SO_REUSEADDR
以后,判断冲突的方式就变了。只要地址不是正好(exactly)相同,那么多个Socket就能绑定到同一ip上。比如0.0.0.0
和192.168.0.100
,虽然逻辑意义上前者包含了后者,但是0.0.0.0
泛指所有本地ip,而192.168.0.100
特指某一ip,两者并不是完全相同,所以Socket B尝试绑定的时候,不会再报EADDRINUSE
,而是绑定成功。
下面是测试不同设置下的绑定情况:
SO_REUSEADDR | socketA | socketB | Result |
---|---|---|---|
ON/OFF | 192.168.0.1:21 | 192.168.0.1:21 | Error (EADDRINUSE) |
ON/OFF | 192.168.0.1:21 | 10.0.0.1:21 | OK |
ON/OFF | 10.0.0.1:21 | 192.168.0.1:21 | OK |
OFF | 0.0.0.0:21 | 192.168.1.0:21 | Error (EADDRINUSE) |
OFF | 192.168.1.0:21 | 0.0.0.0:21 | Error (EADDRINUSE) |
ON | 0.0.0.0:21 | 192.168.1.0:21 | OK |
ON | 192.168.1.0:21 | 0.0.0.0:21 | OK |
ON/OFF | 0.0.0.0:21 | 0.0.0.0:21 | Error (EADDRINUSE) |
可以看到,如果想绑定addr
字符串完全相同的ip,那么无论SO_REUSEADDR
设置与否,都会报地址已使用。但是在设置了SO_REUSEADDR
以后,就可以同时绑定0.0.0.0
和192.168.1.0
两个地址了。
作用二
SO_REUSEADDR
的另一个作用是,可以绑定TIME_WAIT
状态的地址。
TCP Socket的send()
是一个异步调用,当数据送入socket send buffer以后就会返回。也就是说,在send()
返回以后,数据仍然需要经历漫长的tcp拥塞控制冲突避免等过程,才能被成功发送。在没有TIME_WAIT
状态的前提下,假如这个时候上层程序判断通信完成,关闭了socket,那么缓冲区的数据就会丢失。所以TIME_WAIT
这个状态就被用来保证,socket能够续命到buffer中的数据能够全部发送完成或者超时。
TIME_WAIT
的时间,也就是超时的时间取决于一个配置项Linger Time。在大多数系统中,他是非常长的2分钟。这意味着两分钟内,socket对应的地址端口是被占用的,无法重新绑定。
一个非常现实的问题是,假如一个systemd托管的service异常退出了,留下了TIME_WAIT
状态的socket,那么systemd将会尝试重启这个service。但是因为端口被占用,会导致启动失败,造成两分钟的服务空档期,systemd也可能在这期间放弃重启服务。
但是在设置了SO_REUSEADDR
以后,处于TIME_WAIT
状态的地址也可以被绑定,就杜绝了这个问题。因为TIME_WAIT
其实本身就是半死状态,虽然这样重用TIME_WAIT
可能会造成不可预料的副作用,但是在现实中问题很少发生,所以也忽略了它的副作用。
另外,上文中所有SO_REUSEADDR
的生效,只需要在后绑定的socket中设置SO_REUSEADDR
即可,并没有强制要求以前绑定的socket也设置这一个选项才能完成共享。
SO_REUSEPORT
SO_REUSEPORT
干的其实是大众期望SO_REUSEADDR
能够干的事,将多个socket绑定到同一ip和端口。并且它要求所有绑定同一ip/port的socket都设置了SO_REUSEPORT
。不过可能有的操作系统并没有这个option。
Connect() 返回 EADDRINUSE 问题
在默认情况下,一般在bind()
时可能会出现EADDRINUSE
问题,connect()
时因为src ip
和src port
已经不同,不可能报EADDRINUSE
。但是在SO_REUSEADDR
和SO_REUSEPORT
下,因为地址有重用,那么当重用的地址端口尝试连接同一个远端主机的同一端口时,就会报EADDRINUSE
。
比如本机只有两个地址,127.0.0.1
和192.168.0.1
,其中后者是可访问因特网的网卡的地址。在SO_REUSEADDR
下,并且Socket A绑定了Socket A0.0.0.0:8000
, Socket B绑定了192.168.0.1:8000
以后,Socket A发起了与远端主机111.13.101.208:80
的连接。此时根据路由表规则,连接将被绑定到192.168.0.1
,产生的连接ID为{<SOCK_STREAM>, <192.168.0.1>, <8000>, <111.13.101.208>, <80>}
,Socket A连接成功。但是如果Socket B也想尝试发起与远端主机111.13.101.208:80
的连接,就会产生一样的连接ID,所以报了EADDRINUSE
。
操作系统的区别
BSD/mac os
没区别
Linux
Linux < 3.9
在Linux 3.9之前,只存在SO_REUSEADDR
配置项。他的主要逻辑与BSD相同,但是存在两个意外:
Linux在绑定端口上比BSD更加严格,类似于BSD那种同时绑定通配地址和特定地址的行为,在linux中不被允许。比如在Linux中已经绑定了
192.168.0.100:8000
,那么即使设置了SO_REUSEADDR
,也将无法继续绑定0.0.0.0:8000
。另一个区别是,在Linux的client socket(也就是不需要显式绑定端口,不需要
listen
的socket),如果设置了SO_REUSEADDR
,那么它的作用与BSD中的SO_REUSEPORT
完全相同。即Linux允许多个client socket绑定到同一ip的同一端口。这是为了应对需要绑定多个socket到udp地址端口以处理不同的protocol
的场景。
Linux >= 3.9
Linux 3.9及之后的版本都添加了SO_REUSEPORT
选项,它的工作原理与BSD基本相同,但是依旧多了两个限制:
为了防止端口挟持,只用属于同一有效uid的进程可以通过设置选项来共享ip及端口。
对共享端口的UDP socket而言,内核均匀地分发数据报给每一个socket。而对共享端口的TCP socket而言,内核将均匀地分发连接请求(也就是
accept()
阶段)。这个特性可以用来作为朴素的负载均衡。
Windows
Windows中没有SO_REUSEPORT
选项,SO_REUSEADDR
承担了SO_REUSEPORT
的功能。另外,设置了SO_REUSEADDR
的socket总是能绑定到一个已经被占用的ip端口上,即使先来的socket没有设置SO_REUSEADDR
。这是很强的安全风险,所以微软后来新加了一个SO_EXCLUSIVEADDRUS
的选项来让程序显式地绑定到ip端口上,这样其他socket即使设置了SO_REUSEPORT
也无法重用此端口。
Windows提供了详细的介绍,详见Using SO_REUSEADDR and SO_EXCLUSIVEADDRUSE。