套接字选项SO_REUSEADDR和SO_REUSEPORT,它们有什么不同? 他们在所有主要的操作系统中都是一样的吗?

套接字选项SO_REUSEADDRSO_REUSEPORTman pages和程序员文档对于不同的操作系统是不同的,并且通常非常混乱。 有些操作系统甚至没有SO_REUSEPORT选项。 WEB充斥着关于这个主题的矛盾信息,通常你可以find一些特定操作系统的一个socket实现的信息,这些信息甚至可能在文中都没有明确提到。

那么SO_REUSEADDRSO_REUSEPORT什么不同呢?

没有SO_REUSEPORT系统更受限制吗?

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

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

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

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

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

当使用socket()函数创build套接字时,将设置套接字的协议。 源地址和端口是用bind()函数设置的。 目标地址和端口使用connect()函数进行设置。 由于UDP是无连接协议,可以使用UDP套接字而无需连接它们。 然而,它允许连接它们,在某些情况下,对于你的代码和一般的应用程序devise来说是非​​常有利的 在无连接模式下,当数据首次被发送时没有被明确地绑定的UDP套接字通常被系统自动绑定,因为未绑定的UDP套接字不能接收任何(回复)数据。 对于未绑定的TCP套接字也是如此,它将在连接之前被自动绑定。

如果你明确地绑定一个套接字,可以将它绑定到端口0 ,这意味着“任何端口”。 由于套接字不能真正绑定到所有现有的端口,所以在这种情况下(通常来自预定义的,OS特定的源端口范围),系统将不得不select特定的端口本身。 源地址存在一个类似的通配符,可以是“任何地址”(IPv4中为0.0.0.0 ,IPv6中为::)。 与端口不同,套接字可以真正绑定到“任何地址”,意思是“所有本地接口的所有源IP地址”。 如果以后连接套接字,系统必须select一个特定的源IP地址,因为一个套接字不能连接,并且同时绑定到任何本地IP地址。 根据目的地址和路由表的内容,系统将select一个合适的源地址,并用绑定到所选的源IP地址replace“any”绑定。

默认情况下,不能将两个套接字绑定到源地址和源端口的相同组合。 只要源端口不同,源地址实际上是不相关的。 将socketA绑定到A:XsocketBB:Y ,其中AB是地址, XY是端口,只要X != Y成立就可以。 但是,即使X == Y ,只要A != B成立,绑定仍然是可能的。 例如, 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地址冲突。

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

BSD

SO_REUSEADDR

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

没有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.1 不是完全相同的地址,一个是所有本地地址的通配符,另一个是非常特定的本地地址。 请注意,无论socketAsocketB绑定的顺序如何,上面的语句都是true; 没有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确定
    OFF 0.0.0.0:21 192.168.1.0:21错误(EADDRINUSE)
    OFF 192.168.1.0:21 0.0.0.0:21错误(EADDRINUSE)
   开启0.0.0.0:21 192.168.1.0:21确定
    ON 192.168.1.0:21 0.0.0.0:21 OK
   ON / OFF 0.0.0.0:21 0.0.0.0:21错误(EADDRINUSE)

上面的表假定socketA已经成功绑定到socketA给定的地址,然后创buildsocketB ,或者获取SO_REUSEADDR ,最后绑定到socketB给定的地址。 ResultsocketB的绑定操作的socketB 。 如果第一列显示ON/OFF ,则SO_REUSEADDR的值与结果无关。

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

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

内核在closures套接字之前将等待的时间量,无论它是否还有未决的发送数据,称为“ 延迟时间” 。 在大多数系统中, 延迟时间是全局可configuration的,默认时间较长(两分钟是在许多系统上可以find的常见值)。 它也可以使用套接字选项SO_LINGER对每个套接字进行configuration,这可以使超时时间更短或更长,甚至完全禁用它。 但是,完全禁用它是一个非常糟糕的想法,因为正常closuresTCP套接字是一个稍微复杂的过程,并且需要发送和返回几个数据包(以及在丢失数据包的情况下重新发送数据包)以及整个closures过程也是灵儿时代的限制。 如果你禁用延迟,你的套接字可能不仅会丢失未完成的数据,而且总是强制closures而不是优雅地closures,这通常是不推荐的。 有关TCP连接如何正常closures的详细信息超出了本答案的范围,如果您想了解更多信息,我build议您查看此页面 。 即使你禁用了SO_LINGER ,如果你的进程在没有明确closures套接字的情况下死掉,BSD(可能还有其他的系统)仍然会stream连忘返,而忽略你configuration的内容。 例如,如果你的代码只是调用exit() (对于微小的,简单的服务器程序很常见),或者进程被一个信号(包括由于非法的内存访问而导致崩溃的可能性)中止。 所以没有什么可以做,以确保一个套接字永远不会在任何情况下stream连忘返。

