「Linux 网络编程」基于 epoll 的 I/O 复用服务器

0 回顾

I/O 多路复用 是 Linux 网络编程中的核心技术,它允许单个进程同时监视多个文件描述符,当其中任何一个准备就绪时,进程就能得到通知并进行相应的 I/O 操作。

相比 多进程/多线程 的并发服务器的 真正并行 的实现方式,I/O 多路复用实际上采用的是 时分复用 技术,通过高效地轮询和响应大量任务的事件。

在前面的文章中,实现了基于 selectpoll 的 I/O 复用服务器。Linux 下还有着性能更强大的 I/O 复用机制 —— epoll

「Linux 网络编程」基于 select 的 I/O 复用服务器

「Linux 网络编程」基于 poll 的 I/O 复用服务器



1 select、poll、epoll 工作流程对比

1.1 select 工作流程

比喻理解

select 就像传统的邮局。你需要每天去邮局,工作人员逐个检查所有邮箱(文件描述符),告诉你哪些邮箱有信件。即使只有1封信,也要检查所有邮箱。

特点

  • 每次调用都需要重置和传递整个 fd_set
  • 内核需要遍历所有监控的 fd(O(n) 复杂度)
  • 用户空间也需要遍历所有 fd 来找出就绪的
  • 有 1024 的文件描述符限制

1.2 poll 工作流程

比喻理解

poll 就像改进的邮局。邮箱数量没有限制了,但工作人员仍然需要逐个检查每个邮箱。只是检查的方式更统一了。

特点

  • 使用统一的 pollfd 结构体,解决了 fd 数量限制
  • 但仍然是 O(n) 的遍历复杂度
  • 每次调用仍需传递整个数组
  • events 和 revents 分离,不需要每次重置

1.3 epoll 工作流程

比喻理解

epoll 就像智能快递柜。每个柜子(文件描述符)有门铃(回调机制),有快递到达时自动亮灯(加入就绪队列)。你只需要看哪些柜子亮灯,直接去取快递即可。

特点

  • 内核维护事件表:通过 epoll_ctl 注册,一次注册多次使用
  • 回调机制:数据到达时自动触发回调,将 fd 加入就绪队列
  • 直接获取就绪事件epoll_wait 直接返回就绪的 fd,无需遍历
  • 时间复杂度 O(1):与监控的 fd 总数无关,只与就绪的 fd 数量有关

1.4 综合对比

特性 select poll epoll
时间复杂度 O(n) O(n) O(1)
文件描述符限制 1024 系统限制 系统限制
内存拷贝 每次调用都要拷贝 每次调用都要拷贝 仅初始化时注册
触发模式 水平触发 水平触发 水平/边缘触发
内核数据结构 位图 数组 红黑树+就绪队列
适用场景 连接数少 连接数中等 高并发


2 epoll 函数使用和工作原理

epoll 是 Linux 特有的 I/O 多路复用机制,它在处理大量并发连接时具有显著优势。epoll 的核心思想是:避免每次调用时都传递整个文件描述符集合,而是通过内核维护一个事件表来高效管理。


2.1 epoll_create: 创建 epoll 实例

函数原型

1
2
3
#include <sys/epoll.h>

int epoll_create(int size);

参数说明

  • size: 建议内核监听的文件描述符数量(Linux 2.6.8 后自动调整,但必须大于0)

返回值

  • 成功:epoll 文件描述符
  • 失败:-1

2.2 epoll_ctl: 管理 epoll 事件

函数原型

1
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

参数说明

  • epfd: epoll 实例的文件描述符
  • op: 操作类型
    • EPOLL_CTL_ADD: 添加文件描述符
    • EPOLL_CTL_MOD: 修改文件描述符
    • EPOLL_CTL_DEL: 删除文件描述符
  • fd: 要操作的目标文件描述符
  • event: 事件结构体指针

2.3 epoll_wait: 等待事件发生

函数原型

1
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

参数说明

  • epfd: epoll 实例的文件描述符
  • events: 用于接收就绪事件的数组
  • maxevents: 数组的最大容量
  • timeout: 超时时间(毫秒),-1 表示无限等待

