bind()在状态为TIME_WAIT的通配符套接字使用SO_REUSEADDR选项后失败



我正在Linux上运行服务器应用程序。我的服务器使用一个绑定到地址*::<some_specific_port>的套接字(其中*表示通配符ip地址)。

我的程序可能会被破坏(套接字将用close()关闭)或被一些外部信号崩溃。

我想尽快重新启动我的应用程序,而不必担心tcp的可靠性(我在更高的级别上会考虑这一点)。当我加载服务器时,我使用完全相同的地址(*::<same_exact_port>),但使用errno=EADDRINUSE调用bind()系统调用失败,这意味着地址已经在使用中。

我查了一下,发现套接字处于TIME_WAIT状态。在读了一点之后,我发现了Linux和tcp中的地址重用问题。但正如我之前所说,在我的情况下,我并不真正关心可靠性,我所关心的只是尽快重新启动我的程序(它总是使用通配符ip和相同的端口)。

我尝试使用SO_REUSEADDR并将延迟时间设置为0,但问题一直在发生。我看到了SO_REUSEPORT选项,它似乎解决了我的问题,但我更喜欢尽可能避免使用它(出于安全目的)。

我读过Linux中的net.ipv4.tcp_tw_reuse选项,但文档非常模糊和不清楚。我注意到我的机器被配置为net.ipv4.tcp_tw_reuse=0,我想知道启用这个标志是否会有所帮助。

或者这面旗帜没有关联,我错过了其他东西。

我看过这篇文章,SO_REUSEADDR和SO_REUSEPORT有何不同?,关于这个主题有一个很好的答案,但我仍然不明白,当旧的套接字处于TIME_WAIT状态,而新的套接字在Linux中使用SO_REUSEADDR设置时,我是否可以绑定完全相同的地址(通配符和相同的端口)。

将延迟时间设置为零将导致套接字不等待发送未发送的数据(同时丢弃所有未发送数据),但只有当另一端已经关闭其写管道时,它才能确保避免TIME_WAIT状态。

插座可以看作两根管子。一个读管道和一个写管道。您的读取管道连接到另一侧的写入管道,而您的写入管道则连接到另一端的读取管道。打开套接字时,两个管道都将打开,关闭套接字时,这两个管道均将关闭。但是,可以使用shutdown()调用关闭各个管道。

当您使用shutdown关闭写管道(SHUT_WRSHUT_RDWR)时,即使延迟时间为零,套接字也可能最终位于TIME_WAIT中。当您在套接字上调用close()时,它将隐式关闭两个管道,除非已经关闭。如果它确实关闭了写管道,它将不得不等待,即使它从发送缓冲区中删除了任何挂起的数据。

如果对方先调用close(),或者至少用SHUT_WR调用shutdown(),然后才调用close(),则套接字关闭可能只延迟延迟延迟时间,以确保发送未发送的数据或确认飞行中的数据。在发送并确认所有数据后,或者在达到延迟超时后,无论首先发生什么,套接字都将立即关闭,而不会保持在TIME_WAIT状态,因为是另一方首先发起断开连接。

在某些系统上,将延迟时间设置为零会导致套接字通过重置(RST)而不是正常关闭(FIN, ACK)关闭,在这种情况下,所有未发送的数据都会被丢弃,套接字也不会进入TIME_WAIT,因为重置后不需要这样做,即使您先关闭了套接字也不需要。但是,如果零的延迟时间触发重置是否取决于系统,则不能依赖于此,因为没有定义此行为的标准。如果您的套接字是阻塞的还是非阻塞的,以及shutdown()是否在close()之前被调用,也可能会有所不同。

但是,如果您的应用程序在TCP传输过程中崩溃或终止,两个管道都是打开的,系统必须代表您关闭套接字。在这种情况下,一些系统将简单地忽略任何延迟配置,并返回到标准行为,如果完全禁用延迟,也会得到标准行为。这意味着,即使在支持通过重置关闭套接字的系统上,延迟时间为零,您也可能最终处于TIME_WAIT。同样,这是特定于系统的,但过去在macOS系统上已经咬过我了。

对于SO_REUSEADDR,此设置不一定允许在不同进程之间重用处于TIME_WAIT状态的套接字。如果进程X已经打开了socketA,而现在socketA处于TIME_WAIT状态,那么进程X可以确定地将socketB绑定到与socketA相同的地址和端口,如果并且仅当它使用SO_REUSEADDR(在Linux的情况下,两者都,套接字等待和新套接字需要该标志,在BSD中,只有新套接字需要它)。但是进程Y可能无法绑定到与socketA相同地址和端口的套接字,而socketA出于安全原因仍处于TIME_WAIT状态。

同样,这是特定于系统的,Linux的行为并不总是像BSD或POSIX所期望的那样。它也可能取决于您正在使用的端口号。有时,此限制仅适用于1024以下的端口(大多数测试行为的人忘记了同时测试1024以上和1024以下端口)。某些系统会额外限制对同一用户的重用(IIRC Windows有这样的限制)。

那么,你能做些什么来解决这个问题呢?SO_REUSEPORT是一个选项,因为它对在不同进程中使用完全相同的地址+端口组合没有限制,因为它已明确引入Linux,允许不同进程重用端口,以实现多个服务器进程之间的负载平衡。

另一种可能性是捕获程序的任何终止(尽可能多),然后以某种方式让另一侧先关闭套接字。只要对方启动关闭操作,您就永远不会进入TIME_WAIT。当然,在因为应用程序崩溃而被调用的信号处理程序中,实现这一点是很棘手的,而且可能是不可能的,因为你在信号处理程序上所能做的非常有限。通常情况下,您可以通过在处理程序之外处理信号来解决此问题,但如果这是崩溃信号,则不清楚哪些调用仍然可以安全执行,哪些调用不能执行,即使您在与刚刚崩溃的线程不同的线程上处理信号。还要注意,您无法捕获SIGKILL,即使像这样被杀死,系统也会干净地关闭您的套接字。

一个很好的程序化解决方案:制作两个流程。一个父进程,它完成所有套接字管理,并产生一个子进程,然后处理实际的服务器实现。如果子进程被终止,父进程仍然拥有所有套接字,仍然可以干净地关闭它们,可以使用SO_REUSEADDR重新绑定到同一地址和端口,甚至可以生成一个新的子进程,这样服务器就可以继续运行。

一些参考文献:

  • https://groups.google.com/g/comp.os.linux.development.system/c/sqxTvgccEzk

  • https://groups.google.com/g/alt.winsock.programming/c/md6bsoy08Fk

  • https://www.nybek.com/blog/2015/03/05/cross-platform-testing-of-so_linger/

  • https://www.nybek.com/blog/2015/04/29/so_linger-on-non-blocking-sockets/

最新更新