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

使用select()管理多Socket异常:无法进入accept_clients()函数求排查

问题排查:select()多客户端无阻塞服务器无法接受连接的修复方案

我仔细看了你的代码,发现几个核心问题导致服务器无法正常进入连接处理逻辑,甚至连系统的accept调用都没正确执行。下面拆解问题并给出修复后的完整代码:

核心错误点

  • 函数名与系统API重名:你自定义的accept和recv函数和WinSock的系统API完全同名,导致调用混乱——比如你在自定义accept里调用accept(sock, ...)时,实际递归调用了自己的函数,根本没真正触发系统的连接接受逻辑!
  • 全局fd_set被反复清零:所有函数共用全局的sset,但每次调用自定义accept或recv时都执行FD_ZERO(&sset),之前添加的socket被清空,select完全无法检测多个socket的状态。
  • select使用逻辑错误:你没有在主循环中统一管理所有需要检测的socket(监听socket+已连接客户端socket),而是分散在各个函数中单独处理,这完全违背了select的使用范式。
  • recv逻辑颠倒:自定义recv函数中,当recv(x->cs, ...)返回>0(成功收到数据)时,却调用Server_Status(CLIENT_DIS),完全搞反了接收和断开的逻辑;同时没正确处理客户端主动断开的情况(recv返回0)。
  • 客户端状态维护混乱:PR_CONS的增减逻辑不准确,比如重复处理槽位会导致计数错误,断开时的socket资源也没清理。

修复后的完整代码

#pragma comment(lib, "ws2_32.lib")
#include <WinSock2.h>
#include <stdio.h>
#include <signal.h>
#include <conio.h>
#include <assert.h>
#include <Windows.h>
#include <iostream>

// 常量定义
#define CLIENT_CON 10
#define CLIENT_DIS 20
#define BF_SZ 100
#define MAX_CONS 5
#define TIMEVAL_SEC 0
#define TIMEVAL_USEC 100000 // 调整为100ms超时,避免高频轮询

SOCKET listen_sock; // 重命名监听socket,避免混淆
int PR_CONS = 0;

// 客户端结构体
struct _client {
    bool con;                // 是否已连接
    sockaddr_in addr;        // 客户端地址信息
    SOCKET cs;               // 客户端socket
};

_client client[MAX_CONS];
fd_set read_fds; // 专门用于select的读集合,主循环统一管理

// 重命名自定义函数,避免和系统API冲突
int accept_new_client(_client*);
int recv_client_data(_client*, char*, int);
void Server_Status(int);
void handle_new_connections();
void handle_client_messages();

int main() {
    int i = 1;
    int port = 5150;
    WSADATA ws;

    printf("    Echo Server (Multiple client support)\n");

    sockaddr_in ser;
    ser.sin_family = AF_INET;
    ser.sin_addr.S_un.S_addr = INADDR_ANY;
    ser.sin_port = htons(port);

    // 初始化WinSock
    if (WSAStartup(MAKEWORD(2, 2), &ws) != 0) {
        printf("WSAStartup failed: %d\n", WSAGetLastError());
        return 1;
    }

    // 创建监听socket
    listen_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (listen_sock == INVALID_SOCKET) {
        printf("socket failed: %d\n", WSAGetLastError());
        WSACleanup();
        return 1;
    }

    // 设置地址重用
    setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, (char*)&i, sizeof(i));

    // 绑定端口
    if (bind(listen_sock, (SOCKADDR*)&ser, sizeof(ser)) == SOCKET_ERROR) {
        printf("bind failed: %d\n", WSAGetLastError());
        closesocket(listen_sock);
        WSACleanup();
        return 1;
    }

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

    printf("Listening on port %d...\n", port);

    // 设置监听socket为非阻塞
    unsigned long b = 1;
    if (ioctlsocket(listen_sock, FIONBIO, &b) == SOCKET_ERROR) {
        printf("ioctlsocket failed: %d\n", WSAGetLastError());
        closesocket(listen_sock);
        WSACleanup();
        return 1;
    }

    // 初始化客户端数组
    for (int i = 0; i < MAX_CONS; i++) {
        client[i].con = false;
        client[i].cs = INVALID_SOCKET;
    }

    timeval timeout;
    timeout.tv_sec = TIMEVAL_SEC;
    timeout.tv_usec = TIMEVAL_USEC;

    while (true) {
        // 每次循环重新构建读集合:清零后加入监听socket和所有已连接客户端socket
        FD_ZERO(&read_fds);
        FD_SET(listen_sock, &read_fds);

        for (int i = 0; i < MAX_CONS; i++) {
            if (client[i].con && client[i].cs != INVALID_SOCKET) {
                FD_SET(client[i].cs, &read_fds);
            }
        }

        // 调用select检测可读事件
        int activity = select(0, &read_fds, NULL, NULL, &timeout);
        if (activity == SOCKET_ERROR) {
            printf("select failed: %d\n", WSAGetLastError());
            break;
        } else if (activity == 0) {
            // 超时无事件,继续循环
            continue;
        }

        // 处理新连接
        if (FD_ISSET(listen_sock, &read_fds)) {
            handle_new_connections();
        }

        // 处理客户端消息
        handle_client_messages();
    }

    // 清理资源
    closesocket(listen_sock);
    for (int i = 0; i < MAX_CONS; i++) {
        if (client[i].cs != INVALID_SOCKET) {
            closesocket(client[i].cs);
        }
    }
    WSACleanup();
    return 0;
}