返回值

  • > 0: 就绪的文件描述符数量
  • 0: 超时
  • -1: 出错

2.4 epoll_event 结构体

1
2
3
4
5
6
7
8
9
10
11
struct epoll_event {
uint32_t events; /* Epoll 事件 */
epoll_data_t data; /* 用户数据 */
};

typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;

常用事件标志

  • EPOLLIN: 数据可读
  • EPOLLOUT: 数据可写
  • EPOLLERR: 发生错误
  • EPOLLHUP: 连接挂起
  • EPOLLET: 边缘触发模式(默认水平触发)
  • EPOLLONESHOT: 一次性事件

2.5 epoll 的工作模式

水平触发(Level Triggered, LT)

  • 默认工作模式
  • 只要文件描述符处于就绪状态,就会持续通知
  • 类似于 poll 的行为
  • 编程更简单,不容易丢失事件

边缘触发(Edge Triggered, ET)

  • 只有当状态发生变化时才通知
  • 需要一次性读取所有可用数据
  • 性能更高,但编程更复杂
  • 必须使用非阻塞 I/O

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <sys/epoll.h>

int epoll_fd;
struct epoll_event event, events[MAX_EVENTS];

// 创建 epoll 实例
epoll_fd = epoll_create1(0);
if (epoll_fd == -1) {
perror("epoll_create1");
exit(EXIT_FAILURE);
}

// 添加服务器套接字到 epoll
event.events = EPOLLIN; // 监视可读事件
event.data.fd = server_fd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event) == -1) {
perror("epoll_ctl: server_fd");
exit(EXIT_FAILURE);
}

// 等待事件
int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
if (nfds == -1) {
perror("epoll_wait");
exit(EXIT_FAILURE);
}

// 处理就绪事件
for (int i = 0; i < nfds; i++) {
if (events[i].data.fd == server_fd) {
// 处理新连接
handle_new_connection(server_fd, epoll_fd);
} else {
// 处理客户端数据
handle_client_data(events[i].data.fd, epoll_fd);
}
}

2.6 为什么 epoll 是 O(1) 时间复杂度?

epoll 的 O(1) 复杂度来自于其独特的设计:

关键机制

  1. 回调驱动:数据到达时网卡中断触发回调,而不是轮询检查
  2. 就绪队列:内核维护"有数据"的队列,epoll_wait 直接从中获取
  3. 事件分离:监控的 fd 总数与处理时间无关,只与就绪事件数量有关

对比

  • select/pollO(n) - 需要遍历所有监控的 fd
  • epollO(k) - 只处理就绪的 kk 个 fd,通常 k<<nk << n

2.7 epoll 相比 poll 的优势

  1. 高效的事件通知机制

    • poll 需要遍历整个文件描述符数组来查找就绪的描述符
    • epoll 直接返回就绪的事件列表,时间复杂度 O(1)\mathcal{O}(1)
  2. 无文件描述符数量限制

    • poll 受限于数组大小和系统资源
    • epoll 使用红黑树管理,支持数十万并发连接
  3. 内存使用优化

    • poll 每次调用都需要在用户空间和内核空间之间复制整个描述符数组
    • epoll 在内核中维护事件表,只需在添加/修改时复制
  4. 支持边缘触发模式

    • 减少不必要的事件通知,提高性能
    • 特别适合高性能服务器场景


3 基于 epoll 的 I/O 复用服务器

3.1 头文件与宏定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <errno.h>
#include <fcntl.h>

#define MAX_EVENTS 1024 // epoll 最大事件数
#define MAX_CLIENTS 10000 // 最大客户端数(epoll 可以支持更多)
#define BUFFER_SIZE 1024 // 数据缓冲区大小
#define PORT 8080 // 服务器监听端口

参数说明

  • MAX_EVENTS: 单次 epoll_wait 返回的最大事件数
  • MAX_CLIENTS: 服务器支持的最大并发客户端数
  • BUFFER_SIZE: 数据读写缓冲区大小
  • PORT: 服务器监听端口号

