SO_REUSEADDR(setsockopt选项)的含义是什么? - Linux

112

来自 man 手册:

SO_REUSEADDR 指定在验证传递给 bind() 的地址时应使用的规则,如果协议支持,则应允许重用本地地址。此选项采用 int 值。这是一个布尔选项。

我什么时候应该使用它?"重用本地地址" 是什么意思?

3个回答

244
TCP的主要设计目标是在面对数据包丢失、数据包重排序以及 - 关键的是 - 数据包复制时,仍能实现可靠的数据通信。

很明显,在连接处于活动状态时,TCP/IP网络栈可以处理所有这些问题,但是在连接关闭后的那一刻会出现一个边缘情况。如果在会话结束时发送的数据包被复制并延迟,以至于4路关闭数据包在延迟的数据包到达接收方之前到达,栈将会忠实地关闭它的连接。然后稍后,延迟的重复数据包出现了。此时应该怎么做呢?

更重要的是,如果使用给定IP地址+TCP端口组合的开放套接字的程序关闭了其套接字,然后在短时间内另一个程序来监听同一IP地址和TCP端口号,它应该怎么办?(典型情况:一个程序被杀死并被快速重新启动。)

有几个选择:

  1. 禁止在至少两倍于数据包飞行最长时间的时间内重复使用该IP/端口组合。在TCP中,这通常称为2×MSL延迟。有时也会看到2×RTT,它大致相当。

    这是所有常见TCP/IP堆栈的默认行为。 2×MSL通常在30到120秒之间,并显示在netstat输出中作为TIME_WAIT期间。在此时间之后,堆栈假定任何流氓数据包由于过期的TTLs而在途中被丢弃,因此该套接字离开TIME_WAIT状态,允许重新使用该IP/端口组合。

  2. 允许新程序重新绑定到该IP/端口组合。在具有BSD sockets接口的堆栈中 - 基本上是所有Unix和类Unix系统,以及通过Winsock的Windows - 您必须通过在调用bind()之前通过setsockopt()设置SO_REUSEADDR选项来请求此行为。

SO_REUSEADDR通常在网络服务器程序中设置,因为常见的使用模式是进行配置更改,然后需要重新启动该程序以使更改生效。如果没有SO_REUSEADDR,则在您杀死它时,如果先前实例中有连接打开,则重新启动程序的新实例中的bind()调用将失败。这些连接将在30-120秒内保持TCP端口处于TIME_WAIT状态,因此您会遇到上述情况1。

设置SO_REUSEADDR的风险在于它会创建一个歧义:TCP数据包头中的元数据不足以唯一地标识该数据包是否过时,因此堆栈无法可靠地判断该数据包是否应被丢弃而不是传递给新侦听器的套接字,因为它显然是针对一个已死侦听器而言。

如果您没有看到这是真的,那么每个连接的监听机器的TCP/IP堆栈必须处理以下内容才能做出决策:

  1. 本地IP: 每个连接不唯一。实际上,我们在这里的问题定义是故意重用本地IP。

  2. 本地TCP端口: 同上。

  3. 远程IP: 导致歧义的机器可能会重新连接,因此这并不能帮助消除数据包的正确目的地。

  4. 远程端口: 在行为良好的网络堆栈中,出站连接的远程端口不会很快被重用,但它只有16位,因此您有30-120秒的时间来强制堆栈通过几万个选择并重用该端口。计算机在20世纪60年代就可以做到这么快了。

    如果您的答案是远程堆栈应该在其端进行类似于TIME_WAIT的操作以禁止ephemeral TCP port重用,则该解决方案假定远程主机是良性的。恶意参与者可以自由地重用该远程端口。

    我想监听程序的堆栈可以选择严格禁止仅使用TCP 4元组的连接,以便在TIME_WAIT状态下,给定的远程主机无法使用相同的远程临时端口重新连接,但我不知道是否有任何具有特定改进的TCP堆栈。

  5. 本地和远程TCP序列号: 这些也不足以唯一标识一个新的远程程序可能会使用相同的值。

