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

bind() fails after using SO_REUSEADDR option for wildcard socket in state TIME_WAIT

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

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

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

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

我尝试使用SO_REUSEADDR并将延迟时间设置为0,但问题不断发生。我已经看到 SO_REUSEPORT 选项似乎可以解决我的问题,但我更愿意尽可能避免使用它(出于安全目的)。

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

或者可能旗帜不相关,我错过了其他东西。

我看过这个 post How do SO_REUSEADDR and SO_REUSEPORT differ?,关于这个主题的答案很好,但我仍然不明白我是否可以绑定完全相同的地址(通配符和相同的端口)旧套接字处于 TIME_WAIT 状态,新套接字在 Linux.

中设置为 SO_REUSEADDR

将延迟时间设置为零将导致您的套接字不等待发送未发送的数据(所有未发送的数据立即被丢弃),但是它只会肯定避免 TIME_WAIT 状态如果另一个end 已经关闭了它的写入管道。

一个套接字可以看作是两个管道。一个读管道和一个写管道。您的读取管道连接到另一侧的写入管道,您的写入管道连接到另一侧的读取管道。当你打开一个套接字时,两个管道都打开,当你关闭一个套接字时,两个管道都关闭。但是,您可以使用 shutdown() 调用关闭单个管道。

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

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

在某些系统上,将延迟时间设置为零会导致套接字通过重置 (RST) 而不是正常关闭 (FIN, ACK) 关闭,在这种情况下,所有未发送的数据都将被丢弃,并且套接字也不会进入 TIME_WAIT,因为重置后不需要这样做,即使您先关闭套接字也不需要。但是,如果延迟时间为零是否触发重置取决于系统,您不能依赖它,因为没有定义此行为的标准。如果您的套接字阻塞或 non-blocking 以及 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 以上和以下的端口)。某些系统会另外限制对同一用户的重用(IIRC Windows 有此类限制)。

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

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

一个不错的程序work-around: 创建两个进程。一个父进程,它执行所有套接字管理并生成一个子进程,然后处理实际的服务器实现。如果子进程被杀死d,父进程仍然拥有所有套接字,仍然可以干净地关闭它们,可以 re-bind 使用 SO_REUSEADDR 到相同的地址和端口,它甚至可以产生一个新的子进程,所以你的服务器继续 运行.

一些参考资料: