协慌网

登录 贡献 社区

SO_REUSEADDR 和 SO_REUSEPORT 有何区别?

SO_REUSEADDRSO_REUSEPORTman pages和程序员文档因不同的操作系统而有所不同,并且常常令人困惑。某些操作系统甚至没有选项SO_REUSEPORT 。 WEB 充满了与此主题相关的信息,通常您会发现仅对于特定操作系统的一个套接字实现才是正确的信息,甚至在本文中也没有明确提及。

那么SO_REUSEADDRSO_REUSEPORT有何不同?

SO_REUSEPORT系统是否受到更多限制?

如果我在不同的操作系统上使用任一操作系统,预期的行为到底是什么?

答案

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

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

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

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

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

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

默认情况下,没有两个套接字可以绑定到源地址和源端口的相同组合。只要源端口不同,源地址实际上就无关紧要。绑定socketAipA:portAsocketBipB:portB始终是可能的,如果ipA != ipB也是如此,即使在portA == portB 。例如, socketA属于一个 FTP 服务器程序并绑定到192.168.0.1:21socketB属于另一个 FTP 服务器程序并绑定到10.0.0.1:21 ,这两个绑定都将成功。但是请记住,套接字可能在本地绑定到 “任何地址”。如果套接字绑定到0.0.0.0:21 ,则它同时绑定到所有现有的本地地址,在这种情况下,其他套接字都不能绑定到端口21 ,无论它尝试绑定到哪个特定 IP 地址,例如0.0.0.0与所有现有的本地 IP 地址冲突。

到目前为止,对于所有主要操作系统而言,任何说法都差不多。当地址重用发挥作用时,事情开始变得特定于操作系统。我们从 BSD 开始,因为如上所述,它是所有套接字实现的基础。

BSD

SO_REUSEADDR

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

如果没有SO_REUSEADDRsocketA绑定到0.0.0.0:21 ,然后将socketB绑定到192.168.0.1:21将会失败(错误EADDRINUSE ),因为 0.0.0.0 表示 “任何本地 IP 地址”,因此所有本地 IP 地址都被认为在使用中通过此套接字,也192.168.0.1使用SO_REUSEADDR ,它将成功,因为0.0.0.0192.168.0.1不是完全相同的地址,一个是所有本地地址的通配符,另一个是一个非常特定的本地地址。请注意,无论以哪种顺序绑定socketAsocketB如果没有SO_REUSEADDR ,它将始终失败,而对于SO_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)

上表假定socketA已成功绑定到给出的地址socketA ,然后socketB创建,要么得到SO_REUSEADDR设置与否,最后必然会给定的地址socketBResult socketB的绑定操作的结果。如果第一列显示ON/OFF ,则SO_REUSEADDR的值与结果无关。

好的, SO_REUSEADDR对通配符地址有影响,这是众所周知的。但这不是唯一的效果。还有另一个众所周知的效果,这也是大多数人首先在服务器程序中SO_REUSEADDR对于此选项的其他重要用途,我们必须更深入地研究 TCP 协议的工作方式。

套接字具有发送缓冲区,并且如果对send()函数的调用成功,则并不意味着所请求的数据实际上已经被发送出去,仅意味着已将数据添加到了发送缓冲区中。对于 UDP 套接字,数据通常很快就会发送,即使不是立即发送,但对于 TCP 套接字,在将数据添加到发送缓冲区与让 TCP 实现真正发送该数据之间可能会有相对较长的延迟。 send()调用成功,发送缓冲区中仍可能有尚未发送的数据,这些数据尚未发送,但您的代码将其视为已发送。如果 TCP 实现根据您的请求立即关闭套接字,那么所有这些数据都将丢失,并且您的代码甚至都不知道。据说 TCP 是可靠的协议,丢失数据不是很可靠。这就是为什么当您关闭套接字时仍具有要发送的数据的套接字将进入状态TIME_WAIT在这种状态下,它将等待直到所有未决数据已成功发送或直到发生超时为止,在这种情况下,将强制关闭套接字。

