Abel'Blog

我干了什么?究竟拿了时间换了什么?

0%

socketopt-学习

简介

记录一下stackoverflow关于 SO_REUSEPORT SO_REUSEADDR 两个选项的解释。

理论

SO_REUSEADDR

如果在绑定套接字之前在套接字上启用了SO_REUSEADDR,则可以成功绑定该套接字,除非与绑定到完全相同的源地址和端口组合的另一个套接字发生冲突。现在你可能想知道这和以前有什么不同?关键词是“完全正确”。因此,REUSEADDR主要改变了在搜索冲突时处理通配符地址(“任何IP地址”)的方式。

测试结果:

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
ON/OFF 0.0.0.0:21 0.0.0.0:21 Error (EADDRINUSE)
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

套接字有一个发送缓冲区,如果对send()函数的调用成功,并不意味着请求的数据实际上已经发送出去,它只意味着数据已经添加到发送缓冲区。对于UDP套接字,数据通常会很快发送,如果不是立即发送,但是对于TCP套接字,在向发送缓冲区添加数据和让TCP实现真正发送数据之间可能会有相对较长的延迟。因此,当您关闭TCP套接字时,发送缓冲区中可能仍有挂起的数据,该数据尚未发送,但您的代码认为它已发送,因为send()调用已成功。如果TCP实现在您请求时立即关闭套接字,那么所有这些数据都将丢失,您的代码甚至不知道这一点。TCP被认为是一种可靠的协议,像这样丢失数据不是很可靠。这就是为什么当你关闭一个仍然有数据要发送的套接字时,它会进入一种称为TIME_WAIT的状态。在该状态下,它将等待所有挂起的数据成功发送,或者等待超时,在这种情况下,套接字将强制关闭。

内核在关闭套接字之前等待的时间量,不管它是否仍有数据在传输中,都称为延迟时间。在大多数系统上,延迟时间是全局可配置的,默认情况下相当长(在许多系统上,两分钟是一个常见值)。它还可以使用socket选项为每个socket配置,因此可以使用socket选项使超时更短或更长,甚至完全禁用超时。但是,完全禁用它是一个非常糟糕的主意,因为优雅地关闭TCP套接字是一个稍微复杂的过程,需要来回发送两个数据包(以及在数据包丢失时重新发送这些数据包),而整个关闭过程也受到延迟时间的限制。如果禁用“延迟”,套接字可能不仅会在传输过程中丢失数据,而且总是强制关闭而不是优雅地关闭,这通常是不推荐的。有关如何优雅地关闭TCP连接的详细信息超出了本回答的范围,如果您想了解更多信息,建议您查看本页。即使您禁用了延迟SO_LINGER,如果您的进程在没有明确关闭套接字的情况下死亡,BSD(可能还有其他系统)仍然会延迟,忽略您配置的内容。例如,如果您的代码只调用exit()(对于小型、简单的服务器程序来说非常常见),或者进程被信号终止(这包括它可能只是因为非法内存访问而崩溃)。因此,您无法确保套接字在任何情况下都不会出现。

问题是,系统如何处理状态为TIME_WAIT的套接字?如果未设置SO_REUSEADDR,则状态为TIME_WAIT的套接字仍被视为绑定到源地址和端口,任何将新套接字绑定到相同地址和端口的尝试都将失败,直到套接字真正关闭为止,这可能需要与配置的延迟时间一样长的时间。因此,不要期望在关闭套接字后可以立即重新绑定它的源地址。在大多数情况下,这将失败。但是,如果为您尝试绑定的套接字设置了SO_REUSEADDR,那么在TIME_WAIT状态下绑定到相同地址和端口的另一个套接字将被忽略,毕竟它已经“半死不活”,并且您的套接字可以绑定到完全相同的地址而不会出现任何问题。在这种情况下,另一个套接字可能具有完全相同的地址和端口并不起作用。请注意,如果另一个套接字仍在“工作”,则将套接字绑定到时间等待状态下与死亡套接字完全相同的地址和端口可能会产生意外的、通常是不希望的副作用,但这超出了本答案的范围,幸运的是,这些副作用在实践中相当罕见。

还有最后一件事你应该知道。只要要绑定到的套接字启用了地址重用,上面写的所有内容都可以工作。另一个套接字(已绑定或处于TIME_WAIT状态的套接字)在绑定时也不必设置此标志。决定绑定是成功还是失败的代码只检查馈入bind()调用的套接字的SO_REUSEADDR标志,对于所有其他已检查的套接字,甚至不查看该标志。

可以构造一个这样的测试,将SO_LINGER设置很长时间。开启SO_REUSEADDR,socketA发送数据并且关闭,socketB重新bind这个地址,应该是能成功的。

SO_REUSEPORT

所以,大多数人都会期望它会是这样。基本上,SO_REUSEPORT允许您将任意数量的套接字绑定到完全相同的源地址和端口,只要之前绑定的所有套接字在绑定之前也设置了SO_REUSEPORT。如果绑定到地址和端口的第一个套接字未设置SO_REUSEPORT,则在第一个套接字再次释放其绑定之前,任何其他套接字都不能绑定到完全相同的地址和端口,无论该其他套接字是否设置了SO_REUSEPORT。与SO_REUSEADDR的情况不同,处理SO_REUSEPORT的代码不仅会验证当前绑定的套接字是否设置了SO_REUSEPORT,而且还会验证具有冲突地址和端口的套接字在绑定时是否设置了SO_REUSEPORT

SO_REUSEPORT并不意味着SO_REUSEADDR。这意味着,如果一个套接字在绑定时没有设置SO_REUSEPORT,而另一个套接字在绑定到完全相同的地址和端口时设置了SO_REUSEPORT,则绑定会失败,这是预期的,但如果另一个套接字已经死亡并且处于TIME_WAIT状态,则绑定也会失败。为了能够在时间等待状态下将套接字绑定到与另一个套接字相同的地址和端口,需要在该套接字上设置SO_REUSEADDR,或者在绑定之前必须在两个套接字上都设置SO_REUSEPORT。当然,允许在套接字上同时设置SO_REUSEPORTSO_REUSEADDR

关于SO_REUSEPORT,除了它是在SO_REUSEADDR之后添加的之外,没有更多的内容可以说,这就是为什么在其他系统的许多套接字实现中找不到它,在添加此选项之前,它“分叉”了BSD代码,在此选项之前,无法将两个套接字绑定到BSD中完全相同的套接字地址。

实践

如何查看系统的版本

[root@test-qingzhou-01 ~]# hostnamectl
Static hostname: test-qingzhou-01
Icon name: computer-vm
Chassis: vm
Machine ID: 20190711105006363114529432776998
Boot ID: 2abf6a175d9e4dc4a16c0b28d262818b
Virtualization: kvm
Operating System: CentOS Linux 7 (Core)
CPE OS Name: cpe:/o:centos:centos:7
Kernel: Linux 3.10.0-957.21.3.el7.x86_64
Architecture: x86-64

相关代码

golang的代码

1
syscall.SetsockoptInt(s, syscall.IPPROTO_IPV6, syscall.IPV6_V6ONLY, boolint(ipv6only))
1
setsockopt

参考