SO_REUSEADDR和SO_REUSEPORT有何不同?

How do SO_REUSEADDR and SO_REUSEPORT differ?

提问人:Mecki 提问时间:1/18/2013 最后编辑:claymationMecki 更新时间:2/12/2023 访问量:362388

问:

套接字选项的和程序员文档对于不同的操作系统是不同的,并且经常非常混乱。有些操作系统甚至没有 .WWW 充满了关于这个主题的相互矛盾的信息,通常你可以找到只适用于特定操作系统的一个套接字实现的信息,这些信息甚至可能没有在文本中明确提及。man pagesSO_REUSEADDRSO_REUSEPORTSO_REUSEPORT

那么到底有什么不同呢?SO_REUSEADDRSO_REUSEPORT

系统是否没有更多限制?SO_REUSEPORT

如果我在不同的操作系统上使用其中任何一个,预期的行为究竟是什么?

Linux Windows 套接字 UNIX 可移植性

评论


答:

2119赞 Mecki 1/18/2013 #1

欢迎来到便携性的美妙世界......或者更确切地说是缺乏它。在我们开始详细分析这两个选项并更深入地了解不同的操作系统如何处理它们之前,应该注意的是,BSD 套接字实现是所有套接字实现之母。基本上,所有其他系统都在某个时间点(或至少是它的接口)复制了 BSD 套接字的实现,然后开始自己发展它。当然,BSD 套接字的实现也是同时发展起来的,因此后来复制它的系统获得了早期复制它的系统所缺乏的功能。了解 BSD 套接字实现是理解所有其他套接字实现的关键,因此即使您不想为 BSD 系统编写代码,也应该阅读它。

在我们研究这两个选项之前,您应该了解一些基础知识。TCP/UDP 连接由五个值的元组标识:

{<protocol>, <src addr>, <src port>, <dest addr>, <dest port>}

这些值的任何唯一组合都标识连接。因此,任何两个连接都不能具有相同的五个值,否则系统将无法再区分这些连接。

套接字的协议是在使用函数创建套接字时设置的。源地址和端口通过函数设置。目标地址和端口通过该功能设置。由于 UDP 是一种无连接协议,因此可以在不连接 UDP 套接字的情况下使用 UDP 套接字。然而,它被允许连接它们,在某些情况下对你的代码和一般应用程序设计非常有利。在无连接模式下,首次通过UDP套接字发送数据时未显式绑定的UDP套接字通常由系统自动绑定,因为未绑定的UDP套接字无法接收任何(应答)数据。未绑定的 TCP 套接字也是如此,它在连接之前会自动绑定。socket()bind()connect()

如果显式绑定套接字,则可以将其绑定到 port ,这意味着“任何端口”。由于套接字不能真正绑定到所有现有端口,因此在这种情况下,系统必须选择特定端口本身(通常来自预定义的、特定于操作系统的源端口范围)。源地址也存在类似的通配符,可以是“任何地址”(在 IPv4 和 IPv6 的情况下)。与端口不同,套接字实际上可以绑定到“任何地址”,这意味着“所有本地接口的所有源 IP 地址”。如果稍后连接套接字,系统必须选择特定的源 IP 地址,因为套接字无法连接并同时绑定到任何本地 IP 地址。根据目标地址和路由表的内容,系统将选择适当的源地址,并将“any”绑定替换为与所选源 IP 地址的绑定。00.0.0.0::

默认情况下,两个套接字不能绑定到源地址和源端口的相同组合。只要源端口不同,源地址其实是无关紧要的。如果成立 true,则始终可以绑定到 和 to,即使 .例如 属于一个 FTP 服务器程序并绑定到另一个 FTP 服务器程序并绑定到另一个 FTP 服务器程序,两个绑定都将成功。但请记住,套接字可能在本地绑定到“任何地址”。如果套接字绑定到 ,则它同时绑定到所有现有的本地地址,在这种情况下,其他套接字都不能绑定到 端口 ,无论它尝试绑定到哪个特定的 IP 地址,因为与所有现有的本地 IP 地址冲突。socketAipA:portAsocketBipB:portBipA != ipBportA == portBsocketA192.168.0.1:21socketB10.0.0.1:210.0.0.0:21210.0.0.0

