套接字选项 SO_REUSEADDR
和 SO_REUSEPORT
的 man pages
和程序员文档对于不同的操作系统是不同的,并且通常非常混乱 . 某些操作系统甚至没有选项 SO_REUSEPORT
. WEB中充满了关于此主题的矛盾信息,并且通常您可以找到仅对特定操作系统的一个套接字实现的信息,这些信息甚至可能在文本中没有明确提及 .
那么 SO_REUSEADDR
与 SO_REUSEPORT
究竟有何不同?
没有 SO_REUSEPORT
的系统是否更受限制?
如果我在不同的操作系统上使用任何一个,那么预期的行为究竟是什么?
1 回答
欢迎来到可移植性的美妙世界......或者更确切地说,缺乏它 . 在我们开始详细分析这两个选项并深入了解不同操作系统如何处理它们之前,应该注意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
,其中A
和B
是地址,X
和Y
是端口,只要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.0
和192.168.0.1
是 not exactly 相同的地址,一个是所有本地地址的通配符,另一个是非常特定的本地地址 . 请注意,无论socketA
和socketB
的绑定顺序如何,上述声明均为真;没有SO_REUSEADDR
它总会失败,SO_REUSEADDR
它将永远成功 .为了更好地概述,我们在这里制作一个表并列出所有可能的组合:
上表假设
socketA
已成功绑定到socketA
给出的地址,然后创建socketB
,或者设置为SO_REUSEADDR
,最后绑定到为socketB
给出的地址 .Result
是socketB
的绑定操作的结果 . 如果第一列显示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_REUSEPORT
和SO_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_REUSEADDR
和SO_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_REUSEPORT
和SO_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_REUSEADDR
和SO_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.h
和stdbool.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(我不能在这里包含它,答案有一个大小限制,代码将推动此回复超过限制) .