You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

TCP select函数无法处理第二个连接问题求助

TCP服务器关闭连接后无法接受新连接的问题排查与解决

我在DELL E6400设备的Windows 10系统上,用Visual Studio Enterprise 2017开发TCP服务器程序时遇到了一个棘手的问题:第一次客户端连接能正常处理,但关闭第一个连接后,服务器就再也无法接受新的连接了。用Wireshark抓包能看到新连接已经完成SYN、SYN/ACK、ACK三次握手,逻辑上连接已经建立,但程序始终卡在select()调用里出不来。

我的代码执行流程大致是:
gethostname() → gethostbyaddr() → 设置server_addr->sin_family和server_addr->sin_port → socket() → setsockopt() → bind() → getsockname() → listen() → FD_ZERO(); → FD_SET(); → k = select() → closesocket()

实际代码

#include <stdlib.h>
#include <conio.h>
#include "EasyTcp.h" // On Windows, use Ws2_32.dll
/* need to define some linux-only symbols for other systems */
#ifndef SOL_TCP
#ifndef IPPROTO_TCP
#define IPPROTO_TCP 6 // 6 usually = TCP
#endif
#define SOL_TCP IPPROTO_TCP /* tcp's protocol number (from /etc/protocols) */
#endif
#define LISTENING_DEPTH 3
#define MAXHOSTNAMELEN 132
void start_up(void);
SOCKET openserver( unsigned int server_port, char *server_name, struct sockaddr_in *server_addr );
int print_address(char *message, int len, struct sockaddr_in *addr, unsigned int *host, unsigned int *port);
int print_ip_address( unsigned int ip_address );
SOCKET get_local_address( SOCKET fd, unsigned int *host, unsigned int *port);
int get_ip_address( char *name, unsigned int *host );
SOCKET accept_a_client(SOCKET listening_fd, struct sockaddr_in *addr, unsigned int *whole, unsigned int *fraction);

/* Accept a connection on port local_port. Returns the socket or -1 if an error. */
SOCKET EasyAccept( int local_port ) {
    SOCKET listening_fd = -1; // -1 when not connected
    SOCKET fd;
    unsigned int sys_remote_port = 0;
    unsigned int sys_local_host = 0;
    unsigned int sys_remote_host = 0;
    struct sockaddr_in target_addr;
    struct sockaddr_in initiator_addr;
    unsigned int sys_local_port = local_port; // The port to listen on
    unsigned int *whole_ptr = NULL, *fraction_ptr = NULL; // The amount of time to listen
    char *default_host = NULL; // The host you want to listen to, else NULL

    listening_fd = openserver( sys_local_port, default_host, &target_addr );
    if( listening_fd >= 0 ) {
        print_address("Listening", 1, &target_addr, &sys_local_host, &sys_local_port);
    } else {
        printf("openserver returned an error\n");
        return -1;
    }

    if( (fd = accept_a_client(listening_fd, &initiator_addr, whole_ptr,fraction_ptr)) >= 0 ) {/* accept succeeded, fd is connection to new initiator */
        print_address("Server", 1, &initiator_addr, &sys_remote_host, &sys_remote_port);
        get_local_address(fd, &sys_local_host, &sys_local_port);
    } else {/* accept failed */
        printf("accept failed, fd=%p\n", (void *)fd);
        return -1;
    }
    return fd;
}

int EasyConnect( int remote_port, char *remote_server ) {
    // not done yet
    printf("EasyConnect() is not finished\n");
    _getch();
    exit(1);
}