内核最多在关闭套接字之前将等待的时间(无论其是否仍在传输数据)称为 “延迟时间”延迟时间在大多数系统上都是全局可配置的,默认情况下是相当长的(两分钟是很多系统上的常见值)。 SO_LINGER对每个套接字进行配置,该选项可用于使超时时间变短或变长,甚至完全禁用超时。但是,完全禁用它是一个非常糟糕的主意,因为优雅地关闭 TCP 套接字是一个稍微复杂的过程,涉及到来回发送两个数据包(以及在丢失数据包时重新发送)以及整个关闭过程。也受流连时间的限制。如果禁用延迟,则套接字不仅可能会丢失飞行中的数据,而且始终会强制关闭而不是正常关闭,通常不建议这样做。关于如何正常关闭 TCP 连接的详细信息超出了此答案的范围,如果您想了解更多信息,建议您访问此页面。即使您禁用了SO_LINGER ,如果您的进程在未显式关闭套接字的情况下死了,BSD(以及可能的其他系统)仍然会缠绵,而忽略了您已配置的内容。例如,如果您的代码仅调用exit() (在小型,简单的服务器程序中非常常见),或者该进程被信号杀死(包括由于非法内存访问而导致其崩溃的可能性),则会发生这种情况。因此,您无法做任何事情来确保套接字在任何情况下都不会徘徊。

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

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

SO_REUSEPORT

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

SO_REUSEPORT并不意味着SO_REUSEADDR 。这意味着,如果一个套接字SO_REUSEPORT ,而另一个套接字SO_REUSEPORT ,则绑定将失败,这是预料之中的,但是如果另一个套接字已经存在,则绑定也会失败。快要死了,并且处于TIME_WAIT状态。为了能够将套接字绑定到相同的地址和端口在另一座TIME_WAIT状态要求要么SO_REUSEADDR要在该插座上设置或SO_REUSEPORT必须已经之前的结合他们两个插座集。当然,允许在套接字上SO_REUSEPORTSO_REUSEADDR

没有多说关于SO_REUSEPORT比它晚于加入其他SO_REUSEADDR ,这就是为什么你会不会在其他系统中,其中 “分叉” 的许多套接字实现发现该选项之前,BSD 的代码被添加,并且有在此选项之前,无法将两个套接字绑定到 BSD 中完全相同的套接字地址。

Connect()返回 EADDRINUSE?

大多数人都知道bind()可能会因错误EADDRINUSE而失败,但是,当您开始尝试地址重用时,您可能会遇到奇怪的情况,即connect()因该错误而失败。怎么会这样?将连接添加到套接字后,如何才能使用远程地址?将多个套接字连接到完全相同的远程地址以前从来都不是问题,所以这里出了什么问题?

就像我在回复的最上面所说的那样,连接是由五个值的元组定义的,还记得吗?我还说过,这五个值必须唯一,否则系统将无法再区分两个连接,对吗?好了,通过地址重用,您可以将相同协议的两个套接字绑定到相同的源地址和端口。这意味着对于这两个套接字,这五个值中的三个已经相同。如果现在尝试将这两个套接字也都连接到相同的目标地址和端口,则将创建两个已连接的套接字,它们的元组绝对相同。这是行不通的,至少不适用于 TCP 连接(无论如何,UDP 连接都不是真正的连接)。如果两个连接之一的数据到达,系统将无法确定该数据属于哪个连接。至少对于任何一个连接而言,目的地址或目的端口都必须不同,这样系统就可以毫无问题地确定输入数据属于哪个连接。

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

组播地址

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

SO_REUSEADDR的含义对于多播地址有所变化,因为它允许将多个套接字绑定到源多播地址和端口的完全相同的组合。换句话说,对于多播地址SO_REUSEADDR行为完全作为SO_REUSEPORT为单播地址。实际上,该代码对SO_REUSEADDRSO_REUSEPORT ,这意味着您可以说SO_REUSEPORT对所有多播地址都暗含了SO_REUSEADDR


FreeBSD / OpenBSD / NetBSD

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


macOS(MacOS X)

