
一、 TCP并发服务器设计架构1. 基础概念对比单循环服务器一次只能处理一个客户端的任务其余客户端必须阻塞等待效率极低。并发服务器能够同时处理多个客户端的任务是现代高并发网络编程的基础。2. 三种并发实现方案多进程 (Process)优点进程间内存隔离安全性高一个客户端崩溃不影响整体。缺点资源开销内存、CPU巨大进程切换成本高并发量受限。多线程 (Thread)优点线程共享进程内存空间相对进程来说资源开销较小并发量有所提升。缺点存在线程安全问题需要引入互斥锁频繁的创建和销毁线程会带来性能损耗。线程池 (Thread Pool) —— 生产者-消费者模型优点预先创建好一批线程并管理起来避免了高频创建/销毁线程带来的时间消耗和系统资源浪费极其适合高并发任务。原理主线程生产者负责接收客户端连接将对应的文件描述符任务放入任务队列工作线程消费者从队列中取出任务进行I/O处理。二、 Linux 常用 I/O 模型阻塞 I/O (Blocking I/O)现象应用层调用read/recv/fgets时若内核数据未准备好进程会被挂起阻塞。缺点单线程下处理多个连接时会发生“死等”导致其他连接饿死。非阻塞 I/O (Non-blocking I/O)现象通过设置O_NONBLOCK轮询内核“数据是否准备好”。缺点虽然不阻塞进程但需要不断进行系统调用轮询会极大浪费 CPU 资源。信号驱动 I/O (Signal-driven I/O)原理应用层注册信号处理函数SIGIO内核数据准备好后发送信号通知应用层去读取。缺点信号个数有限且在高并发时无法有效区分多个不同的 I/O 事件。I/O 多路复用 (I/O Multiplexing)原理内核提供机制在一个进程/线程内同时监控多个文件描述符fd的状态。一旦某个 fd 就绪内核便通知应用层。优势单线程即可管理成千上万个连接极大地节省了系统资源。核心 API 为select、poll、epoll。三、 I/O 多路复用机制核心对比 (select vs poll vs epoll)特性selectpollepoll底层数据结构数组/位图 (bitmap)链表红黑树 (RB-tree)最大连接数限制有通常受FD_SETSIZE限制为 1024无基于链表理论上只受内存限制无受限于系统可用内存内核态与用户态拷贝每次调用均需全量拷贝集合到内核每次调用均需全量拷贝数组到内核仅在首次epoll_ctl时拷贝事件发生时无需反复拷贝查找就绪事件方式低效应用层需遍历整个位图查找 (O(N))低效应用层需遍历整个链表查找 (O(N))高效内核直接返回就绪链表无需全量遍历 (O(1))触发模式支持仅支持水平触发 (LT)仅支持水平触发 (LT)支持水平触发 (LT)与边缘触发 (ET)两种模式四、 核心 API 函数详解与代码示例1.select系列 APIselect允许程序监视多个文件描述符等待其中任何一个变为“就绪”。核心宏函数FD_ZERO(fd_set *set)清空文件描述符集合。FD_SET(int fd, fd_set *set)将指定 fd 加入集合。FD_ISSET(int fd, fd_set *set)判断 fd 是否在集合中即是否有事件。FD_CLR(int fd, fd_set *set)从集合中清除指定的 fd。核心业务函数int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);select代码示例监控标准输入#include sys/select.h #include stdio.h #include unistd.hint main() {fd_set read_fds;struct timeval timeout;int ret;FD_ZERO(amp;read_fds); // 1. 清空集合 FD_SET(0, amp;read_fds); // 2. 将标准输入(0)加入集合 timeout.tv_sec 5; // 3. 设置超时时间 5 秒 timeout.tv_usec 0; ret select(1, amp;read_fds, NULL, NULL, amp;timeout); // 4. 调用 selectnfds 最大fd 1 if (ret -1) { perror(select error); } else if (ret 0) { printf(数据未到达5秒超时结束。\n); } else { if (FD_ISSET(0, amp;read_fds)) { // 5. 判断标准输入是否就绪 char buf[1024]; read(0, buf, sizeof(buf)); // 读取数据 printf(读取到数据: %s, buf); } } return 0;}2.poll函数深度解析poll解决了select在文件描述符数量和事件分离方面的痛点但自身依然存在局限性。️ 函数原型与结构体#include poll.h int poll(struct pollfd *fds, nfds_t nfds, int timeout);poll不再使用位图而是使用struct pollfd结构体数组struct pollfd { int fd; /* 需要检测的文件描述符 */ short events; /* 用户关心的事件类型可读、可写 */ short revents; /* 内核实际发生的事件类型内核填充 */ };events由用户填写表示想让内核检测什么如POLLIN表示可读。revents由内核填写表示实际发生了什么事件如数据到达后revents会被置为POLLIN。timeout单位毫秒-1无限等待直到事件发生阻塞。0立即返回不阻塞非阻塞轮询。0等待指定的毫秒数后超时返回。poll代码示例监控标准输入#include stdio.h #include unistd.h #include poll.hint main() {struct pollfd fds[1]; // 1. 定义结构体数组fds[0].fd 0; // 2. 监控标准输入 fds[0].events POLLIN; // 用户关心可读事件 fds[0].revents 0; // 初始化 int ret poll(fds, 1, 5000); // 3. 调用 poll超时时间 5000ms if (ret -1) { perror(poll error); } else if (ret 0) { printf(超时5秒内无输入。\n); } else { if (fds[0].revents amp; POLLIN) { // 4. 使用位运算检查 revents char buf[1024]; read(0, buf, sizeof(buf)); // 读取数据 printf(读取到数据: %s, buf); } } return 0;}pollvsselect总结*优点突破 1024 限制使用链表事件分离将输入的events和输出的revents分离不需要反复重置集合。*缺点依然存在全量拷贝每次将数组拷入内核依然需要应用层遍历数组寻找就绪事件时间复杂度 O(N)只能工作在水平触发模式。3.epoll系列 API高性能之王epoll是 Linux 特有的高性能多路复用方案解决了select/poll的所有痛点底层使用红黑树管理所有 fd。int epoll_create(int size);创建 epoll 实例返回一个文件描述符epfd。int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);操作 epoll 实例对红黑树进行增删改。参数opEPOLL_CTL_ADD添加、EPOLL_CTL_MOD修改、EPOLL_CTL_DEL删除。int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);阻塞等待监听的事件发生。不需要应用层遍历内核直接将发生事件的 fd 填充到events数组中并返回计数。epoll核心逻辑示例#include sys/epoll.h #include stdio.h #include stdlib.h #include unistd.h#define MAX_EVENTS 10int main() {// 1. 创建 epoll 实例int epfd epoll_create(1);struct epoll_event ev, events[MAX_EVENTS]; // 假设 sockfd 是一个已经监听 (listen) 的 TCP 套接字 // int sockfd socket(...); bind(...); listen(sockfd,...); // 2. 将 sockfd 加入 epoll 红黑树监听可读事件 ev.events EPOLLIN; ev.data.fd sockfd; epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, amp;ev); while (1) { // 3. 调用 epoll_wait 等待事件-1 表示无限阻塞 int nfds epoll_wait(epfd, events, MAX_EVENTS, -1); // 4. 直接遍历内核返回的就绪队列 for (int i 0; i lt; nfds; i) { int cur_fd events[i].data.fd; if (cur_fd sockfd) { // 监听套接字就绪有新客户端连接 // 调用 accept() 接受连接得到 client_fd // 然后 epoll_ctl(..., EPOLL_CTL_ADD, client_fd, amp;ev) 加入红黑树 } else { // 客户端套接字就绪有数据可读 // recv(...) 处理数据若 recv0 表示断开需 EPOLL_CTL_DEL } } } close(epfd); return 0;}epoll特性补充LT水平触发Level Triggered默认模式。只要缓冲区有数据就会不断通知。编码简单不易丢包。ET边缘触发Edge Triggered高速模式。只有当数据状态发生改变时通知一次。必须搭配非阻塞 I/O (Non-blocking)使用且在读取数据时要循环读取直到报EAGAIN错误否则极易漏掉数据。ET模式下系统调用次数极少性能极致。五、 I/O 多路复用的实际应用场景基于上述技术特性多路复用已在现代高性能网络组件中成为标配1. 网络服务器TCP 服务端单线程利用多路复用技术同时处理成千上万个客户端 Socket避免为了每个连接单独开启一个进程/线程。Redis远程字典服务完全基于内存的数据库。利用epoll实现单线程、高性能的 I/O 多路复用避开了多线程编程中复杂的锁竞争问题。Nginx轻量级 Web 服务器采用事件驱动模型结合epoll、非阻塞 I/O 等技术在一个进程中就能轻松支撑数万个并发连接且具备高稳定性和极低的内存占用。2. 双向通信 / 多路监测设备多路数据通信场景当需要同时监控标准输入键盘、串口、管道、多个网络端口或者需要同时处理 Socket 和定时器时采用epoll可以避免多线程/多进程并发带来复杂的资源抢夺和死锁问题高效地在单线程内完成协调。