linux - socket通信流程 - unix socket是什么



套接字选项SO_REUSEADDR和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 ,这意味着“任何端口”。 由于套接字无法真正绑定到所有现有端口,因此在这种情况下(通常来自预定义的,特定于操作系统的源端口范围),系统必须自己选择特定的端口。 源地址存在类似的通配符,可以是“任何地址”(IPv4中为0.0.0.0 ,而IPv6中为::)。 与端口不同,套接字可以真正绑定到“任何地址”,意思是“所有本地接口的所有源IP地址”。 如果套接字稍后连接,则系统必须选择特定的源IP地址,因为套接字无法连接并且同时绑定到任何本地IP地址。 根据目标地址和路由表的内容,系统将选择一个合适的源地址,并用绑定到所选的源IP地址替换“any”绑定。

默认情况下,不能将两个套接字绑定到源地址和源端口的相同组合。 只要源端口不同,源地址实际上是不相关的。 只要X != Y成立,将socketB绑定到A:XsocketBB:Y ,其中AB是地址, XY是端口。 但是,即使X == Y ,只要A != B成立,绑定仍然是可能的。 例如, socketA属于FTP服务器程序,绑定到192.168.0.1:21socketB属于另一个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.0192.168.0.1 不是完全相同的地址,一个是所有本地地址的通配符,另一个是非常特定的本地地址。 请注意,无论socketAsocketB绑定的顺序如何,上面的语句都是true; 没有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的绑定操作的socketB 。 如果第一列表示ON/OFF ,则SO_REUSEADDR的值与结果无关。

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

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

内核在关闭套接字之前等待的时间量,无论它是否仍有待发送数据,称为延迟时间 。 在大多数系统中, 延迟时间是全局可配置的,默认时间很长(两分钟是在许多系统上可以找到的常见值)。 它也可以使用套接字选项SO_LINGER对每个套接字进行配置,可用于使超时更短或更长,甚至完全禁用它。 但是,完全禁用它是一个非常糟糕的想法,因为正常关闭TCP套接字是一个稍微复杂的过程,并且需要发送和返回几个数据包(以及在发生丢失的情况下重新发送这些数据包)以及整个关闭过程也受到灵儿时间的限制。 如果您禁用延迟,您的套接字可能不仅会丢失未完成的数据,还会始终强制关闭而不是优雅地关闭,这通常不被推荐。 有关TCP连接如何正常关闭的详细信息超出了此答案的范围,如果您想了解更多信息,我建议您查看此页面 。 即使你禁用了SO_LINGER ,如果你的进程在没有明确关闭套接字的情况下死掉,BSD(可能还有其他系统)仍然会流连忘返,忽略你配置的内容。 例如,如果你的代码只是调用exit() (对于微小的,简单的服务器程序很常见),或者进程被一个信号杀死(包括它因为非法内存访问而崩溃的可能性),就会发生这种情况。 所以没有什么可以做的,以确保套接字永远不会在任何情况下流连。

问题是,系统如何处理状态为TIME_WAIT的套接字? 如果没有设置SO_REUSEADDR ,那么一个处于TIME_WAIT状态的套接字被认为仍然绑定到源地址和端口,并且任何尝试将新套接字绑定到相同的地址和端口都将失败,直到套接字真正关闭,这可能需要只要配置好灵儿时间 。 所以不要指望在关闭它之后立即重新绑定套接字的源地址。 在大多数情况下,这将失败。 但是,如果为要尝试绑定的套接字设置了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 ,直到第一个套接字释放其绑定再次。 与SO_REUESADDR的情况不同,处理SO_REUSEPORT的代码不仅会验证当前绑定的套接字是否设置了SO_REUSEPORT而且还会验证具有冲突地址和端口的套接字在绑定时是否设置了SO_REUSEPORT