从本质上讲,macOS 只是一个名为 “ Darwin ” 的 BSD 风格的 UNIX,它基于 BSD 代码(BSD 4.3)的更新版本,后来又与当时的 FreeBSD 重新同步。 Mac OS 10.3 发行版的 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 之前,仅存在SO_REUSEADDR此选项的行为通常与 BSD 中的行为相同,但有两个重要的例外:

  1. 只要将侦听(服务器)TCP 套接字绑定到特定端口,针对该端口的所有套接字都将完全忽略SO_REUSEADDR SO_REUSEADDR BSD 中也可以,将第二个套接字绑定到同一端口才是可能的。 SO_REUSEADDR ,那么您将无法绑定到通配符地址,然后再绑定到更具体的一个或另一个地址,在 BSD 中都可以绑定两者。您可以做的是,可以绑定到同一端口和两个不同的非通配符地址,这是始终允许的。在这方面,Linux 比 BSD 更具限制性。

  2. 第二个例外是,对于客户端套接字,此选项的行为与SO_REUSEPORT完全相同,只要它们在绑定之前都设置了此标志。允许这样做的原因很简单,对于多种协议,能够将多个套接字完全绑定到相同的 UDP 套接字地址是很重要的,并且由于 3.9 之前以前SO_REUSEPORT SO_REUSEADDR的行为也进行了相应的更改以填充那个差距。在这方面,Linux 的限制不如 BSD 严格。

Linux> = 3.9

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

但是,在其他系统上SO_REUSEPORT仍然有两个差异:

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

  2. SO_REUSEPORT套接字执行某些 “特殊魔术”,而在其他操作系统中则找不到:对于 UDP 套接字,它尝试平均分配数据报,对于 TCP 侦听套接字,它尝试分配传入的连接请求(通过调用接受的连接请求)在共享相同地址和端口组合的所有套接字上平均分配accept()因此,应用程序可以轻松地在多个子进程中打开同一端口,然后使用SO_REUSEPORT获得非常便宜的负载平衡。


安卓

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


视窗

Windows 仅知道SO_REUSEADDR选项,没有SO_REUSEPORT 。在 Windows 中的套接字上设置SO_REUSEADDR行为就像在 BSD 中的套接字上SO_REUSEPORTSO_REUSEADDR

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

Microsoft 意识到了这一点,并添加了另一个重要的套接字选项: SO_EXCLUSIVEADDRUSE 。在套接字上设置SO_EXCLUSIVEADDRUSE可确保如果绑定成功,则此套接字将专有地拥有源地址和端口的组合,并且即使设置了SO_REUSEADDR

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


的 Solaris

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

与 Windows 相似,Solaris 也可以选择为套接字提供独占绑定。此选项名为SO_EXCLBIND 。如果在绑定套接字之前在套接字上设置了此选项,则如果测试两个套接字的地址冲突,则在另一个套接字上SO_REUSEADDR例如,如果socketA被绑定到一个通配符地址socketB具有SO_REUSEADDR启用并绑定到一个非通配符地址和同一端口socketA ,这个绑定通常会成功,除非socketA已经SO_EXCLBIND启用,在这种情况下,它会不管失败socketB SO_REUSEADDR标志。


其他系统

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

代码需要构建的只是一点点 POSIX API(用于网络部分)和 C99 编译器(实际上,大多数非 C99 编译器都可以正常工作,只要它们提供inttypes.hstdbool.h ;例如, gcc支持提供完整的 C99 支持很久以前)。

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

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

  • TCP 和 UDP 协议
  • 普通套接字,侦听(服务器)套接字,多播套接字
  • SO_REUSEADDR在套接字 1,套接字 2 或两个套接字上设置
  • SO_REUSEPORT在套接字 1,套接字 2 或两个套接字上设置
  • 0.0.0.0 (通配符), 127.0.0.1 (特定地址)和第二个特定地址中的所有地址组合(对于多播,在所有测试中224.1.2.3

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

程序无法轻松测试的是SO_REUSEADDR TIME_WAIT状态的套接字起作用,因为强制并保持套接字处于该状态非常棘手。幸运的是,大多数操作系统似乎在这里的行为就像 BSD 一样,并且大多数时候程序员可以简单地忽略该状态的存在。

这是代码(我不能在这里包括它,答案有大小限制,并且代码会将此答复推到限制之上)。

Mecki 的回答绝对完美,但是值得一提的是,FreeBSD 还支持SO_REUSEPORT_LB ,它模仿 Linux 的SO_REUSEPORT行为 - 它平衡了负载;参见setsockopt(2)