775

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

那么究竟有什么SO_REUSEADDR不同SO_REUSEPORT呢?

没有SO_REUSEPORT更多限制的系统吗?

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

4

2 回答 2

1922

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

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

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

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

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

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

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

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

BSD

SO_REUSEADDR

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

如果没有SO_REUSEADDR,绑定socketA0.0.0.0:21然后绑定socketB192.168.0.1:21将失败(出现错误EADDRINUSE),因为 0.0.0.0 表示“任何本地 IP 地址”,因此此套接字认为所有本地 IP 地址都在使用中,这也包括192.168.0.1。有了SO_REUSEADDR它就会成功,因为0.0.0.0192.168.0.1不是完全相同的地址,一个是所有本地地址的通配符,另一个是非常特定的本地地址。socketA请注意,无论以何种顺序和socketB约束,上述陈述都是正确的;没有SO_REUSEADDR它总是失败,有SO_REUSEADDR它总是成功。

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

SO_REUSEADDR socketA socketB 结果
-------------------------------------------------- ------------------
  开/关 192.168.0.1:21 192.168.0.1:21 错误(EADDRINUSE)
  开/关 192.168.0.1:21 10.0.0.1:21 确定
  开/关 10.0.0.1:21 192.168.0.1:21 确定
   关闭 0.0.0.0:21 192.168.1.0:21 错误(EADDRINUSE)
   关闭 192.168.1.0:21 0.0.0.0:21 错误(EADDRINUSE)
   开 0.0.0.0:21 192.168.1.0:21 好
   开 192.168.1.0:21 0.0.0.0:21 好
  开/关 0.0.0.0:21 0.0.0.0:21 错误(EADDRINUSE)

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

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

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

至多,内核在关闭套接字之前将等待的时间,无论它是否仍有数据在传输中,称为Linger Time。在大多数系统上,Linger Time是全局可配置的,默认情况下相当长(两分钟是许多系统上的常见值)。也可以使用 socket 选项对每个套接字进行配置,该选项SO_LINGER可用于缩短或延长超时时间,甚至完全禁用它。但是,完全禁用它是一个非常糟糕的主意,因为优雅地关闭 TCP 套接字是一个稍微复杂的过程,并且涉及发送和返回几个数据包(以及重新发送这些数据包以防它们丢失)和整个关闭过程也受到Linger Time的限制. 如果你禁用 lingering,你的 socket 不仅可能会丢失数据,而且总是强制关闭而不是优雅地关闭,通常不建议这样做。有关如何优雅关闭 TCP 连接的详细信息超出了此答案的范围,如果您想了解更多信息,我建议您查看此页面。即使您禁用了 lingering SO_LINGER,如果您的进程在没有明确关闭套接字的情况下死亡,BSD(可能还有其他系统)仍然会徘徊,忽略您的配置。例如,如果您的代码只是调用,就会发生这种情况exit()(对于小型、简单的服务器程序来说很常见)或者进程被信号杀死(包括由于非法内存访问而导致它简单地崩溃的可能性)。因此,您无法确保套接字在任何情况下都不会逗留。

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

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

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_REUSEPORT和。SO_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_REUSEADDR方式SO_REUSEPORT相同,这意味着您可以说这SO_REUSEADDR意味着SO_REUSEPORT所有多播地址,反之亦然。


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 ”)中有一个微内核(“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选项就会完全忽略所有针对该端口的套接字。只有在 BSD 中也可以在没有SO_REUSEADDR设置的情况下将第二个套接字绑定到同一个端口。例如,您不能绑定到通配符地址,然后绑定到更具体的一个或相反的方式,如果您在 BSD 中设置SO_REUSEADDR. 你可以做的是你可以绑定到同一个端口和两个不同的非通配符地址,因为这总是被允许的。在这方面,Linux 比 BSD 更具限制性。

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

Linux >= 3.9

Linux 3.9SO_REUSEPORT也为 Linux 添加了该选项。此选项的行为与 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. SO_REUSEADDR在 Windows 中设置套接字的行为类似于在 BSD 中设置和SO_REUSEPORT设置SO_REUSEADDR套接字,但有一个例外:

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

微软意识到这一点并添加了另一个重要的套接字选项:SO_EXCLUSIVEADDRUSE. 对套接字进行设置SO_EXCLUSIVEADDRUSE可确保如果绑定成功,源地址和端口的组合将由该套接字独占拥有,并且没有其他套接字可以绑定到它们,即使SO_REUSEADDR设置也不行。

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


索拉里斯

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

与 Windows 类似,Solaris 可以选择为套接字提供独占绑定。此选项名为SO_EXCLBIND. 如果在绑定之前在套接字上设置了此选项,则SO_REUSEADDR在测试两个套接字是否存在地址冲突时,在另一个套接字上设置将无效。例如,如果socketA绑定到通配符地址并socketBSO_REUSEADDR启用并且绑定到非通配符地址和与 相同的端口socketA,则此绑定通常会成功,除非socketASO_EXCLBIND启用,在这种情况下,无论 的SO_REUSEADDR标志如何,它都会失败socketB


其他系统

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

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

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

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

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

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

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

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

于 2013-01-17T21:45:44.917 回答
15

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

于 2020-06-10T22:34:05.610 回答