3.2 服务器 Socket 创建和地址配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
int server_init(int port) {
int server_fd;
struct sockaddr_in server_addr;

// 创建 TCP 套接字
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket 创建失败");
exit(EXIT_FAILURE);
}

// 设置 SO_REUSEADDR 选项
int opt = 1;
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == -1) {
perror("setsockopt 失败");
close(server_fd);
exit(EXIT_FAILURE);
}

// 配置服务器地址
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(port);

// 绑定地址和端口
if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("bind 失败");
close(server_fd);
exit(EXIT_FAILURE);
}

// 开始监听
if (listen(server_fd, 128) == -1) {
perror("listen 失败");
close(server_fd);
exit(EXIT_FAILURE);
}

printf("服务器启动成功,监听端口: %d\n", port);
return server_fd;
}

功能说明

  • 创建非阻塞 TCP socket(为 ET 模式准备)
  • 设置地址重用选项
  • 绑定到指定端口并开始监听

3.3 设置文件描述符为非阻塞模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int set_nonblocking(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
if (flags == -1) {
perror("fcntl F_GETFL");
return -1;
}

if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) {
perror("fcntl F_SETFL");
return -1;
}

return 0;
}

说明:在边缘触发(ET)模式下,必须使用非阻塞 I/O,以避免阻塞在某个文件描述符上。


3.4 主函数设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
int main() {
int server_fd, epoll_fd;
struct epoll_event event, events[MAX_EVENTS];
char buffer[BUFFER_SIZE];

// 初始化服务器
server_fd = server_init(PORT);

// 设置服务器 socket 为非阻塞模式
if (set_nonblocking(server_fd) == -1) {
close(server_fd);
exit(EXIT_FAILURE);
}

// 创建 epoll 实例
epoll_fd = epoll_create1(0);
if (epoll_fd == -1) {
perror("epoll_create1");
close(server_fd);
exit(EXIT_FAILURE);
}

// 添加服务器 socket 到 epoll
event.events = EPOLLIN | EPOLLET; // 边缘触发模式
event.data.fd = server_fd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event) == -1) {
perror("epoll_ctl: server_fd");
close(server_fd);
close(epoll_fd);
exit(EXIT_FAILURE);
}

printf("epoll 服务器启动成功,等待连接...\n");

while (1) {
// 等待事件发生
int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);

if (nfds == -1) {
perror("epoll_wait");
break;
}

// 处理所有就绪的事件
for (int i = 0; i < nfds; i++) {
// 服务器 socket 就绪 - 新连接
if (events[i].data.fd == server_fd) {
handle_new_connection(server_fd, epoll_fd);
}
// 可读事件
else if (events[i].events & EPOLLIN) {
handle_client_data(events[i].data.fd, epoll_fd, buffer);
}
// 错误事件
else if (events[i].events & (EPOLLERR | EPOLLHUP)) {
printf("客户端连接错误,关闭连接: %d\n", events[i].data.fd);
close_client(events[i].data.fd, epoll_fd);
}
}
}

close(server_fd);
close(epoll_fd);
return 0;
}

流程说明

  • 初始化服务器和 epoll 实例
  • 设置服务器 socket 为边缘触发模式
  • 进入主事件循环,等待 epoll 事件
  • 根据事件类型分发到相应的处理函数

3.5 处理新的客户端连接请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
void handle_new_connection(int server_fd, int epoll_fd) {
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
int client_fd;
struct epoll_event event;

// 接受所有挂起的连接(ET 模式需要这样处理)
while (1) {
client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &addr_len);

if (client_fd == -1) {
// 如果没有更多连接可接受,退出循环
if (errno == EAGAIN || errno == EWOULDBLOCK) {
break;
} else {
perror("accept");
break;
}
}

printf("新的客户端连接, socket fd: %d, IP: %s, port: %d\n",
client_fd, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

// 设置客户端 socket 为非阻塞模式
if (set_nonblocking(client_fd) == -1) {
close(client_fd);
continue;
}

// 添加客户端到 epoll 监视(边缘触发模式)
event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;
event.data.fd = client_fd;

if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event) == -1) {
perror("epoll_ctl: client_fd");
close(client_fd);
continue;
}