如果我们今天重新设计TCP,我认为我们会将TLS或类似的东西作为一个非可选功能集成进去,其中一个影响是使这种无意和恶意连接劫持变得不可能。这需要添加大字段(128位及以上),这在1981年发布当前版本TCP(RFC 793)的文件中根本不实用。
没有这样的硬化,允许在TIME_WAIT期间重新绑定所创建的歧义意味着你可以要么a) 旧侦听器的陈旧数据被错误地传递到属于新侦听器的套接字上,从而打破了侦听器的协议或在连接中不正确地注入陈旧数据; 或者b) 新侦听器套接字的新数据错误地分配给旧侦听器套接字,从而无意中丢失。
安全的做法是等待TIME_WAIT期间结束。
最终,问题归结为成本选择:等待TIME_WAIT期间结束,还是承担不希望的数据丢失或意外数据注入的风险。
许多服务器程序冒这个风险,决定立即使服务器重新运行,以便不错过任何更多的传入连接。
这不是一种普遍的选择。许多程序 - 即使是需要重新启动以应用设置更改的服务器程序 - 选择将SO_REUSEADDR保持不变。程序员可能知道这些风险并选择保持默认设置,或者他们可能对问题一无所知,但正在获得明智默认设置的好处。

一些网络程序会让用户选择配置选项,将责任转嫁给最终用户或系统管理员。


20
非常好且非常有帮助的答案。顺便提一下,注意以下引用:man 7 ip绑定了的TCP本地套接字地址在关闭后一段时间内不可用,除非设置了SO_REUSEADDR标志。 使用此标志时应小心,因为它会使TCP不太可靠。 没有上面的解释,这句话并没有什么帮助。 - patryk.beza
7
重要更正:在Windows/WinSock上,SO_REUSEADDR选项并不像大多数人认为的那样起作用 - 实际上它会做一些非常可怕的事情。引用MSDN的话说:*SO_REUSEADDR套接字选项允许套接字强制绑定到另一个套接字正在使用的端口。[...] * - ntninja
1
一旦第二个套接字成功绑定,绑定到该端口的所有套接字的行为是不确定的。例如,如果同一端口上的所有套接字都提供TCP服务,则无法保证通过该端口传入的任何TCP连接请求都将由正确的套接字处理 - 行为是非确定性的。 MSDN页面进一步解释说,您只应在多播套接字上(如果有)使用此功能,并且所有其他应用程序应改用SO_EXCLUSIVEADDRUSE来可靠地防止其他应用程序恶意使用SO_REUSEADDR。 - ntninja
6
要在Windows上获得类似Unix中的SO_REUSEADDR行为,请在套接字上调用setsockopt(sock, SOL_SOCKET, SO_DONTLINGER, &"\x00\x00\x00\x00", 4); - ntninja
6
对于UDP套接字,多播使用SO_REUSEADDR。基本上,多个套接字可以绑定到同一个端口,并且它们都会收到传入的数据报。 - leoll2
1
你的 TIME_WAIT 优化是一个非常棒的想法。 - armani

47

SO_REUSEADDR允许您的服务器绑定到处于TIME_WAIT状态的地址。

此套接字选项告诉内核,即使此端口正忙(处于TIME_WAIT状态),也要继续重用它。 如果它很忙,但是处于另一种状态,则仍会收到“地址已在使用中”的错误。 如果您的服务器已关闭,然后立即重新启动,而套接字仍处于其端口上活动状态,则此选项很有用。

来自unixguide.net


