Skip to content

SO_REUSEADDR

参考如下文章:

1、zhihu 网络编程:SO_REUSEADDR的使用

2、idea.popcoun Bind before connect

其中对 SO_REUSEADDR 的使用场景进行了非常好的总结,非常值得借鉴。

3、ip(7)

A TCP local socket address that has been bound is unavailable for some time after closing, unless the SO_REUSEADDR flag has been set. Care should be taken when using this flag as it makes TCP less reliable.

4、 socket(7)

**SO_REUSEADDR: **

**Indicates that the rules used in validating addresses supplied in a bind(2) call should allow reuse of local addresses. **

For AF_INET sockets this means that a socket may bind, except when there is an active listening socket bound to the address. When the listening socket is bound to INADDR_ANY with a specific port then it is not possible to bind to this port for any local address. Argument is an integer boolean flag.

zhihu 网络编程:SO_REUSEADDR的使用

NOTE:

这篇文章给出了例子,非常好

SO_REUSEADDR是一个很有用的选项,一般服务器的监听socket都应该打开它。它的大意是允许服务器bind一个地址,即使这个地址当前已经存在已建立的连接,比如:

1、服务器启动后,有客户端连接并已建立,如果服务器主动关闭,那么和客户端的连接会处于TIME_WAIT状态,此时再次启动服务器,就会bind不成功,报:Address already in use

2、服务器父进程监听客户端,当和客户端建立链接后,fork一个子进程专门处理客户端的请求,如果父进程停止,因为子进程还和客户端有连接,所以再次启动父进程,也会报**Address already in use**。

服务器程序1

这次我们直接上代码,看看这两种情况:

先看看服务器程序1:

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main(int argc, char const *argv[])
{
    int lfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (lfd == -1)
    {
        perror("socket: ");
        return -1;
    }

    struct sockaddr_in sockaddr;
    memset(&sockaddr, 0, sizeof(struct sockaddr_in));
    sockaddr.sin_family = AF_INET;
    sockaddr.sin_port = htons(3459);
    inet_pton(AF_INET, "127.0.0.1", &sockaddr.sin_addr);

    // int optval = 1;
    // setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));

    if (bind(lfd, (struct sockaddr*) &sockaddr, sizeof(sockaddr)) == -1)
    {
        perror("bind: ");
        return -1;
    }

    if (listen(lfd, 128) == -1)
    {
        perror("listen: ");
        return -1;
    }

    struct sockaddr_storage claddr;
    socklen_t addrlen = sizeof(struct sockaddr_storage);
    int cfd = accept(lfd, (struct sockaddr*) &claddr, &addrlen);
    if (cfd == -1)
    {
        perror("accept: ");
        return -1;
    }
    printf("client connected: %d\n", cfd);

    char buff[100];
    for (;;)
    {
        ssize_t num = read(cfd, buff, 100);
        if (num == 0)
        {
            printf("client close: %d\n", cfd);
            close(cfd);
            break;
        }
        else if (num == -1)
        {
            int no = errno;
            if (no != EINTR && no != EAGAIN && no != EWOULDBLOCK)
            {
                printf("client error: %d\n", cfd);
                close(cfd);
            }
        }
        else
        {
            if (write(cfd, buff, num) != num)
            {
                printf("client error: %d\n", cfd);
                close(cfd);
            }
        }
    }
    return 0;
}
// gcc test.c

注意中间注释掉的两行,编译程序并执行它:

gcc -o treuseaddr treuseaddr.c
./treuseaddr

然后用nc模拟客户端连接:

nc 127.0.0.1 3459

连接后可以正常和服务器通常,用netstat看看连接的状态:

$ netstat -tna | grep 3459
tcp        0      0 127.0.0.1:3459          0.0.0.0:*               LISTEN     
tcp        0      0 127.0.0.1:36463         127.0.0.1:3459          ESTABLISHED
tcp        0      0 127.0.0.1:3459          127.0.0.1:36463         ESTABLISHED

第1条是监听套接字,第2条客户端的连接,第3条是服务器和客户端的连接;我们强制中止服务器(Ctrl+C),再看看netstat:

$ netstat -tna | grep 3459
tcp        0      0 127.0.0.1:3459          127.0.0.1:36463         TIME_WAIT

