当前位置: 首页 > ops >正文

linux 网络:并发服务器及IO多路复用

目录

一、服务器模型:从单客户端到多客户端

1. 核心概念与基础流程

二、单循环服务器(迭代服务器)

1. 实现代码

2. 核心特点

三、并发服务器:多进程与多线程模型

1. 核心思想

2. 多进程并发服务器

(1)实现代码

(2)关键细节

(3)优缺点

3. 多线程并发服务器

(1)实现代码

(2)关键细节

(3)优缺点

四、IO 模型:阻塞与非阻塞

1. 阻塞 IO 模型(默认)

2. 非阻塞 IO 模型

五、IO 多路复用(高并发)

1. 核心思想

2. select函数(基础 IO 多路复用)

(1)核心函数与参数

(2)select 服务器实现

(3)select 优缺点

3. poll函数(优化版)

(1)核心函数与参数

(2)poll 服务器实现

(3)poll 优缺点

4. epoll函数(高性能)(Linux 特有)

(1)核心函数与参数

(2)epoll 服务器实现

(3)epoll 的触发(关键优化)

(4)epoll 优缺点

六、服务器模型对比

一、服务器模型:从单客户端到多客户端

1. 核心概念与基础流程

网络服务器的核心是通过socket接口实现客户端与服务器的通信,基础流程包含 4 个关键步骤:

  1. 创建 socket:生成用于通信的文件描述符(listenfd
  2. 绑定地址(bind):将socket与服务器的 IP 和端口绑定
  3. 监听连接(listen):使socket进入监听状态,创建连接请求队列
  4. 接受连接(accept):从请求队列中提取客户端连接,生成通信文件描述符(connfd

二、单循环服务器(迭代服务器)

1. 实现代码

#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <string.h>int main() {int listenfd, connfd;struct sockaddr_in serv_addr, cli_addr;socklen_t cli_len = sizeof(cli_addr);char buf[1024];// 1. 创建socket(TCP协议)listenfd = socket(AF_INET, SOCK_STREAM, 0);if (listenfd < 0) { perror("socket fail"); return -1; }// 2. 绑定地址(IP+端口)memset(&serv_addr, 0, sizeof(serv_addr));serv_addr.sin_family = AF_INET;         // IPv4协议serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听所有网卡serv_addr.sin_port = htons(8080);       // 端口号(主机字节序转网络字节序)if (bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {perror("bind fail"); return -1;}// 3. 监听连接(请求队列大小默认)if (listen(listenfd, 5) < 0) { perror("listen fail"); return -1; }// 4. 单循环处理客户端(一次只能处理一个)while (1) {// 接受客户端连接(阻塞,直到有连接请求)connfd = accept(listenfd, (struct sockaddr*)&cli_addr, &cli_len);if (connfd < 0) { perror("accept fail"); continue; }// 与客户端通信(循环读写)while (1) {memset(buf, 0, sizeof(buf));// 读客户端数据(阻塞,直到有数据)int n = read(connfd, buf, sizeof(buf)-1);if (n <= 0) { printf("client disconnect\n"); break; // 客户端断开或读错误}printf("recv from client: %s", buf);// 向客户端回送数据sprintf(buf, "server reply: %s", buf);write(connfd, buf, strlen(buf));}// 关闭通信socketclose(connfd);}// 关闭监听socket(实际不会执行,需信号处理)close(listenfd);return 0;
}

2. 核心特点

  • 优点:逻辑简单,代码量少,适合学习基础流程
  • 缺点
    1. 一次只能处理一个客户端,其他客户端需排队等待
    2. 若当前客户端通信耗时(如大文件传输),后续客户端会严重阻塞
    3. 效率极低,仅适用于测试或极低并发场景

三、并发服务器:多进程与多线程模型

1. 核心思想

将 “接受连接” 与 “通信” 两个任务分离:

  • 父进程 / 主线程:仅负责accept接受新连接
  • 子进程 / 子线程:为每个新连接创建独立进程 / 线程,专门处理该客户端的通信
  • 实现 “同时处理多个客户端” 的并发能力

2. 多进程并发服务器

(1)实现代码
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#include <signal.h>// 信号处理:回收僵尸进程(避免资源泄漏)
void sig_chld(int sig) {while (waitpid(-1, NULL, WNOHANG) > 0); // 非阻塞回收所有子进程
}// 子进程:处理单个客户端通信
void do_client(int connfd) {char buf[1024];while (1) {memset(buf, 0, sizeof(buf));int n = read(connfd, buf, sizeof(buf)-1);if (n <= 0) {printf("client disconnect (pid: %d)\n", getpid());break;}printf("pid: %d, recv: %s", getpid(), buf);sprintf(buf, "server(pid:%d) reply: %s", getpid(), buf);write(connfd, buf, strlen(buf));}close(connfd); // 子进程关闭通信socketexit(0);       // 子进程退出
}int main() {int listenfd, connfd;struct sockaddr_in serv_addr, cli_addr;socklen_t cli_len = sizeof(cli_addr);pid_t pid;// 注册信号处理函数(回收僵尸进程)signal(SIGCHLD, sig_chld);// 1. 创建socketlistenfd = socket(AF_INET, SOCK_STREAM, 0);if (listenfd < 0) { perror("socket fail"); return -1; }// 关键:开启地址重用(避免服务器重启时端口被占用)int on = 1;setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));// 2. 绑定地址memset(&serv_addr, 0, sizeof(serv_addr));serv_addr.sin_family = AF_INET;serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);serv_addr.sin_port = htons(8080);if (bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {perror("bind fail"); return -1;}// 3. 监听连接if (listen(listenfd, 5) < 0) { perror("listen fail"); return -1; }// 4. 父进程循环接受连接,创建子进程处理通信while (1) {connfd = accept(listenfd, (struct sockaddr*)&cli_addr, &cli_len);if (connfd < 0) { perror("accept fail"); continue; }// 创建子进程pid = fork();if (pid < 0) { perror("fork fail"); close(connfd); // 创建失败需关闭connfd,避免泄漏continue;} else if (pid == 0) { close(listenfd); // 子进程不需要监听socket,关闭!do_client(connfd); // 子进程处理通信} else { close(connfd); // 父进程不需要通信socket,关闭!}}close(listenfd);return 0;
}
(2)关键细节
  • 地址重用:通过setsockopt设置,解决服务器重启时 “端口已被占用(TIME_WAIT 状态)” 的问题
  • 僵尸进程回收:通过SIGCHLD信号和waitpid非阻塞回收,避免子进程退出后成为僵尸进程占用资源
  • 文件描述符关闭
    • 子进程必须关闭listenfd(无需监听新连接)
    • 父进程必须关闭connfd(无需与客户端通信)
(3)优缺点
  • 优点
    1. 实现真正的并发,多个客户端可同时通信
    2. 进程间地址空间独立,一个客户端崩溃不影响其他
  • 缺点
    1. 进程创建 / 销毁开销大(内存、CPU 资源占用高)
    2. 进程间通信复杂(需管道、共享内存等)
    3. 并发量受限(系统能创建的进程数有限)

3. 多线程并发服务器

(1)实现代码
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>// 线程参数:需用结构体封装(pthread_create仅支持单个void*参数)
typedef struct {int connfd;struct sockaddr_in cli_addr;
} ThreadArg;// 线程处理函数:处理单个客户端通信
void* do_client(void* arg) {ThreadArg* targ = (ThreadArg*)arg;int connfd = targ->connfd;char buf[1024];// 关键:设置线程分离属性(无需主线程pthread_join回收)pthread_detach(pthread_self());free(targ); // 释放参数内存while (1) {memset(buf, 0, sizeof(buf));int n = read(connfd, buf, sizeof(buf)-1);if (n <= 0) {printf("client disconnect (tid: %lu)\n", pthread_self());break;}printf("tid: %lu, recv: %s", pthread_self(), buf);sprintf(buf, "server(tid:%lu) reply: %s", pthread_self(), buf);write(connfd, buf, strlen(buf));}close(connfd);return NULL;
}int main() {int listenfd, connfd;struct sockaddr_in serv_addr, cli_addr;socklen_t cli_len = sizeof(cli_addr);pthread_t tid;// 1. 创建socketlistenfd = socket(AF_INET, SOCK_STREAM, 0);if (listenfd < 0) { perror("socket fail"); return -1; }// 开启地址重用int on = 1;setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));// 2. 绑定地址memset(&serv_addr, 0, sizeof(serv_addr));serv_addr.sin_family = AF_INET;serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);serv_addr.sin_port = htons(8080);if (bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {perror("bind fail"); return -1;}// 3. 监听连接if (listen(listenfd, 5) < 0) { perror("listen fail"); return -1; }// 4. 主线程循环接受连接,创建子线程处理通信while (1) {connfd = accept(listenfd, (struct sockaddr*)&cli_addr, &cli_len);if (connfd < 0) { perror("accept fail"); continue; }// 分配线程参数(堆内存,避免栈内存被覆盖)ThreadArg* targ = (ThreadArg*)malloc(sizeof(ThreadArg));targ->connfd = connfd;targ->cli_addr = cli_addr;// 创建子线程if (pthread_create(&tid, NULL, do_client, targ) != 0) {perror("pthread_create fail");free(targ);close(connfd);continue;}}close(listenfd);return 0;
}
(2)关键细节
  • 线程参数传递:必须用堆内存(malloc)封装参数,避免栈内存被主线程循环覆盖
  • 线程分离(pthread_detach):设置后线程退出时自动释放资源,无需主线程调用pthread_join
  • 资源共享:线程共享进程地址空间(如全局变量),需注意互斥锁(pthread_mutex_t)保护共享资源
(3)优缺点
  • 优点
    1. 线程创建 / 销毁开销远小于进程(共享进程内存,无需复制地址空间)
    2. 线程间通信简单(直接访问全局变量,需加锁)
    3. 支持更高的并发量
  • 缺点
    1. 线程共享地址空间,一个线程崩溃可能导致整个进程崩溃
    2. 需处理线程安全问题(互斥、同步),代码复杂度高于多进程

四、IO 模型:阻塞与非阻塞

1. 阻塞 IO 模型(默认)

  • 定义:当调用read/write/accept等 IO 函数时,若资源未就绪,进程 / 线程会一直等待(阻塞),直到资源就绪才返回
  • eg
    • read(connfd, buf, ...):若客户端未发送数据,read会阻塞,进程暂停执行
    • accept(listenfd, ...):若没有新连接请求,accept会阻塞
  • 特点:逻辑简单,但 IO 等待时 CPU 空闲,资源利用率低

2. 非阻塞 IO 模型

  • 定义:通过fcntl设置文件描述符为非阻塞模式后,IO 函数会立即返回
    • 资源就绪:返回实际读写的字节数
    • 资源未就绪:返回-1,并设置errno = EAGAINEWOULDBLOCK
  • 实现代码(设置非阻塞)
#include <fcntl.h>// 将fd设置为非阻塞模式
int set_nonblock(int fd) {int flags = fcntl(fd, F_GETFL, 0); // 获取当前文件状态标志if (flags < 0) { perror("fcntl F_GETFL fail"); return -1; }// 添加非阻塞标志(O_NONBLOCK),不影响其他标志if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) {perror("fcntl F_SETFL fail"); return -1;}return 0;
}
  • 特点
    • 优点:IO 等待时 CPU 可处理其他任务,资源利用率高
    • 缺点:需通过 “轮询”(循环调用 IO 函数)检查资源是否就绪,会占用大量 CPU 时间

五、IO 多路复用(高并发)

1. 核心思想

  • 问题:多进程 / 多线程模型中,每个客户端对应一个进程 / 线程,并发量高时资源开销大;非阻塞 IO 的轮询机制 CPU 利用率低
  • 解决方案:用一个进程 / 线程监控多个文件描述符(IO 事件),仅当某个文件描述符就绪(有数据可读 / 可写)时才处理,实现 “多路 IO 复用一个进程 / 线程”
  • 适用场景:高并发服务器(如 Web 服务器、即时通讯服务器),支持上万级并发

2. select函数(基础 IO 多路复用)

(1)核心函数与参数
#include <sys/select.h>int select(int nfds, fd_set *readfds,  // 监控“读就绪”的fd集合fd_set *writefds, // 监控“写就绪”的fd集合fd_set *exceptfds,// 监控“异常”的fd集合struct timeval *timeout); // 超时时间
  • 关键宏(操作 fd 集合)
    • FD_ZERO(fd_set *set):清空 fd 集合
    • FD_SET(int fd, fd_set *set):将 fd 添加到集合
    • FD_CLR(int fd, fd_set *set):将 fd 从集合中移除
    • FD_ISSET(int fd, fd_set *set):判断 fd 是否在就绪集合中
  • 参数说明
    • nfds:监控的 fd 的最大值 + 1(select 按 fd 序号遍历,需知道遍历上限)
    • timeout
      • NULL:永久阻塞,直到有 fd 就绪
      • tv_sec=0, tv_usec=0:非阻塞,立即返回
      • 其他值:阻塞指定时间(秒 + 微秒),超时后返回
(2)select 服务器实现

#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>#define MAX_FD 1024 // select默认最大监控fd数(FD_SETSIZE)int main() {int listenfd, connfd, maxfd;struct sockaddr_in serv_addr, cli_addr;socklen_t cli_len = sizeof(cli_addr);fd_set readfds, tmpfds; // readfds:总集合;tmpfds:临时集合(select会修改)char buf[1024];// 1. 创建socketlistenfd = socket(AF_INET, SOCK_STREAM, 0);if (listenfd < 0) { perror("socket fail"); return -1; }// 开启地址重用int on = 1;setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));// 2. 绑定地址memset(&serv_addr, 0, sizeof(serv_addr));serv_addr.sin_family = AF_INET;serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);serv_addr.sin_port = htons(8080);if (bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {perror("bind fail"); return -1;}// 3. 监听连接if (listen(listenfd, 5) < 0) { perror("listen fail"); return -1; }// 4. 初始化select监控集合FD_ZERO(&readfds);FD_SET(listenfd, &readfds); // 监控listenfd(新连接就绪)maxfd = listenfd; // 初始最大fd为listenfdwhile (1) {tmpfds = readfds; // 复制集合(select会修改原集合,需备份)// 调用select监控读就绪事件(永久阻塞)int ret = select(maxfd + 1, &tmpfds, NULL, NULL, NULL);if (ret < 0) { perror("select fail"); continue; }else if (ret == 0) { printf("select timeout\n"); continue; }// 遍历所有监控的fd,判断是否就绪for (int i = 0; i <= maxfd; i++) {if (FD_ISSET(i, &tmpfds)) { // i fd就绪if (i == listenfd) { // 新连接就绪connfd = accept(listenfd, (struct sockaddr*)&cli_addr, &cli_len);if (connfd < 0) { perror("accept fail"); continue; }// 将新的connfd加入监控集合FD_SET(connfd, &readfds);if (connfd > maxfd) { maxfd = connfd; } // 更新最大fdprintf("new client connect, connfd: %d\n", connfd);} else { // 客户端通信fd就绪(有数据可读)memset(buf, 0, sizeof(buf));int n = read(i, buf, sizeof(buf)-1);if (n <= 0) { // 客户端断开或读错误printf("client disconnect, connfd: %d\n", i);FD_CLR(i, &readfds); // 从监控集合中移除close(i); // 关闭fd// 优化:更新maxfd(避免后续无效遍历)for (int j = maxfd; j >= 0; j--) {if (FD_ISSET(j, &readfds)) {maxfd = j;break;}}} else { // 正常读取数据printf("recv from connfd %d: %s", i, buf);sprintf(buf, "server reply: %s", buf);write(i, buf, strlen(buf));}}}}close(listenfd);return 0;}
}
(3)select 优缺点
  • 优点
    1. 跨平台支持(Windows、Linux、macOS)
    2. 实现简单,适合入门学习
  • 缺点
    1. 最大监控 fd 数受限(默认FD_SETSIZE=1024,修改需重新编译内核)
    2. 每次调用需复制 fd 集合到内核,开销大(fd 数多时明显)
    3. 返回后需遍历所有 fd 判断就绪状态,时间复杂度O(n)
    4. 每次调用需重新初始化 fd 集合(内核会修改原集合)