// 发送欢迎消息
const char *welcome_msg = "欢迎连接到 epoll 服务器!输入 'quit' 退出连接。\n";
send(client_fd, welcome_msg, strlen(welcome_msg), 0);

printf("客户端连接已添加到 epoll: %d\n", client_fd);
}
}

ET 模式特点

  • 使用 while 循环接受所有挂起的连接
  • 必须处理 EAGAIN/EWOULDBLOCK 错误
  • 确保在一次事件中处理所有可用连接

3.6 处理客户端数据通信

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
void handle_client_data(int client_fd, int epoll_fd, char buffer[]) {
ssize_t bytes_read;
int total_bytes = 0;

// ET 模式:必须读取所有可用数据
while (1) {
bytes_read = recv(client_fd, buffer + total_bytes,
BUFFER_SIZE - total_bytes - 1, 0);

if (bytes_read == -1) {
// 没有更多数据可读
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// 处理已读取的数据
if (total_bytes > 0) {
process_client_data(client_fd, epoll_fd, buffer, total_bytes);
}
break;
} else {
perror("recv");
close_client(client_fd, epoll_fd);
break;
}
}
else if (bytes_read == 0) {
// 客户端断开连接
printf("客户端断开连接: %d\n", client_fd);
close_client(client_fd, epoll_fd);
break;
}
else {
total_bytes += bytes_read;

// 如果缓冲区已满,立即处理
if (total_bytes >= BUFFER_SIZE - 1) {
process_client_data(client_fd, epoll_fd, buffer, total_bytes);
total_bytes = 0;
}
}
}
}

ET 模式数据处理

  • 使用循环读取所有可用数据
  • 必须处理缓冲区满的情况
  • 正确处理 EAGAIN/EWOULDBLOCK 错误

3.7 处理客户端数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void process_client_data(int client_fd, int epoll_fd, char buffer[], int length) {
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);

// 确保字符串以 null 结尾
buffer[length] = '\0';

// 去除换行符
buffer[strcspn(buffer, "\r\n")] = '\0';

printf("接收到客户端 %d 数据: %s\n", client_fd, buffer);

// 检查是否为退出命令
if (strncmp("quit", buffer, 4) == 0) {
printf("客户端请求断开连接: %d\n", client_fd);
getpeername(client_fd, (struct sockaddr*)&client_addr, &addr_len);
close_client(client_fd, epoll_fd);
return;
}

// 回显数据给客户端
send(client_fd, buffer, strlen(buffer), 0);
}

3.8 关闭客户端连接

1
2
3
4
5
6
7
8
9
void close_client(int client_fd, int epoll_fd) {
// 从 epoll 中移除
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, client_fd, NULL);

// 关闭连接
close(client_fd);

printf("客户端连接已关闭: %d\n", client_fd);
}

3.9 完整实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <errno.h>
#include <fcntl.h>

#define MAX_EVENTS 1024
#define MAX_CLIENTS 10000
#define BUFFER_SIZE 1024
#define PORT 8080

// 函数声明
int server_init(int port);
int set_nonblocking(int fd);
void handle_new_connection(int server_fd, int epoll_fd);
void handle_client_data(int client_fd, int epoll_fd, char buffer[]);
void process_client_data(int client_fd, int epoll_fd, char buffer[], int length);
void close_client(int client_fd, int epoll_fd);

int server_init(int port) {
int server_fd;
struct sockaddr_in server_addr;

if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket 创建失败");
exit(EXIT_FAILURE);
}

int opt = 1;
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == -1) {
perror("setsockopt 失败");
close(server_fd);
exit(EXIT_FAILURE);
}

server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(port);

if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("bind 失败");
close(server_fd);
exit(EXIT_FAILURE);
}

if (listen(server_fd, 128) == -1) {
perror("listen 失败");
close(server_fd);
exit(EXIT_FAILURE);
}

printf("服务器启动成功,监听端口: %d\n", port);
return server_fd;
}

int set_nonblocking(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
if (flags == -1) {
perror("fcntl F_GETFL");
return -1;
}

if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) {
perror("fcntl F_SETFL");
return -1;
}

