问题

对于不同的操作系统,套接字选项 SO_REUSEADDR SO_REUSEPORT 手册页和编程器文档是不同的,一些操作系统甚至没有选项 SO_REUSEPORT . WEB中充满了关于这个主题的矛盾信息,通常你可以找到只有一个特定操作系统的套接字实现的信息,甚至可能在文本中没有明确提到.

那么 SO_REUSEADDR SO_REUSEPORT 有何不同?

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

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



解决方法

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

默认情况下,没有两个套接字可以绑定到源地址和源端口的相同组合.只要源端口不同,源地址实际上是不相关的.将 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服务器程序, code> 10.0.0.1:21 ,这两个绑定都会成功.请记住,一个套接字可能在本地绑定到“任何地址”.如果套接字绑定到 0.0.0.0:21 ,它同时绑定到所有现有的本地地址,在这种情况下没有其他套接字可以绑定到端口 21 ,无论它尝试绑定到哪个特定的IP地址,因为 0.0.0.0 与所有现有的本地IP地址冲突.

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

BSD

SO_REUSEADDR

如果套接字在绑定之前启用了 SO_REUSEADDR ,则套接字可以成功绑定,除非与绑定到完全的同一组合的另一套接字发生冲突源地址和端口.现在你可能想知道与以前有什么不同?关键字是“exactly”. 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       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 给定的地址. 结果 socketB 的绑定操作的结果.如果第一列表示 ON / OFF ,则 SO_REUSEADDR 的值与结果无关.

好吧, SO_REUSEADDR 对通配符地址有影响,很好知道.然而,这不是它的唯一的效果.还有另一个众所周知的效果,这也是为什么大多数人在服务器程序中首先使用 SO_REUSEADDR 的原因.对于这个选项的其他重要用途,我们必须深入了解TCP协议的工作原理.

套接字有一个发送缓冲区,如果对 send()函数的调用成功,这并不意味着请求的数据实际上已经发出,它只意味着数据已经添加到发送缓冲区.对于UDP套接字,数据通常很快发送,如果不是立即发送,但是对于TCP套接字,在向发送缓冲区添加数据和使TCP实现真正发送数据之间可能存在相对长的延迟.因此,当关闭TCP套接字时,发送缓冲区中可能还有未发送的未决数据,但由于 send()调用成功,您的代码将其视为已发送.如果TCP实现在您的请求立即关闭套接字,所有这些数据将丢失,您的代码甚至不知道. TCP被认为是一个可靠的协议,丢失数据就像这样不是很可靠.这就是为什么仍然有数据发送的套接字在关闭时会进入一个名为 TIME_WAIT 的状态.在这种状态下,它将等待所有挂起的数据被成功发送,或者直到命中超时,在这种情况下套接字被强制关闭.

内核在关闭套接字之前等待的时间量,无论它是否仍有待处理的发送数据,都会被调用 Linger Time . 在大多数系统上可以进行全局配置,默认情况下长时间(两分钟是许多系统上常见的值).它也可以使用套接字选项 SO_LINGER 进行配置,可以使超时更短或更长,甚至完全禁用它.然而,完全禁用它是一个非常糟糕的主意,因为正常关闭TCP套接字是一个稍微复杂的过程,并涉及发送和回退几个数据包(以及在这些数据包丢失的情况下重新发送)和这个整个关闭进程也受到 Linger Time 的限制.如果禁用延迟,您的套接字可能不仅丢失待处理的数据,它也总是强制关闭,而不是优雅,这通常不推荐.关于如何优雅地关闭TCP连接的细节超出了这个答案的范围,如果你想了解更多,我建议你看看此页.即使你使用 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 允许绑定任意数量的套接字到完全相同的源地址和端口,只要所有 SO_REUSEPORT 在绑定之前设置.如果绑定到地址和端口的第一个套接字没有设置 SO_REUSEPORT ,则没有其他套接字可以绑定到完全相同的地址和端口,而不管这个套接字有 SO_REUSEPORT < / code>设置或不,直到第一个套接字再次释放它的绑定.与 SO_REUESADDR 的情况不同,处理 SO_REUSEPORT 的代码不仅会验证当前绑定的套接字是否设置了 SO_REUSEPORT ,还会验证套接字具有冲突的地址和端口在绑定时设置了 SO_REUSEADDR .

SO_REUSEPORT 不表示 SO_REUSEADDR .这意味着如果一个套接字绑定的时候没有设置 SO_REUSEPORT ,而当它绑定到完全相同的地址和端口时,另一个套接字设置了 SO_REUSEPORT 这是预期的,但是如果另一个套接字已经死亡并且处于 TIME_WAIT 状态,它也会失败.要在 TIME_WAIT 状态中将套接字绑定到与另一个套接字相同的地址和端口,需要在该套接字或 SO_REUSEPORT 上设置 SO_REUSEADDR 必须先在 套接字上设置,然后再绑定它们.当然,允许在套接字上设置 SO_REUSEPORT SO_REUSEADDR .

除了 SO_REUSEADDR 之后添加的 SO_REUSEPORT 没有太多的说法,这就是为什么你不能在其他系统的许多套接字实现中找到它,它在添加此选项之前“分叉”BSD代码,并且没有办法在此选项之前将两个套接字绑定到BSD中的完全相同的套接字地址.

Connect() Returning EADDRINUSE?

大多数人都知道 bind()可能会失败,错误代码 EADDRINUSE ,然而,当你开始玩地址重用,你可能会遇到奇怪的情况, connect()也会失败.这怎么可能?如何可以远程地址,在所有的连接添加到套接字,已经在使用中?将多个套接字连接到完全相同的远程地址从来不是一个问题,这里有什么问题?