3. poll函数(优化版)

(1)核心函数与参数
#include <poll.h>int poll(struct pollfd *fds,  // 监控的fd数组nfds_t nfds,         // 数组中fd的数量int timeout);        // 超时时间(ms):-1=永久阻塞,0=非阻塞,>0=阻塞ms
  • struct pollfd结构体
struct pollfd {int   fd;         // 要监控的文件描述符(-1表示忽略)short events;     // 期望监控的事件(输入参数)short revents;    // 实际就绪的事件(输出参数)
};
  • 常用事件标志
    • POLLIN:读就绪(有数据可读)
    • POLLOUT:写就绪(有空间可写)
    • POLLERR:错误事件(无需主动设置,内核自动返回)
(2)poll 服务器实现
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <string.h>
#include <poll.h>
#include <stdio.h>#define MAX_CLIENT 1024 // 最大支持客户端数int main() {int listenfd, connfd, nfds = 0;struct sockaddr_in serv_addr, cli_addr;socklen_t cli_len = sizeof(cli_addr);struct pollfd fds[MAX_CLIENT]; // poll监控的fd数组char buf[1024];// 1. 创建socketlistenfd = socket(AF_INET, SOCK_STREAM, 0);if (listenfd < 0) { perror("socket fail"); return -1; }// 开启地址重用int on = 1;setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));// 2. 绑定地址memset(&serv_addr, 0, sizeof(serv_addr));serv_addr.sin_family = AF_INET;serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);serv_addr.sin_port = htons(8080);if (bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {perror("bind fail"); return -1;}// 3. 监听连接if (listen(listenfd, 5) < 0) { perror("listen fail"); return -1; }// 4. 初始化poll监控数组memset(fds, 0, sizeof(fds));fds[0].fd = listenfd;       // 第0个元素监控listenfdfds[0].events = POLLIN;     // 监控读就绪(新连接)nfds = 1;                   // 初始监控fd数量为1while (1) {// 调用poll监控事件(永久阻塞)int ret = poll(fds, nfds, -1);if (ret < 0) { perror("poll fail"); continue; }else if (ret == 0) { printf("poll timeout\n"); continue; }// 遍历监控数组,处理就绪fdfor (int i = 0; i < nfds; i++) {if (fds[i].revents & POLLIN) { // 读就绪事件if (fds[i].fd == listenfd) { // 新连接就绪connfd = accept(listenfd, (struct sockaddr*)&cli_addr, &cli_len);if (connfd < 0) { perror("accept fail"); continue; }// 检查是否超过最大客户端数if (nfds >= MAX_CLIENT) {printf("too many clients\n");close(connfd);continue;}// 将新connfd加入poll数组fds[nfds].fd = connfd;fds[nfds].events = POLLIN; // 监控读就绪nfds++; // 增加监控fd数量printf("new client, connfd: %d, total: %d\n", connfd, nfds-1);} else { // 客户端通信fd就绪memset(buf, 0, sizeof(buf));int n = read(fds[i].fd, buf, sizeof(buf)-1);if (n <= 0) { // 客户端断开printf("client disconnect, connfd: %d\n", fds[i].fd);close(fds[i].fd);// 移除该fd:用最后一个元素覆盖,减少数组遍历fds[i] = fds[nfds - 1];nfds--;i--; // 重新检查当前位置(已被覆盖)} else { // 正常通信printf("recv from connfd %d: %s", fds[i].fd, buf);sprintf(buf, "server reply: %s", buf);write(fds[i].fd, buf, strlen(buf));}}}}}close(listenfd);return 0;
}
(3)poll 优缺点
  • 优点(对比 select)
    1. 无最大 fd 数限制(仅受限于MAX_CLIENT和系统 fd 上限)
    2. 无需重新初始化监控集合(events输入,revents输出,分离)
    3. 无需计算maxfd,直接遍历数组,代码更简洁
  • 缺点
    1. 每次调用仍需将整个fds数组复制到内核,fd 数多时开销大
    2. 返回后需遍历所有 fd 判断就绪状态,时间复杂度O(n)

