Answers:
欢迎来到美好的便携性世界……或者说缺少它。在我们开始详细分析这两个选项并深入了解不同的操作系统如何处理它们之前,应注意的是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地址的绑定。
默认情况下,没有两个套接字可以绑定到源地址和源端口的相同组合。只要源端口不同,源地址实际上就无关紧要。绑定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
,既绑定会成功。但是请记住,套接字可能在本地绑定到“任何地址”。如果套接字绑定到0.0.0.0:21
,它会同时绑定到所有现有的本地地址,在这种情况下,其他套接字都无法绑定到port 21
,无论它尝试绑定到哪个特定IP地址,都会0.0.0.0
与所有现有的本地IP地址发生冲突。
到目前为止,所有主要操作系统都说的差不多。当地址重用发挥作用时,事情开始变得特定于操作系统。我们从BSD开始,因为如上所述,它是所有套接字实现的基础。
如果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套接字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确定 开/关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
在关闭时进入一种状态。在这种状态下,它将等待,直到所有未决数据已成功发送或直到发生超时为止,在这种情况下,将强制关闭套接字。
内核在关闭套接字之前将等待的时间(无论是否还有数据在飞行)都被称为“ 延迟时间”。在逗留时间是在大多数系统中,默认情况下相当长的全球配置(两分钟,你会发现在许多系统中的常见值)。还可以使用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_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_REUSEPORT
和SO_REUSEADDR
。
没有多说关于SO_REUSEPORT
其他比它晚于加入SO_REUSEADDR
,这就是为什么你会不会在其他系统中,其中“分叉”的许多套接字实现发现该选项之前,BSD的代码被添加,并且没有在此选项之前,将两个套接字绑定到BSD中完全相同的套接字地址的方法。
大多数人都知道这bind()
可能会因错误而失败EADDRINUSE
,但是,当您开始尝试地址重用时,您也可能会遇到同样connect()
因该错误而失败的奇怪情况。怎么会这样?将连接添加到套接字后,如何才能使用远程地址?将多个套接字连接到完全相同的远程地址以前从来都不是问题,所以这里出了什么问题?
正如我在答复的最开头所说的那样,连接由五个值的元组定义,还记得吗?我还说过,这五个值必须唯一,否则系统将无法再区分两个连接,对吗?好了,通过地址重用,您可以将具有相同协议的两个套接字绑定到相同的源地址和端口。这意味着对于这两个套接字,这五个值中的三个已经相同。如果现在尝试将这两个套接字也都连接到相同的目标地址和端口,则将创建两个已连接的套接字,它们的元组绝对相同。这是行不通的,至少不适用于TCP连接(无论如何,UDP连接都不是真正的连接)。如果两个连接之一的数据到达,系统将无法确定该数据属于哪个连接。
因此,如果将具有相同协议的两个套接字绑定到相同的源地址和端口,并尝试将它们都连接到相同的目标地址和端口,connect()
则实际上将EADDRINUSE
因尝试连接的第二个套接字而出错,这意味着具有五个值的相同元组的套接字已经连接。
大多数人忽略多播地址存在的事实,但是它们确实存在。当单播地址用于一对一通信时,多播地址用于一对多通信。大多数人在了解IPv6时就知道了组播地址,但是IPv4中也存在组播地址,即使此功能从未在公共Internet上广泛使用。
SO_REUSEADDR
多播地址更改的含义,因为它允许将多个套接字绑定到源多播地址和端口的完全相同的组合。换句话说,对于多播地址,SO_REUSEADDR
其行为与SO_REUSEPORT
单播地址完全相同。事实上,代码对待SO_REUSEADDR
和SO_REUSEPORT
相同的多播地址,这意味着你可以说,SO_REUSEADDR
意味着SO_REUSEPORT
所有的组播地址和其他方式轮。
所有这些都是原始BSD代码的较晚分支,这就是为什么它们三个都提供与BSD相同的选项,而且它们的行为方式也与BSD相同。
从本质上讲,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只是一个macOS分支,具有经过稍微修改和修剪的内核,稍微精简的用户空间工具集和稍有不同的默认框架集。watchOS和tvOS是iOS的分支,其功能进一步简化(尤其是watchOS)。据我所知,它们的行为均与macOS完全相同。
在Linux 3.9之前,仅SO_REUSEADDR
存在该选项。此选项的行为与BSD大致相同,但有两个重要的例外:
只要侦听(服务器)TCP套接字绑定到特定端口,SO_REUSEADDR
针对该端口的所有套接字都将完全忽略该选项。只有在没有SO_REUSEADDR
设置的情况下,在BSD中也可以将第二个套接字绑定到同一端口。例如,您不能将其绑定到通配符地址,然后再绑定到更具体的一个或另一个地址,如果您设置了BSD,则两者都可以SO_REUSEADDR
。您可以做的是,您可以绑定到同一端口和两个不同的非通配地址,这是始终允许的。在这方面,Linux比BSD更具限制性。
第二个例外是,对于客户端套接字,此选项的行为与SO_REUSEPORT
BSD中的行为完全相同,只要它们在绑定之前都设置了此标志。允许这样做的原因很简单,对于多种协议来说,能够将多个套接字完全绑定到同一UDP套接字地址是很重要的,并且因为以前没有SO_REUSEPORT
3.9之前的版本,所以SO_REUSEADDR
相应地更改了行为以填补该空白。 。在这方面,Linux的限制不如BSD严格。
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仅知道该SO_REUSEADDR
选项,没有SO_REUSEPORT
。设置SO_REUSEADDR
在Windows中的行为像设置一个插座上SO_REUSEPORT
,并SO_REUSEADDR
在BSD插座,但有一个例外:与插座SO_REUSEADDR
可以随时绑定完全相同的源地址和端口为已绑定套接字,即使其他插座没有这个选项绑定时设置。此行为有些危险,因为它允许应用程序“窃取”另一个应用程序的连接端口。不用说,这可能会带来重大的安全隐患。Microsoft意识到这可能是一个问题,因此添加了另一个套接字选项SO_EXCLUSIVEADDRUSE
。设置SO_EXCLUSIVEADDRUSE
确保套接字成功绑定后,源地址和端口的组合将独占该套接字,并且即使SO_REUSEADDR
设置了套接字,也无法将其他套接字绑定到它们。
有关标志SO_REUSEADDR
和SO_EXCLUSIVEADDRUSE
在Windows 上如何工作以及它们如何影响绑定/重新绑定的更多详细信息,Microsoft谨在该答复顶部附近提供了一个类似于我的表的表。只需访问此页面并向下滚动一点即可。实际上有三个表,第一个表显示旧行为(Windows 2003之前的版本),第二个表显示行为(Windows 2003及更高版本),第三个表显示在Windows 2003中行为的变化方式,如果bind()
通过不同的用户。
Solaris是SunOS的继承者。SunOS最初基于BSD的分支,SunOS 5后来基于SVR4的分支,但是SVR4是BSD,System V和Xenix的合并,因此在某种程度上,Solaris也是BSD的分支,并且相当早。结果Solaris只知道SO_REUSEADDR
,没有SO_REUSEPORT
。SO_REUSEADDR
行为与BSD中的行为几乎相同。据我所知,没有办法获得与SO_REUSEPORT
Solaris中相同的行为,这意味着不可能将两个套接字绑定到完全相同的地址和端口。
与Windows相似,Solaris也可以选择为套接字提供排他绑定。此选项名为SO_EXCLBIND
。如果在绑定套接字之前在套接字上设置了此选项,则SO_REUSEADDR
如果测试两个套接字的地址冲突,则在另一个套接字上设置无效。例如,如果socketA
绑定到通配符地址并socketB
已SO_REUSEADDR
启用并绑定到非通配符地址和与该端口相同的端口socketA
,则除非绑定socketA
已SO_EXCLBIND
启用,否则此绑定通常将成功,在这种情况下,无论的SO_REUSEADDR
标志如何,绑定都会失败socketB
。
如果您的系统未在上面列出,我编写了一个小测试程序,您可以使用该程序来了解系统如何处理这两个选项。另外,如果您认为我的结果有误,请先运行该程序,然后再发表任何评论,甚至可能提出虚假声明。
该代码需要构建的只是一个POSIX API(用于网络部分)和一个C99编译器(实际上,大多数非C99编译器只要提供inttypes.h
和就可以正常工作stdbool.h
;例如,gcc
在提供完整的C99支持之前很久都支持) 。
该程序只需要运行,就是为系统中的至少一个接口(本地接口除外)分配IP地址,并设置使用该接口的默认路由。该程序将收集该IP地址并将其用作第二个“特定地址”。
它测试您可能想到的所有可能的组合:
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一样,大多数时候程序员可以简单地忽略该状态的存在。
这是代码(我不能在这里包括它,答案有大小限制,并且代码会将此答复推到限制之上)。
INADDR_ANY
,我从未说过它不会绑定到将来的地址。而且listen
根本不会创建任何套接字,这会使您的整个句子有些奇怪。
INADDR_ANY
不会绑定现有的本地地址,但也会绑定所有将来的本地地址。listen
即使您说不可能,也可以使用完全相同的协议,本地地址和本地端口来创建套接字。