return 0;
}

void handle_new_connection(int server_fd, int epoll_fd) {
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
int client_fd;
struct epoll_event event;

while (1) {
client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &addr_len);

if (client_fd == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
break;
} else {
perror("accept");
break;
}
}

printf("新的客户端连接, socket fd: %d, IP: %s, port: %d\n",
client_fd, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

if (set_nonblocking(client_fd) == -1) {
close(client_fd);
continue;
}

event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;
event.data.fd = client_fd;

if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event) == -1) {
perror("epoll_ctl: client_fd");
close(client_fd);
continue;
}

const char *welcome_msg = "欢迎连接到 epoll 服务器!输入 'quit' 退出连接。\n";
send(client_fd, welcome_msg, strlen(welcome_msg), 0);

printf("客户端连接已添加到 epoll: %d\n", client_fd);
}
}

void handle_client_data(int client_fd, int epoll_fd, char buffer[]) {
ssize_t bytes_read;
int total_bytes = 0;

while (1) {
bytes_read = recv(client_fd, buffer + total_bytes,
BUFFER_SIZE - total_bytes - 1, 0);

if (bytes_read == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
if (total_bytes > 0) {
process_client_data(client_fd, epoll_fd, buffer, total_bytes);
}
break;
} else {
perror("recv");
close_client(client_fd, epoll_fd);
break;
}
}
else if (bytes_read == 0) {
printf("客户端断开连接: %d\n", client_fd);
close_client(client_fd, epoll_fd);
break;
}
else {
total_bytes += bytes_read;

if (total_bytes >= BUFFER_SIZE - 1) {
process_client_data(client_fd, epoll_fd, buffer, total_bytes);
total_bytes = 0;
}
}
}
}

void process_client_data(int client_fd, int epoll_fd, char buffer[], int length) {
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);

buffer[length] = '\0';
buffer[strcspn(buffer, "\r\n")] = '\0';

printf("接收到客户端 %d 数据: %s\n", client_fd, buffer);

if (strncmp("quit", buffer, 4) == 0) {
printf("客户端请求断开连接: %d\n", client_fd);
getpeername(client_fd, (struct sockaddr*)&client_addr, &addr_len);
close_client(client_fd, epoll_fd);
return;
}

send(client_fd, buffer, strlen(buffer), 0);
}

void close_client(int client_fd, int epoll_fd) {
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, client_fd, NULL);
close(client_fd);
printf("客户端连接已关闭: %d\n", client_fd);
}

int main() {
int server_fd, epoll_fd; // 3, 4
struct epoll_event event, events[MAX_EVENTS];
char buffer[BUFFER_SIZE];

server_fd = server_init(PORT);

if (set_nonblocking(server_fd) == -1) {
close(server_fd);
exit(EXIT_FAILURE);
}

epoll_fd = epoll_create1(0);
if (epoll_fd == -1) {
perror("epoll_create1");
close(server_fd);
exit(EXIT_FAILURE);
}

event.events = EPOLLIN | EPOLLET;
event.data.fd = server_fd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event) == -1) {
perror("epoll_ctl: server_fd");
close(server_fd);
close(epoll_fd);
exit(EXIT_FAILURE);
}

printf("epoll 服务器启动成功,等待连接...\n");

while (1) {
int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);

if (nfds == -1) {
perror("epoll_wait");
break;
}

for (int i = 0; i < nfds; i++) {
if (events[i].data.fd == server_fd) {
handle_new_connection(server_fd, epoll_fd);
}
else if (events[i].events & EPOLLIN) {
handle_client_data(events[i].data.fd, epoll_fd, buffer);
}
else if (events[i].events & (EPOLLERR | EPOLLHUP)) {
printf("客户端连接错误,关闭连接: %d\n", events[i].data.fd);
close_client(events[i].data.fd, epoll_fd);
}
}
}

close(server_fd);
close(epoll_fd);
return 0;
}


4 服务器测试

4.1 测试客户端代码

可以使用与 poll 服务器相同的客户端代码进行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// client.c(与 poll 测试相同)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define SERVER_IP "127.0.0.1"
#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
int sock;
struct sockaddr_in server_addr;
char buffer[BUFFER_SIZE];