4. epoll函数(高性能)(Linux 特有)

(1)核心函数与参数

epoll 通过 3 个函数实现,采用 “事件驱动” 模型,仅返回就绪的 fd,效率极高:

函数功能
epoll_create(int size)创建 epoll 实例(返回 epoll fd),size已忽略(需 > 0)
epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)控制 epoll 实例(添加 / 修改 / 删除 fd 监控)
epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)等待就绪事件(返回就绪 fd 的数量)
  • struct epoll_event结构体
typedef union epoll_data {void        *ptr;   // 自定义数据(如客户端信息)int          fd;    // 监控的fduint32_t     u32;uint64_t     u64;
} epoll_data_t;struct epoll_event {uint32_t     events;  // 监控的事件epoll_data_t data;    // 关联的数据(通常存fd)
};
  • 关键参数与事件
    • epoll_ctlop
      • EPOLL_CTL_ADD:添加 fd 到 epoll 实例
      • EPOLL_CTL_MOD:修改 fd 的监控事件
      • EPOLL_CTL_DEL:从 epoll 实例中删除 fd
    • events标志:
      • EPOLLIN:读就绪
      • EPOLLOUT:写就绪
      • EPOLLET:边沿触发(ET 模式,高效,默认水平触发 LT)
      • EPOLLONESHOT:只触发一次事件,需重新添加监控