问题是,系统如何处理状态为TIME_WAIT的套接字? 如果SO_REUSEADDR没有被设置,一个处于TIME_WAIT状态的套接字被认为仍然被绑定到源地址和端口,并且任何试图将一个新的套接字绑定到相同的地址和端口将会失败,直到套接字已经被closures,只要configuration好灵儿时间 。 所以,不要指望在closures它之后立即重新绑定套接字的源地址。 在大多数情况下,这将失败。 但是,如果为要绑定的套接字设置了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 ,则不能将其他套接字绑定到完全相同的地址和端口,而不pipe其他套接字是否设置了SO_REUSEPORT ,直到第一个套接字释放其绑定再次。 与SO_REUESADDR不同的是,处理SO_REUSEPORT的代码将不仅validation当前绑定的套接字是否设置了SO_REUSEPORT而且还将validation具有冲突地址和端口的套接字在绑定时是否设置了SO_REUSEPORT

SO_REUSEPORT不暗示SO_REUSEADDR 。 这意味着如果一个套接字在绑定时没有设置SO_REUSEPORT ,而另一个套接字在绑定到完全相同的地址和端口时设置了SO_REUSEPORT ,则绑定失败,这是预期的,但是如果另一个套接字已经死亡,并处于TIME_WAIT状态。 为了能够将一个套接字绑定到与TIME_WAIT状态下的另一个套接字相同的地址和端口,需要在该套接字上设置SO_REUSEPORT ,或者在绑定套接字之前必须在两个套接字设置SO_REUSEPORT 。 当然可以在套接字上同时设置SO_REUSEPORTSO_REUSEADDR

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

Connect()返回EADDRINUSE?

大多数人都知道bind()可能会失败并显示EADDRINUSE错误,但是当你开始使用地址重用时,你可能会遇到connect()失败的奇怪情况。 这怎么可能? 一个远程地址怎么能连接到一个socket上,毕竟已经在使用了? 将多个套接字连接到完全相同的远程地址从来没有问题,所以这里有什么问题?

正如我在回复的最上面所说的,连接是由一个五值的元组定义的,请记住? 而且我也说过,这五个值必须是唯一的,否则系统不能再区分两个连接了吧? 那么,通过地址重用,您可以将同一协议的两个套接字绑定到同一个源地址和端口。 这意味着这五个值中的三个对于这两个套接字已经是相同的了。 如果现在尝试将这两个套接字连接到相同的目标地址和端口,则可以创build两个连接的套接字,这些套接字的元组完全相同。 这不能工作,至less不能用于TCP连接(UDP连接无论如何都不是真正的连接)。 如果数据到达两个连接中的任何一个,则系统无法知道数据属于哪个连接。 至less目标地址或目标端口对于任一连接都必须是不同的,以便系统没有问题来识别input数据属于哪个连接。

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

多播地址

大多数人忽略了组播地址存在的事实,但它们确实存在。 虽然单播地址用于一对一通信,但多播地址用于一对多通信。 大多数人在了解IPv6时都知道了组播地址,但是组播地址也存在于IPv4中,尽pipe这个function在公共互联网上从未广泛使用。

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

FreeBSD的/ OpenBSD系统/ NetBSD的

所有这些都是原始BSD代码的后期分支,这就是为什么他们三个都提供与BSD相同的选项,他们的行为也和BSD一样。

macOS(MacOS X)

