SO_REUSEADDR和SO_REUSEPORT有什么不同?

klh5stk1  于 2022-10-17  发布在  Unix
关注(0)|答案(2)|浏览(163)

套接字选项SO_REUSEADDRSO_REUSEPORTman pages和程序员文档对于不同的操作系统是不同的,通常非常令人困惑。有些操作系统甚至没有SO_REUSEPORT选项。Web上充满了关于这个主题的相互矛盾的信息,通常您可以找到只对特定操作系统的一个套接字实现正确的信息,甚至可能在正文中没有明确提到。
那么SO_REUSEADDRSO_REUSEPORT到底有什么不同呢?
没有SO_REUSEPORT的系统是否更加有限?
如果我在不同的操作系统上使用其中的任何一个,那么预期的行为到底是什么?

lo8azlld

lo8azlld1#

欢迎来到可移植的奇妙世界……或者更确切地说,是因为它的缺乏。在我们开始详细分析这两个选项并深入了解不同操作系统如何处理它们之前,应该注意到,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,这意味着“任何端口”。由于套接字不能真正绑定到所有现有端口,因此在这种情况下,系统将不得不自己选择一个特定的端口(通常是从预定义的、操作系统特定的源端口范围中)。源地址也存在类似的通配符,可以是“any Address”(如果是IPv4,则为0.0.0.0;如果是IPv6,则为::)。与端口不同,套接字可以真正绑定到“任何地址”,这意味着“所有本地接口的所有源IP地址”。如果套接字稍后连接,则系统必须选择特定的源IP地址,因为套接字不能同时连接到任何本地IP地址。根据目的地址和路由表的内容,系统将选择适当的源地址,并使用到所选源IP地址的绑定来替换“Any”绑定。
默认情况下,任何两个套接字都不能绑定到源地址和源端口的相同组合。只要源端口不同,源地址实际上是无关紧要的。如果ipA != ipB成立,即使当portA == portB为真时,也总是可以将socketA绑定到ipA:portA,并将socketB绑定到ipB: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_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的绑定顺序如何,上面的语句都是正确的;如果没有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,最后绑定到为socketB指定的地址。ResultsocketB的绑定操作的结果。如果第一列显示ON/OFF,则SO_REUSEADDR的值与结果无关。
好的,SO_REUSEADDR对通配符地址有影响,很高兴知道。然而,这并不是它唯一的效果。还有另一个众所周知的影响,这也是为什么大多数人首先在服务器程序中使用SO_REUSEADDR的原因。对于此选项的其他重要用法,我们必须更深入地了解TCP协议是如何工作的。
如果正在关闭一个TCP套接字,通常会执行三次握手;该序列称为FIN-ACK。这里的问题是,该序列的最后一个ACK可能已经到达另一端,或者可能没有到达,并且只有当它已经到达时,另一端也认为该套接字是完全关闭的。为了防止重复使用地址+端口组合,该地址+端口组合可能仍被一些远程对等方认为是打开的,系统在发送最后一个ACK之后不会立即将套接字视为失效,而是将该套接字置于通常称为TIME_WAIT的状态。它可以处于该状态数分钟(系统相关设置)。在大多数系统上,您可以通过启用延迟并将延迟时间设置为零1来绕过这种状态,但不能保证这总是可能的,系统将始终响应这个请求,即使系统支持它,这也会导致通过重置(RST)来关闭套接字,这并不总是一个好主意。要了解更多关于Linger Time的信息,请查看my answer about this topic
问题是,系统如何处理状态为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_REUSEADDR的情况不同,处理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()也会失败并出现该错误。这怎么可能呢?远程地址怎么可能已经在使用中,毕竟这是CONNECT向套接字添加的东西?将多个套接字连接到完全相同的远程地址以前从来都不是问题,那么这里出了什么问题呢?
正如我在回复的顶部所说的,连接是由五个值的元组定义的,记得吗?我还说,这五个值必须是唯一的,否则系统不能再区分两个连接,对吗?通过地址重用,您可以将相同协议的两个套接字绑定到相同的源地址和端口。这意味着这五个值中的三个对于这两个套接字已经是相同的。如果您现在尝试将这两个套接字连接到相同的目标地址和端口,您将创建两个连接的套接字,它们的元组完全相同。这是行不通的,至少不适用于TCP连接(UDP连接无论如何都不是真正的连接)。如果数据到达两个连接中的任何一个,系统都无法判断数据属于哪个连接。对于这两个连接,至少目的地址或目的端口必须不同,这样系统就不会有问题来识别传入数据属于哪个连接。
因此,如果您将相同协议的两个套接字绑定到相同的源地址和端口,并尝试将它们连接到相同的目标地址和端口,则对于您尝试连接的第二个套接字,connect()实际上将失败并显示错误EADDRINUSE,这意味着已经连接了一个具有相同的五个值元组的套接字。

