小编典典

SO_REUSEADDR和SO_REUSEPORT有何区别?

linux

man pages和套接字选项程序员单证SO_REUSEADDR,并SO_REUSEPORT针对不同的操作系统,不同的,往往混淆高度。有些操作系统甚至没有该选项SO_REUSEPORT。WEB充满了与此主题相关的信息,通常您会发现仅对于特定操作系统的一个套接字实现才是正确的信息,甚至在本文中也没有明确提及。

那么到底有什么SO_REUSEADDR不同SO_REUSEPORT呢?

系统没有SO_REUSEPORT更多限制吗?

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


阅读 257

收藏
2020-06-02

共1个答案

小编典典

欢迎来到美好的便携性世界……或者说缺少它。在开始详细分析这两个选项并深入了解不同的操作系统如何处理它们之前,应注意的是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地址。根据目标地址和路由表的内容,系统将选择适当的源地址,并将“
any”绑定替换为对所选源IP地址的绑定。

默认情况下,没有两个套接字可以绑定到源地址和源端口的相同组合。只要源端口不同,源地址实际上就无关紧要。绑定socketAA: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,它会同时绑定到所有现有的本地地址,在这种情况下,其他套接字都无法绑定到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
不完全
一样的地址,一个是为所有本地地址的通配符,另一个是一个非常具体的本地地址。请注意,上面的语句,无论是真实的次序socketAsocketB绑定;
没有SO_REUSEADDR它,将永远失败,有了SO_REUSEADDR它,它将永远成功。

为了让您有更好的概览,让我们在此处制作表格并列出所有可能的组合:

SO_REUSEADDR套接字A套接字B结果
-------------------------------------------------- -------------------
  ON / OFF 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确定
   开启192.168.1.0:21 0.0.0.0:21 OK
  开/关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在关闭时进入一种状态。在这种状态下,它将等待,直到所有未决数据已成功发送或直到发生超时为止,在这种情况下,将强制关闭套接字。

内核在关闭套接字之前将等待的时间(无论是否还有数据在飞行)都称为“ 延迟时间” 。在 逗留时间
是在大多数系统中,默认情况下相当长的全球配置(两分钟,你会发现在许多系统中的常见值)。还可以使用socket选项对每个套接字进行配置,该选项SO_LINGER可用于使超时时间变短或变长,甚至完全禁用超时。但是,完全禁用它是一个非常糟糕的主意,因为优雅地关闭TCP套接字是一个稍微复杂的过程,涉及到来回发送两个数据包(以及在丢失数据包时重新发送)和整个关闭过程也受
流连时间的 限制 __。
如果禁用延迟,则套接字不仅可能会丢失飞行中的数据,而且始终会强制关闭而不是正常关闭,通常不建议这样做。关于如何正常关闭TCP连接的详细信息不在此答案的范围内,如果您想了解更多信息,建议您浏览此页面。即使禁用了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_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只是一个名为“ 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中也可以将第二个套接字绑定到同一端口。例如,您不能将其绑定到通配符地址,然后再绑定到更具体的一个或另一个地址,如果您设置了BSD,则两者都可以SO_REUSEADDR。您可以做的是,可以绑定到同一端口和两个不同的非通配地址,这是始终允许的。在这方面,Linux比BSD更具限制性。

  2. 第二个例外是,对于客户端套接字,此选项的行为与SO_REUSEPORTBSD中的行为完全相同,只要它们在绑定之前都设置了此标志。允许这样做的原因很简单,对于多种协议来说,能够将多个套接字完全绑定到同一UDP套接字地址是很重要的,并且因为SO_REUSEPORT以前在3.9之前没有,所以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。设置SO_REUSEADDR在Windows中的行为像设置一个插座上SO_REUSEPORT,并SO_REUSEADDR在BSD插座,但有一个例外:与插座SO_REUSEADDR可以随时绑定完全相同的源地址和端口为已绑定套接字,
即使其他插座没有这个选项绑定时设置
。此行为有些危险,因为它允许应用程序“窃取”另一个应用程序的连接端口。不用说,这可能会带来重大的安全隐患。Microsoft意识到这可能是一个问题,因此添加了另一个套接字选项SO_EXCLUSIVEADDRUSE。设置SO_EXCLUSIVEADDRUSE确保套接字上的绑定成功后,源地址和端口的组合将独占该套接字,并且即使SO_REUSEADDR设置了套接字,也无法将其他套接字绑定到它们。

有关标志SO_REUSEADDRSO_EXCLUSIVEADDRUSE在Windows
上如何工作以及它们如何影响绑定/重新绑定的更多详细信息,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_REUSEPORT。其SO_REUSEADDR行为与BSD中的行为几乎相同。据我所知,没有办法获得与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 设置在套接字1,套接字2或两个套接字上
  • SO_REUSEPORT 设置在套接字1,套接字2或两个套接字上
  • 您可以在主接口上找到的所有地址组合0.0.0.0(通配符),127.0.0.1(特定地址)和第二个特定地址(对于多播,仅224.1.2.3在所有测试中)

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

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

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

2020-06-02