此时只有服务器建立的连接,处于是TIME_WAIT状态,再次启动服务器,会报下面错误:

$ ./treuseaddr 
bind: : Address already in use

如果把上面两行注释的去掉,就能解决这个问题,可以自己动手试试看。

另外一个例子

再来看另外一个例子;

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main(int argc, char const *argv[]) {
    int lfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (lfd == -1) {
        perror("socket: ");
        return -1;
    }

    struct sockaddr_in sockaddr;
    memset(&sockaddr, 0, sizeof(struct sockaddr_in));
    sockaddr.sin_family = AF_INET;
    sockaddr.sin_port = htons(3459);
    inet_pton(AF_INET, "127.0.0.1", &sockaddr.sin_addr);

    // int optval = 1;
    // setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));

    if (bind(lfd, (struct sockaddr*)&sockaddr, sizeof(sockaddr)) == -1) {
        perror("bind: ");
        return -1;
    }

    if (listen(lfd, 128) == -1) {
        perror("listen: ");
        return -1;
    }

    struct sockaddr_storage claddr;
    socklen_t addrlen = sizeof(struct sockaddr_storage);
    int cfd = accept(lfd, (struct sockaddr*)&claddr, &addrlen);
    if (cfd == -1) {
        perror("accept: ");
        return -1;
    }
    printf("[PARENT] client connected: %d\n", cfd);

    int pid = fork();
    if (pid == -1) {
        perror("fork: ");
        return -1;
    } else if (pid == 0) {      // child
        char buff[100];
        for (;;) {
            ssize_t num = read(cfd, buff, 100);
            if (num == 0) {
                printf("[CHILD] client close: %d\n", cfd);
                close(cfd);
                break;
            } else if (num == -1) {
                int no = errno;
                if (no != EINTR && no != EAGAIN && no != EWOULDBLOCK) {
                    printf("[CHILD] client error: %d\n", cfd);
                    close(cfd);
                }
            } else {
                if (write(cfd, buff, num) != num) {
                    printf("[CHILD] client error: %d\n", cfd);
                    close(cfd);
                }
            }
        }
    } else {    // parent
        close(cfd);
        printf("[PARENT] parent exit\n");
    }
    return 0;
}

逻辑和第1个差不多,只是和客户端的通信放在子进程中了。当我们执行nc 127.0.0.1 3459时,服务器有如下输出:

[PARENT] client connected: 4
[PARENT] parent exit

说明父进程退出了,但是子进程在,而且与客户端能正常通讯,此时我们再启动父进程:

$ ./treuseaddr2
bind: : Address already in use

没法绑定地址了,如果同样把注释的行去掉,就可以正常启动。

通过上面两个例子, 是不是对SO_REUSEADDR有更直接的认识呢?关于SO_REUSEADDR其实还有很多细节,在这儿就不抄书了,有兴趣的直接看UNIX Network Programming

stackoverflow What are the use cases of SO_REUSEADDR?

I have used SO_REUSEADDR to have my server which got terminated to restart with out complaining that the socket is already is in use. I was wondering if there are other uses of SO_REUSEADDR? Have anyone used the socket option for other than the said purpose?

A

For TCP, the primary purpose is to restart a closed/killed process on the same address.

The flag is needed because the port goes into a TIME_WAIT state to ensure all data is transferred.

If two sockets are bound to the same interface and port, and they are members of the same multicast group, data will be delivered to both sockets.

NOTE:

这段话的意思是从client发送过来的data将multicast到所有bind到这个port的socket

I guess an alternative use would be a security attack to try to intercept data.

(Source)


For UDP, SO_REUSEADDR is used for multicast.

More than one process may bind to the same SOCK_DGRAM UDP port if the bind() is preceded by:

int one = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));

In this case, every incoming multicast or broadcast UDP datagram destined to the shared port is delivered to all sockets bound to the port.

(Source)

stackoverflow What is the purpose of SO_REUSEADDR? [duplicate]

A

For UDP sockets, setting the SO_REUSEADDR option allows multiple sockets to be open on the same port.

If those sockets are also joined to a multicast group, any multicast packet coming in to that group and port will be delivered to all sockets open on that port.