大部分内容来自stackoverflow上的回答:Socket options SO_REUSEADDR and SO_REUSEPORT, how do they differ? Do they mean the same across all major operating systems?
由于现有的操作系统上的socket都来自BSD socket,且每种操作系统都后续进行了相应的改变。下面先说 BSD socket中的行为,在最后一节单独介绍linux系统下的socket特定行为。
socket五元组
一个连接由五元组(src_ip, src_port, protocol, dst_ip, dst_port)来唯一确定,系统通过tcp/udp报头+IP报头中的这些信息来确定数据包来自远端的哪个进程,以及数据包是发往本地的哪个进程。
对应到socket编程,一个连接由本地和远端的两个socket来确定。
socket通过bind()指定本地使用的ip和端口(如果没有bind,对于udp或tcp客户端系统会随机分配);tcp客户端使用
connect()连接到远端的ip和端口,tcp服务器通过accept()获得远端的ip和端口,无连接的udp,通过udp报文获得远端的ip和端
口,有连接的udp,通过connect()来确定远端的ip和端口。
任意端口和IP
在 socket进行bind()操作时,可以指定“任意”ip或端口,其中“任意”ip指系统可选择本机上所有网卡上的ip,或者127.0.0.1(系统 会根据本地到对端之间的路由来合理选择使用哪个网卡上的ip,或127.0.0.1);“任意”端口是指让系统来随机选择一个可用的端口。
TIME_WAIT 和 Linger time
一个socket有一个发送缓冲区,当调用send()函数成功后,这并不意味着所有数据都真正被发送出去了,它只意味着数据都被送到了发送缓冲区中。对 于UDP socket来说,如果不是立刻发送的话,数据通常也会很快的发送出去,但对于TCP socket,在数据加入到缓冲区和真正被发送出去之间的时延会相当长。这就导致当我们close一个TCP socket的时候,可能在发送缓冲区中保存着等待发送的数据(由于send()成功返回,因此你也许认为数据已经被发送了)。
所以当我们close一个TCP socket的时候,如果它仍然有数据等待发送,那么该socket会进入TIME_WAIT状态。这种状态将持续到数据被全部发送或者发生超时。
在内核彻底关闭socket之前等待的总时间(不管是否有数据在发送缓冲区中等待发送)叫做Linger Time。Linger
Time在大部分系统上都是一个全局性的配置项而且在默认情况下时间相当长(在大部分系统上是两分钟)。socket选项SO_LINGER可以设置
Linger time,但强烈不建议这样做。
SO_REUSEADDR
由 于Linger time的存在,当我们close一个TCP socket A之后,再用另一个socket B去bind相同的ip和port,就会出现 EADDRINUSE 错误。这是对socket B在bind之前先去设置 SO_REUSEADDR,就可以绑定成功。注意:谁要去bind,就是谁去设置 SO_REUSEADDR。
注意,SO_REUSEADDR 只有在socket A close之后,对socket B 设置 SO_REUSEADDR 选项,然后bind才能成功;如果socket A 先bind,然后socket B 在socket 没有close时就bind到相同的端口和ip,仍然会出错! 由于“任意”ip和端口的存在,如何判断两个ip+端口是否相同情况就比较复杂,具体如下表所示(socket A先bind,在未close之前socket B bind,结果):
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) |
SO_REUSEPORT
SO_REUSEADDR 并不能使两个socket同时使用本地相同的ip和端口,SO_REUSEPORT可以实现这个目的。
为了实现多个socket同时使用相同的ip和端口,要求所有这些socket在bind之前都设置SO_REUSEPORT选项。
connect出现EADDRINUSE错误
如 果对端有多个socket同时绑定了相同的ip IP1和端口PORT1,如果本地有socket A 和 B都绑定在了相同的本地ip IP2和端口 PORT2,那么socket A 先connect 到对端的(IP1, PORT1),然后socket B再connect到对端的(IP1, PORT1),则会出现 EADDRINUSE错误,因为这是 connection 五元组出现了重复。
多播
对 多播地址来说,SO_REUSEADDR的含义发生了改变,因为它允许多个socket绑定到完全一样的多播地址和端口,也就是说,对多播地址 SO_REUSEADDR的行为与SO_REUSEPORT对单播地址完全一样。事实上,对于多播地址,对SO_REUSEADDR和 SO_REUSEPORT的处理完全一样,对所有多播地址,SO_REUSEADDR也就意味着SO_REUSEPORT。
linux的 SO_REUSEADDR 和 SO_REUSEPORT
在linux 3.9之前,只存在选项SO_REUSEADDR。且该选项的行为大体上与BSD一样,除了两个差别:
(1)当一个监听(listening)TCP
socket绑定到通配地址和一个特定的端口,无论其它的socket或者是所有的socket(包括监听socket)都设置了
SO_REUSEADDR,其它的TCP
socket都无法绑定到相同的端口(BSD中可以),就更不用说使用一个特定地址了。这个限制并不用在非监听TCP
socket上,当一个监听socket绑定到一个特定的地址和端口组合,然后另一个socket绑定到通配地址和相同的端口,这样是可行的。
(2)当把SO_REUSEADDR用在UDP socket上时,它的行为与BSD上SO_REUSEPORT完全相同,因此两个UDP socket只要都设置了SO_REUSEADDR,那么它们可以绑定到相同的地址和端口。
Linux 3.9加入了SO_REUSEPORT。这个选项允许多个socket(TCP or
UDP)不管是监听socket还是非监听socket只要都在绑定之前都设置了它,那么就可以绑定到完全相同的地址和端口。为了阻止"port
劫持"(Port hijacking)有一个特别的限制:所有希望共享源地址和端口的socket都必须拥有相同的有效用户id(effective user ID)。因此一个用户就不能从另一个用户那里"偷取"端口。
另外,内核在处理SO_REUSEPORT socket的时候使用了其它系统上没有用到的"特别魔法":
对于UDP
socket,内核尝试平均的转发数据报,对于TCP监听socket,内核尝试将新的客户连接请求(由accept返回)平均的交给共享同一地址和端口
的socket(监听socket)。这意味着在其他系统上socket收到一个数据报或连接请求或多或少是随机的,但是linux尝试优化分配。例如:一个简单的服务器程序的多个实例可以使用SO_REUSEPORT socket实现一个简单的负载均衡,因为内核已经把复制的分配都做了。