(2)epoll 服务器实现
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <string.h>
#include <sys/epoll.h>
#include <stdio.h>#define MAX_EVENTS 1024 // 每次epoll_wait返回的最大就绪事件数int main() {int listenfd, connfd, epfd;struct sockaddr_in serv_addr, cli_addr;socklen_t cli_len = sizeof(cli_addr);struct epoll_event ev, events[MAX_EVENTS]; // ev:添加事件;events:就绪事件char buf[1024];// 1. 创建socketlistenfd = socket(AF_INET, SOCK_STREAM, 0);if (listenfd < 0) { perror("socket fail"); return -1; }// 开启地址重用int on = 1;setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));// 2. 绑定地址memset(&serv_addr, 0, sizeof(serv_addr));serv_addr.sin_family = AF_INET;serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);serv_addr.sin_port = htons(8080);if (bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {perror("bind fail"); return -1;}// 3. 监听连接if (listen(listenfd, 5) < 0) { perror("listen fail"); return -1; }// 4. 创建epoll实例epfd = epoll_create(1); // size=1(已忽略)if (epfd < 0) { perror("epoll_create fail"); return -1; }// 5. 将listenfd添加到epoll监控(读就绪事件)ev.events = EPOLLIN;    // 水平触发(LT),默认ev.data.fd = listenfd;  // 关联listenfdif (epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev) < 0) {perror("epoll_ctl add listenfd fail"); return -1;}while (1) {// 等待就绪事件(永久阻塞,超时时间-1)int nready = epoll_wait(epfd, events, MAX_EVENTS, -1);if (nready < 0) { perror("epoll_wait fail"); continue; }else if (nready == 0) { printf("epoll_wait timeout\n"); continue; }// 遍历就绪事件(仅处理nready个,效率高)for (int i = 0; i < nready; i++) {int fd = events[i].data.fd;if (fd == listenfd) { // 新连接就绪connfd = accept(listenfd, (struct sockaddr*)&cli_addr, &cli_len);if (connfd < 0) { perror("accept fail"); continue; }// 将新connfd添加到epoll监控ev.events = EPOLLIN;ev.data.fd = connfd;if (epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev) < 0) {perror("epoll_ctl add connfd fail");close(connfd);continue;}printf("new client, connfd: %d\n", connfd);} else { // 客户端通信fd就绪memset(buf, 0, sizeof(buf));int n = read(fd, buf, sizeof(buf)-1);if (n <= 0) { // 客户端断开或读错误printf("client disconnect, connfd: %d\n", fd);epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL); // 从epoll中删除close(fd);} else { // 正常通信printf("recv from connfd %d: %s", fd, buf);sprintf(buf, "server reply: %s", buf);write(fd, buf, strlen(buf));}}}}// 释放资源close(listenfd);close(epfd);return 0;
}
(3)epoll 的触发(关键优化)

  • 水平触发(LT)
    • 只要 fd 就绪(如还有数据可读),每次epoll_wait都会返回该 fd
    • 优点:逻辑简单,无需一次性读完所有数据
    • 缺点:若数据未读完,会重复触发,略有开销
  • 边沿触发(ET)
    • 仅在 fd 状态从 “未就绪” 变为 “就绪” 时触发一次(如数据刚到达时)
    • 优点:触发次数少,效率极高,适合高并发
    • 缺点:需一次性读完所有数据(用非阻塞 fd + 循环读),否则后续数据无法触发
  • 边沿触发实现
