套接字选项 之 SO_REUSEADDR && SO_REUSEPORT
说明
本文下面内容基本上是截取自stackoverflow,针对这两个选项,在另外一篇文章中做了总结,请移步<Linux TCP套接字选项 之 SO_REUSEADDR && SO_REUSEPORT>
原文部分翻译
基本知识点
TCP/UDP连接是由一个五元组(如下)标识的,不允许存在多个连接具有完全相同的五元组,否则无法对它们进行区分;
{<protocol>, <src addr>, <src port>, <dest addr>, <dest port>}
protocol-协议通过socket()函数在创建套接字时设置;
srcaddr:srcport-源地址和源端口通过bind()函数设置;
destaddr:destport-目的地址和目的端口通过connect()函数设置;
尽管UDP是无连接协议,但是仍然允许它调用connect()建立连接;
在无连接模式下工作,UDP套接字没有显示的绑定地址端口,系统将会在套接字第一次发送数据时自动绑定,否则UDP无法接收到任何应答数据;对于未显示绑定的TCP套接字也是一样的,在建立连接之前会系统会自动绑定地址端口;
如果显示的绑定套接字,可以将绑定端口设置为0,意味着绑定”任何端口”,由于套接字不能真正绑定到所有端口,所以在这种情况下,系统必须选择特定的端口(通常来自预定义的、特定于OS的源端口范围)。源地址也可以设置为通配符,即”任何地址”(0.0.0.0为IPv4的通配符, ::为IPv6的通配符)。与端口不同的是,套接字可以绑定到”所有地址”,这意味着”绑定所有本地端口的所有IP地址”;如果套接字稍后进行连接,系统就必须选择特定的源IP地址,因为在”任何”本地地址的情况下,不能建立连接;系统会根据目的IP地址和路由表选择一个合适的源IP地址,并绑定该地址,替换原来的”任何地址”;
默认情况下,没有两各套接字能够绑定到源地址和源端口相同的组合,只要源端口不同,源地址实际上也是不相关的;绑定socketA到A:X,绑定socket到B:Y,A和B标识源地址,X和Y表示源端口;当X!=Y的情况下,总是绑定成功的;但是,即使X==Y,如果A!=B,绑定仍然是成功的;例如:socketA属于一个FTP服务器程序,绑定到了192.168.0.1:21,socketB属于另外一个FTP服务器程序,绑定到了10.0.0.1:21,这两个绑定是成功的;但是套接字可能绑定本地的”任何地址”,如果一个套接字绑定到了0.0.0.0:21,同一时间,其他任何绑定到21端口的尝试都会失败,无论指定哪个特定IP地址,因为0.0.0.0与现有的所有本地地址冲突;
不同系统重用标记
上面这些绑定规则在所有操作系统上都很类似,但是当设置了重用标志时,各个操作系统的表现就会有所不同;BSD是所有类Unix系统的鼻祖,其它操作系统都在某个时间点上拷贝了BSD套接字的实现,之后才发展处自己特有的特征,所以理解好BSD的套接字实现是理解其他操作系统的关键;(下面只关心BSD和Linux,其他部分省略,全文请看本文后面的连接)
BSD
SO_REUSEADDR
(1) 通配地址的绑定
如果在绑定之前设置了SO_REUSEADDR套接字选项,那么除了IP地址和端口完全相同的情况下之外,其他情况均能够成功绑定;与不启用该选项的区别在哪?关键在于精确的匹配,SO_REUSEADDR改变了在查找冲突过程中地址通配符(任何地址)的的处理方法;
在没有启用SO_REUSEADDR选项时,绑定到socketA到0.0.0.0:21和绑定socketB到192.168.0.1:21会失败(返回EADDRINUSE错误),因为0.0.0.0是通配地址,即”任何地址”,所以所有的本地IP地址都认为被该套接字使用了,包括192.168.0.1;
在启用SO_REUSEADDR选项时,上述绑定是成功的;因为0.0.0.0和192.168.0.1不是完全精确相同的两个地址,一个是通配地址,一个是精确的本地地址;
上述描述与socketA和socketB的绑定顺序无关,只要设置了SO_REUSEADDR就会绑定成功,不设置就会绑定失败;
为了更好的理解,下面用一个表格来描述所有情况:
1
2
3
4
5
6
7
8
9
10
|
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)
|
表格假定socketA已经成功绑定到给定的socketA地址上,然后创建socketB,在设置或者不设置SO_REUSEADDR选项的情况下,socketB绑定到给定的地址;结果指示socketB的操作结果;如果第一例是ON/OFF,则SO_REUSEADDR设置与否对结果不构成影响;
(2) TIME_WAIT状态的绑定
通过上面(1)中的内容,我们知道SO_REUSEADDR对通配地址起作用,但是那并不是该选项的唯一作用,还有一个众所周知的作用,也是大多数人使用该选项的首要原因;
为了更好的理解这个作用,我们需要深入了解下TCP是如何工作的;
socket有一个发送缓冲区,在调用send()调用成功之后,并不意味着数据真的被发送出去了,而是数据被添加到发送缓冲区中;对于UDP套接字数据会很快发送,如果不是立即发送的话;但是对于TCP来讲,向缓冲区添加数据和数据真正被发出去之间可能有很长的延迟;当关闭TCP套接字时,发送缓冲区内可能还有没被发送出去的数据,但是应用程序认为已经发送了,因为调用send()调用成功了;TCP是一个可靠的协议,而如果直接丢失数据则不那么可靠;这就是为什么一个还有数据要发送的套接字在关闭时会进入TIME_WAIT状态的原因;在这种状态下,要么等待数据发送完毕,要么会因为在该状态时间过长超时,进而强制关闭连接;
内核在关闭套接字之前会等待一段时间,不管是否有数据要发送,这段时间称为Linger Time,Linger Time在在大多数系统上都是全局配置的,其默认时间较长(大多数系统都默认是2分钟);当然也可以通过套接字的SO_LINGER选项来为每个套接字配置Linger Time,可以设置的更长,更短,或者完全禁用;完全禁用是很糟糕的,虽然TCP优雅的关闭会经过一些列很复杂的过程,包括发送数据包用于关闭连接(以及重传策略防止数据包丢失),但这些关闭过程也在Linger Time的限制内;如果完全禁用的话,不仅会丢失掉尚未发送的数据,还会用强制关闭的方式替代优雅关闭,这是不推荐使用的;如果通过SO_LINGER配置了完全禁用Linger Time,但是程序停止时没有明确的调用关闭来关闭连接,BSD(或者其他系统)会忽略禁用Linger Time的配置;比如发生在如下情况下:代码中调用了exit()或者程序被信号杀死了;这种情况下,程序无法保证套接字在任何情况下都不逗留,操作系统就会忽略这个SO_LINGER配置;
那么操作系统是怎么处理TIME_WAIT状态的?如果套接字选项SO_REUSEADDR 没设置的话,处于TIME_WAIT状态的套接字仍然被认为是绑定在自己的地址和端口上的,在套接字被真正关闭之前,任何新套接字尝试绑定改地址和端口的请求将会失败,而这可能需要等待Linger Time时长;所以不要期望在关闭一个套接字之后立即重新绑定该地址,大多数情况下都会失败;但是如果在将要进行绑定的套接字上设置了SO_REUSEADDR选项,难么已经绑定相同地址和端口并且处于TIME_WAIT状态的套接字将会忽略检查,毕竟它已经处于”half dead”状态了,新的套接字将会成功绑定该地址和端口;当一个套接字处于TIME_WAIT状态,另外一个套接字绑定完全相同的地址端口时,可能会发生些副作用,因为TIME_WAIT状态的套接字仍然在工作,但是这种情况比较少见;
选项开启规则:
最后关于SO_REUSEADDR选项,需要知道的是,它只会检查将要绑定的套接字是否开启该选项,而已经绑定了该地址的其他套接字(包括TIME_WAIT状态)是否启用该选项并不影响,甚至都不会检查他们是否有这个选项;
SO_REUSEPORT
SO_REUSEPORT选项允许多个套接字绑定在相同地址和端口上,只要他们在绑定之前都设置了该选项;如果第一个绑定到该地址和端口的套接字没有设置该选项,那么直到它释放以前,其他任何想要绑定到该地址和端口的套接字都会失败,无论该套接字是否设置了SO_REUSEPORT选项;与SO_REUSEADDR不同的是,SO_REUSEPORT选项不仅要检查当前要绑定的套接字,也要检查其他已经绑定的具有相同地址和端口的套接字是否设置了该选项;
SO_REUSERPORT并不意味着SO_REUSEADDR;现在已经存在一个没有启用SO_REUSEPORT选项的套接字,而另外一个设置了SO_REUSEPORT选项的套接字要绑定与第一个套接字相同的地址和端口,这种情况会绑定失败,就算已绑定套接字是一个处于在TIME_WAIT状态的连接,也无法成功;要绑定一个与TIME_WAIT状态相同地址和端口的套接字,要么设置在新套接字上设置SO_REUSEADDR选项,要么在原套接字和将要绑定的套接字上都设置SO_REUSEPORT选项;当然也允许同时设置SO_REUSEADDR和SO_REUSEPORT选项;
Linux
Linux < 3.9
在3.9之前,只有SO_REUSEADDR选项,除了以下两点之外,其余与BSD相同;
如果一个服务器端监听套接字绑定到了一个指定端口,那么所有其他绑定到这个指定端口的套接字SO_REUSEADDR选项将会被忽略;不使用SO_REUSEADDR选项的新套接字绑定与BSD是一样的;另外,不能先绑定到一个通配地址,然后再在相同的端口上绑定一个指定地址,在BSD上使用SO_REUSEADDR选项是可以这么绑定的;但是允许两个套接字绑定到两个不同的确定地址的相同端口;这方面Linux比BSD更加严格;
对于客户端套接字,如果它们都在绑定之前设置了SO_REUSEADDR选项,那么与BSD中设置了SO_REUSEPORT选项表项一致;这填补了3.9之前没有SO_REUSEPORT选项的缺口;这方面linux比BSD限制更加宽松;
Linux >= 3.9
Linux在3.9版本也加入了SO_REUSEPORT选项;如果绑定相同地址和端口的套接字在绑定之前设置了该选项,那么其效果与BSD中该选项的表现是一样的;
Linux的SO_REUSEPORT与其他系统也有两点不同:
(2) 为了防止端口劫持,有一个限制:想要共享相同地址和端口的套接字必须属于具有相同有效用户ID的进程;这样,一个用户便不能窃取其他用户的端口;
(2) linux在处于SO_REUSEPORT套接字也使用了其他系统没有的特殊魔法:对于UDP套接字,它试图均匀的分发数据包;对于TCP监听套接字,它试图将连接请求均匀的分发到所有绑定该地址和端口的套接字上;因此,应用程序可以很容易的在多个子进程打开同一端口,从而获得一个廉价的负载均衡;
connect()返回EADDRINUSE
除了bind()系统调用会返回EADDRINUSE之外,connect()系统调用也会返回EADDRINUSE;上面说过,一个连接是由五元组定义的,通过地址重用,我们可以确定五元组中的三个(协议,源地址,源端口),如果要连接的对端地址和端口也相同,那么就会产生相同五元组的连接,这是不允许的,进而返回EADDRINUSE,至少TCP是这样(UDP无论如何都不是真正的连接);至少要有对端地址或者对端端口不同,才能区分开连接,区分开输入的数据到底属于哪个连接;