基于epoll的io复用管理,一种文件监听方案 2 - 教程
(仓库链接:https://github.com/12379biu/epoll_manager.git)
目录
此封装旨在将epoll从事件处理中解耦出来,更专注与对文件本身的监听,仅作为事件唤醒的手段,将事件处理与之解耦。
上一篇介绍了此封装API函数的使用,以及监听标准输入流的例子。这一篇接着介绍epoll组件的特性,以及此封装源码。并使用它创建一个多人服务器。
注册epoll
epoll的一些性质
epoll的主要函数epoll_create()创建epoll,epoll_ctl()注册/修改/注销,epoll_wait()等待事件
struct epoll_event epoll_e;
epoll_fd = epoll_create1(0);
epoll_e.events = EPOLLIN | EPOLLET;//边缘触发,输入事件
epoll_e.data.ptr = static_cast(p);//假设指针p中存放有文件句柄和其他信息
/*注册epoll成员*/
temp_fd = epoll_ctl(epoll_fd,EPOLL_CTL_ADD,p->fd,&epoll_e);
通过epoll_ctl,参数epoll_fd:创建的epoll句柄,EPOLL_CTL_ADD:行为(注册监听文件),
p->fd:要监听的文件句柄,&epoll_e:存放属性与内容信息的容器(一次性用品,注册完被linux内核记录,就可以销毁epoll_e了)将要监听的属性:events注册到epoll;剩下的epoll成员,无论是data.ptr还是data.fd都属于内容,epoll机制不关心内容是什么,因此无论epoll_.data存什么都不会干扰到它的唤醒机制,排序机制,这给了我们极大的操作空间,我们可以在epoll_event.data.ptr中存储任意我们希望存储的信息。
struct epoll_event events[10];
int nfds = 0;
nfds = epoll_wait(epoll1_fd,events,Max_Event,-1);
for (int i = 0;i < nfds;i ++)
{
...(对events[i]的处理)
}
这段代码中,文件注册到epoll后epoll_wait()的会将注册的的监听文件,events[10]作为储存监听文件产生事件的容器等待处理,nfds:产生的事件数量。注意events[10]并非是最大能等待的事件的数目。这是一次性能能处理的最大事件数量。什么意思呢?举个例子,通过epoll_ctl()注册了100个文件,epoll会监听这一百个文件而不是10个,加入一次性产生了13个可读事件发生,那么前10个会在for()循环被处理,剩下3个不会被丢弃或者覆盖,而是会等待前10个文件处理完后在第二轮循环中被处理,nfds会立刻返回3,进入第二轮循环。
通过epoll_manager封装写一个多人服务器
#include
#include
#include
#include
#include
#include
#include
#include "epoll_manager.h"
constexpr uint16_t Server_port = 8080;
constexpr uint32_t Max_Event = 10;
constexpr size_t Buf_Size = 1024;
void fun_cb(epoll_info* info, std::string& buf)
{
std::cout << info->fd << ":" << buf << std::endl;
write(info->fd,"OK",3);
}
/*初始化网络资源*/
void sock_init(int &sock_fd)
{
//初始化套接字
struct sockaddr_in address;
int temp_fd = 0;
int opt = 1;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_family = AF_INET;
address.sin_port = htons(Server_port);
//创建套接字
sock_fd = socket(AF_INET,SOCK_STREAM,0);
ErrHandle("socket",sock_fd);
//绑定端口
temp_fd = bind(sock_fd,(struct sockaddr*)&address,sizeof(address));
ErrHandle("bind",temp_fd);
std::cout << "绑定监听端口:" << Server_port << std::endl;
//监听端口
temp_fd = listen(sock_fd,64);
ErrHandle("listen",temp_fd);
//优化断开重连:"Address already in use" 错误
temp_fd = setsockopt(sock_fd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
ErrHandle("setsockopt",temp_fd);
}
/*main----------------------------------------------------------------
------------------------------------------------------------------------*/
int main(int argc,char** argv)
{
/*创建资源*/
system("clear");
EpollManager epoll1;
struct epoll_event events[Max_Event];
epoll_info temp_info = {0};
temp_info.epoll_fd = epoll1.get_epoll_fd();
socklen_t len = sizeof(temp_info.address);
//创建流
int listen_socket_fd,nfds,temp;
std::string buf(Buf_Size,0);
/*服务器网络初始化----------------------------------------------------------
-------------------------------------------------------------------------*/
/*套接字初始化:地址族初始化---绑定---监听---连接*/
sock_init(listen_socket_fd);
temp_info.fd = listen_socket_fd;
epoll1.info_register(&temp_info,true);//录入客户端文件信息
std::cout << "监听中..." << std::endl;
/*----------------------------------------------------------------------------*/
while(1)
{
//等待事件
nfds = epoll_wait(epoll1.get_epoll_fd(),events,Max_Event,-1);
ErrHandle("epoll_wait",nfds);
for (int i = 0;i < nfds;i ++)
{
/*来自监听文件的客户端连接--------------------------------------------------------------
-------------------------------------------------------------------------*/
//服务器有可读事件-->有新客户端连接
if ((static_cast(events[i].data.ptr))->fd == listen_socket_fd)
{
std::cout << "新连接" << std::endl;
temp_info.fd = accept(listen_socket_fd,(struct sockaddr*)&(temp_info.address),&len);
epoll1.info_register(&temp_info);//录入客户端文件信息
std::cout << temp_info.fd << ":已连接" << std::endl;
}
/*客户端发送数据--------------------------------------------------------------
-------------------------------------------------------------------------*/
else //已有的连接
{
//传入一个epoll_info*指针,一个缓冲区,一个自定义函数,后面填参数
epoll1.Epoll_EventHandle(static_cast(events[i].data.ptr),buf,fun_cb);
}
}
}
//释放资源
close(listen_socket_fd);
return 0;
}
注册
在上面这段代码中,我们来着重分析epoll的机制和epoll_manager封装,重点放到主函数
int main(int argc,char** argv)
{
/*创建资源*/
system("clear");
EpollManager epoll1;
struct epoll_event events[Max_Event];
epoll_info temp_info = {0};
temp_info.epoll_fd = epoll1.get_epoll_fd();
socklen_t len = sizeof(temp_info.address);
//创建流
int listen_socket_fd,nfds,temp;
std::string buf(Buf_Size,0);
/*服务器网络初始化----------------------------------------------------------
-------------------------------------------------------------------------*/
/*套接字初始化:地址族初始化---绑定---监听---连接*/
sock_init(listen_socket_fd);
temp_info.fd = listen_socket_fd;
epoll1.info_register(&temp_info,true);//录入客户端文件信息
std::cout << "监听中..." << std::endl;
/*----------------------------------------------------------------------------*/
while(1)
{
//等待事件
nfds = epoll_wait(epoll1.get_epoll_fd(),events,Max_Event,-1);
ErrHandle("epoll_wait",nfds);
for (int i = 0;i < nfds;i ++)
{
/*来自监听文件的客户端连接--------------------------------------------------------------
-------------------------------------------------------------------------*/
//服务器有可读事件-->有新客户端连接
if ((static_cast(events[i].data.ptr))->fd == listen_socket_fd)
{
std::cout << "新连接" << std::endl;
temp_info.fd = accept(listen_socket_fd,(struct sockaddr*)&(temp_info.address),&len);
epoll1.info_register(&temp_info);//录入客户端文件信息
std::cout << temp_info.fd << ":已连接" << std::endl;
}
/*客户端发送数据--------------------------------------------------------------
-------------------------------------------------------------------------*/
else //已有的连接
{
//传入一个epoll_info*指针,一个缓冲区,一个自定义函数,后面填参数
epoll1.Epoll_EventHandle(static_cast(events[i].data.ptr),buf,fun_cb);
}
}
}
//释放资源
close(listen_socket_fd);
return 0;
}
在sock_init()中完成对网络的绑定,监听,等待连接。epoll1.info_register(&temp_info,true);
注册套接字文件,将套接字信息存入创建中间信息体temp_info中,将其作为参数传入,其他文件不需要填第二个参数,只有网络监听套接字需要填true。在下面的源码中,我们将信息传入指针epoll_info* p,并申请data.ptr的空间,在这里void *epoll_event data.ptr存储自定义结构体epoll_info的数据。通过epoll_ctl()完成注册。监听的文件句柄与开辟的空间指针会被记录到链表中。
除了fd,epoll_fd外,其他结构体成员是可以用户自定义的,比如struct sockaddr_in,用于辅助处理网络地址信息,但实际上,此成员并不参与内部逻辑,由用户自己来决定是否使用它。
// 客户端信息
typedef struct{
struct sockaddr_in address;//存放网络ip,分配端口
int fd;
int epoll_fd;
}epoll_info;
epoll1.info_register(&temp_info,true);//录入客户端文件信息
//源文件
void EpollManager::info_register(epoll_info *pclient_info,bool is_sockt)
{
//信息录入内核
int temp_fd = 0;
struct epoll_event epoll_e;
epoll_info* p;
p = new epoll_info;//为epoll文件申请空间
if (p == nullptr)
std::cout << "register:memory error" << std::endl;
/*记录epoll信息*/
*p = *pclient_info;
epoll_e.events = EPOLLIN | EPOLLET;//边缘触发,输入事件
epoll_e.data.ptr = static_cast(p);
/*注册epoll成员*/
temp_fd = epoll_ctl(epoll_fd,EPOLL_CTL_ADD,p->fd,&epoll_e);
if (temp_fd < 0)
{
perror("epoll_e:ctl");
close(epoll_fd);
delete p;
delete sock_ptr;
exit(1);
}
list_append(list,p->fd,(void*)p);//索引信息录入链表
if (is_sockt)//此地址为套接字申请,在析构时释放
sock_ptr = p;
}
处理
在注册步骤中,data.ptr->fd存储了我们的文件句柄,将events[i].data.ptr转一下数据类型,判断fd是否是套接字文件,如果是则说明有新的客户端连接,否则说明是已经注册的客户端发来了数据等待处理,看下一步。
//服务器有可读事件-->有新客户端连接
if ((static_cast(events[i].data.ptr))->fd == listen_socket_fd)
{
std::cout << "新连接" << std::endl;
temp_info.fd = accept(listen_socket_fd,(struct sockaddr*)&(temp_info.address),&len);
epoll1.info_register(&temp_info);//录入客户端文件信息
std::cout << temp_info.fd << ":已连接" << std::endl;
}
/*客户端发送数据--------------------------------------------------------------
-------------------------------------------------------------------------*/
else //已有的连接
{
//传入一个epoll_info*指针,一个缓冲区,一个自定义函数,后面填参数
epoll1.Epoll_EventHandle(static_cast(events[i].data.ptr),buf,fun_cb);
}
事件处理与自定义函数
数据处理函数Epoll_EventHandle(),它是一个变参函数,第一个参数是epoll_info*,它会从中获取信息,第二个参数是主函数中定义的缓冲区buf,注意,缓冲区需要初始化长度 buf(Buf_Size,0);第三个参数是自定义的函数指针fun_cb,由使用者更具自己的实际需求自己实现,后面还能填不限数量类型的参数传递给自定函数,在这个例子中不需要传参。自定义函数(名字自定义)fun_cb(epoll_info* info, std::string& buf),前两个参数定义必须是epoll_info* ;std::string& ;后续参数可自定义。
在这里,buf中存储者客户端,或者是监听文件写入的数据,在我的处理中,将显示:文件句柄:数据,并向监听文件也就是客户端发送“OK”。用户可以通过自定义结构体 epoll_info 来做一些其他的操作,比如回显,输出地址ip等。
//自定义数据处理函数
void fun_cb(epoll_info* info, std::string& buf)
{
std::cout << info->fd << ":" << buf << std::endl;
write(info->fd,"OK",3);
}
//数据处理函数
epoll1.Epoll_EventHandle(static_cast(events[i].data.ptr),buf,fun_cb);
可变参处理函数原型
这是一个变参函数,通过 万能引用 可以传入左值或右值,以 完美转发 确保保留参数原本的性质。
首先通过信息体读取文件信息,并作出错误处理。在else{}中调用使用者传入的函数指针,第一个参数pclient_info:用户通过此参数调用一下自定义的性质,data:存储着文件接收到的信息。因为这两个参数传入了f(),所以,第一个和第二个参数固定为epoll_info*,std::string &buf。
std::forward<Args>(args)...:通过完美转发保存 参数的性质,通过此功能,用户在上面的fun_cb()的例子中可定义多种参数使用。
template
void EpollManager::Epoll_EventHandle(epoll_info *pclient_info,std::string &buf,
F &&f,Args&&... args)
{
int count = read(pclient_info->fd,&buf[0],buf.capacity()-1);
if (count < 0)
{
perror("Epoll_EventHandle");
epoll_ctl(pclient_info->epoll_fd, EPOLL_CTL_DEL, pclient_info->fd, NULL);//移出等待序列
close(pclient_info->fd);//关闭客户端文件
node_free(list,pclient_info->fd);//释放记录链表节点
delete pclient_info;//释放内存
pclient_info = nullptr;
std::cout << "读取错误" << std::endl;
}
else if (count == 0)
{
std::cout << pclient_info->fd << ":已下线。资源已回收" << std::endl;
epoll_ctl(pclient_info->epoll_fd, EPOLL_CTL_DEL, pclient_info->fd, NULL);//移出等待序列
close(pclient_info->fd);
node_free(list,pclient_info->fd);//释放记录链表节点
delete pclient_info;//释放资源
pclient_info = nullptr;
}
else
{
// 创建一个只包含有效数据的子字符串
std::string data = buf.substr(0, count);
f(pclient_info, data, std::forward(args)...);
}
}
实验结果

在这里,Linux本地使用一个客户端连接,win下使用一个客户端连接,在自定义消息处理中,服务器输出:fd:内容并回发“OK”,这里是符合自定义处理函数fun_cb()的。断开连接,资源正确回收。
这里就是此epoll封装的介绍了,如果对你有用的话点个赞吧。
浙公网安备 33010602011771号