if ((sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket 创建失败");
exit(1);
}

memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr);

if (connect(sock, (struct sockaddr*)&server_addr,
sizeof(server_addr)) == -1) {
perror("连接服务器失败");
close(sock);
exit(1);
}

printf("已连接到 epoll 服务器 %s:%d\n", SERVER_IP, PORT);

memset(buffer, 0, BUFFER_SIZE);
recv(sock, buffer, BUFFER_SIZE - 1, 0);
printf("%s", buffer);

while (1) {
printf("请输入消息: ");
fgets(buffer, BUFFER_SIZE, stdin);

send(sock, buffer, strlen(buffer), 0);

if (strncmp(buffer, "quit", 4) == 0) {
break;
}

memset(buffer, 0, BUFFER_SIZE);
ssize_t bytes_received = recv(sock, buffer, BUFFER_SIZE - 1, 0);
if (bytes_received > 0) {
buffer[bytes_received] = '\0';
printf("服务器回复: %s\n", buffer);
}
}

close(sock);
printf("连接已关闭\n");

return 0;
}

4.2 运行测试

编译和运行

1
2
3
4
5
6
7
8
9
# 编译服务器和客户端
gcc -o epoll_server epoll_server.c
gcc -o client client.c

# 运行服务器
./epoll_server

# 在多个终端运行客户端
./client

测试结果

服务端输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
marisa@marisa-virtual-machine:~/epoll_test$ ./epoll
服务器启动成功,监听端口: 8080
epoll 服务器启动成功,等待连接...
新的客户端连接, socket fd: 5, IP: 127.0.0.1, port: 47752
客户端连接已添加到 epoll: 5
新的客户端连接, socket fd: 6, IP: 127.0.0.1, port: 38968
客户端连接已添加到 epoll: 6
接收到客户端 6 数据: Hello, this is Marisa.
接收到客户端 5 数据: Hi, my name is Alice
接收到客户端 5 数据: Bye~
客户端断开连接: 5
客户端连接已关闭: 5
接收到客户端 6 数据: I love Gensokyo!
客户端断开连接: 6
客户端连接已关闭: 6
^C

客户端输出

1
2
3
4
5
6
7
8
9
marisa@marisa-virtual-machine:~/epoll_test$ ./client 
已连接到服务器 127.0.0.1:8080
欢迎连接到 epoll 服务器!输入 'quit' 退出连接。
请输入消息: Hello, this is Marisa.
服务器回复: Hello, this is Marisa.
请输入消息: I love Gensokyo!
服务器回复: I love Gensokyo!
请输入消息: quit
连接已关闭
1
2
3
4
5
6
7
8
9
marisa@marisa-virtual-machine:~/epoll_test$ ./client
已连接到服务器 127.0.0.1:8080
欢迎连接到 epoll 服务器!输入 'quit' 退出连接。
请输入消息: Hi, my name is Alice
服务器回复: Hi, my name is Alice
请输入消息: Bye~
服务器回复: Bye~
请输入消息: quit
连接已关闭

5 总结

epoll 是 Linux 下高性能网络编程的核心技术,相比传统的 select/poll 具有显著优势:

核心优势总结:

  1. 高性能事件通知:基于回调机制,时间复杂度 O(1)
  2. 高可扩展性:支持数十万并发连接
  3. 内存效率:内核维护事件表,减少内存拷贝
  4. 灵活触发模式:支持水平触发和边缘触发

适用场景:

  • select:连接数少(< 1024),兼容性要求高
  • poll:连接数中等,需要突破 1024 限制
  • epoll:高并发场景,性能要求极致

实际应用:

epoll 是现代高性能网络服务器的首选方案,被广泛应用于:

  • Nginx:高性能 Web 服务器
  • Redis:内存数据库
  • 各种实时通信系统和游戏服务器

「Linux 网络编程」基于 epoll 的 I/O 复用服务器
https://marisamagic.github.io/2025/10/27/20251027/
作者
MarisaMagic
发布于
2025年10月27日
许可协议