前言
epoll模式涉及到系统底层的I/O多路复用机制,可以处理高并发的场景。本文使用开源的libuv库以及原生的scoket来分享epoll的运作机制,方便更加深入的理解网络编程。
libuv库实现epoll
这是一个C库,之所以先分享libuv,是因为它更像QT的信号-槽机制(适合对网络编程编程不熟,但有希望深入理解epoll功能的QT开发读者阅读),并且做了一定的封装,比原生socket容易理解。
下载源码
gitecode的libuv库
https://gitcode.com/gh_mirrors/li/libuv?source_module=search_result_repo
源码编译
cd libuv
sh autogen.sh # 生成必要的脚本文件
./configure # 配置你的系统环境,可以选择性地添加一些参数,例如--prefix=/usr/local来指定安装目录
make # 编译库
sudo make install # 安装库到系统目录(安装之后,Linux在/usr/local/lib/可看到libuv.so 和 libuv.a)
gcc编译的时候需要加上-luv引用即可。
功能讲解
以下按照功能的引用顺序提供一个表格,方便直观的了解工作流程。
| libuv函数 | 函数分类 | 功能描述 | 用途说明 |
|---|---|---|---|
uv_default_loop() |
事件循环 | 获取默认的事件循环实例 | 创建并返回libuv的默认事件循环,用于管理所有异步I/O操作 |
uv_tcp_init() |
TCP通信 | 初始化TCP句柄 | 创建服务器端socket并初始化为非阻塞模式,准备进行网络通信 |
uv_tcp_bind() |
TCP通信 | 绑定TCP服务器到指定地址和端口 | 将TCP socket与特定的网络接口和端口号关联 |
uv_listen() |
TCP通信 | 开始监听连接请求 | 设置TCP socket为监听状态,并指定最大连接队列长度 |
uv_accept() |
TCP通信 | 接受客户端连接 | 从连接队列中取出已建立的连接,创建客户端socket |
uv_read_start() |
数据读写 | 开始异步读取数据 | 注册读取事件到事件循环,当有数据可读时触发回调 |
uv_write() |
数据读写 | 异步发送数据 | 将数据写入socket,非阻塞方式,通过回调通知完成状态 |
uv_close() |
资源管理 | 关闭句柄 | 安全关闭连接或定时器等资源,并在关闭后调用回调函数 |
uv_tcp_getpeername() |
TCP通信 | 获取对端地址信息 | 获取已连接客户端的IP地址和端口信息 |
uv_timer_init() |
定时器 | 初始化定时器句柄 | 创建定时器,用于在指定时间间隔执行回调函数 |
uv_timer_start() |
定时器 | 启动定时器 | 设置定时器的首次延迟、重复间隔和回调函数 |
uv_run() |
事件循环 | 运行事件循环 | 启动I/O多路复用,阻塞等待事件发生并处理回调 |
可以看到,libuv还是保留了原始scoket服务器端的典型流程:
socket->bind->listen->accept->read->write->close
libuv库通过以下几步机制,封装引用了epoll及I/O多路复用机制:
(1)在开头引用uv_default_loop(),在结尾引用uv_run(),提供了封装的epoll事件循环;
(2)侦听客户端时,使用uv_listen()进入监听状态但不会阻塞主线程,事件循环通过uv_run持续运行并处理各种I/O事件,当新的客户端连接请求到达时,libuv会在事件循环中触发预先注册的回调函数on_new_connection;
(3)uv_accept并非传统的阻塞式accept,而是处理已经被操作系统接受并放入完成队列的连接,整个过程都是在事件驱动的异步框架下完成的;
(4)使用uv_read_start()注册读取参照到事件循环中,监听可读事件,通过调用回调函数on_read()方式实现了异步读取数据的方法;
(5)使用uv_close()进行资源清理,使用回调函数on_client_closed释放需要释放的资源(比如业务功能涉及的内存资源)。
以上的uv_listen()、uv_read_start()、uv_close()都由回调函数进行响应,这个机制与QT的信号和槽机制非常像,加上事件循环的机制,就更加像了。
服务端源码
//uv_epollserver.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <uv.h>
#define PORT 8080 //服务器监听端口号
#define MAX_CLIENTS 10000 //最大支持的客户端连接数
#define BUFFER_SIZE 1024 //数据缓冲区大小(字节)
typedef struct {
uv_tcp_t handle; //libuv TCP句柄,用于管理TCP连接
struct sockaddr_in addr; //客户端网络地址信息
char buffer[BUFFER_SIZE]; //数据接收缓冲区
int client_id; //客户端唯一标识符
time_t connect_time; //客户端连接时间戳
} client_t;
uv_loop_t *loop; //libuv事件循环实例
client_t *clients[MAX_CLIENTS]; //客户端指针数组
int client_count = 0; //当前连接客户端计数
//分配客户端ID
int allocate_client_id() {
for (int i = 0; i < MAX_CLIENTS; i++) {
if (clients[i] == NULL) { //查找空闲槽位
return i; //返回可用的客户端ID
}
}
return -1; //无可用ID,返回错误码
}
//释放客户端资源
void free_client(client_t *client) {
for (int i = 0; i < MAX_CLIENTS; i++) {
if (clients[i] == client) { //定位客户端在数组中的位置
clients[i] = NULL; //清空数组对应位置
client_count--; //减少客户端计数
break;
}
}
free(client); //释放客户端结构体内存
}
//数据发送回调
void on_write_end(uv_write_t *req, int status) {
if (status) { //检查发送是否出错
fprintf(stderr, "Write error: %s\n", uv_strerror(status));
}
free(req); //释放写请求结构体内存
}
//读取客户端数据
void on_read(uv_stream_t *stream, ssize_t nread, const uv_buf_t *buf) {
client_t *client = (client_t*)stream;//转换类型
//
char client_ip[INET_ADDRSTRLEN];
const char *ip_str = "Unknown";
if (client != NULL) {
uv_ip4_name(&client->addr, client_ip, sizeof(client_ip));
ip_str = client_ip;
}
if (nread > 0) {//成功读取到数据
//处理接收到的数据
buf->base[nread] = '\0';//添加字符串终止符
printf("Received from client: %s\n", buf->base);
//回显数据给客户端
uv_write_t *req = (uv_write_t*)malloc(sizeof(uv_write_t));//分配写请求内存
uv_buf_t wrbuf = uv_buf_init(buf->base, nread);//初始化写缓冲区
uv_write(req, stream, &wrbuf, 1, on_write_end);//异步发送数据
} else if (nread < 0) {//读取发生错误
if (nread != UV_EOF) {//非正常断开连接
fprintf(stderr, "Read error: %s\n", uv_strerror(nread));
}else {//客户端正常断开连接
printf("[%s] Client disconnected\n", ip_str);
}
uv_close((uv_handle_t*)stream, NULL);//关闭连接句柄
}
free(buf->base);//释放读取缓冲区内存
}
//分配读取缓冲区
void alloc_buffer(uv_handle_t *handle, size_t suggested_size, uv_buf_t *buf) {
buf->base = (char*)malloc(suggested_size);//动态分配内存
buf->len = suggested_size;//设置缓冲区长度
}
//客户端关闭回调
void on_client_closed(uv_handle_t *handle) {
client_t *client = (client_t*)handle;
free_client(client);//调用资源释放函数
}
//新连接处理
void on_new_connection(uv_stream_t *server, int status) {
if (status < 0) {//检查连接状态是否异常
fprintf(stderr, "New connection error: %s\n", uv_strerror(status));
return;
}
//分配客户端结构体内存
client_t *client = (client_t*)malloc(sizeof(client_t));
int client_id = allocate_client_id();//获取客户端ID
if (client_id == -1) {//为-1时,表示已经达到最大连接数
fprintf(stderr, "Too many clients\n");
free(client);//释放已分配的内存
return;
}
clients[client_id] = client;//将客户端指针存入数组
client_count++;//增加客户端计数
//初始化TCP句柄-创建客户端socket
uv_tcp_init(loop, &client->handle);
//非阻塞方式建立客户端连接
if (uv_accept(server, (uv_stream_t*)&client->handle) == 0) {
//获取客户端地址
int addr_len = sizeof(client->addr);
uv_tcp_getpeername(&client->handle, (struct sockaddr*)&client->addr, &addr_len);
client->connect_time = time(NULL);//记录连接建立时间
client->client_id = client_id;//记录客户端ID
char client_ip[INET_ADDRSTRLEN];
uv_ip4_name(&client->addr, client_ip, sizeof(client_ip));
printf("New client connected: ID=%d, IP=%s, Total clients: %d\n",
client_id, client_ip, client_count);
//开始异步读取数据--多路复用注册
uv_read_start((uv_stream_t*)&client->handle, alloc_buffer, on_read);
} else {//接受连接失败
//资源清理,使用回调函数on_client_closed
uv_close((uv_handle_t*)&client->handle, on_client_closed);
}
}
//定时器回调 - 定期统计
void on_timer(uv_timer_t *handle) {
printf("Server status - Connected clients: %d\n", client_count);
printf("Memory usage: %ld KB\n", (long)(client_count * sizeof(client_t) / 1024));
printf("========================\n");
}
int main() {
loop = uv_default_loop();//获取默认事件循环,在系统底层关联上I/O多路复用器
//创建TCP服务器
uv_tcp_t server;
//初始化TCP句柄-创建服务器端socket
uv_tcp_init(loop, &server);
//绑定服务器地址和端口
struct sockaddr_in addr;
uv_ip4_addr("0.0.0.0", PORT, &addr);
uv_tcp_bind(&server, (const struct sockaddr*)&addr, 0);//绑定到指定地址
//listen创建连接请求队列,队列长度为SOMAXCONN(通常128),同时将服务器socket注册到I/O多路复用器(epoll/IOCP等)
//当客户端发起连接时,已完成三次握手的连接会放入这个队列,然后由I/O多路复用器激活调用on_new_connection
int r = uv_listen((uv_stream_t*)&server, SOMAXCONN, on_new_connection);
if (r) {//监听失败处理
fprintf(stderr, "Listen error: %s\n", uv_strerror(r));
return 1;
}
printf("Server listening on port %d\n", PORT);
printf("Maximum clients: %d\n", MAX_CLIENTS);
//初始化客户端数组
memset(clients, 0, sizeof(clients));
//启动统计定时器--显示全局信息
uv_timer_t timer;
uv_timer_init(loop, &timer);
uv_timer_start(&timer, on_timer, 0, 5000);
//启动事件循环,调用epoll_wait等函数阻塞等待
return uv_run(loop, UV_RUN_DEFAULT);
}
编译方法:gcc uv_epollserver.c -oserver
客户端源码
这是配套测试的,不详细讲解了
//uv_epollclient.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <uv.h>
#define PORT 8080 //服务器监听端口号
#define MAX_CLIENTS 10000 //最大支持的客户端连接数
#define BUFFER_SIZE 1024 //数据缓冲区大小(字节)
typedef struct {
uv_tcp_t handle; //libuv TCP句柄,用于管理TCP连接
struct sockaddr_in addr; //客户端网络地址信息
char buffer[BUFFER_SIZE]; //数据接收缓冲区
int client_id; //客户端唯一标识符
time_t connect_time; //客户端连接时间戳
} client_t;
uv_loop_t *loop; //libuv事件循环实例
client_t *clients[MAX_CLIENTS]; //客户端指针数组
int client_count = 0; //当前连接客户端计数
//分配客户端ID
int allocate_client_id() {
for (int i = 0; i < MAX_CLIENTS; i++) {
if (clients[i] == NULL) { //查找空闲槽位
return i; //返回可用的客户端ID
}
}
return -1; //无可用ID,返回错误码
}
//释放客户端资源
void free_client(client_t *client) {
for (int i = 0; i < MAX_CLIENTS; i++) {
if (clients[i] == client) { //定位客户端在数组中的位置
clients[i] = NULL; //清空数组对应位置
client_count--; //减少客户端计数
break;
}
}
free(client); //释放客户端结构体内存
}
//数据发送回调
void on_write_end(uv_write_t *req, int status) {
if (status) { //检查发送是否出错
fprintf(stderr, "Write error: %s\n", uv_strerror(status));
}
free(req); //释放写请求结构体内存
}
//读取客户端数据
void on_read(uv_stream_t *stream, ssize_t nread, const uv_buf_t *buf) {
client_t *client = (client_t*)stream;//转换类型
//
char client_ip[INET_ADDRSTRLEN];
const char *ip_str = "Unknown";
if (client != NULL) {
uv_ip4_name(&client->addr, client_ip, sizeof(client_ip));
ip_str = client_ip;
}
if (nread > 0) {//成功读取到数据
//处理接收到的数据
buf->base[nread] = '\0';//添加字符串终止符
printf("Received from client: %s\n", buf->base);
//回显数据给客户端
uv_write_t *req = (uv_write_t*)malloc(sizeof(uv_write_t));//分配写请求内存
uv_buf_t wrbuf = uv_buf_init(buf->base, nread);//初始化写缓冲区
uv_write(req, stream, &wrbuf, 1, on_write_end);//异步发送数据
} else if (nread < 0) {//读取发生错误
if (nread != UV_EOF) {//非正常断开连接
fprintf(stderr, "Read error: %s\n", uv_strerror(nread));
}else {//客户端正常断开连接
printf("[%s] Client disconnected\n", ip_str);
}
uv_close((uv_handle_t*)stream, NULL);//关闭连接句柄
}
free(buf->base);//释放读取缓冲区内存
}
//分配读取缓冲区
void alloc_buffer(uv_handle_t *handle, size_t suggested_size, uv_buf_t *buf) {
buf->base = (char*)malloc(suggested_size);//动态分配内存
buf->len = suggested_size;//设置缓冲区长度
}
//客户端关闭回调
void on_client_closed(uv_handle_t *handle) {
client_t *client = (client_t*)handle;
free_client(client);//调用资源释放函数
}
//新连接处理
void on_new_connection(uv_stream_t *server, int status) {
if (status < 0) {//检查连接状态是否异常
fprintf(stderr, "New connection error: %s\n", uv_strerror(status));
return;
}
//分配客户端结构体内存
client_t *client = (client_t*)malloc(sizeof(client_t));
int client_id = allocate_client_id();//获取客户端ID
if (client_id == -1) {//为-1时,表示已经达到最大连接数
fprintf(stderr, "Too many clients\n");
free(client);//释放已分配的内存
return;
}
clients[client_id] = client;//将客户端指针存入数组
client_count++;//增加客户端计数
//初始化TCP句柄-创建客户端socket
uv_tcp_init(loop, &client->handle);
//非阻塞方式建立客户端连接
if (uv_accept(server, (uv_stream_t*)&client->handle) == 0) {
//获取客户端地址
int addr_len = sizeof(client->addr);
uv_tcp_getpeername(&client->handle, (struct sockaddr*)&client->addr, &addr_len);
client->connect_time = time(NULL);//记录连接建立时间
client->client_id = client_id;//记录客户端ID
char client_ip[INET_ADDRSTRLEN];
uv_ip4_name(&client->addr, client_ip, sizeof(client_ip));
printf("New client connected: ID=%d, IP=%s, Total clients: %d\n",
client_id, client_ip, client_count);
//开始异步读取数据--多路复用注册
uv_read_start((uv_stream_t*)&client->handle, alloc_buffer, on_read);
} else {//接受连接失败
//资源清理,使用回调函数on_client_closed
uv_close((uv_handle_t*)&client->handle, on_client_closed);
}
}
//定时器回调 - 定期统计
void on_timer(uv_timer_t *handle) {
printf("Server status - Connected clients: %d\n", client_count);
printf("Memory usage: %ld KB\n", (long)(client_count * sizeof(client_t) / 1024));
printf("========================\n");
}
int main() {
loop = uv_default_loop();//获取默认事件循环,在系统底层关联上I/O多路复用器
//创建TCP服务器
uv_tcp_t server;
//初始化TCP句柄-创建服务器端socket
uv_tcp_init(loop, &server);
//绑定服务器地址和端口
struct sockaddr_in addr;
uv_ip4_addr("0.0.0.0", PORT, &addr);
uv_tcp_bind(&server, (const struct sockaddr*)&addr, 0);//绑定到指定地址
//listen创建连接请求队列,队列长度为SOMAXCONN(通常128),同时将服务器socket注册到I/O多路复用器(epoll/IOCP等)
//当客户端发起连接时,已完成三次握手的连接会放入这个队列,然后由I/O多路复用器激活调用on_new_connection
int r = uv_listen((uv_stream_t*)&server, SOMAXCONN, on_new_connection);
if (r) {//监听失败处理
fprintf(stderr, "Listen error: %s\n", uv_strerror(r));
return 1;
}
printf("Server listening on port %d\n", PORT);
printf("Maximum clients: %d\n", MAX_CLIENTS);
//初始化客户端数组
memset(clients, 0, sizeof(clients));
//启动统计定时器--显示全局信息
uv_timer_t timer;
uv_timer_init(loop, &timer);
uv_timer_start(&timer, on_timer, 0, 5000);
//启动事件循环,调用epoll_wait等函数阻塞等待
return uv_run(loop, UV_RUN_DEFAULT);
}
原生socket实现epoll
不需要额外下载开发包。
功能讲解
以下按照功能的引用顺序提供一个表格,方便直观的了解工作流程。
| 函数 | 函数分类 | 功能描述 | 用途说明 | 对应libuv函数 |
|---|---|---|---|---|
socket() |
套接字创建 | 创建套接字描述符 | 创建TCP通信端点,指定协议族和类型 | uv_tcp_init() |
bind() |
地址绑定 | 绑定套接字地址 | 将服务器socket与特定IP和端口关联 | uv_tcp_bind() |
listen() |
连接监听 | 监听连接请求 | 设置socket为监听状态,指定连接队列长度 | uv_listen() |
fcntl() |
模式设置 | 设置属性 | 将服务器端ocket设置为非阻塞模式 | libuv自动处理非阻塞 |
epoll_create1() |
多路复用 | 创建epoll实例 | 创建事件多路分离器,用于监控多个文件描述符 | uv_default_loop()内部封装 |
epoll_ctl() |
多路复用 | 控制epoll监控列表 | 添加、修改或移除被监控的文件描述符 | libuv事件循环内部管理 |
epoll_wait() |
多路复用 | 等待事件发生 | 阻塞等待被监控的文件描述符上事件发生 | uv_run()内部封装 |
accept() |
连接管理 | 接受客户端连接 | 从连接队列中取出已建立的连接,创建客户端socket | uv_accept() |
fcntl() |
模式设置 | 设置属性 | 将客户端socket设置为非阻塞模式 | libuv自动处理非阻塞 |
read()/recv() |
数据读写 | 从套接字读取数据 | 从客户端socket接收数据 | uv_read_start()回调机制 |
write()/send() |
数据读写 | 向套接字写入数据 | 向客户端socket发送数据 | uv_write() |
close() |
资源管理 | 关闭文件描述符 | 释放socket资源,终止连接 | uv_close() |
从以上流程中可看到,在原始的socket->bind->listen->accept->read->write->close流程中,在listen()侦听之后,加入了fcntl()->epoll_create1()->epoll_ctl()->epoll_wait()流程,此流程是引入非阻塞的服务端接收连接机制,以及将服务端文件描述符添加到epoll实例中进行监控,这样当socket缓存区中有数据时,会触发epoll_wait()通知事件。
另外,在accept连接上客户端之后,需要将客户端socket设置为非阻塞模式,才能达到异步的效果。
服务端源码
以下代码中有详细的注释
//src_scoketepoll.c
#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 <fcntl.h>
#include <errno.h>
#include <stdio.h>
#define MAX_EVENTS 1024
#define BUFFER_SIZE 4096 //数据缓冲区大小(字节)
#define SERVER_PORT 8080 //服务器监听端口号
#define PRINTF_ERR_MSG(format, ...) fprintf(stderr, "[ERROR]<%s:%d>:" format "\n", __FILE__, __LINE__, ##__VA_ARGS__)
#define SCREEN_PRINTF(format,args...) printf("%s-%s-%d:" format "\n",__FILE__,__FUNCTION__,__LINE__,##args)
// 设置文件描述符为非阻塞模式
static int set_nonblocking(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
if (flags == -1) return -1;
return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}
int main() {
int server_fd;
// 创建服务器socket
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
// 设置socket选项,允许端口重用
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 绑定服务器地址
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
server_addr.sin_addr.s_addr = INADDR_ANY;//所有可用的网络接口
//server_addr.sin_addr.s_addr = inet_addr("192.168.1.123");//固定IP
//绑定服务端地址
if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("bind failed");
close(server_fd);
exit(EXIT_FAILURE);
}
// 开始监听,listen创建连接请求队列,队列长度为SOMAXCONN(通常128)
//当客户端发起连接时,已完成三次握手的连接会放入这个队列
if (listen(server_fd, SOMAXCONN) == -1) {
perror("listen failed");
close(server_fd);
exit(EXIT_FAILURE);
}
// 设置服务器socket为非阻塞
if (set_nonblocking(server_fd) == -1) {
perror("set_nonblocking server_fd failed");
close(server_fd);
exit(EXIT_FAILURE);
}
SCREEN_PRINTF("Epoll server started on port %d\n", SERVER_PORT);
struct epoll_event event, events[MAX_EVENTS];
// 创建epoll实例
int epoll_fd= epoll_create1(0);//老方法是int epoll_fd = epoll_create(1)
if (epoll_fd == -1) {
perror("epoll_create1 failed");
close(server_fd);
exit(EXIT_FAILURE);
}
//EPOLLIN默认是水平方式读事件,只要socket缓存区中有数据,就会一直触发epoll_wait通知(下文while循环中)
//EPOLLIN | EPOLLET边缘触发读事件,socket缓存区由空->非空时,只触发一次epoll_wait通知(下文while循环中)
event.events = EPOLLIN | EPOLLET; //边缘触发的可读事件
event.data.fd = server_fd;
//epoll_ctl 将服务端文件描述符添加到epoll实例中进行监控
//EPOLL_CTL_ADD - 添加新的文件描述符到监控列表
//EPOLL_CTL_MOD - 修改已监控文件描述符的事件设置
//EPOLL_CTL_DEL - 从监控列表中移除文件描述符
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event) == -1) {
perror("epoll_ctl add server_fd failed");
close(server_fd);
close(epoll_fd);
exit(EXIT_FAILURE);
}
// 主事件循环
while (1) {
//如果成功,nfds接收返回的事件个数,把就绪的事件放在events数组中
int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
if (nfds == -1) {
perror("epoll_wait failed");
break;
}
//事件处理
for (int i = 0; i < nfds; i++) {
// 处理新连接
if (events[i].data.fd == server_fd) {//处理服务端描述符事件
SCREEN_PRINTF("接收到epoll_wait推送的服务端链接事件\n");
while (1) {
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
//从已完成连接的队列里面,获取一个客户端信息,生成一个新的文件描述符,这是与客户端通信的文件描述符
int client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_len);
if (client_fd == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
break; // 已处理所有待处理连接
} else {
perror("accept failed");
break;
}
}
// 设置客户端socket为非阻塞
if (set_nonblocking(client_fd) == -1) {
perror("set_nonblocking failed");
close(client_fd);
continue;
}
SCREEN_PRINTF("[%s:%d] 客户端连接成功 client_fd=%d \n",\
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port), client_fd);
// 添加客户端socket到epoll监控
//EPOLLIN:当客户端发送数据到服务器时触发
//EPOLLET:只在socket缓冲区从空变为非空时通知一次
//EPOLLRDHUP:当客户端断开连接时触发
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 add client_fd failed");
close(client_fd);
}
}
}
// 处理客户端数据
else {
//SCREEN_PRINTF("接收到epoll_wait推送的客户端交互事件\n");
int client_fd = events[i].data.fd;
// 检查连接是否关闭
//EPOLLRDHUP:当客户端断开连接时触发
//EPOLLHUP:当客户端强制终止连接时
if (events[i].events & (EPOLLRDHUP | EPOLLHUP)) {
printf("Client disconnected (fd: %d)\n", client_fd);
close(client_fd);
continue;
}
// 处理可读事件
if (events[i].events & EPOLLIN) {
char buffer[BUFFER_SIZE];
ssize_t bytes_read;
int total_bytes = 0;
// 读取客户端数据--读取socket缓存区中的数据
while (1) {
//可以用bytes_read = recv(client_fd, buffer, BUFFER_SIZE - 1, 0),多了一个参数
bytes_read = read(client_fd, buffer, BUFFER_SIZE - 1);
if (bytes_read > 0) {
buffer[bytes_read] = '\0';
total_bytes += bytes_read;
SCREEN_PRINTF("Received from client %d: %s", client_fd, buffer);
// 回显数据给客户端--测试发送是否成功
if (write(client_fd, buffer, bytes_read) != bytes_read) {
perror("write failed");
break;
}
}
if (bytes_read == 0) {
SCREEN_PRINTF("Client disconnected (fd: %d)", client_fd);
//epoll_ctl(epoll_fd, EPOLL_CTL_DEL, client_fd, NULL);// 从epoll监控中移除client_fd
//当调用 close(fd) 时,内核会自动将fd文件描述符从所有epoll实例中移除,以上代码不需要显示EPOLL_CTL_DEL,只是展示有这个动作
close(client_fd);
}
else {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// 没有更多数据可读,这是正常情况
if (total_bytes > 0) {
printf("Finished reading from client %d, total: %d bytes\n",
client_fd, total_bytes);
} else {
perror("read failed");
close(client_fd);
}
break;
}
}
}
}
}
}
}
close(server_fd);
close(epoll_fd);
return 0;
}
使用gcc src_socketepoll.c -oserver编译即可。
客户端可以使用libuv中的客户端源码来测试。
篇尾
以上的epoll服务端可以处理万级以上的高并发需求场景,本篇也是进程间通信(IPC)-socket内容的补充。