1
非常感谢,我也找到了这个,但仍然不理解... - Ray Templeton
10
假设您打开了一个TCP连接。在传输数据后,您关闭套接字。但实际上,它将被设置为TIME_WAIT状态(TIME_WAIT ==“可能尚未传递某些数据,或者其他原因,因此我们以谨慎的TCP实现方式等待 :)”)一段时间。您不能再次打开到相同IP /端口的连接,除非使用REUSEADDR。 - William Briand
1
灵光一闪。非常感谢! - Ray Templeton
问题在于它是处于 TIME_WAIT 状态的 TCP 连接。一个连接由两个地址标识:远程端口/IP 和本地端口/IP。请注意,当 TCP 服务器没有重新启动(始终使用相同的套接字)时,客户端连接一直在不断地进出并循环通过 TIME_WAIT 状态。新客户端不会因为之前的客户端处于 TIME_WAIT 而被阻止连接。那么,为什么只是因为服务器重新启动并重新创建套接字就会出现这种情况呢?文档中的解释站不住脚。 - Kaz
@Kaz 这是一个处于 TIME_WAIT 状态的 TCP 端口,这表明它只出现在一端。请参考 RFC 793 中的状态图。另一端可以立即重用其端口,并且(正如您自己所说)如果是客户端,则可以创建到同一目标的另一个连接,甚至可以重用相同的源端口。 - user207421

13
创建socket时,你并不真正拥有它。操作系统(TCP协议栈)为你创建并分配一个句柄(文件描述符)来访问它。当你的socket关闭时,它需要经过几个状态才能“完全关闭”。正如EJP在评论中提到的那样,最长延迟通常是从TIME_WAIT状态开始。这种额外的延迟是为了处理终止序列的最后边缘情况,并确保最后的终止确认已通过或者由于超时而使对方重置自身。你可以在这里找到一些关于此状态的其他考虑。主要注意事项如下:

请记住,TCP保证所有传输的数据都会被交付(如果可能)。当你关闭socket时,服务器进入TIME_WAIT状态,只是为了确保所有数据都已经传输完毕。当socket关闭时,双方通过向彼此发送消息来达成共识,即他们将不再发送任何数据。在握手完成后,看起来这已经足够了,socket应该关闭。问题有两个。首先,无法确定最后的ack是否成功通信。其次,如果传递了“漫游重复项”,则必须处理。

如果尝试快速地使用相同的IP和端口对创建多个socket,则会出现“地址已在使用”错误,因为早期的socket尚未完全释放。使用SO_REUSEADDR将消除此错误,因为它将覆盖对任何先前实例的检查。

1
我认为这句话最重要,但之前没有人提到过 - “如果您试图非常快地创建具有相同ip:端口对的多个套接字,则会收到“地址已在使用中”的错误消息。” 值得一票👍 :) - ultimate cause
3
操作系统不需要时间来“察觉”它。TCP协议中有一个被定义的状态称为TIME_WAIT,规定操作系统在最终释放端口之前必须维护该状态。这种状态是与相关套接字关闭后发生的。 - user207421
@user207421 这个状态是在相关套接字关闭之后发生的 - 如果启用了 SO_LINGER,那么这是否也是正确的,即前台滞留(阻塞 close())不包括 TIME_WAIT?我知道 SO_LINGER 的值为零将完全避免 TIME_WAIT(因为它完全避免了终止序列并发送 RST)。 - haelix
在iOS上,使用SO_REUSEADDR曾经让我吃过大亏——服务器套接字似乎是打开的,但所有客户端(本地主机)都无法连接它,出现ECONNRESET错误。原来,如果iOS上的应用程序意外关闭,服务器套接字可能会持续很长时间并仍然保持活动状态,而使用SO_REUSEADDR后,新的服务器套接字将处于某种“幽灵”状态并且会断开客户端连接。当我禁用了SO_REUSEADDR后,立即注意到了这个问题,因为listen()失败了,我可以通过使用带有listen()的循环来解决这个问题,直到幽灵套接字消失并创建了新的套接字。 - JustAMartin
@haelix 这没有任何区别。Linger时间不包括TIME_WAIT时间。Linger时间在操作系统实际关闭之前,而TIME_WAIT时间则在其后。您唯一可以产生影响的方法是通过将SO_LINGER设置为重置连接,从而绕过所有关闭后状态。 - user207421

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接