正如我在我的回答的最顶端说,连接是由五个值的元组定义,记得吗?我还说,这五个值必须是唯一的,否则系统不能再区分两个连接,对不对?那么,使用地址重用,您可以将同一协议的两个套接字绑定到相同的源地址和端口.这意味着这五个值中的三个对于这两个套接字已经是相同的.如果现在尝试将这两个套接字也连接到相同的目标地址和端口,您将创建两个连接的套接字,其元组是完全相同的.这不能工作,至少不是TCP连接(UDP连接也不是真正的连接).如果数据到达两个连接中的任一个,则系统不能知道数据属于哪个连接.至少目标地址或目标端口对于任一连接都必须不同,以便系统可以确定进入数据属于哪个连接.

因此,如果您将同一协议的两个套接字绑定到同一个源地址和端口,并尝试将它们连接到相同的目标地址和端口,则 connect()实际上会失败, EADDRINUSE 用于您尝试连接的第二个套接字,这意味着已经连接了具有五个值的相同元组的套接字.

Multicast Addresses

大多数人忽略了多播地址存在的事实,但它们确实存在.虽然单播地址用于一对一通信,但是多播地址用于一对多通信.大多数人知道组播地址时,他们知道IPv6但组播地址也存在于IPv4,即使这个功能从未广泛使用在公共互联网上.

SO_REUSEADDR 的含义会更改多播地址,因为它允许多个套接字绑定到源多播地址和端口的完全相同的组合.换句话说,对于组播地址 SO_REUSEADDR ,与单播地址的 SO_REUSEPORT 完全一样.实际上,对于多播地址,代码对待 SO_REUSEADDR SO_REUSEPORT ,这意味着你可以说 SO_REUSEADDR 意味着 SO_REUSEPORT 所有多播地址和其他方式.


FreeBSD/OpenBSD/NetBSD

所有这些都是原来BSD代码的晚期分发,这就是为什么他们都提供与BSD相同的选项,他们的行为方式与BSD相同.


MacOS X

MacOS X的核心是简单的BSD式UNIX,基于相当晚的BSD代码,甚至与Mac OS 10.3版本的FreeBSD 5同步.这就是为什么MacOS X提供与BSD相同的选项,它们的行为方式与BSD相同.


iOS

iOS只是修改MacOS X的核心,所以适用于MacOS X的一切也适用于iOS.


Linux

在Linux 3.9之前,只有选项 SO_REUSEADDR 存在.此选项的行为通常与BSD中的相同,但有两个重要的例外.一个例外是,如果监听(服务器)TCP套接字已经绑定到通配符IP地址和特定端口,则无论其中一个或两个套接字是否都设置了此标志,其他TCP套接字不能绑定到同一端口.即使它将使用更具体的地址(如在BSD的情况下允许的).此限制不适用于非侦听(客户端)TCP套接字,并且也可以首先将侦听TCP套接字绑定到特定的IP地址和端口组合,然后将另一个绑定到通配符IP地址和相同的端口.第二个例外是,对于UDP套接字,此选项的行为与BSD中的 SO_REUSEPORT 完全相同,因此两个UDP套接字可以绑定到完全相同的地址和端口组合,只要两者都设置了此标记绑定.

Linux 3.9还向Linux添加了 SO_REUSEPORT 选项.此选项允许两个(或多个)套接字TCP或UDP,侦听(服务器)或非侦听(客户端)绑定到完全相同的地址和端口组合,只要所有套接字(包括第一个)这个标志在绑定它们之前设置.为了防止“端口劫持”,有一个特殊的限制:所有想要共享相同地址和端口组合的套接字必须属于共享相同有效用户ID的进程!所以一个用户不能“窃取”另一个用户的端口.此外,内核为 SO_REUSEPORT 套接字执行一些“特殊魔法”,这在目前为止在任何其他操作系统中都找不到:对于UDP套接字,它试图平均分配数据报,对于TCP侦听套接字,通过在共享相同地址和端口组合的所有套接字上均匀分布传入连接请求(通过调用 accept()接受的连接请求).这意味着,当其它操作系统允许完全地址重用时,套接字接收到数据报或连接请求时,Linux尝试优化分布,以便例如简单服务器进程的多个实例可以轻松地使用 SO_REUSEPORT 套接字,以实现一种简单的负载平衡,绝对免费的,因为内核正在为他们做“所有的辛勤工作”.


Android

即使整个Android系统与大多数Linux发行版有些不同,它的核心工作是一个略微修改的Linux内核,因此适用于Linux的一切也适用于Android.


Windows

Windows只知道 SO_REUSEADDR 选项,没有 SO_REUSEPORT .在Windows中的套接字上设置 SO_REUSEADDR 的行为类似于在BSD的套接字上设置 SO_REUSEPORT SO_REUSEADDR ,但有一个例外: SO_REUSEADDR 可以始终绑定到与已绑定套接字完全相同的源地址和端口,即使其他套接字在绑定时未设置此选项.这种行为有点危险,因为它允许应用程序“窃取”另一个应用程序的连接端口.不用说,这可能有重大的安全隐患. Microsoft意识到这可能是一个问题,从而添加了另一个套接字选项 SO_EXCLUSIVEADDRUSE .在套接字上设置 SO_EXCLUSIVEADDRUSE 确保绑定成功,源地址和端口的组合由该套接字拥有,并且没有其他套接字可以绑定到它们,即使它具有 SO_REUSEADDR 设置.


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 .




相关问题推荐