// 添加connfd时设置ET模式 + 非阻塞
set_nonblock(connfd); // 先设置fd为非阻塞
ev.events = EPOLLIN | EPOLLET; // 开启边沿触发
ev.data.fd = connfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);// 读数据时循环读取(直到errno=EAGAIN)
int n;
while (1) {memset(buf, 0, sizeof(buf));n = read(fd, buf, sizeof(buf)-1);if (n > 0) {// 处理数据printf("recv: %s", buf);} else if (n == 0) {// 客户端断开break;} else {if (errno == EAGAIN || errno == EWOULDBLOCK) {// 数据已读完,退出循环break;} else {// 其他错误perror("read fail");break;}}
}
(4)epoll 优缺点
  • 优点(对比 select/poll)
    1. 高效的事件通知机制:仅返回就绪 fd,时间复杂度O(1)
    2. 无 fd 数限制(仅受系统 fd 上限)
    3. 共享内存机制:fd 集合无需每次复制到内核(仅初始化时复制一次)
    4. 支持 LT/ET 两种触发模式,灵活适配不同场景
  • 缺点
    1. 仅支持 Linux 系统,不跨平台
    2. 代码复杂度高于 select/poll(尤其是 ET 模式)

六、服务器模型对比

模型并发能力资源开销代码复杂度适用场景
单循环服务器极低(1 个客户端)测试、学习
多进程服务器中(数百个)高(进程创建 / 通信)要求稳定性、进程独立的场景
多线程服务器中高(数千个)中(线程创建 / 锁)中高(线程安全)中等并发、需共享资源的场景
select低(≤1024)中(fd 复制 / 遍历)跨平台、低并发场景
poll中(数千个)中(数组复制 / 遍历)跨平台、中等并发场景
epoll(ET)高(数万至数十万)低(事件驱动)高(ET 模式)Linux 高并发服务器(Web、IM、游戏)
http://www.xdnf.cn/news/18931.html

相关文章:

  • 如何将yolo训练图像数据库的某个分类的图像取出来
  • element-plus的el-scrollbar显示横向滚动条
  • 使用华为 USG6000防火墙配置安全策略
  • 传输层协议介绍
  • 企业通讯软件以安全为基,搭建高效的通讯办公平台
  • Python篇---返回类型
  • 【论文阅读】PEPNet
  • amis上传组件导入文件接口参数为base64格式的使用示例
  • 计算机三级嵌入式填空题——真题库(22)原题附答案速记
  • 强化学习与注意力机制的AlignSAM框架解析
  • 微算法科技(NASDAQ:MLGO)推出创新型混合区块链共识算法,助力物联网多接入边缘计算
  • [n8n] 工作流数据库管理SQLite | 数据访问层-REST API服务
  • Paimon——官网阅读:Flink 引擎
  • 前端javascript在线生成excel,word模板-通用场景(免费)
  • AbMole小课堂丨详解野百合碱在动物肺动脉高压、急性肺损伤、静脉闭塞肝病造模中的原理及应用
  • Go 语言常用命令使用与总结
  • 微信小程序对接EdgeX Foundry详细指南
  • 云计算学习100天-第31天
  • 从零开始的云计算生活——第五十三天,发愤图强,kubernetes模块之Prometheus和发布
  • 【SpringAI】快速上手,详解项目快速集成主流大模型DeepSeek,ChatGPT
  • 【TEC045-KIT】基于复旦微 FMQL45T900 的全国产化 ARM 开发套件
  • Uniapp中自定义导航栏
  • 如何将iPhone上的隐藏照片传输到电脑
  • Flask测试平台开发实战-第二篇
  • 服务器核心组件:CPU 与 GPU 的核心区别、应用场景、协同工作
  • 麒麟操作系统挂载NAS服务器
  • React中优雅管理CSS变量的最佳实践
  • 【动态规划】子数组、子串问题
  • 保姆级教程 | 在Ubuntu上部署Claude Code Plan Mode全过程
  • 设计模式相关面试题