SO_REUSEPORT不暗示SO_REUSEADDR 。 这意味着如果套接字在绑定时没有设置SO_REUSEPORT ,而另一个套接字在绑定到完全相同的地址和端口时设置了SO_REUSEPORT ,则绑定会失败,这是预期的,但如果另一个套接字已经死亡并处于TIME_WAIT状态。 为了能够将套接字绑定到与处于TIME_WAIT状态的另一个套接字相同的地址和端口,需要在该套接字上设置SO_REUSEPORT ,或者在绑定套接字之前必须在两个套接字设置SO_REUSEPORT 。 当然,可以在套接字上同时设置SO_REUSEPORTSO_REUSEADDR

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

Connect()返回EADDRINUSE?

大多数人知道bind()可能会失败并显示错误EADDRINUSE ,但是,当您开始使用地址重用时,可能会遇到connect()失败并出现该错误的奇怪情况。 怎么会这样? 毕竟这是一个远程地址,这是什么连接添加到套接字,已被使用? 将多个套接字连接到完全相同的远程地址之前从来没有出现过问题,所以这里出了什么问题?

正如我在回复的顶部所说的,连接是由五个值的元组定义的,请记住? 我还说过,这五个值必须是唯一的,否则系统不能再区分两个连接,对吗? 那么,通过地址重用,您可以将同一协议的两个套接字绑定到相同的源地址和端口。 这意味着这五个值中的三个对于这两个套接字已经是相同的。 如果您现在尝试将这两个套接字连接到相同的目标地址和端口,则可以创建两个连接的套接字,其元组完全相同。 这是行不通的,至少不能用于TCP连接(UDP连接无论如何都不是真正的连接)。 如果数据到达两个连接中的任何一个,则系统无法分辨数据属于哪个连接。 至少目标地址或目标端口对于任一连接都必须是不同的,以便系统没有问题来识别传入数据属于哪个连接。

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

多播地址

大多数人忽略了多播地址存在的事实,但它们确实存在。 虽然单播地址用于一对一通信,但多播地址用于一对多通信。 大多数人在了解IPv6时都知道组播地址,但组播地址也存在于IPv4中,即使此功能在公共Internet上从未广泛使用过。

SO_REUSEADDR的含义因组播地址而改变,因为它允许将多个套接字绑定到完全相同的源组播地址和端口组合。 换句话说,对于组播地址, SO_REUSEADDR行为与单播地址的SO_REUSEADDR完全相同。 实际上,代码将SO_REUSEADDRSO_REUSEPORT同等对待多播地址,这意味着您可以说SO_REUSEPORT对所有多播地址都意味着SO_REUSEPORTSO_REUSEADDR


FreeBSD的/ OpenBSD系统/ NetBSD的

所有这些都是原BSD代码的后期分支,这就是为什么他们三个都提供与BSD相同的选项,并且它们的行为方式与BSD中的相同。


macOS(MacOS X)

在其核心上,macOS仅仅是一个名为“ Darwin ”的BSD风格的UNIX,它基于BSD代码(BSD 4.3)的后期分支,后来甚至与之前的(当时的)FreeBSD进行了重新同步5代码基础的Mac OS 10.3版本,以便Apple可以获得完整的POSIX合规性(macOS通过POSIX认证)。 尽管核心有一个微内核(“ Mach ”),但内核的其余部分(“ XNU ”)基本上只是一个BSD内核,这就是macOS提供与BSD相同的选项的原因,它们的行为方式与BSD中的相同。

iOS / watchOS / tvOS

iOS只是一个带有稍微修改和修剪的内核的macOS分支,稍微剥离了用户空间工具集和略有不同的默认框架集。 watchOS和tvOS是iOS分叉,甚至进一步剥离(特别是watchOS)。 据我所知,他们都像macOS一样行事。


Linux的

Linux <3.9

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

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

  2. 第二个例外是,对于客户端套接字,这个选项的行为与BSD中的SO_REUSEPORT完全相同,只要这两个标记在绑定前都设置了该标记。 允许这样做的原因很简单,就是能够将多个套接字准确绑定到各种协议的相同UDP套接字地址并且因为在3.9之前没有SO_REUSEPORT ,所以SO_REUSEADDR的行为相应地被修改为填充那个差距。 在这方面,Linux的限制性比BSD小。

