SO_REUSEADDR和SO_REUSEPORT区别

内容来源于StackOverflow的精彩回答,StackOverflow.

以BSD系统为例。
首先,一个TCP/UDP连接(Connection)的id,就是由下面五个值组成元组。

{, , , , }

任何合法的五个值的组合都可以定义一个连接,同时,没有任何两个连接具有完全相同的元组。

第一个值protocol是在socket()设定的,src addrsrc port是在bind()的时候设定的,dest addrdest 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.0192.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.0192.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 ipsrc port已经不同,不可能报EADDRINUSE。但是在SO_REUSEADDRSO_REUSEPORT下,因为地址有重用,那么当重用的地址端口尝试连接同一个远端主机的同一端口时,就会报EADDRINUSE

比如本机只有两个地址,127.0.0.1192.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为{, <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相同,但是存在两个意外:

  1. Linux在绑定端口上比BSD更加严格,类似于BSD那种同时绑定通配地址和特定地址的行为,在linux中不被允许。比如在Linux中已经绑定了192.168.0.100:8000,那么即使设置了SO_REUSEADDR,也将无法继续绑定0.0.0.0:8000

  2. 另一个区别是,在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基本相同,但是依旧多了两个限制:

  1. 为了防止端口挟持,只用属于同一有效uid的进程可以通过设置选项来共享ip及端口。

  2. 对共享端口的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。

你可能感兴趣的:(SO_REUSEADDR和SO_REUSEPORT区别)