欢迎来到便携性的美妙世界……或者说缺乏便携性。在我们开始详细分析这两个选项并深入了解不同的操作系统如何处理它们之前,应该注意的是,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,即“任意端口”。由于套接字不能真正绑定到所有现有端口,因此在这种情况下系统将不得不自己选择一个特定端口(通常来自预定义的、操作系统特定的源端口范围)。源地址存在类似的通配符,可以是“任何地址”(0.0.0.0 在 IPv4 的情况下,:: 在 IPv6 的情况下)。与端口不同,套接字实际上可以绑定到“任何地址”,这意味着“所有本地接口的所有源 IP 地址”。如果套接字稍后连接,系统必须选择特定的源 IP 地址,因为套接字无法连接,同时绑定到任何本地 IP 地址。根据目标地址和路由表的内容,系统将选择一个适当的源地址,并将“any”绑定替换为与所选源 IP 地址的绑定。
默认情况下,不能将两个套接字绑定到相同的源地址和源端口组合。只要源端口不同,源地址其实是无关紧要的。如果ipA != ipB 成立,即使portA == portB 成立,也始终可以将socketA 绑定到ipA:portA 和socketB 到ipB:portB。例如。 socketA属于一个FTP服务器程序,绑定192.168.0.1:21,socketB属于另一个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.0 和192.168.0.1 是不完全相同的地址,一个是所有本地地址的通配符,另一个是非常具体的本地地址。请注意,无论socketA 和socketB 绑定的顺序如何,上述陈述都是正确的;没有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 确定
关闭 0.0.0.0:21 192.168.1.0:21 错误(EADDRINUSE)
关闭 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 好
开/关 0.0.0.0:21 0.0.0.0:21 错误(EADDRINUSE)
上表假设socketA已经成功绑定到socketA给定的地址,然后socketB被创建,要么设置SO_REUSEADDR,最后绑定到给定的地址socketB。 Result 是 socketB 的绑定操作的结果。如果第一列显示ON/OFF,则SO_REUSEADDR 的值与结果无关。
好的,SO_REUSEADDR 对通配符地址有影响,很高兴知道。然而,这不是它唯一的效果。还有另一个众所周知的效果,这也是大多数人首先在服务器程序中使用SO_REUSEADDR 的原因。对于此选项的其他重要用途,我们必须更深入地了解 TCP 协议的工作原理。
一个套接字有一个发送缓冲区,如果调用send()函数成功,并不意味着请求的数据真的被发送出去了,它只是意味着数据已经被添加到发送缓冲区。对于 UDP 套接字,数据通常很快就会发送,如果不是立即发送的话,但是对于 TCP 套接字,在将数据添加到发送缓冲区和让 TCP 实现真正发送该数据之间可能存在相对较长的延迟。因此,当您关闭 TCP 套接字时,发送缓冲区中可能仍有待处理的数据,尚未发送,但您的代码认为它已发送,因为 send() 调用成功。如果 TCP 实现根据您的请求立即关闭套接字,那么所有这些数据都将丢失,您的代码甚至都不知道这一点。据说TCP是一个可靠的协议,像这样丢失数据并不是很可靠。这就是为什么仍然有数据要发送的套接字在您关闭它时会进入称为TIME_WAIT 的状态。在这种状态下,它会一直等待,直到所有待处理的数据都已成功发送或达到超时,在这种情况下,套接字将被强制关闭。
至多,内核在关闭套接字之前等待的时间,无论是否仍有数据在传输中,称为Linger Time。 Linger Time 在大多数系统上是全局可配置的,默认情况下相当长(两分钟是您在许多系统上会发现的常见值)。还可以使用套接字选项SO_LINGER 对每个套接字进行配置,该选项可用于缩短或延长超时时间,甚至完全禁用它。但是,完全禁用它是一个非常糟糕的主意,因为优雅地关闭 TCP 套接字是一个稍微复杂的过程,并且涉及发送和返回几个数据包(以及重新发送这些数据包以防它们丢失)和整个关闭过程也受到逗留时间的限制。如果你禁用 lingering,你的 socket 不仅可能会丢失数据,而且总是强制关闭而不是优雅地关闭,通常不建议这样做。有关如何优雅关闭 TCP 连接的详细信息超出了此答案的范围,如果您想了解更多信息,我建议您查看this page。即使您使用SO_LINGER 禁用了逗留,如果您的进程在没有明确关闭套接字的情况下死亡,BSD(可能还有其他系统)仍然会逗留,而忽略您的配置。例如,如果您的代码只是调用exit()(对于小型、简单的服务器程序很常见)或进程被信号杀死(包括由于非法内存访问而简单崩溃的可能性),就会发生这种情况。因此,您无法确保套接字在任何情况下都不会逗留。
问题是,系统如何处理状态为TIME_WAIT 的套接字?如果未设置SO_REUSEADDR,则认为状态为TIME_WAIT 的套接字仍绑定到源地址和端口,并且任何将新套接字绑定到相同地址和端口的尝试都将失败,直到套接字真正关闭,这可能需要与配置的 Linger Time 一样长的时间。所以不要指望你可以在关闭套接字后立即重新绑定它的源地址。在大多数情况下,这将失败。但是,如果为您尝试绑定的套接字设置了SO_REUSEADDR,则在状态TIME_WAIT 中绑定到相同地址和端口的另一个套接字将被忽略,毕竟它已经“半死”,并且您的套接字可以绑定到完全相同的地址没有任何问题。在这种情况下,另一个套接字可能具有完全相同的地址和端口是没有作用的。请注意,将一个套接字绑定到与处于TIME_WAIT 状态的垂死套接字完全相同的地址和端口可能会产生意想不到的、通常是不希望的副作用,以防另一个套接字仍然“工作”,但这超出了这个答案,幸运的是,这些副作用在实践中相当罕见。
关于SO_REUSEADDR,您应该知道最后一件事。只要您要绑定的套接字启用了地址重用,上面编写的所有内容都将起作用。另一个套接字(已经绑定或处于TIME_WAIT 状态的套接字)在绑定时也没有必要设置此标志。决定绑定是成功还是失败的代码只检查输入到bind() 调用中的套接字的SO_REUSEADDR 标志,对于检查的所有其他套接字,甚至不查看该标志。
SO_REUSEPORT
SO_REUSEPORT 是大多数人所期望的SO_REUSEADDR。基本上,SO_REUSEPORT 允许您将任意数量的套接字绑定到完全相同相同的源地址和端口,只要 all 之前绑定的套接字之前也设置了 SO_REUSEPORT他们被束缚了。如果绑定到地址和端口的第一个套接字没有设置SO_REUSEPORT,则没有其他套接字可以绑定到完全相同的地址和端口,无论这个另一个套接字是否设置了SO_REUSEPORT,直到第一个socket 再次释放它的绑定。与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_REUSEPORT 和SO_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_REUSEADDR 和SO_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 / tvOS
iOS 只是一个 macOS 分支,带有略微修改和修剪的内核,在某种程度上剥离了用户空间工具集和略有不同的默认框架集。 watchOS 和 tvOS 是 iOS 的分支,它们被进一步剥离(尤其是 watchOS)。据我所知,它们的行为都与 macOS 完全一样。
Linux
Linux
在 Linux 3.9 之前,只有选项 SO_REUSEADDR 存在。此选项的行为与 BSD 中的行为基本相同,但有两个重要例外:
-
只要侦听(服务器)TCP 套接字绑定到特定端口,SO_REUSEADDR 选项就会完全忽略所有针对该端口的套接字。只有在 BSD 中也可以在没有设置 SO_REUSEADDR 的情况下将第二个套接字绑定到同一个端口。例如。您不能绑定到通配符地址,然后绑定到更具体的地址或相反,如果您设置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 中的选项完全相同,只要所有套接字在绑定之前都设置了此选项,就允许绑定到完全相同的地址和端口号。
然而,在其他系统上与SO_REUSEPORT 仍有两个不同之处:
-
为了防止“端口劫持”,有一个特殊限制:所有想要共享相同地址和端口组合的套接字必须属于共享相同有效用户 ID 的进程!所以一个用户不能“窃取”另一个用户的端口。这是一些特殊的魔法,可以在一定程度上弥补缺少的 SO_EXCLBIND/SO_EXCLUSIVEADDRUSE 标志。
-
此外,内核还为SO_REUSEPORT 套接字执行了一些其他操作系统中没有的“特殊魔法”:对于 UDP 套接字,它会尝试平均分配数据报,对于 TCP 侦听套接字,它会尝试分配传入的连接请求(通过调用accept() 接受的那些)均匀分布在所有共享相同地址和端口组合的套接字上。因此,一个应用程序可以轻松地在多个子进程中打开同一个端口,然后使用SO_REUSEPORT 来获得非常便宜的负载平衡。
安卓
尽管整个 Android 系统与大多数 Linux 发行版有些不同,但它的核心是稍微修改过的 Linux 内核,因此适用于 Linux 的所有内容也应该适用于 Android。
Windows
Windows 只知道SO_REUSEADDR 选项,没有SO_REUSEPORT。在 Windows 中的套接字上设置 SO_REUSEADDR 的行为类似于在 BSD 中的套接字上设置 SO_REUSEPORT 和 SO_REUSEADDR,但有一个例外:
在 Windows 2003 之前,带有SO_REUSEADDR 的套接字始终可以与已绑定的套接字绑定到完全相同的源地址和端口,即使其他套接字在绑定时没有设置此选项。这种行为允许应用程序“窃取”另一个应用程序的连接端口。不用说,这具有重大的安全隐患!
Microsoft 意识到这一点并添加了另一个重要的套接字选项:SO_EXCLUSIVEADDRUSE。在套接字上设置SO_EXCLUSIVEADDRUSE 可以确保如果绑定成功,则源地址和端口的组合由该套接字独占,没有其他套接字可以绑定到它们,甚至如果它有@ 987654443@设置。
此默认行为首先在 Windows 2003 中进行了更改,Microsoft 称之为“增强的套接字安全性”(所有其他主要操作系统默认行为的有趣名称)。更多详情just visit this page。共有三个表:第一个显示经典行为(在使用兼容模式时仍在使用!),第二个显示当bind() 调用由同一用户进行时 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 具有@987654457 @ 启用,在这种情况下,无论socketB 的SO_REUSEADDR 标志是什么,它都会失败。
其他系统
如果您的系统未在上面列出,我编写了一个小测试程序,您可以使用它来了解您的系统如何处理这两个选项。 另外,如果您认为我的结果有误,请先运行该程序,然后再发布任何 cmets 并可能做出虚假声明。
构建代码所需的只是一点 POSIX API(用于网络部分)和 C99 编译器(实际上,只要它们提供 inttypes.h 和 stdbool.h,大多数非 C99 编译器都可以正常工作;例如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_REUSEADDR 如何作用于处于TIME_WAIT 状态的套接字,因为强制并保持套接字处于该状态非常棘手。幸运的是,大多数操作系统在这里似乎只是表现得像 BSD,大多数时候程序员可以简单地忽略该状态的存在。
Here's the code(我不能在这里包含它,答案有大小限制,代码会将这个回复推送到限制之外)。