/* Opens a new TCP socket for the server at interface server_name, */
/* port server_port. Returns in server_addr the server's internet */
/* address structure. The server is NOT connected to a client on return. */
/* Returns an fd that is a "listening post" on which to make connections. */
SOCKET openserver( unsigned int server_port, char *server_name, struct sockaddr_in *server_addr ) {
    int len;
    SOCKET fd;
    struct sockaddr_in address1;
    struct in_addr address0;
    struct hostent *node_ptr;
    char local_node[MAXHOSTNAMELEN];
    char *host_name;
    char buffer[64];

    start_up();

    /* get the internet name of the local host node on which we are running */
    if( gethostname(local_node, MAXHOSTNAMELEN) < 0 ) {
        printf("openserver gethostname: %s\n", strerror(errno));
        strcpy(local_node, "Unknown Host");
    }

    if( server_name == NULL )
        host_name = local_node; /* default to local node */
    else
        host_name = server_name;

    /* get structure for local interface server is to use */
    if( isdigit((int)host_name[0]) ) {/* must be an ip address, not a DNS name */
#ifdef __sun__
        if( (*((int *)&address0) = inet_addr(host_name)) == -1 )
#else
        if( !inet_aton(host_name, &address0) )
#endif
        {
            printf("Invalid IP address \"%s\"\n", host_name);
            return -1;
        }
        if( (node_ptr = gethostbyaddr((char *)&address0, sizeof(address0), AF_INET)) == NULL ) {
            herror(host_name);
            return -1;
        }
    } else if( (node_ptr = gethostbyname(host_name)) == NULL ) {
        herror(host_name);
        return -1;
    }

    /* set up Internet address structure for the server */
    if (server_addr == NULL) /* user doesn't want this back */
    {
        server_addr = &address1; /* so store it locally */
    }
    memset(server_addr, 0, sizeof(struct sockaddr_in));
    server_addr->sin_family = AF_INET; // node_ptr->h_addrtype; /* should be AF_INET */
    server_addr->sin_port = htons( (u_short)server_port );
    // 原代码此处截断,默认处理为绑定所有网卡
    server_addr->sin_addr.s_addr = htonl(INADDR_ANY);

    // 创建套接字
    fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (fd == INVALID_SOCKET) {
        printf("socket creation failed: %d\n", WSAGetLastError());
        return -1;
    }

    // 设置端口复用,避免TIME_WAIT导致bind失败
    int optval = 1;
    if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (const char*)&optval, sizeof(optval)) == SOCKET_ERROR) {
        printf("setsockopt SO_REUSEADDR failed: %d\n", WSAGetLastError());
        closesocket(fd);
        return -1;
    }

    // 绑定地址
    if (bind(fd, (struct sockaddr*)server_addr, sizeof(struct sockaddr_in)) == SOCKET_ERROR) {
        printf("bind failed: %d\n", WSAGetLastError());
        closesocket(fd);
        return -1;
    }

    // 开始监听
    if (listen(fd, LISTENING_DEPTH) == SOCKET_ERROR) {
        printf("listen failed: %d\n", WSAGetLastError());
        closesocket(fd);
        return -1;
    }

    return fd;
}

问题分析与解决方法

1. 核心问题:监听套接字被重复创建+select集合维护错误

你的EasyAccept函数每次调用都会重新创建监听套接字,处理完客户端连接后也没有关闭它——这会导致第二次调用时bind端口失败(旧套接字可能还处于TIME_WAIT状态);另外,从流程看你可能没有在每次select调用前重新初始化fd_set,导致监听套接字没有被加入监听集合,程序自然无法感知新的连接请求。

2. 具体修改方案

(1)重构监听逻辑:只创建一次监听套接字

把监听套接字的创建移到主循环外,避免重复创建和绑定:

int main() {
    // 初始化Winsock(确保start_up()里包含WSAStartup)
    start_up();

    // 只创建一次监听套接字
    SOCKET listen_fd = openserver(12345, NULL, NULL);
    if (listen_fd == INVALID_SOCKET) {
        printf("Failed to create listen socket\n");
        WSACleanup();
        return 1;
    }

    fd_set read_fds;
    int max_fd = listen_fd;

    while (1) {
        // 每次select前必须重新初始化fd_set
        FD_ZERO(&read_fds);
        FD_SET(listen_fd, &read_fds);

        // 调用select监听可读事件
        int ret = select(max_fd + 1, &read_fds, NULL, NULL, NULL);
        if (ret == SOCKET_ERROR) {
            printf("select error: %d\n", WSAGetLastError());
            break;
        }

        // 监听套接字可读,说明有新连接
        if (FD_ISSET(listen_fd, &read_fds)) {
            struct sockaddr_in client_addr;
            int addr_len = sizeof(client_addr);
            SOCKET client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &addr_len);
            if (client_fd != INVALID_SOCKET) {
                printf("New client connected\n");
                // 这里可以处理客户端连接,比如加入客户端列表
                if (client_fd > max_fd) {
                    max_fd = client_fd;
                }
            }
        }

        // 处理已连接客户端的读写事件(省略,需自行实现)
        // ...
    }

    // 清理资源
    closesocket(listen_fd);
    WSACleanup();
    return 0;
}
(2)添加端口复用选项

在openserver函数中添加SO_REUSEADDR选项,避免旧连接处于TIME_WAIT状态时无法重新绑定端口:

// 在socket创建后添加
int optval = 1;
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (const char*)&optval, sizeof(optval)) == SOCKET_ERROR) {
    printf("setsockopt SO_REUSEADDR failed: %d\n", WSAGetLastError());
    closesocket(fd);
    return -1;
}
(3)正确维护客户端套接字集合

当客户端连接关闭时,要调用closesocket(client_fd)并将其从fd_set中移除,同时更新max_fd(如果该套接字是当前最大的),避免select一直监听无效的套接字。

内容的提问来源于stack exchange,提问作者RobertSm

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.29 07:36:04