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

基于select()的C++ TCP多客户端异步交互服务器改造问询

基于select实现多客户端异步处理的服务器改造方案

核心思路

select的作用是监听多个文件描述符的就绪状态,你需要让服务器每次调用select后,遍历所有就绪的描述符(包括监听套接字和已连接客户端套接字),分别处理连接请求和客户端消息,而不是阻塞在单个客户端的IO操作上。

具体修改步骤

1. 维护已连接客户端的套接字集合

  • 初始化一个fd_set类型的集合,用来保存所有需要监听的套接字(包括服务器监听套接字+所有已连接客户端套接字)
  • 同时记录当前最大的文件描述符,因为select需要这个参数确定监听范围

2. 循环调用select,轮询就绪事件

每次循环执行以下操作:

  • 重新复制监听集合(select会修改传入的集合,所以每次都要重置)
  • 调用select(max_fd + 1, &read_fds, NULL, NULL, NULL)等待就绪的读事件
  • 遍历所有文件描述符,判断就绪状态:
    • 若为监听套接字就绪:调用accept接受新连接,将新客户端套接字加入监听集合,更新max_fd
    • 若为客户端套接字就绪:调用recv读取消息,根据返回值处理:
      • 返回>0:处理接收到的消息(如打印、回复)
      • 返回0:客户端断开,关闭套接字并从集合中移除
      • 返回-1:区分非致命错误(如EAGAIN/EINTR)和致命错误,前者忽略,后者关闭套接字

3. 设置非阻塞IO模式

确保所有套接字(监听套接字和客户端套接字)都设置为非阻塞模式,这样accept和recv不会阻塞整个服务器,即使某个客户端无数据,服务器也能处理其他请求。

改造后的代码示例

#include <iostream>
#include <vector>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>

#define PORT 8080

int main() {
    int server_fd, new_socket;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);
    char buffer[1024] = {0};

    // 创建监听套接字
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 设置端口复用
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }

    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);

    // 绑定端口
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }

    // 开始监听
    if (listen(server_fd, 3) < 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }

    // 设置监听套接字为非阻塞
    int flags = fcntl(server_fd, F_GETFL, 0);
    fcntl(server_fd, F_SETFL, flags | O_NONBLOCK);

    fd_set read_fds;
    int max_fd = server_fd;
    std::vector<int> client_sockets;

    while (true) {
        FD_ZERO(&read_fds);
        FD_SET(server_fd, &read_fds);

        // 加入所有客户端套接字
        for (int sock : client_sockets) {
            FD_SET(sock, &read_fds);
            if (sock > max_fd) max_fd = sock;
        }

        // 等待就绪事件
        int activity = select(max_fd + 1, &read_fds, NULL, NULL, NULL);
        if ((activity < 0) && (errno != EINTR)) {
            perror("select error");
        }

        // 处理新连接
        if (FD_ISSET(server_fd, &read_fds)) {
            while ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) >= 0) {
                printf("New connection, socket fd: %d, ip: %s, port: %d\n",
                       new_socket, inet_ntoa(address.sin_addr), ntohs(address.sin_port));

                // 设置客户端套接字为非阻塞
                flags = fcntl(new_socket, F_GETFL, 0);
                fcntl(new_socket, F_SETFL, flags | O_NONBLOCK);

                client_sockets.push_back(new_socket);
            }
            // 处理非阻塞accept的正常返回(无更多连接)
            if (new_socket == -1 && errno != EAGAIN && errno != EWOULDBLOCK) {
                perror("accept error");
            }
        }

        // 处理客户端消息
        for (auto it = client_sockets.begin(); it != client_sockets.end();) {
            int sock = *it;
            if (FD_ISSET(sock, &read_fds)) {
                int valread = recv(sock, buffer, 1024, 0);
                if (valread == 0) {
                    // 客户端断开
                    getpeername(sock, (struct sockaddr*)&address, (socklen_t*)&addrlen);
                    printf("Host disconnected, ip: %s, port: %d\n",
                           inet_ntoa(address.sin_addr), ntohs(address.sin_port));

                    close(sock);
                    it = client_sockets.erase(it);
                } else if (valread > 0) {
                    buffer[valread] = '\0';
                    printf("Received from client %d: %s\n", sock, buffer);

                    // 回复客户端
                    const char* reply = "Message received";
                    send(sock, reply, strlen(reply), 0);
                } else {
                    // 处理非致命错误
                    if (errno != EAGAIN && errno != EINTR) {
                        perror("recv error");
                        close(sock);
                        it = client_sockets.erase(it);
                    } else {
                        ++it;
                    }
                }
            } else {
                ++it;
            }
        }
    }

    return 0;
}

关键注意点

  • 非阻塞模式:必须给所有套接字设置非阻塞,避免单个IO操作阻塞整个服务器。
  • fd_set重置:每次循环都要重新初始化fd_set,因为select会修改传入的集合。
  • 客户端套接字管理:用容器维护已连接客户端,断开时及时关闭并移除,避免资源泄漏。
  • 错误区分:不要把EAGAIN/EINTR这类非致命错误当成故障,这类情况说明当前无数据,继续循环即可。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.10 22:10:44