SO_REUSEADDR和SO_REUSEPORT有什么不同?

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

SO_REUSEADDRSO_REUSEPORT有何不同?

没有SO_REUSEPORT的系统是否更受限?

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

333451 次浏览

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

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

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

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

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

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

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

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

BSD

SO_REUSEADDR

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

如果没有SO_REUSEADDR,将socketA绑定到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.1socketA4的相同地址,一个是所有本地地址的通配符,另一个是非常特定的本地地址。请注意,无论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    OKON/OFF          10.0.0.1:21   192.168.0.1:21    OKOFF             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    OKON          192.168.1.0:21       0.0.0.0:21    OKON/OFF           0.0.0.0:21       0.0.0.0:21    Error (EADDRINUSE)

上表假设socketA已经成功绑定到为socketA给出的地址,然后创建socketB,要么设置SO_REUSEADDR,要么不设置,最后绑定到为socketB给出的地址。ResultsocketB的绑定操作的结果。如果第一列说ON/OFFSO_REUSEADDR的值与结果无关。

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

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

问题是,系统如何处理处于状态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_REUSEADDR0相同的源地址和端口,只要SO_REUSEADDR1之前绑定的套接字在绑定之前也设置了SO_REUSEPORT。如果绑定到地址和端口的第一个套接字没有设置SO_REUSEPORT,则没有其他套接字可以绑定到完全相同的地址和端口,无论其他套接字是否设置了SO_REUSEPORT,直到第一个套接字再次释放其绑定。与SO_REUSEADDR的情况不同,代码处理SO_REUSEPORT不仅会验证当前绑定的套接字是否设置了SO_REUSEPORT,还会验证具有冲突地址和端口的套接字在绑定时是否设置了SO_REUSEPORT

SO_REUSEPORT并不意味着SO_REUSEADDR。这意味着如果一个套接字在绑定时没有设置SO_REUSEPORT,而另一个套接字在绑定到完全相同的地址和端口时设置了SO_REUSEPORT,则绑定会失败,这是意料之中的,但如果另一个套接字已经死亡并且处于TIME_WAIT状态,它也会失败。为了能够将一个套接字绑定到与处于TIME_WAIT状态的另一个套接字相同的地址和端口,需要在该套接字上设置SO_REUSEADDR,或者在绑定它们之前必须设置SO_REUSEPORTSO_REUSEADDR0个套接字。当然,在套接字上同时设置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_REUSEADDR对所有多播地址意味着SO_REUSEPORT,反之亦然。


FreeBSD/OpenBSD/NetBSD

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


macOS(MacOS X)

在其核心,macOS只是一个名为“达尔文”的BSD风格的UNIX,基于BSD代码的一个相当晚的分支(BSD 4.3),后来甚至与Mac OS 10.3版本的(当时最新的)FreeBSD 5代码库重新同步,因此Apple可以获得完全的POSIX合规性(macOS通过了POSIX认证)。尽管其核心有一个微内核(“马赫”),但内核的其余部分(“XNU”)基本上只是一个BSD内核,这就是为什么macOS提供与BSD相同的选项,它们的行为方式也与BSD相同。

iOS/watch OS/tvOS

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


Linux

Linux<3.9

在Linux3.9之前,只有选项SO_REUSEADDR存在。除了两个重要的例外,这个选项的行为与BSD中的大致相同:

  1. 只要监听的(服务器)TCP套接字绑定到特定的端口,所有针对该端口的套接字都完全忽略SO_REUSEADDR选项。只有在BSD中不设置SO_REUSEADDR也可以的情况下,才能将第二个套接字绑定到同一个端口。例如,你不能绑定到一个通配符地址,然后绑定到一个更具体的地址,或者反过来,如果你设置了SO_REUSEADDR,两者都可以在BSD中实现。你可以做的是绑定到同一个端口和两个不同的非通配符地址,因为这总是允许的。在这方面Linux比BSD更受限制。

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

Linux>=3.9

Linux3.9也添加了选项SO_REUSEPORTLinux。此选项的行为与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的套接字始终可以绑定到与已经绑定的套接字即使另一个套接字在绑定时没有设置此选项完全相同的源地址和端口。这种行为允许应用程序“窃取”另一个应用程序的连接端口。不用说,这具有重大的安全隐患!

微软意识到这一点,并添加了另一个重要的套接字选项: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和X的合并,所以在某种程度上Solaris也是BSD分支,而且是相当早期的分支。因此Solaris只知道SO_REUSEADDR,没有SO_REUSEPORTSO_REUSEADDR的行为与BSD中的行为几乎相同。据我所知,在Solaris中没有办法获得与SO_REUSEPORT相同的行为,这意味着不可能将两个套接字绑定到完全相同的地址和端口。

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


其他系统

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

代码需要构建的只是一个POSIX API(用于网络部分)和一个C99编译器(实际上大多数非C99编译器只要提供inttypes.hstdbool.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,大多数时候程序员可以简单地忽略该状态的存在。

这是密码(我不能在这里包含它,答案有大小限制,代码会将此回复推送超过限制)。

Mecki的回答绝对完美,但值得补充的是,FreeBSD还支持SO_REUSEPORT_LB,它模仿Linux的SO_REUSEPORT行为-它平衡了负载;见设置袜子选择(2)