int accept_new_client(_client* x) {
    int addr_len = sizeof(sockaddr_in);
    unsigned long b = 1;

    // 调用系统的accept函数(已避免重名冲突)
    x->cs = accept(listen_sock, (SOCKADDR*)&x->addr, &addr_len);
    if (x->cs == INVALID_SOCKET) {
        int err = WSAGetLastError();
        // 非阻塞模式下无新连接时返回WSAEWOULDBLOCK,属于正常情况
        if (err != WSAEWOULDBLOCK) {
            printf("accept failed: %d\n", err);
        }
        return false;
    }

    // 设置客户端socket为非阻塞
    if (ioctlsocket(x->cs, FIONBIO, &b) == SOCKET_ERROR) {
        printf("ioctlsocket for client failed: %d\n", WSAGetLastError());
        closesocket(x->cs);
        return false;
    }

    x->con = true;
    return true;
}

int recv_client_data(_client* x, char* buffer, int sz) {
    int recv_len = recv(x->cs, buffer, sz - 1, 0); // 留一位存字符串结束符
    if (recv_len > 0) {
        // 收到数据,添加结束符
        buffer[recv_len] = '\0';
        return recv_len;
    } else if (recv_len == 0) {
        // 客户端主动断开连接
        return 0;
    } else {
        int err = WSAGetLastError();
        // 非阻塞模式下无数据时返回WSAEWOULDBLOCK,属于正常情况
        if (err != WSAEWOULDBLOCK) {
            printf("recv failed: %d\n", err);
        }
        return -1;
    }
}

void handle_new_connections() {
    if (PR_CONS >= MAX_CONS) {
        printf("Max clients reached, rejecting new connection\n");
        // 接受后直接关闭,避免客户端等待
        SOCKET temp_sock = accept(listen_sock, NULL, NULL);
        if (temp_sock != INVALID_SOCKET) {
            closesocket(temp_sock);
        }
        return;
    }

    // 找到第一个空闲客户端槽位
    for (int i = 0; i < MAX_CONS; i++) {
        if (!client[i].con) {
            if (accept_new_client(&client[i])) {
                Server_Status(CLIENT_CON);
                printf(" - Client IP: %s\n", inet_ntoa(client[i].addr.sin_addr));
                break;
            }
        }
    }
}

void handle_client_messages() {
    char buffer[BF_SZ];
    for (int i = 0; i < MAX_CONS; i++) {
        if (client[i].con && FD_ISSET(client[i].cs, &read_fds)) {
            int recv_len = recv_client_data(&client[i], buffer, BF_SZ);
            if (recv_len > 0) {
                // 打印收到的消息并回显给客户端
                printf("Received from client %d: %s\n", i, buffer);
                send(client[i].cs, buffer, recv_len, 0);
            } else if (recv_len == 0) {
                // 客户端断开,清理资源
                Server_Status(CLIENT_DIS);
                printf(" - Client %d disconnected\n", i);
                closesocket(client[i].cs);
                client[i].con = false;
                client[i].cs = INVALID_SOCKET;
            }
        }
    }
}

void Server_Status(int msg) {
    if (msg == CLIENT_CON) {
        PR_CONS++;
        printf("Client has connected (Total: %d)", PR_CONS);
    } else if (msg == CLIENT_DIS) {
        PR_CONS--;
        printf("Client has disconnected (Total: %d)", PR_CONS);
    } else {
        printf("Unknown status message");
    }
}

关键改动说明

  1. 解决函数重名冲突:把自定义的accept和recv分别改为accept_new_client和recv_client_data,确保能正确调用系统API。
  2. 统一管理fd_set:在主循环中每次重新构建读集合,包含监听socket和所有已连接客户端socket,让select一次性检测所有需要关注的事件。
  3. 正确处理非阻塞socket:区分非阻塞模式下的正常返回(WSAEWOULDBLOCK)和真正的错误,避免误报。
  4. 修复recv逻辑:正确识别recv返回值的含义,处理数据接收、客户端断开两种核心场景。
  5. 完善资源清理:客户端断开时关闭socket并重置结构体状态,避免无效资源占用。
  6. 添加错误检查:给WinSock关键调用添加错误处理,方便调试定位问题。

修改后服务器就能正常接受多客户端连接,并无阻塞处理消息了。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.27 10:09:30