Linux> = 3.9

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

但是,其他系统上的SO_REUSEPORT仍然存在两个不同之处:

  1. 为了防止“端口劫持”,有一个特殊的限制: 所有希望共享相同地址和端口组合的套接字必须属于共享相同有效用户ID的进程! 所以一个用户不能“窃取”另一个用户的端口。 这是一些特殊的魔法来补偿丢失的SO_EXCLBIND / SO_EXCLUSIVEADDRUSE标志。

  2. 另外,内核对SO_REUSEPORT套接字执行一些在其他操作系统中找不到的“特殊魔法”:对于UDP套接字,它试图平均分配数据报,对于TCP监听套接字,它试图分发传入连接请求(通过调用accept() )平均分配到共享相同地址和端口组合的所有套接字。 因此,应用程序可以轻松地在多个子进程中打开同一个端口,然后使用SO_REUSEPORT获得非常便宜的负载平衡。


Android的

尽管整个Android系统与大多数Linux发行版有所不同,但其核心工作原理是稍微修改了Linux内核,因此适用于Linux的所有内容也适用于Android。


视窗

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

有关SO_REUSEADDRSO_EXCLUSIVEADDRUSE标志在Windows上的工作原理的更多详细信息,它们如何影响绑定/重新绑定,Microsoft慷慨地提供了一个类似于我的表格的表格,该表格接近该答复的顶部。 只需访问此页面并向下滚动一下。 实际上有三个表,第一个显示旧行为(先前的Windows 2003),第二个行为(Windows 2003及更高版本),第三个显示如何在Windows 2003和更高版本中改变行为bind()如果bind()调用由不同的用户制作。


的Solaris

Solaris是SunOS的继任者。 SunOS最初基于BSD,SunOS 5的分支,后来基于SVR4的分支,但是SVR4是BSD,System V和Xenix的合并,因此Solaris在某种程度上也是BSD分支,相当早。 因此,Solaris只知道SO_REUSEADDR ,没有SO_REUSEPORTSO_REUSEADDR行为与BSD中的行为非常相似。 据我所知,无法在Solaris中获得与SO_REUSEPORT相同的行为,这意味着无法将两个套接字绑定到完全相同的地址和端口。

与Windows类似,Solaris可以为套接字提供独占绑定。 该选项名为SO_EXCLBIND 。 如果在绑定之前在套接字上设置此选项,则在测试两个套接字的地址冲突时,在另一个套接字上设置SO_REUSEADDR不起作用。 例如,如果socketA绑定到通配符地址,并且socketB启用了SO_REUSEADDR并绑定到非通配符地址和与socketA相同的端口,则该绑定通常会成功,除非SO_EXCLBIND启用了SO_EXCLBIND ,否则socketA都将失败socketB SO_REUSEADDR标志。


其他系统

如果你的系统没有在上面列出,我写了一个小测试程序,你可以用它来找出你的系统如何处理这两个选项。 此外,如果您认为我的结果不正确 ,请在发布任何评论并可能提出虚假声明之前先运行该程序。

代码需要构建的只是一点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,大多数时候程序员可以忽略该状态的存在。

这里是代码 (我不能在这里包括它,答案有一个大小限制,代码会超过限制推动这个回复)。

对于不同的操作系统,套接字选项SO_REUSEADDRSO_REUSEPORTman pages和程序员文档有所不同,并且通常非常混乱。 有些操作系统甚至没有SO_REUSEPORT选项。 WEB充斥着关于这个主题的矛盾信息,通常你可以找到一些信息,这些信息只适用于特定操作系统的一个套接字实现,这些信息甚至可能在本文中都没有明确提及。

那么SO_REUSEADDR究竟与SO_REUSEADDR不同呢?

没有SO_REUSEPORT系统更受限制?

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





portability