accept() 在重复连接尝试时无限期地阻塞

accept() block indefinitely on repeated connection attempts

我正在编写一个 TCP 服务器,我希望一次接受一个连接,并通过重复使用它用于侦听的地址和端口。与服务器启动实例的第一次连接(例如通过 netcat)总是成功,但随后的连接尝试停止在 accept() 不返回套接字描述符。我已经尝试过不同的队列长度,并且在前一个连接处于 TIME_WAIT 状态时进行连接,并且在它被清除之后进行连接,但结果是一样的。 netcat和netstat都报告新的连接尝试成功,并报告连接已建立(不管之前的连接是在TIME_WAIT还是过期),但是我的服务器卡在了accept()调用,从而它不注册新连接。此行为并不总是在 第一次 后续连接尝试时立即发生,但几乎总是在前三次尝试期间发生。

代码:


main() {
    Socket socket(10669);
    
    while (true) {
        socket.establish_connection();
        
        socket.receive(callback);
        socket.close_connection();
    }
}



void Socket::establish_connection() {
    // Creating socket file descriptor
    int server_fd = 0;
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        throw ...;
    }

    // Setting socket options
    int socket_options = 1;
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEPORT, &socket_options, sizeof(socket_options))) {
        throw ...;
    }

    struct sockaddr_in address;
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(port);

    if (bind(server_fd, (sockaddr *) &address, sizeof(address)) < 0) {
        throw ...;
    }

    if (listen(server_fd, 1) < 0) {
        throw ...;
    }

    spdlog::info("Listening for clients on port {}", port);

    // this is where it blocks at repeated connection attempts
    struct sockaddr_in client_address;
    int addrlen = sizeof(client_address);
    if ((socket = accept(server_fd, (sockaddr *) &client_address,  (socklen_t*) &addrlen)) < 0) {
        throw ...;
    }

    spdlog::info("Client connected\n");
}


void Socket::receive(SocketCallback callback) {
    while (true) {
        fd_set read_socket_fd;
        FD_ZERO(&read_socket_fd);
        FD_SET(socket, &read_socket_fd);

        int sel = select(socket+1, &read_socket_fd, NULL, NULL, NULL);

        if (sel > 0) {
            // receiving data, no problems here
        }
    }
}


void Socket::close_connection() {
    close(socket);
}

来自服务器的一些打印输出,以及 netstat:

启动时(服务器):

[2020-07-07 13:33:53.387] [info] Socket initialised to use port 10669
[2020-07-07 13:33:53.387] [info] Listening for clients on port 10669

启动时 (netstat):

tcp        0      0 0.0.0.0:10669           0.0.0.0:*               LISTEN

首次连接(服务器):

[2020-07-07 13:34:35.481] [info] Client connected

首次连接时 (netstat):

tcp        0      0 0.0.0.0:10669           0.0.0.0:*               LISTEN
tcp        0      0 localhost:54860         localhost:10669         ESTABLISHED
tcp        0      0 localhost:10669         localhost:54860         ESTABLISHED

第一次与客户端(服务器)断开连接时:

[2020-07-07 13:35:47.903] [warning] Client disconnected
[2020-07-07 13:35:47.903] [info] Listening for clients on port 10669

第一次与客户端断开连接时 (netstat):

tcp        0      0 0.0.0.0:10669           0.0.0.0:*               LISTEN
tcp        0      0 0.0.0.0:10669           0.0.0.0:*               LISTEN
tcp        0      0 localhost:54860         localhost:10669         TIME_WAIT

在第二次连接尝试时,服务器没有报告任何内容,因为它停留在“正在侦听客户端...”行,表明在 accept() 处被阻止。这是 netstat 报告的内容(这是我在第一次断开连接后立即连接时的情况,因此之前的连接处于 TIME_WAIT 状态):

tcp        0      0 0.0.0.0:10669           0.0.0.0:*               LISTEN
tcp        1      0 0.0.0.0:10669           0.0.0.0:*               LISTEN
tcp        0      0 localhost:54968         localhost:10669         TIME_WAIT
tcp        0      0 localhost:54970         localhost:10669         ESTABLISHED
tcp        0      0 localhost:10669         localhost:54970         ESTABLISHED

同样的情况发生在我等待 TIME_WAIT 过期之后才尝试连接:

tcp        0      0 0.0.0.0:10669           0.0.0.0:*               LISTEN
tcp        1      0 0.0.0.0:10669           0.0.0.0:*               LISTEN
tcp        0      0 localhost:10669         localhost:55134         ESTABLISHED
tcp        0      0 localhost:55134         localhost:10669         ESTABLISHED

在这两种情况下,netcat 中的连接都是活动的,我可以随意输入,但当然什么也没有收到;没有其他进程可以拦截连接。

我知道我可能会尝试非阻塞的 accept(),但是 accept() 的这种阻塞行为非常适合我的用法,当它按预期运行时,所以问题是 - 为什么它会在重新连接时阻塞,我在这里错过了什么?

您应该创建一个 服务器套接字,然后在同一个套接字上重复调用accept。每次调用 accept 时,您似乎都在创建一个新的服务器套接字,并让旧的套接字保持打开状态。

通常情况下,这是无效的,但是您使用 SO_REUSEPORT 告诉操作系统您确实需要它。与SO_REUSEPORTincoming connections are balanced across all the server sockets on the same port。显然,操作系统选择将您的新连接发送到第一个套接字,然后您尝试从没有新连接等待的第二个套接字 accept 发送它。

要修复它,请创建一个服务器套接字一次,然后总是从同一个套接字accept