本文将介绍一个基于 epoll 的封装库(仓库链接:https://github.com/12379biu/epoll_manager.git),此封装旨在简化 epoll 监听文件描述符的操作流程。通过封装,将 epoll 的底层细节与业务逻辑解耦,更专注于文件(或 socket 等)的实际操作。
以下代码将先介绍 epoll 的核心特性,再结合这个封装库的使用示例(监听标准输入、实现多人复用服务器),展示如何通过封装快速实现 epoll 功能。
阅读本文需要一定的 Linux IO 编程和网络编程基础。
本章节以监听标准输入流为例,下一章以io复用多人服务器为例子介绍,之后会介绍源码。
目录
epoll介绍
struct epoll_event
struct epoll_event是epoll的核心--------既可以作为事件注册的容器,也可以以数组的形式作为储存事件的容器。
成员events储存着事件的性质与触发方式。成员data是一个 联合体,内部有一个fd句柄和一个万能指针 void* ptr,这意味着我们可以传入任意类型的数据类型,注意,data是联合体,故而ptr和fd最好只使用其中一个,修改其中一个数据必然会破环另一个数据的结构,只监控文件建议使用fd,如果要传递其他数据则使用ptr。
struct epoll_event {
__uint32_t events; // 事件类型
epoll_data_t data; // 用户数据
};
typedef union epoll_data {
void *ptr; // 用户自定义指针
int fd; // 文件描述符
__uint32_t u32; // 用户自定义 32 位数据
__uint64_t u64; // 用户自定义 64 位数据
} epoll_data_t;
接下来,我会先介绍这个封装库提供的 API 函数,再通过两个实例(监听标准输入、实现多人复用网络服务器),展示如何利用此封装快速实现 epoll 监听逻辑,减少重复的底层调用,让开发者更专注于文件事件与数据本身的操作。
API介绍
// 客户端信息
typedef struct{
//此成员由用户自定义,这里作为网络ip地址结构体使用,修改不影响文件操作
struct sockaddr_in address;//存放网络ip,分配端口
int fd;//<----存放监听文件的句柄
int epoll_fd;//<-----存放监听者epoll的句柄
}epoll_info;
//epoll封装的类
class EpollManager
{
private:
int epoll_fd; //epoll注册的fd句柄
plist_t list; //储存信息索引的链表
epoll_info *sock_ptr; //专门为网络套接字准备的指向epoll_event.data.ptr的指针
public:
//创建链表与epoll组件
EpollManager(){
list = list_create();
epoll_fd = epoll_create1(0);
}
//获取epoll组件句柄
int get_epoll_fd(){return epoll_fd;}
//注册文件信息
void info_register(epoll_info *pclient_info,bool is_sockt = false);
//变参回调函数,由用户自定义数据处理的方案
template<class F,class... Args>
void Epoll_EventHandle(epoll_info *pclient_info,std::string &buf,
F &&f,Args&&... args);
//释放索引链表与套接字的资源
~EpollManager(){
if (sock_ptr)
delete sock_ptr;
list_delete(list);
}
};
在这里的结构体epoll_info作为信息体贮存于万能指针 epoll_event.data.ptr 中,epoll_event.data.fd被舍弃,故而fd信息都存储在epoll_info.fd中。struct sockaddr_in address是用户自定义结构体,可以任意修改,这里作为网络地址结构体储存网络信息。事实上,除了fd,和epoll_fd会在内部用到以外,其他所有的结构体都是可以自定义的。
// 客户端信息
typedef struct{
//此成员由用户自定义,这里作为网络ip地址结构体使用,修改不影响文件操作
struct sockaddr_in address;//存放网络ip,分配端口
int fd;//<----存放监听文件的句柄
int epoll_fd;//<-----存放监听者epoll的句柄
}epoll_info;
在构造函数中创建epoll与信息索引链表list。list作为底层调用处理信息,并不出现于应用层,使用无需了解它,我会在后面的源码解析中介绍它。
void info_register(epoll_info *pclient_info,bool is_sockt = false);将数据信息注册到epoll,第一个参数为epoll_info*,数据由此结构体传递到epoll,第二个参数只有当监听数文件是网络服务器套接字时需要填true,其他时候不需要填。
接下来的API函数我会通过监听一个标准输入的简单例子介绍他们。
文件监听例子
#include "epoll_manager.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string>
/*自定义信息处理函数---------------------------------------------------------*/
void fun_cb(epoll_info* info, std::string& buf)
{
std::cout << info->fd << ":" << buf << std::endl;
}
/*-------------------------------------------------------------------------*/
int main(int argc,char** argv)
{
system("clear");//清理一下端口
std::string buf(1024,0);//初始化缓冲区,注意需要给定长度
int nfds = 0;
EpollManager epoll1;//创建epoll
struct epoll_event events[3];//epoll事件容器
epoll_info temp_info = {0};//用于储存中间信息的容器
temp_info.epoll_fd = epoll1.get_epoll_fd();//获取epoll句柄
temp_info.fd = 0;//监听标准输入流
epoll1.info_register(&temp_info);//录入客户端文件信息并注册到epoll
std::cout << "正在监听标准输入" << std::endl;
while(1)
{
nfds = epoll_wait(epoll1.get_epoll_fd(),events,3,-1);//监听的事件数量
for (int i = 0;i < nfds;i ++)
{
//数据处理,参数(epoll_info*,string,fun,...[任意可变参])
epoll1.Epoll_EventHandle(static_cast<epoll_info*>(events[i].data.ptr),buf,fun_cb);
}
}
return 0;
}
在这个例子中,创建epoll组件,事件容器events[3]:一次能处理3个事件,信息结构体:temp_info ,作为参数传给API使用。
首先,注册事件到epoll,要监听的文件temp_info.fd = 0,标准输入流文件句柄为0,输出1,标准错误输出2。info_register会将事件注册为边沿触发,输入事件。并为epoll_event.data.ptr开辟一个epoll_info大小的空间。定义缓冲区,注意,缓冲区buf必须初始化明确长度,这里是1024。
当我们输入从屏幕输入数据时,epoll_wait()取消阻塞,调用
void Epoll_EventHandle(epoll_info *pclient_info,
std::string &buf, F &&f,Args&&... args);
第一个参数,消息体的指针,而events[i].data,ptr本身就是我们开辟的消息体,将其强转为(epoll_info*)类型的指针即可,参数2:buf,缓冲区,参数3:由你自定义的函数fun,在这个函数中,由你自己决定要如何去处理数据,参数4,是你传递给你的自定义函数fun的参数,数量是可变的,可以是任意参数,在上面的例子中没有传参。
自定义函数的第一个参数必须是信息体epoll_info *,第二个参数是std::tring& ,后面的是自定义参数,由用户自己决定。在这里,我们直接将监听的文件句柄和输入的信息输出,来看结果。
/*自定义信息处理函数---------------------------------------------------------*/
void fun_cb(epoll_info* info, std::string& buf)
{
std::cout << info->fd << ":" << buf << std::endl;
}

输入与输出一致,文件句柄0,当标准输入按下ctrl + d,会使文件读取到0个数据,触发结束信号,回收,回收文件资源。ctrl + c结束进程。
如此,便让epoll专注于监听文件本身,简化使用流程,将数据处理分离出来。下一章使用以此封装实现io复用多人网络服务器。如果对你有帮助的话就点个赞吧。
1838

被折叠的 条评论
为什么被折叠?