到目前为止,对于所有主要操作系统来说,所说的任何事情几乎都是平等的。当地址重用发挥作用时,事情开始变得特定于操作系统。我们从 BSD 开始,因为正如我上面所说,它是所有套接字实现之母。

BSD的

SO_REUSEADDR

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

如果没有 ,绑定到然后绑定到将失败(有错误),因为 0.0.0.0 表示“任何本地 IP 地址”,因此所有本地 IP 地址都被视为由此套接字使用,这也包括 。有了它,它就会成功,因为 和 不是完全相同的地址,一个是所有本地地址的通配符,另一个是非常具体的本地地址。请注意,无论以何种顺序和绑定,上述陈述都是正确的;没有它,它将永远失败,它将永远成功。SO_REUSEADDRsocketA0.0.0.0:21socketB192.168.0.1:21EADDRINUSE192.168.0.1SO_REUSEADDR0.0.0.0192.168.0.1socketAsocketBSO_REUSEADDRSO_REUSEADDR

为了给您一个更好的概述,让我们在这里制作一个表格并列出所有可能的组合:

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)

上表假设已经成功绑定到 的地址,然后创建,设置或不设置,最后绑定到 给定的地址。 是 的绑定操作的结果。如果第一列说 ,则 的值与结果无关。socketAsocketAsocketBSO_REUSEADDRsocketBResultsocketBON/OFFSO_REUSEADDR

好的,对通配符地址有影响,很高兴知道。然而,这并不是它唯一的影响。还有另一个众所周知的效果,这也是大多数人首先在服务器程序中使用的原因。对于此选项的另一个重要用途,我们必须更深入地了解 TCP 协议的工作原理。SO_REUSEADDRSO_REUSEADDR

如果 TCP 套接字正在关闭,通常会执行 3 次握手;该序列称为 FIN-ACK。这里的问题是,该序列的最后一个 ACK 可能已经到达另一端,也可能没有到达,只有当它到达时,另一端也认为套接字是完全关闭的。为了防止重复使用地址+端口组合,该组合可能仍被某些远程对等方视为打开,系统不会在发送最后一个套接字后立即将套接字视为死套接字,而是将套接字置于通常称为 的状态。它可以处于该状态几分钟(取决于系统设置)。在大多数系统上,您可以通过启用延迟和设置延迟时间 zero1 来绕过该状态,但不能保证这始终是可能的,系统将始终遵守此请求,即使系统遵守它,这会导致套接字被重置 (RST) 关闭,这并不总是一个好主意。要了解有关逗留时间的更多信息,请查看我对此主题的回答ACKTIME_WAIT

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

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

SO_REUSEPORT

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

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

除了添加它晚于之外,没有什么可说的,这就是为什么你不会在其他系统的许多套接字实现中找到它的原因,这些系统在添加此选项之前“分叉”了 BSD 代码,并且没有办法将两个套接字绑定到此选项之前 BSD 中完全相同的套接字地址。SO_REUSEPORTSO_REUSEADDR

connect() 返回 EADDRINUSE?

大多数人都知道可能会因错误而失败,但是,当您开始尝试地址重用时,您可能会遇到该错误失败的奇怪情况。这怎么可能?远程地址,毕竟这是连接添加到套接字的内容,怎么可能已经在使用中?将多个套接字连接到完全相同的远程地址以前从未成为问题,那么这里出了什么问题呢?bind()EADDRINUSEconnect()

正如我在回复的最上面所说,连接是由五个值组成的元组定义的,还记得吗?我还说,这五个值必须是唯一的,否则系统就无法再区分两个连接了,对吧?好吧,通过地址重用,您可以将同一协议的两个套接字绑定到相同的源地址和端口。这意味着这两个套接字的这五个值中的三个已经相同。如果现在尝试将这两个套接字也连接到相同的目标地址和端口,则将创建两个连接的套接字,其元组完全相同。这行不通,至少对于TCP连接不起作用(UDP连接无论如何都不是真正的连接)。如果数据到达两个连接中的任何一个,系统无法判断数据属于哪个连接。对于任一连接,至少目标地址或目标端口必须不同,以便系统在识别传入数据属于哪个连接时没有问题。

