基于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
相关产品推荐
相关产品推荐

