TCP中SO_REUSEADDR及SO_REUSEPORT区别

本文主要讲述Linux下SO_REUSEADDRSO_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.110.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是不一样的,自然也就不会存在惊群问题。

Reference