因此,如果将相同协议的两个套接字绑定到相同的源地址和端口,并尝试将它们都连接到相同的目标地址和端口,则实际上会失败,并出现您尝试连接的第二个套接字的错误,这意味着具有相同元组的五个值的套接字已经连接。connect()EADDRINUSE

组播地址

大多数人忽略了多播地址存在的事实,但它们确实存在。单播地址用于一对一通信,而组播地址用于一对多通信。大多数人在了解 IPv6 时就知道组播地址,但组播地址也存在于 IPv4 中,尽管此功能从未在公共 Internet 上广泛使用。

更改组播地址的含义,因为它允许将多个套接字绑定到完全相同的源组播地址和端口组合。换言之,对于组播地址的行为与单播地址的行为完全相同。实际上,代码对组播地址的处理方式相同,这意味着您可以说这意味着所有组播地址,反之亦然。SO_REUSEADDRSO_REUSEADDRSO_REUSEPORTSO_REUSEADDRSO_REUSEPORTSO_REUSEADDRSO_REUSEPORT


FreeBSD/OpenBSD/NetBSD

所有这些都是原始 BSD 代码的后期分支,这就是为什么它们都提供与 BSD 相同的选项,并且它们的行为方式也与 BSD 相同。


macOS (MacOS X)

从本质上讲,macOS 只是一个名为“Darwin”的 BSD 风格的 UNIX,它基于 BSD 代码(BSD 4.3)的一个相当晚的分支,后来甚至与 Mac OS 10.3 版本的 FreeBSD 5 代码库重新同步,以便 Apple 可以获得完全的 POSIX 合规性(macOS 已获得 POSIX 认证)。尽管其核心有一个微内核(“Mach”),但内核的其余部分(“XNU”)基本上只是一个 BSD 内核,这就是为什么 macOS 提供与 BSD 相同的选项,并且它们的行为方式也与 BSD 相同。

iOS/watchOS / tvOS

iOS 只是一个 macOS 分支,具有略微修改和修剪的内核、略微精简的用户空间工具集和略有不同的默认框架集。watchOS 和 tvOS 是 iOS 的分支,它们被进一步精简(尤其是 watchOS)。据我所知,它们的行为都与 macOS 完全相同。


Linux操作系统

Linux < 3.9

在 Linux 3.9 之前,只有该选项存在。此选项的行为与 BSD 中的行为大致相同,但有两个重要的例外:SO_REUSEADDR

  1. 只要侦听(服务器)TCP 套接字绑定到特定端口,则对于面向该端口的所有套接字,该选项将被完全忽略。只有在 BSD 中也可以将第二个套接字绑定到同一端口而无需设置时才有可能。例如,你不能绑定到一个通配符地址,然后绑定到一个更具体的地址,或者相反,如果你设置了 .您可以做的是绑定到相同的端口和两个不同的非通配符地址,因为这始终是允许的。在这方面,Linux 比 BSD 更具限制性。SO_REUSEADDRSO_REUSEADDRSO_REUSEADDR

  2. 第二个例外是,对于客户端套接字,此选项的行为与 BSD 中的行为完全相同,只要两者在绑定之前都设置了此标志即可。允许这样做的原因很简单,能够将多个套接字绑定到各种协议的同一 UDP 套接字地址非常重要,并且由于 3.9 之前没有,因此相应地更改了 的行为以填补这一空白。在这方面,Linux 的限制比 BSD 少。SO_REUSEPORTSO_REUSEPORTSO_REUSEADDR

Linux >= 3.9

Linux 3.9 也为 Linux 添加了该选项。此选项的行为与 BSD 中的选项完全相同,并且允许绑定到完全相同的地址和端口号,只要所有套接字在绑定之前都设置了此选项。SO_REUSEPORT

