首页 文章

套接字选项SO_REUSEADDR和SO_REUSEPORT,它们有何不同?它们在所有主要操作系统中的含义是否相同?

提问于
浏览
554

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

那么 SO_REUSEADDRSO_REUSEPORT 究竟有何不同?

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

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

1 回答

  • 1367

    欢迎来到可移植性的美妙世界......或者更确切地说,缺乏它 . 在我们开始详细分析这两个选项并深入了解不同操作系统如何处理它们之前,应该注意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 port" . 由于套接字实际上不能绑定到所有现有端口,因此系统必须在这种情况下选择特定端口(通常来自预定义的OS特定的源端口范围) . 源地址存在类似的通配符,可以是"any address"(在IPv4情况下为 0.0.0.0 ,在IPv6情况下为 :: ) . 与端口的情况不同,套接字实际上可以绑定到"any address",这意味着"all source IP addresses of all local interfaces" . 如果稍后连接套接字,系统必须选择特定的源IP地址,因为套接字无法连接,同时绑定到任何本地IP地址 . 根据目标地址和路由表的内容,系统将选择适当的源地址,并将"any"绑定替换为对所选源IP地址的绑定 .

    缺省情况下,没有两个套接字可以绑定到源地址和源端口的同一组合 . 只要源端口不同,源地址实际上是无关紧要的 . 将 socketA 绑定到 A:X 并将 socketB 绑定到 B:Y ,其中 AB 是地址, XY 是端口,只要 X != Y 成立,就始终可以绑定 . 但是,即使 X == Y ,只要 A != B 成立,绑定仍然可行 . 例如 . socketA 属于一个FTP服务器程序,并且绑定到 192.168.0.1:21 并且 socketB 属于另一个FTP服务器程序并且绑定到 10.0.0.1:21 ,两个绑定都将成功 . 但请记住,套接字可能在本地绑定到"any address" . 如果套接字绑定到 0.0.0.0:21 ,则它同时绑定到所有现有本地地址,在这种情况下,无论其尝试绑定哪个特定IP地址,都不能将其他套接字绑定到端口 21 ,因为 0.0.0.0 与所有地址冲突现有的本地IP地址 .

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

    BSD

    SO_REUSEADDR

    如果先前在套接字上启用了 SO_REUSEADDR 为了绑定它,套接字可以成功绑定,除非与另一个绑定到源地址和端口相同组合的套接字发生冲突 . 现在您可能想知道与以前有什么不同?关键字是"exactly" . SO_REUSEADDR 主要更改搜索冲突时如何处理通配符地址("any IP address")的方式 .

    如果没有 SO_REUSEADDR ,将 socketA 绑定到 0.0.0.0:21 然后将 socketB 绑定到 192.168.0.1:21 将失败(错误 EADDRINUSE ),因为0.0.0.0表示"any local IP address",因此此套接字认为所有本地IP地址都在使用,这也包括 192.168.0.1 . 使用 SO_REUSEADDR 它将成功,因为 0.0.0.0192.168.0.1not exactly 相同的地址,一个是所有本地地址的通配符,另一个是非常特定的本地地址 . 请注意,无论 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协议的工作原理 .

    套接字有一个发送缓冲区,如果对 send() 函数的调用成功,并不意味着所请求的数据实际上已经被发送出去,它只意味着数据已被添加到发送缓冲区 . 对于UDP套接字,数据通常很快发送,如果不是立即发送,但对于TCP套接字,在向发送缓冲区添加数据和使TCP实现真正发送该数据之间可能存在相对长的延迟 . 因此,当您关闭TCP套接字时,发送缓冲区中可能仍有未决数据,但尚未发送,但您的代码认为它已发送,因为 send() 调用成功 . 如果TCP实现在您的请求中立即关闭套接字,则所有这些数据都将丢失,并且您的代码将无法解决仍然有数据要发送的套接字在关闭时进入名为 TIME_WAIT 的状态的原因 . 在该状态下,它将等待所有挂起的数据成功发送或直到超时,在这种情况下,套接字被强制关闭 .

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

    问题是,系统如何处理状态 TIME_WAIT 中的套接字?如果未设置 SO_REUSEADDR ,则状态为 TIME_WAIT 的套接字仍被视为仍绑定到源地址和端口,并且任何将新套接字绑定到同一地址和端口的尝试都将失败,直到套接字真正关闭为止,这可能需要只要配置了Linger Time . 因此,不要指望在关闭套接字后立即重新绑定套接字的源地址 . 在大多数情况下,这将失败 . 但是,如果为您尝试绑定的套接字设置了 SO_REUSEADDR ,则只需忽略绑定到状态 TIME_WAIT 中相同地址和端口的另一个套接字,并且它已经"half dead",并且您的套接字可以绑定到完全相同的地址而没有任何问题 . 在这种情况下,它不起任何作用,其他套接字可能具有完全相同的地址和端口 . 请注意,在 TIME_WAIT 状态下将套接字绑定到与死亡套接字完全相同的地址和端口可能会产生意外的,通常是不希望的副作用,以防其他套接字仍然是"at work",但这超出了本答案的范围,幸运的是副作用在实践中相当罕见 .

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

    SO_REUSEPORT

    SO_REUSEPORT 是大多数人所期望的 SO_REUSEADDR . 基本上, SO_REUSEPORT 允许您将任意数量的套接字绑定到 exactly 相同的源地址和端口,只要 all 之前的绑定套接字在绑定之前也设置了 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() 也因此错误而失败的奇怪情况 . 怎么会这样?怎么可以远程地址,毕竟's what connect adds to a socket, be already in use? Connecting multiple sockets to exactly the same remote address has never been a problem before, so what'在这里出错了?

    正如我在回复的最上面所说,连接由五个值的元组定义,记得吗?我还说,这五个值必须是唯一的,否则系统不能再区分两个连接,对吧?好吧,通过地址重用,您可以将同一协议的两个套接字绑定到相同的源地址和端口 . 这意味着这五个值中的三个对于这两个套接字已经是相同的 . 如果您现在尝试将这两个套接字也连接到相同的目标地址和端口,您将创建两个连接的套接字,其元组完全相同 . 这不起作用,至少不适用于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 ", based on a rather late fork of the BSD code (BSD 4.3), which was then later on even re-synchronized with the (at that time current) FreeBSD 5 code base for the Mac OS 10.3 release, so that Apple could gain full POSIX compliance (macOS is POSIX certified). Despite having a microkernel at its core (" Mach "), the rest of the kernel (" XNU”的BSD风格的UNIX,它基本上只是一个BSD内核,这就是为什么macOS提供与BSD相同的选项,它们的行为方式与BSD相同 .

    iOS / watchOS / tvOS

    iOS只是一个带有略微修改和修剪的内核的macOS fork,稍微削弱了用户空间工具集和稍微不同的默认框架集 . watchOS和tvOS是iOS分叉,甚至进一步被剥离(特别是watchOS) . 据我所知,他们的行为与macOS完全一样 .

    Linux

    Linux <3.9

    在Linux 3.9之前,只存在选项 SO_REUSEADDR . 此选项的行为通常与BSD中的相同,但有两个重要的例外:

    • 只要侦听(服务器)TCP套接字绑定到特定端口,就会针对该端口的所有套接字完全忽略 SO_REUSEADDR 选项 . 只有在没有设置 SO_REUSEADDR 的BSD中也可以将第二个套接字绑定到同一个端口 . 例如 . 你不能绑定到通配符地址,然后绑定到更具体的一个或另一个方向,如果你设置 SO_REUSEADDR ,则在BSD中都可以 . 你可以做的是你可以绑定到同一个端口和两个不同的非通配符地址,因为这总是允许的 . 在这方面,Linux比BSD更具限制性 .

    • 第二个例外是对于客户端套接字,此选项的行为与BSD中的 SO_REUSEPORT 完全相同,只要它们在绑定之前都设置了此标志 . 允许这种情况的原因很简单,能够将多个套接字精确地绑定到各种协议的相同UDP套接字地址并且因为3.9之前没有 SO_REUSEPORT 非常重要, SO_REUSEADDR 的行为相应地改为填充差距 . 在这方面,Linux比BSD限制性更小 .

    Linux> = 3.9

    Linux 3.9也为Linux添加了选项 SO_REUSEPORT . 此选项的行为与BSD中的选项完全相同,只要所有套接字在绑定它们之前设置了此选项,就允许绑定到完全相同的地址和端口号 .

    然而,在其他系统上仍有两个不同之处:

    • 为防止"port hijacking",有一个特殊限制: All sockets that want to share the same address and port combination must belong to processes that share the same effective user ID! 因此一个用户不能"steal"另一个用户的端口 . 这是一些特殊的魔法来弥补丢失的 SO_EXCLBIND / SO_EXCLUSIVEADDRUSE 标志 .

    • 此外,内核为其他操作系统中找不到的 SO_REUSEPORT 套接字执行一些"special magic":对于UDP套接字,它尝试均匀分配数据报,对于TCP侦听套接字,它尝试分发传入的连接请求(通过调用 accept() 接受的那些)均匀地跨越共享相同地址和端口组合的所有套接字 . 因此,应用程序可以轻松地在多个子进程中打开相同的端口,然后使用 SO_REUSEPORT 来获得非常便宜的负载 balancer .

    Android

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

    Windows

    Windows只知道 SO_REUSEADDR 选项,没有 SO_REUSEPORT . 在Windows中的套接字上设置 SO_REUSEADDR 的行为类似于在BSD中的套接字上设置 SO_REUSEPORTSO_REUSEADDR ,但有一个例外:带有 SO_REUSEADDR 的套接字始终可以绑定到与已绑定套接字 even if the other socket did not have this option set when it was bound 完全相同的源地址和端口 . 此行为有点危险,因为它允许应用程序"to steal"另一个应用程序的连接端口 . 不用说,这可能会产生重大的安全隐患 . Microsoft意识到这可能是一个问题,因此添加了另一个套接字选项 SO_EXCLUSIVEADDRUSE . 在套接字上设置 SO_EXCLUSIVEADDRUSE 确保如果绑定成功,源地址和端口的组合将由此套接字专有,并且没有其他套接字可以绑定到它们,即使它已设置 SO_REUSEADDR .

    有关标志 SO_REUSEADDRSO_EXCLUSIVEADDRUSE 如何在Windows上工作的更多详细信息,它们如何影响绑定/重新绑定,Microsoft在该回复顶部附近提供了一个类似于我的表的表 . Just visit this page并向下滚动一下 . 实际上有三张 table ,第一个显示旧行为(先前的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中的行为几乎相同 . 据我所知,在Solaris中无法获得与 SO_REUSEPORT 相同的行为,这意味着无法将两个套接字绑定到完全相同的地址和端口 .

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

    其他系统

    如果你的系统没有在上面列出,我写了一个小测试程序,你可以用来了解你的系统如何处理这两个选项 . Also if you think my results are wrong ,请先发布该程序,然后发布任何评论并可能提出虚假声明 .

    代码需要构建的所有东西都是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(我不能在这里包含它,答案有一个大小限制,代码将推动此回复超过限制) .

相关问题