目录
[1. 核心概念与基础流程](#1. 核心概念与基础流程)
[1. 实现代码](#1. 实现代码)
[2. 核心特点](#2. 核心特点)
[1. 核心思想](#1. 核心思想)
[2. 多进程并发服务器](#2. 多进程并发服务器)
[3. 多线程并发服务器](#3. 多线程并发服务器)
[四、IO 模型:阻塞与非阻塞](#四、IO 模型:阻塞与非阻塞)
[1. 阻塞 IO 模型(默认)](#1. 阻塞 IO 模型(默认))
[2. 非阻塞 IO 模型](#2. 非阻塞 IO 模型)
[五、IO 多路复用(高并发)](#五、IO 多路复用(高并发))
[1. 核心思想](#1. 核心思想)
[2. select函数(基础 IO 多路复用)](#2. select函数(基础 IO 多路复用))
[(2)select 服务器实现](#(2)select 服务器实现)
[(3)select 优缺点](#(3)select 优缺点)
[3. poll函数(优化版)](#3. poll函数(优化版))
[(2)poll 服务器实现](#(2)poll 服务器实现)
[(3)poll 优缺点](#(3)poll 优缺点)
[4. epoll函数(高性能)(Linux 特有)](#4. epoll函数(高性能)(Linux 特有))
[(2)epoll 服务器实现](#(2)epoll 服务器实现)
[(3)epoll 的触发(关键优化)](#(3)epoll 的触发(关键优化))
[(4)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));
}
// 关闭通信socket
close(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); // 子进程关闭通信socket
exit(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. 创建socket
listenfd = 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. 创建socket
listenfd = 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. 创建socket
listenfd = 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为listenfd
while (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; } // 更新最大fd
printf("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. 创建socket
listenfd = 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个元素监控listenfd
fds[0].events = POLLIN; // 监控读就绪(新连接)
nfds = 1; // 初始监控fd数量为1
while (1) {
// 调用poll监控事件(永久阻塞)
int ret = poll(fds, nfds, -1);
if (ret < 0) { perror("poll fail"); continue; }
else if (ret == 0) { printf("poll timeout\n"); continue; }
// 遍历监控数组,处理就绪fd
for (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; // 监控的fd
uint32_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. 创建socket
listenfd = 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; // 关联listenfd
if (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、游戏) |