基于epoll的io复用管理,一种文件监听方案 2 - 教程

(仓库链接:https://github.com/12379biu/epoll_manager.git

目录

epoll的一些性质

通过epoll_manager封装写一个多人服务器

注册

处理

事件处理与自定义函数

可变参处理函数原型

实验结果


此封装旨在将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封装的介绍了,如果对你有用的话点个赞吧。

posted on 2025-11-18 21:30  slgkaifa  阅读(0)  评论(0)    收藏  举报

导航