news 2026/6/11 21:36:35

C++:实现多路复用select模型实例(附带源码)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++:实现多路复用select模型实例(附带源码)

一、项目背景详细介绍

在网络服务器开发中,如何同时处理多个客户端连接是一个绕不开的核心问题。

在最初级的网络程序中,服务器通常采用:

  • 阻塞式单客户端模型

  • 一个客户端一个进程 / 线程

这种模型在客户端数量很少时可以工作,但一旦并发连接数增多,就会暴露出明显缺陷:

  • 线程 / 进程数量快速增长

  • 上下文切换成本高

  • 系统资源消耗巨大

  • 程序可扩展性差

为了解决单线程同时监听多个 I/O 事件的问题,操作系统引入了
I/O 多路复用(I/O Multiplexing)机制

在 Linux/Unix 系统中,I/O 多路复用主要有三种实现:

  1. select(最早、最经典)

  2. poll

  3. epoll(高性能版本)

虽然select 在性能和规模上不如 epoll
但它具有以下重要价值:

  • 接口简单,容易理解

  • 跨平台性极好(Linux / Unix / Windows)

  • 是理解 poll / epoll 的理论基础

因此,select 模型是学习高性能网络编程的第一块基石

本项目目标是:

使用 C++ 实现一个基于 select 的多路复用服务器示例,完整展示其工作原理


二、项目需求详细介绍

2.1 功能需求

  1. 基于TCP 协议

  2. 使用select 模型实现 I/O 多路复用

  3. 支持多个客户端同时连接

  4. 服务端能够:

    • 接收客户端数据

    • 原样回显(Echo)

  5. 客户端断开时正确处理并释放资源


2.2 技术要求

  • 平台:Linux / Unix

  • 使用系统原生:

    • select

    • fd_set

  • 阻塞式 select(教学清晰)

  • 单线程事件循环

  • 代码结构清晰、注释详尽


2.3 设计要求

  • 使用 C++ 编写

  • 所有代码集中在一个代码块

  • 使用注释模拟“多文件”结构

  • 关键逻辑必须有中文解释

  • 适合课堂逐行讲解


三、相关技术详细介绍

3.1 什么是 I/O 多路复用

I/O 多路复用的本质是:

一个线程,通过系统调用,同时监听多个文件描述符的 I/O 状态

当任意一个文件描述符就绪时,系统调用返回,程序再逐个处理就绪的描述符。


3.2 select 模型的基本思想

select 模型的核心思想可以总结为一句话:

“我把所有关心的 fd 告诉内核,你帮我看着,有动静再叫我。”

其核心流程如下:

  1. 将所有需要监听的 fd 放入集合

  2. 调用select阻塞等待

  3. select返回后,遍历 fd 集合

  4. 处理就绪的 fd


3.3 select 的核心数据结构:fd_set

fd_set本质是一个位图结构,用于表示哪些 fd 需要被监听。

常用宏:

FD_ZERO(&set); // 清空集合 FD_SET(fd, &set); // 添加 fd FD_CLR(fd, &set); // 移除 fd FD_ISSET(fd, &set);// 判断 fd 是否就绪


3.4 select 的局限性

虽然 select 易学,但存在明显缺点:

  • 最大 fd 数量受限(通常 1024)

  • 每次调用都要全量遍历

  • 用户态和内核态频繁拷贝 fd_set

这也是后续poll / epoll 出现的根本原因


四、实现思路详细介绍

4.1 整体架构思路

  1. 创建监听 socket

  2. 初始化 fd_set

  3. 将监听 socket 加入 fd_set

  4. 循环调用 select:

    • 判断监听 socket 是否就绪(新连接)

    • 判断客户端 socket 是否就绪(收发数据)

  5. 客户端断开时从 fd_set 中移除


4.2 核心事件分类

在 select 服务器中,主要处理两类事件:

1️⃣ 监听 socket 可读

  • 表示有新的客户端连接

  • 调用accept接收连接

  • 将新的客户端 fd 加入 fd_set


2️⃣ 客户端 socket 可读

  • 表示客户端发送了数据

  • 调用recv接收数据

  • 原样回显(Echo)

  • 客户端关闭时清理资源


4.3 fd 管理策略

  • 使用一个数组保存所有客户端 fd

  • 维护当前最大 fd 值(select 需要)

  • 断开连接时及时移除 fd


五、完整实现代码

