网络上有很多的文章在讲述端口复用 (SO_REUSEPORT) 和地址复用 (SO_REUSEADDR) 的区别,大多数都在强调地址复用是用来消除TCP的Time_wait阶段的,开启后如果相同的端点正处于 time_wait 阶段即可直接占用。而端口复用可以用来达到负载均衡,并且解决TCP服务端的惊群效应。
但是最近在写一些通讯层的东西,先是写了 Udp 的通信,以为由单播和多播,多播绑定给的 IP地址为Any,因此需要使用到端口复用的问题。我自己的测试是两个进程同时占用一个端口号进行UDP单播消息的发送和接收,对于 端口复用 (SO_REUSEPORT) 和地址复用 (SO_REUSEADDR) 而言,他们的表现都是后面打开的端口会直接抢占掉旧端口的消息的接收(这里我并不清楚是因为负载均衡选择了后来绑定的 socket 还是直接被后来绑定的 socket 抢占了,后续有待验证)。而对于组播而言,在绑定了Ip 地址为Any 的组播并打开端口复用和地址复用中任意一个之后,并在具体的ip地址和相同的端口号绑定一个单播,则单播的消息则会优先由单播接收,没有具体单播接收的消息则会由ip地址为any的套接字来接收。
Udp REUSEADDR 测试:
- 在设置 REUSEADDR 属性之后,能够在多个 socket 套接字上绑定本地的同一个 IP 地址和端口号。
- 所有的 Udp 消息都会被最后发生绑定的 socket 套接字接收到。即,最后发生的绑定操作的 socket 套接字会抢占掉前面发生的绑定操作的 socket 套接字绑定端点的所有权,仅有最后发生绑定操作的 socket 能够在该端点上发送和接收消息。
Udp REUSEPORT测试:
- 在发送时,对于 REUSEPORT 的测试和 REUSEADDR 的测试结果相同,只不过使用 REUSEPORT 并不是最后进行绑定的 socket 抢占绑定端点的所有权,而是选择其中一个 socket 来独占端点。在我的测试中并没有找到占用所有权的 socket 的规律,猜测是和接受时消息分发给不同 socket 套接字时使用的优先级策略中优先级最高的 socket 套接字独占当前端点的发送权限。
- 在接收时,如果该端点同时收到来自多个发送者的大量消息,则会将这些消息按照一定的策略分发给不同进程中绑定当前端点的多个 socket 套接字。
而对于TCP通信来说
1)开启 REUSRADDR,并且所有 sock 都不是 TCP_LISTEN 状态。这种情况下无论是否支持 REUSEPORT 都不会产生冲突。
2) 未开启 REUSEADDR 或者有存在 TCP_LISTEN 状态的 sock 情况下,这时候分为三种情况:
2.a: 未开启 REUSEPORT, 直接冲突
2.b: 开启 REUSEPORT, 但是存在某个 sock 状态不是 TCP_TIME_WAIT,并且 euid 不同。冲突
2.c: 其他情况不冲突
Tcp REUSEADDR 测试:
- Server:能够在多线程/多进程中绑定同一个 IP 地址 + 端口号,但是一旦有一个进程已经绑定该端点并且完成了 Listen 操作,那么其他后续在该端点上的 Bind 和 Listen 操作都会导致 Address already in use 的错误。
- Client:能够在多线程/多进程中绑定同一个 IP 地址 + 端口号,并且能够同时建立与远程服务端的通信。但是由于 Tcp 通过四元组来唯一标定一个 Tcp 连接,因此不能多个绑定相同端点的 Tcp Client 同时 Connect 同一个远程端点的服务端,会导致 Cannot assign requested address 的错误。并且 REUSEADDR 的设置能够在 Tcp 客户端断开连接后无视 time_wait 状态,立刻在处于 time_wait 状态的端点绑定新的 socket 套接字,并与服务端建立连接进行通信。对于没有客户端绑定到固定端点需求的(即直接调用 Connect 连接远程服务端),在使用 Tcp 客户端时则不需要设置该参数,在使用完所有的端点后会自动进行端口复用。
Tcp REUSEPORT测试:
- Server:能够在多线程或者多进程中绑定和监听同一个 IP 地址 + 端口号。并且多线程/进程中绑定同一个端点的 socket 监听套接字能够独立的进行 accept 以及后续的通信操作。客户端请求连接后会随机分配给监听该端点的多个 socket 监听套接字中的其中一个,其他监听套接字不会再接收到同一个客户端的 Connect 请求。
- Client:与开启 REUSEADDR 的行为完全相同。有趣的是在设置 REUSEPORT后断开 Tcp 连接之后仍然能立刻绑定到当前端点,但是在 REUSEPORT 设置下建立 Tcp 连接并断开后,立刻更换为 REUSEADDR 则会因为当前端点仍处于 time_wait 状态导致绑定失败,导致 Address already in use 错误。因此对于需要在同一个端点绑定多次的情况,最好还是统一使用 REUSEPORT。
- 在开启 REUSEPORT 之后,在同一个进程/线程中能够同时创建绑定到同一端点上的 Tcp Client 和 Tcp Server。如果使用场景是 “需要在一个端点上创建一个服务端和多个客户端”,应该都使用 REUSEPORT。
- 在开启 REUSEPORT 之后,PC1 创建 Tcp 服务端和客户端同时绑定和监听在同一端点 192.168.0.100:9999,PC2 创建 Tcp 服务端和客户端同时绑定和监听在统一端点 192.168.0.200:9999,然后两台主机的客户端分别与对端发起 connect 连接申请,会出现 Cannot assign requested address 的错误,再次验证了,四元组能够唯一标志 Tcp 连接。
总结:
REUSEADDR 和 REUSEPORT 对于 UDP,TCP 客户端来说是完全一样的,TCP 客户端不管设置 REUSEADDR 还是 REUSEPORT 都能支持多个 socket 绑定同一端点,并且独立的进行Connect和通信。而对于TCP服务端来说,只有REUSEPORT才能真正意义上做到端口复用。利用REUSEADDR 只能支持到 Bind,而只能有一个 socket 完成 listen。
REUSEADDR 对于 TCP 而言还有一个功能是,解决了 time_wait 的等待阶段。设置SO_REUSEADDR 会通知内核,如果端口忙,但TCP状态位于TIME_WAIT,可以重用端口。这个一般用于当你的程序停止后想立即重启的时候,如果没有设定这个选项,会报错EADDRINUSE,需要等到TIME_WAIT结束才能重新绑定到同一个 ip+port 上。而 SO_REUSEPORT 用于多核环境下,允许多个线程或者进程绑定和监听同一个 ip+port ,无论 UDP、TCP(以及 TCP 是什么状态)。
一个端口最多能有多少个 TCP 连接是和机器内存性能有关的,不同的五元组(协议,源地址,源端口,目的地址,目的端口)代表不同的 TCP连接,理论上来说一个端口号上能部署无数个 TCP 连接,只要他们的五元组不同。
TCP 客户端建立通信时,如果不使用 Bind 函数,而是直接使用 Connect 函数来获取一个随机的端口号,那么这种情况下在端口号不足时是会自动进行端口复用的,只要是四元组不同,就是不同的TCP链接,因此是能够布置在本地网卡上同一个端点的。
下面是关于总结相关知识所浏览的文章:
一台机器最多能撑多少个TCP连接? 今天掰扯清楚! - 知乎 (zhihu.com)
一个端口到底可以建立多少TCP连接?_一个端口可以建立多个tcp连接吗-优快云博客
4.18 TCP 和 UDP 可以使用同一个端口吗? | 小林coding (xiaolincoding.com)
Linux内核中reuseport的演进 - 187J3X1 - SegmentFault 思否