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 个关键步骤:
- 创建 socket:生成用于通信的文件描述符(
listenfd
) - 绑定地址(bind):将
socket
与服务器的 IP 和端口绑定 - 监听连接(listen):使
socket
进入监听状态,创建连接请求队列 - 接受连接(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. 核心思想
将 “接受连接” 与 “通信” 两个任务分离:
- 父进程 / 主线程:仅负责
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)优缺点
- 优点:
- 实现真正的并发,多个客户端可同时通信
- 进程间地址空间独立,一个客户端崩溃不影响其他
- 缺点:
- 进程创建 / 销毁开销大(内存、CPU 资源占用高)
- 进程间通信复杂(需管道、共享内存等)
- 并发量受限(系统能创建的进程数有限)
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)优缺点
- 优点:
- 线程创建 / 销毁开销远小于进程(共享进程内存,无需复制地址空间)
- 线程间通信简单(直接访问全局变量,需加锁)
- 支持更高的并发量
- 缺点:
- 线程共享地址空间,一个线程崩溃可能导致整个进程崩溃
- 需处理线程安全问题(互斥、同步),代码复杂度高于多进程
四、IO 模型:阻塞与非阻塞
1. 阻塞 IO 模型(默认)
- 定义:当调用
read
/write
/accept
等 IO 函数时,若资源未就绪,进程 / 线程会一直等待(阻塞),直到资源就绪才返回 - eg:
read(connfd, buf, ...)
:若客户端未发送数据,read
会阻塞,进程暂停执行accept(listenfd, ...)
:若没有新连接请求,accept
会阻塞
- 特点:逻辑简单,但 IO 等待时 CPU 空闲,资源利用率低
2. 非阻塞 IO 模型
- 定义:通过
fcntl
设置文件描述符为非阻塞模式后,IO 函数会立即返回:- 资源就绪:返回实际读写的字节数
- 资源未就绪:返回
-1
,并设置errno = EAGAIN
或EWOULDBLOCK
- 实现代码(设置非阻塞)
#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 优缺点
- 优点:
- 跨平台支持(Windows、Linux、macOS)
- 实现简单,适合入门学习
- 缺点:
- 最大监控 fd 数受限(默认
FD_SETSIZE=1024
,修改需重新编译内核) - 每次调用需复制 fd 集合到内核,开销大(fd 数多时明显)
- 返回后需遍历所有 fd 判断就绪状态,时间复杂度
O(n)
- 每次调用需重新初始化 fd 集合(内核会修改原集合)
- 最大监控 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):
- 无最大 fd 数限制(仅受限于
MAX_CLIENT
和系统 fd 上限) - 无需重新初始化监控集合(
events
输入,revents
输出,分离) - 无需计算
maxfd
,直接遍历数组,代码更简洁
- 无最大 fd 数限制(仅受限于
- 缺点:
- 每次调用仍需将整个
fds
数组复制到内核,fd 数多时开销大 - 返回后需遍历所有 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_ctl
的op
: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 - 优点:逻辑简单,无需一次性读完所有数据
- 缺点:若数据未读完,会重复触发,略有开销
- 只要 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):
- 高效的事件通知机制:仅返回就绪 fd,时间复杂度
O(1)
- 无 fd 数限制(仅受系统 fd 上限)
- 共享内存机制:fd 集合无需每次复制到内核(仅初始化时复制一次)
- 支持 LT/ET 两种触发模式,灵活适配不同场景
- 高效的事件通知机制:仅返回就绪 fd,时间复杂度
- 缺点:
- 仅支持 Linux 系统,不跨平台
- 代码复杂度高于 select/poll(尤其是 ET 模式)
六、服务器模型对比
模型 | 并发能力 | 资源开销 | 代码复杂度 | 适用场景 |
---|---|---|---|---|
单循环服务器 | 极低(1 个客户端) | 低 | 低 | 测试、学习 |
多进程服务器 | 中(数百个) | 高(进程创建 / 通信) | 中 | 要求稳定性、进程独立的场景 |
多线程服务器 | 中高(数千个) | 中(线程创建 / 锁) | 中高(线程安全) | 中等并发、需共享资源的场景 |
select | 低(≤1024) | 中(fd 复制 / 遍历) | 低 | 跨平台、低并发场景 |
poll | 中(数千个) | 中(数组复制 / 遍历) | 低 | 跨平台、中等并发场景 |
epoll(ET) | 高(数万至数十万) | 低(事件驱动) | 高(ET 模式) | Linux 高并发服务器(Web、IM、游戏) |