本文主要讲述Linux下
SO_REUSEADDR
及SO_REUSEPORT
这两个socket选项的用法及区别,不同平台下的实现可能不同
众所周知,一个TCP/UDP连接是有如下一个五元组来确定的,没有两个连接是具有完全相同的五元组的。
1 | [协议,源地址,源端口,目的地址,目的端口] |
其中,协议部分是在创建socket指定的,源地址及源端口是服务端在bind
时指定的,目的地址及目的端口是客户端通过connect
时指定的。因为UDP是无连接的通信协议,一个UDP无需connect
就使用,这种无连接的socket
在第一次发送数据的时候会有系统自动绑定一个五元组,否则的话该socket将无法接收任何数据。对于TCP连接同样是如此,系统会在连接建立之前隐式绑定一个五元组。
SO_REUSEADDDR
无法将一个socket绑定值相同的五元组容易带来如下问题:
TIME_WAIT
状态时间过长,造成期间socket的某个端口无法复用,这个问题在tcp server重启的时候尤其严重- 通过
0.0.0.0
将socket绑定至本地任意一个地址后,其他本地地址将无法再使用
在Linux中,SO_REUSERADDR
就是用以解决这个问题的。假设某个主机有两个本地地址分别是192.168.0.1
及10.0.0.1
,对SocketA及SocketB通过不同方式绑定有如下结果:
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.1:21 | 192.168.0.1:21 | Error(EADDRINUSE) |
OFF | 192.168.0.1:21 | 0.0.0.1:21 | Error(EADDRINUSE) |
ON | 0.0.0.1:21 | 192.168.0.1:21 | Ok |
ON | 192.168.0.1:21 | 0.0.0.1:21 | Ok |
ON/OFF | 0.0.0.1:21 | 0.0.0.1:21 | Error(EADDRINUSE) |
SO_REUSEPORT
如上文所述,SO_REUSEADDR
选项并不是用以将socket真正绑定至同一五元组的,Linux在3.9版本之后添加了新选项SO_REUSEPORT
,通过该选项,不同线程或者进程就可以绑定至同一端口。其中需要注意的是,所以绑定至该端口的socket都需设置SO_REUSEPORT
选项,否则将会绑定失败。同时基于安全性考虑,第一个进程绑定后,后续需要板顶至该端口的进程所属用户要么是root用户,要么是跟第一个进程归属于同一用户。
网络服务中一种常见的模式是用一个listen线程来接收连接后将分配工作线程来处理新连接,这种模式下listen线程容易存在瓶颈。为了解决这种问题,第二种常见模式是运行多个进程来对循环监听一个端口。在没有SO_REUSEPORT
选项之前,是通过fork
来实现的,父进程绑定至一个端口后,fork出多个子进程来循环监听这个socket。但是这种模式存在一个问题,当有新连接到来时,那个进程能成功获取到这个连接呢?在Linux 2.6.18之前,所有监听的进程都会被唤醒,但只有一个进程能成功获取到这个连接,这既是accept惊群
问题,在Linux2.6.18之后的版本已经修复了这个问题。但是如果使用epoll去监听accept socket
上的可读事件,仍会存在惊群问题。
有了SO_REUSEPORT
选项,我们就可以不通过fork的方式来让多个进程监听统一端口,这样每个进程中的socket fd是不一样的,自然也就不会存在惊群问题。