在其核心上,macOS只是一个名为“ Darwin ”的BSD风格的UNIX,基于BSD代码(BSD 4.3)的一个相当晚的分支,后来甚至与当时的FreeBSD进行了重新同步5代码库的Mac OS 10.3版本,以便苹果可以获得完整的POSIX合规性(macOS是POSIXauthentication)。 尽pipe在其核心(“ 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情况下也是可能的。 例如,你不能绑定到通配符地址,然后再绑定到一个更具体的方法,如果你设置SO_REUSEADDR在BSD中都是可能的。 你可以做的是你可以绑定到相同的端口和两个不同的非通配符地址,因为这是始终允许的。 在这方面,Linux比BSD更具有限制性。

  2. 第二个例外是,对于客户端套接字,这个选项的行为与BSD中的SO_REUSEPORT完全相同,只要这两个标记在绑定之前都设置好了。 允许这么做的原因很简单,就是能够将多个套接字准确地绑定到相同的UDP套接字地址,对于不同的协议,并且因为在3.9之前没有SO_REUSEPORT ,所以SO_REUSEADDR的行为被相应地改变为填充那个差距。 在这方面,Linux的限制性比BSDless。

Linux> = 3.9

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

但是,在其他系统上SO_REUSEPORT还是有两点区别的:

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

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

Android的

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

视窗

Windows只知道SO_REUSEADDR选项,没有SO_REUSEPORT 。 在Windows的套接字上设置SO_REUSEPORT就像在BSD套接字上设置SO_REUSEPORTSO_REUSEADDR一样,只有一个例外:带有SO_REUSEADDR的套接字可以始终绑定到与已绑定的套接字完全相同的源地址和端口, 即使其他套接字绑定时没有设置此选项 。 这种行为有点危险,因为它允许应用程序“窃取”另一个应用程序的连接端口。 不用说,这可能会有重大的安全隐患。 微软意识到这可能是一个问题,因此增加了另一个套接字选项SO_EXCLUSIVEADDRUSE 。 在套接字上设置SO_EXCLUSIVEADDRUSE可以确保绑定成功,源地址和端口的组合仅由此套接字拥有,并且没有其他套接字可以绑定到它们,即使设置了SO_REUSEADDR

有关SO_REUSEADDRSO_EXCLUSIVEADDRUSE标志如何在Windows上工作的更多细节,Microsoft如何影响绑定/重新绑定,Microsoft提供了一个类似于我的表的附近的答复。 只要访问这个页面并向下滚动一下。 实际上,有三个表,第一个显示旧行为(以前的Windows 2003),第二个行为(Windows 2003及更高版本),第三个显示如何在Windows 2003和更高版本中的行为更改如果bind()调用是由不同的用户。

的Solaris

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

与Windows相似,Solaris可以给套接字一个独占绑定的选项。 这个选项被命名为SO_EXCLBIND 。 如果在绑定之前在套接字上设置此选项,则在另一个套接字上设置SO_REUSEADDR ,如果两个套接字都testing了地址冲突,则不起作用。 例如,如果socketA被绑定到一个通配符地址,并且socketB已经启用了SO_REUSEADDR并且被绑定到一个非通配符地址和与socketA相同的端口, socketA这个绑定通常会成功,除非socketA已经启用了SO_EXCLBIND ,在这种情况下, socketB SO_REUSEADDR标志。

其他系统

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

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

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

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

  • TCP和UDP协议
  • 正常套接字,侦听(服务器)套接字,多播套接字
  • 在socket1,socket2或两个套接字上设置SO_REUSEADDR
  • 在socket1,socket2或两个套接字上设置SO_REUSEPORT
  • 0.0.0.0 (通配符), 127.0.0.1 (特定地址)和在主接口上find的第二个特定地址(对于多播,在所有testing中只有224.1.2.3

并将结果打印在一个不错的表格中。 它也可以在不知道SO_REUSEPORT系统上工作,在这种情况下,这个选项没有被testing。

程序不能轻易testing的是SO_REUSEADDRTIME_WAIT状态下如何作用于套接字,因为强制并保持套接字处于非常困难的状态。 幸运的是,大多数操作系统似乎只是像BSD一样行为,大多数时候程序员可以忽略该状态的存在。

这里的代码 (我不能在这里包括它,答案有一个大小限制,代码会超过这个限制推动这个答复)。