然而,与其他系统相比,仍然有两个区别:SO_REUSEPORT

  1. 为了防止“端口劫持”,有一个特殊的限制:所有想要共享相同地址和端口组合的套接字必须属于共享相同有效用户 ID 的进程!因此,一个用户不能“窃取”另一个用户的端口。这是一些特殊的魔法,可以在一定程度上弥补丢失的/标志。SO_EXCLBINDSO_EXCLUSIVEADDRUSE

  2. 此外,内核还对套接字执行了一些在其他操作系统中没有的“特殊魔术”:对于 UDP 套接字,它尝试均匀地分配数据报,对于 TCP 侦听套接字,它尝试在共享相同地址和端口组合的所有套接字之间均匀地分配传入的连接请求(通过调用接受的请求)。因此,应用程序可以很容易地在多个子进程中打开同一个端口,然后用于获得非常便宜的负载平衡。SO_REUSEPORTaccept()SO_REUSEPORT


人造人

尽管整个 Android 系统与大多数 Linux 发行版有些不同,但其核心工作是略微修改的 Linux 内核,因此适用于 Linux 的所有内容也应该适用于 Android。


窗户

Windows只知道这个选项,没有.在 Windows 中的套接字上进行设置的行为类似于 BSD 中的设置和套接字,但有一个例外:SO_REUSEADDRSO_REUSEPORTSO_REUSEADDRSO_REUSEPORTSO_REUSEADDR

在 Windows 2003 之前,一个套接字始终可以绑定到与已绑定套接字完全相同的源地址和端口,即使另一个套接字在绑定时没有设置此选项。此行为允许应用程序“窃取”另一个应用程序的连接端口。毋庸置疑,这具有重大的安全隐患!SO_REUSEADDR

Microsoft意识到了这一点,并添加了另一个重要的套接字选项:.在套接字上设置可确保如果绑定成功,源地址和端口的组合仅由此套接字拥有,并且没有其他套接字可以绑定到它们,即使它已经设置了。SO_EXCLUSIVEADDRUSESO_EXCLUSIVEADDRUSESO_REUSEADDR

此默认行为首先在 Windows 2003 中更改,Microsoft 将其称为“增强套接字安全性”(在所有其他主要操作系统上默认的行为的有趣名称)。有关更多详细信息,请访问此页面。有三个表:第一个表显示经典行为(使用兼容模式时仍在使用!),第二个表显示 Windows 2003 及更高版本由同一用户进行调用时的行为,第三个表显示不同用户进行调用时的行为。bind()bind()


索拉里斯

Solaris 是 SunOS 的继任者。SunOS 最初是基于 BSD 的一个分支,SunOS 5 和后来的 SVR4 是基于 SVR4 的一个分支,但 SVR4 是 BSD、System V 和 Xenix 的合并,所以在某种程度上 Solaris 也是一个 BSD 分支,而且是一个相当早期的分支。结果Solaris只知道,没有。其行为与在 BSD 中的行为几乎相同。据我所知,没有办法获得与Solaris相同的行为,这意味着不可能将两个套接字绑定到完全相同的地址和端口。SO_REUSEADDRSO_REUSEPORTSO_REUSEADDRSO_REUSEPORT

与 Windows 类似,Solaris 可以选择为套接字提供独占绑定。此选项名为 。如果在绑定套接字之前在套接字上设置了此选项,则在测试两个套接字是否存在地址冲突时,在另一个套接字上设置该选项将不起作用。例如,如果绑定到通配符地址并已启用并绑定到非通配符地址和与 相同的端口,则此绑定通常会成功,除非已启用,在这种情况下,无论 .SO_EXCLBINDSO_REUSEADDRsocketAsocketBSO_REUSEADDRsocketAsocketASO_EXCLBINDSO_REUSEADDRsocketB


其他系统

如果你的系统没有在上面列出,我写了一个小测试程序,你可以用它来了解你的系统如何处理这两个选项。另外,如果您认为我的结果是错误的,请先运行该程序,然后再发表任何评论并可能做出虚假声明。