组播地址

大多数人忽略了组播地址存在的事实,但它们确实存在。单播地址用于一对一通信,而组播地址用于一对多通信。大多数人在了解IPv6时就知道了组播地址,但组播地址也存在于IPv4中,尽管这一功能从未在公共互联网上广泛使用。
对于多播地址,SO_REUSEADDR的含义改变了,因为它允许将多个套接字绑定到源多播地址和端口的完全相同的组合。换句话说,对于多播地址,SO_REUSEADDR的行为与单播地址的SO_REUSEPORT完全相同。实际上,代码将SO_REUSEADDRSO_REUSEPORT等同于多播地址,这意味着您可以说SO_REUSEADDR表示所有多播地址的SO_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/twOS

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

Linux

Linux<3.9

在Linux3.9之前,只有选项SO_REUSEADDR存在。此选项的行为与BSD中的基本相同,但有两个重要的例外:

1.只要监听(服务器)套接字绑定到特定端口,则针对该端口的所有套接字都会完全忽略SO_REUSEADDR选项。仅当在没有设置SO_REUSEADDR的情况下,在BSD中也可以将第二个套接字绑定到同一端口时,才可以将第二个套接字绑定到相同的端口。例如,您不能绑定到通配符地址,然后绑定到更具体的地址,或者反过来,如果您设置了SO_REUSEADDR,则在BSD中这两种情况都是可能的。您可以做的是绑定到相同的端口和两个不同的非通配符地址,这始终是允许的。在这方面,Linux比BSD限制更多。
1.第二个例外是,对于客户端套接字,此选项的行为与BSD中的SO_REUSEPORT完全相同,只要两者在绑定之前都设置了此标志。允许这样做的原因很简单,因为对于不同的协议,能够将多个套接字绑定到完全相同的UDP套接字地址是很重要的,并且在3.9之前没有SO_REUSEPORT,因此SO_REUSEADDR的行为被相应地改变以填补这一空白。在这方面,Linux没有BSD那么严格。

Linux>=3.9

Linux3.9还向Linux添加了选项SO_REUSEPORT。此选项的行为与BSD中的选项完全相同,只要所有套接字在绑定之前设置了此选项,就可以绑定到完全相同的地址和端口号。
然而,在其他系统上,SO_REUSEPORT仍然有两个不同之处:
1.为了防止端口劫持,有一个特殊的限制:**所有想要共享相同地址和端口组合的Socket必须属于共享同一个有效用户ID的进程!**这样一个用户就不能“窃取”另一个用户的端口。这是一些特殊的魔术,可以在一定程度上弥补缺少的SO_EXCLBIND/SO_EXCLUSIVEADDRUSE标志。
1.此外,内核对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将其称为“增强的套接字安全”(对所有其他主要操作系统上的默认行为的有趣名称)。有关更多详细信息,请参阅just visit this page。有三个表:第一个表显示了经典行为(在使用兼容模式时仍在使用!),第二个表显示了同一用户进行bind()调用时Windows 2003及更高版本的行为,第三个表显示了不同用户进行bind()调用时的行为。

索拉里斯
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不起作用。例如,如果socketA绑定到通配符地址,并且socketB启用了SO_REUSEADDR,并且绑定到非通配符地址和与socketA相同的端口,则此绑定通常会成功,除非socketA启用了SO_EXCLBIND,在这种情况下,无论socketBSO_REUSEADDR标志是什么,它都将失败。

其他系统
如果您的系统没有在上面列出,我编写了一个小测试程序,您可以使用它来了解您的系统如何处理这两个选项。如果你认为我的结果是错误的,请先运行该程序,然后再发表任何评论,并可能做出虚假声明。
代码需要构建的只是一个位POSIX API(用于网络部分)和一个C99编译器(实际上,大多数非C99编译器只要提供inttypes.hstdbool.h就可以工作得很好;例如,gcc在提供完全C99支持之前很久就支持两者)。
该程序需要运行的全部内容是:系统中至少有一个接口(本地接口除外)分配了IP地址,并且设置了使用该接口的默认路由。该程序将收集该IP地址,并将其用作第二个“特定地址”。
它测试你能想到的所有可能的组合:

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

并将结果打印在漂亮的表格中。它还可以在不知道SO_REUSEPORT的系统上运行,在这种情况下,这个选项根本不会被测试。
程序不能轻松测试的是SO_REUSEADDR如何作用于处于TIME_WAIT状态的套接字,因为强制和保持套接字处于该状态非常棘手。幸运的是,大多数操作系统在这里的行为似乎就像BSD一样,大多数时候程序员可以简单地忽略这种状态的存在。
Here's the code(我不能在这里包括它,答案有大小限制,代码会将此回复推到超过限制)。

9jyewag0

9jyewag02#

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

相关问题