/**************************************************** * 文件名:SelectServer.cpp * 描述:C++ 基于 select 的多路复用模型示例 ****************************************************/ #include <iostream> #include <cstring> #include <unistd.h> #include <arpa/inet.h> #include <sys/select.h> using namespace std; const int PORT = 8000; const int MAX_CLIENTS = 1024; const int BUFFER_SIZE = 1024; /**************************************************** * 主函数 ****************************************************/ int main() { // 1. 创建监听 socket int listenFd = socket(AF_INET, SOCK_STREAM, 0); sockaddr_in serverAddr{}; serverAddr.sin_family = AF_INET; serverAddr.sin_port = htons(PORT); serverAddr.sin_addr.s_addr = INADDR_ANY; bind(listenFd, (sockaddr*)&serverAddr, sizeof(serverAddr)); listen(listenFd, 5); cout << "select 服务器启动,端口:" << PORT << endl; // 2. 初始化 fd 集合 fd_set masterSet, readSet; FD_ZERO(&masterSet); FD_SET(listenFd, &masterSet); int maxFd = listenFd; // 客户端 fd 数组 int clientFds[MAX_CLIENTS]; memset(clientFds, -1, sizeof(clientFds)); // 3. 主循环 while (true) { // 每次 select 前需要拷贝集合 readSet = masterSet; int ready = select(maxFd + 1, &readSet, nullptr, nullptr, nullptr); if (ready < 0) { perror("select"); break; } // 4. 判断监听 socket if (FD_ISSET(listenFd, &readSet)) { sockaddr_in clientAddr{}; socklen_t len = sizeof(clientAddr); int connFd = accept(listenFd, (sockaddr*)&clientAddr, &len); cout << "新客户端连接:" << connFd << endl; // 保存客户端 fd for (int i = 0; i < MAX_CLIENTS; ++i) { if (clientFds[i] < 0) { clientFds[i] = connFd; break; } } FD_SET(connFd, &masterSet); if (connFd > maxFd) maxFd = connFd; } // 5. 处理客户端 socket for (int i = 0; i < MAX_CLIENTS; ++i) { int fd = clientFds[i]; if (fd < 0) continue; if (FD_ISSET(fd, &readSet)) { char buffer[BUFFER_SIZE]; memset(buffer, 0, BUFFER_SIZE); int bytes = recv(fd, buffer, BUFFER_SIZE, 0); if (bytes <= 0) { cout << "客户端断开:" << fd << endl; close(fd); FD_CLR(fd, &masterSet); clientFds[i] = -1; } else { cout << "收到数据:" << buffer << endl; // Echo 回显 send(fd, buffer, bytes, 0); } } } } close(listenFd); return 0; }

六、代码详细解读(仅解读方法作用)

  • FD_ZERO / FD_SET / FD_CLR:维护监听的文件描述符集合

  • select:阻塞等待任意 fd 就绪

  • FD_ISSET:判断某个 fd 是否就绪

  • 监听 fd:负责接收新客户端连接

  • 客户端 fd:负责接收和发送数据


七、项目详细总结

通过该项目,你已经完整掌握:

  • I/O 多路复用的核心思想

  • select 模型的完整工作流程

  • fd_set 的使用方法

  • 单线程处理多客户端的基本模式

  • Reactor 模型的最原始形态

这是从:

阻塞式网络编程 → 高并发网络编程

第一道门槛


八、项目常见问题及解答

Q1:为什么每次 select 前要拷贝 fd_set?
A:select 会修改 fd_set,只保留就绪 fd。

Q2:select 能支持多少客户端?
A:受限于 FD_SETSIZE,通常为 1024。

Q3:select 和 epoll 的本质区别?
A:select 是轮询模型,epoll 是事件通知模型。


九、扩展方向与性能优化

  1. 升级为poll 模型

  2. 升级为epoll 模型

  3. 使用非阻塞 socket

  4. 封装为 Reactor 类

  5. 引入线程池

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/28 23:20:53

FSMN VAD错误日志:lsof与kill命令停止服务操作详解

FSMN VAD错误日志&#xff1a;lsof与kill命令停止服务操作详解 1. 背景与问题引入 在部署基于阿里达摩院FunASR的FSMN VAD语音活动检测系统时&#xff0c;用户常通过run.sh脚本启动WebUI服务。默认情况下&#xff0c;该服务运行在7860端口&#xff0c;可通过浏览器访问http:/…

作者头像 李华
网站建设 2026/6/9 15:33:52

Scrapy与Splash结合爬取JavaScript渲染页面

在网络爬虫的开发过程中&#xff0c;我们经常会遇到一类 “棘手” 的目标网站 —— 基于 JavaScript 动态渲染的页面。这类网站不会在初始 HTML 中直接返回完整数据&#xff0c;而是通过前端脚本异步加载、渲染内容。传统的 Scrapy 爬虫直接解析响应文本&#xff0c;往往只能拿…

作者头像 李华
网站建设 2026/6/10 3:10:30

学习大模型新技术:RexUniNLU低成本实践路径

学习大模型新技术&#xff1a;RexUniNLU低成本实践路径 你是不是也和我一样&#xff0c;曾经是个朝九晚五的程序员&#xff0c;每天敲代码、改Bug、赶项目&#xff1f;但突然有一天&#xff0c;行业风向变了&#xff0c;AI来得比想象中还快。你开始焦虑&#xff1a;会不会被淘…

作者头像 李华
网站建设 2026/6/9 22:02:51

未来将上线日漫风、3D风,敬请期待新版本

未来将上线日漫风、3D风&#xff0c;敬请期待新版本&#xff1a;基于UNet的人像卡通化技术实践 1. 功能概述与应用场景 随着AI生成技术的快速发展&#xff0c;图像风格迁移在娱乐、社交、内容创作等领域展现出巨大潜力。本项目基于阿里达摩院ModelScope平台提供的cv_unet_per…

作者头像 李华
网站建设 2026/6/10 19:50:56

Keil添加文件流程梳理:新建、添加、编译全过程

Keil添加文件实战指南&#xff1a;从新建到编译&#xff0c;一文讲透嵌入式开发核心操作你有没有遇到过这种情况——辛辛苦苦写好了.c和.h文件&#xff0c;兴冲冲地打开Keil点下“Build”&#xff0c;结果编译器却报错&#xff1a;error: #5: cannot open source input file &q…

作者头像 李华