构建代码所需的只是一个 POSIX API(用于网络部分)和一个 C99 编译器(实际上大多数非 C99 编译器只要它们提供 和 就可以工作;例如 早在提供完整的 C99 支持之前就支持两者)。inttypes.hstdbool.hgcc

程序运行所需的只是系统中至少有一个接口(本地接口除外)分配了一个 IP 地址,并且设置了使用该接口的默认路由。该程序将收集该 IP 地址并将其用作第二个“特定地址”。

它测试了您能想到的所有可能的组合:

  • TCP 和 UDP 协议
  • 普通套接字、侦听(服务器)套接字、组播套接字
  • SO_REUSEADDR在 socket1、socket2 或两个套接字上设置
  • SO_REUSEPORT在 socket1、socket2 或两个套接字上设置
  • 您可以从(通配符)、(特定地址)和在主接口找到的第二个特定地址组合(对于多播,它只是在所有测试中)0.0.0.0127.0.0.1224.1.2.3

并将结果打印在一个漂亮的表格中。它也将在不知道的系统上运行,在这种情况下,此选项根本没有经过测试。SO_REUSEPORT

程序无法轻松测试的是,套接字如何作用于处于该状态的套接字,因为强制并保持套接字处于该状态非常棘手。幸运的是,大多数操作系统在这里似乎只是简单地表现得像 BSD,大多数时候程序员可以简单地忽略该状态的存在。SO_REUSEADDRTIME_WAIT

这是代码(我不能在这里包含它,答案有大小限制,代码会把这个回复推到限制之外)。

评论

11赞 Ben Voigt 7/12/2013
例如,“源地址”实际上应该是“本地地址”,接下来的三个字段也是如此。绑定不会绑定现有的本地地址,但也会绑定所有将来的地址。 当然会创建具有相同协议、本地地址和本地端口的套接字,即使您说这是不可能的。INADDR_ANYlisten
10赞 Mecki 7/26/2013
@Ben 源和目标是用于 IP 寻址的官方术语(我主要指的是)。本地和远程是没有意义的,因为远程地址实际上可以是“本地”地址,而目标的反义词是源而不是本地。我不知道你的问题是什么,我从来没有说过它不会绑定到未来的地址。而且根本不创建任何套接字,这让你的整个句子有点奇怪。INADDR_ANYlisten
8赞 Mecki 7/26/2013
@Ben 当一个新地址被添加到系统中时,它也是一个“现有的本地地址”,它刚刚开始存在。我没有说“到所有当前存在的本地地址”。实际上,我什至说套接字实际上确实绑定到通配符,这意味着套接字绑定到任何与此通配符匹配的东西,现在、明天和一百年后。与源和目的地类似,您只是在这里吹毛求疵。你有什么真正的技术贡献吗?
10赞 Ben Voigt 7/26/2013
@Mecki:你真的认为“存在”这个词包括现在不存在但将来会存在的东西吗?源和目标不是吹毛求疵。当传入的数据包与套接字匹配时,您是说数据包中的目标地址将与套接字的“源”地址匹配?这是错误的,你知道的,你已经说过头和目的地是对立的。套接字上的本地地址与传入数据包的目标地址匹配,并放置在传出数据包的地址中。
13赞 Ben Voigt 8/20/2013
@Mecki:如果你说“套接字的本地地址是传出数据包的源地址和传入数据包的目标地址”,那就更有意义了。数据包具有源地址和目标地址。主机和主机上的套接字则不然。对于数据报套接字,两个对等体是相等的。对于 TCP 套接字,由于三次握手,有一个发起方(客户端)和一个响应方(服务器),但这仍然不意味着连接或连接的套接字也有目标,因为流量是双向流动的。
27赞 Edward Tomasz Napierala 6/11/2020 #2

Mecki 的回答是绝对完美的,但值得补充的是,FreeBSD 也支持 ,它模仿了 Linux 的行为 - 它平衡了负载;参见 setsockopt(2)SO_REUSEPORT_LBSO_REUSEPORT

评论

1赞 Mecki 6/12/2020
不错的发现。当我检查时,我没有在手册页上看到它。绝对值得一提,因为它在将 Linux 软件移植到 FreeBSD 时非常有帮助。