关于Epoll的epoll_event.data的使用

        这两天公司开了新项目,后端使用Golang写的,于是想着用Golang来写个小玩意练练手,于是把目标对准了Golang中的net/http包,写个简单的收发文件的应用,在查找net/http的API的时候意外发现它底层使用的是epoll实现,这才突然想起来之前做C++网络编程的时候看到过epoll的部分,但是当时没有去详细了解,也就粗略知道个轮询的关键字就没再深究,这次正好有时间,就正好借着这个机会补一下这块的缺漏。

        另外吐槽一下找这种奇葩的资料真难找,找了很多资料才搞明白。这篇博客也是希望帮助更多想深入了解epoll_event.data用法的哥们。

1、Epoll基础知识

        Epoll主要用于实现多路复用技术,关于多路复用的实现方式总共有三种:select、poll、epoll,其中poll是对select的优化,但是治标不治本,所以这里仅对比一下select和epoll。

        

        Epoll向来以性能著称,但是它和select之间的性能到底差在哪里,这是必须知晓的,要谈性能差距,肯定要从他们的实现方式开始聊了。

        select的实现方式是给定一个文件描述符的队列,持续监听这个队列,当内核监听到有文件描述符的状态发生变化时,通知select进行处理,当select收到通知后,它只知道队列中有描述符就绪了,但是是哪个描述符并不清楚,所以需要遍历这个队列,查找哪些文件描述符准备就绪,然后进行接下来的操作,于是时间复杂度来到了O(N),同时它有一个比较大的弊端就是一个select只能监听1024个文件描述符,虽然可以通过改写头文件重新编译内核或者进行多线程select来缓解这个问题,但是终归是治标不治本,于是就由epoll来解决这两个比较大的问题了。

        epoll的实现方式是给定一个epoll事件池与一个epoll_event数组(我称之为事件数组),这个事件池不限制最多能放入的事件个数,这就解决了select最多只能监听1024个文件描述符的问题,指定事件池后,同样由内核来监听每个事件的文件描述符与其指定的事件(每个事件是一个结构体,其中包含文件描述符和要等待的事件,看后面就能明白),当监听到文件描述符到达指定的状态时,由内核主动将就绪的事件拷贝到epoll_event数组中并返回就绪的描述符数量(这个拷贝由内核完成,效率很高),于是epoll能够直接获取到是哪些事件就绪,可以直接对epoll_event进行遍历,而不需要将事件池中的所有事件遍历,则直接将时间复杂度优化到了O(1),同时能够对每个事件附带一个data数据,在得到就绪的文件描述符时就能够一并得到它的状态。

        介绍完了实现方式,他们之间的性能差距也就一目了然了,对于epoll的系统实现,可以看这篇文章,它对epoll的底层解释的比较详细。

2、Epoll在C++中的使用

        epoll在C++中的头文件为<sys/epoll.h>,其中我们只用到三个API:

        第一个是int epoll_create1(int flags),这个函数用于创建一个事件池句柄,flags一般设置为0,也有更详细的用法请参阅Linux手册

int epollFd = epoll_create1(0);

        第二个是int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event),这个函数用于向事件池中添加一个监听事件,其中epfd为事件池句柄,op为你要进行的操作,用三个宏来表示:
        EPOLL_CTL_ADD:添加新的监听文件描述符到事件池中;
        EPOLL_CTL_MOD:修改已经添加的文件描述符的监听事件;
        EPOLL_CTL_DEL:从事件池中删除一个监听对象;
        fd为你要监听的文件描述符,*event为一个结构体,其中包含了你要监听的事件和你要附带的消息(这个参数一会再细说)。

        epoll_event类型的声明如下

typedef union epoll_data {
    void *ptr;
    int fd;
    __uint32_t u32;
    __uint64_t u64;
} epoll_data_t;

struct epoll_event {
    __uint32_t events; /* Epoll events */
    epoll_data_t data; /* User data variable */
};

        所以epoll_event对象的声明和初始化以及epoll_ctl的使用如下

epoll_event event;
event.events = EPOLLIN;          // 监听可读事件
event.data.fd = serverSocket;    // 将事件附带的消息设置为serverSocket

// 将服务器套接字添加到epoll事件监听中
// 将serverSocket添加到事件池中,并当Socket可读时被触发
if (epoll_ctl(epollFd, EPOLL_CTL_ADD, serverSocket, &event) == -1) {
    perror("Epoll control error");
    return -1;
}

        第三个是int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout),第一个参数epfd是事件池句柄,第二个参数events就是我们声明的事件数组,第三个参数maxevents是一次最多返回的描述符个数,一般设置为和事件数组的大小一样,epoll将会把发生的事件拷贝到 events数组中(events不可以是空指针,内核只负责把数据复制到这个 events数组中,不会去帮助我们在用户态中分配内存),第四个参数timeout是阻塞最大时长,timeout为0时立刻返回,不论是否有文件描述符就绪,timeout为-1时为永久阻塞,直到有事件被触发。

        对于epoll_wait的使用一般放在while(1)中反复读取客户端连接是否就绪。

while (true) {
    // 声明一个events数组用来接受就绪的文件描述符
    epoll_event events[MAX_EVENTS];
    // 等待事件发生
    int numEvents = epoll_wait(epollFd, events, MAX_EVENTS, -1);

    // 到这里后epoll已经将所有就绪的事件对象拷贝到了events数组中,对其进行遍历
    for (int i = 0; i < numEvents; ++i) {
        if (events[i].data.fd == serverSocket) {
            // 如果收到的事件的fd对象为服务端Socket,代表有新的连接请求
            clientSocket = accept(serverSocket, (struct sockaddr *) &clientAddr, &clientAddrLen);
            if (clientSocket == -1) {
                perror("Accept error");
                continue;
            }

            std::cout << "New client connected" << std::endl;

            // 将新客户端套接字添加到epoll事件监听中
            event.events = EPOLLIN;
            event.data.fd = clientSocket;
            if (epoll_ctl(epollFd, EPOLL_CTL_ADD, clientSocket, &event) == -1) {
                perror("Epoll control error");
                return -1;
            }
        } else {
            // 如果不是服务端Socket,则为客户端数据
            char buffer[1024];
            memset(buffer, 0, sizeof(buffer));
            int bytesRead = read(events[i].data.fd, buffer, sizeof(buffer));
            if (bytesRead <= 0) {
                // 客户端关闭连接
                std::cout << "Client disconnected" << std::endl;
                close(events[i].data.fd);
            } else {
                std::cout << "Received data from client: " << buffer << std::endl;
                // 在这里可以对接收到的数据进行处理
            }
        }
    }
}

        至此三个API介绍完毕,有一个细节问题需要强调:我们在epoll_ctl函数中给了一个int fd参数又给了一个epoll_event *event参数,但是你会发现epoll_event结构体中又包含了一个int fd参数,为什么会需要两个fd?
        因为在epoll_wait中,epoll虽然监听的是fd描述符的状态变化,但是最后返回时,返回的是这个fd描述符注册时的epoll_event指针,并没有将fd描述符也拷贝到事件数组中,所以我们需要显示的在epoll_event.data中指定fd参数,用来判断这个返回的event到底属于哪个描述符。

3、epoll_event.data的使用

        细心观察epoll_event,会发现它的结构体构成中存在一个epoll_data,我们一般只用其中的fd参数,这一节我们探讨一下关于u64参数和ptr参数。

typedef union epoll_data {
    void *ptr;
    int fd;
    __uint32_t u32;
    __uint64_t u64;
} epoll_data_t;

struct epoll_event {
    __uint32_t events; /* Epoll events */
    epoll_data_t data; /* User data variable */
};

        epoll_data是一个union,所占的内存大小为其中最大的数据类型,即__uint64_t,如果我们以64位模式编译,那么我们可以利用其中的u64存储一个fd和一个我们自己定义的数据,前32位存储我们自己的消息类型,后32位存储fd,则能够附带更多的信息,以下为一个简单的示例

static int Epoll_Control(const int sockfd, const int oper, const int events, unsigned int CallbackType)
{
    epoll_event event;
 
    memset(&event, 0, sizeof(ep_event));
    event.events = events;
    // 先存自定义消息类型,并左移(向高位移)32位,不同消息类型对应不同的回调函数,低位32位存储fd
    event.data.u64 = (uint64_t)(((uint64_t)(CallbackType) << 32) | (uint64_t)(int)sockfd);
 
    if (epoll_ctl(g_Main_Epoll_Fd, oper, sockfd, &event) == -1) {
        perror("Epoll control error");
        return -1;
    }
 
    return 0;
}

        我们使用u64即可存储fd的同时还能够存储一个自定义的四字节消息类型,在使用epoll_wait函数读取事件时解析同理。

        接下来我们来看ptr参数的使用,这才是今天的重头戏!!!!当我们能存储一个指针的时候也就意味着我们能存储无限的数据,来看一下存指针的示例:

// 创建我们自己的结构体
struct client
{
    int sockfd;
    char client_name[256];
    __uint64_t CallBack;
};

// 事件数组
epoll_event *events = NULL;
// 绑定ServerSocket的事件
epoll_event ev;
// 我们创建的结构体的指针
client *c = new client;

// 向结构体中绑定fd描述符
c.fd = (socket);


int efd = epoll_create1(0);
ev.data.fd = c.fd;
ev.events = EPOLLIN;
// 将epoll_event.data.ptr赋值为我们的结构体指针
ev.data.ptr = c;
epoll_ctl ( efd , EPOLL_CTL_ADD , c.fd , &ev );

//为事件数组分配空间
events = (epoll_event*)calloc ( MAX_EVENTS , sizeof event );

while(1) {
    int n = epoll_wait ( efd , events , MAX_EVENT , -1 ); 
    for ( int i = 0 ; i  < n ; i++ ) {
        client *event_c = NULL;
        // 读取事件中附带的指针
        event_c = (client*) events[i].data.ptr;
        cout << "SOCKET: " << event_c->fd << endl;

        if (c->fd == events[i].data.fd ) {
            // 如果当前描述符是服务端
            struct client *new_c = new client;
            struct epoll_event new_ev;
            struct sockaddr inaddr;
            sockletn_t in_len;
            int nfd = accept ( c->fd , &inaddr , &in_len );
            new_c->fd = nfd;
            new_c->connection_status = 1;
            new_ev.data.fd = nfd;
            new_ev.events = EPOLLIN;
            new_ev.data.ptr = client;
            int r = epoll_ctl ( efd , EPOLL_CTL_ADD , nfd , &new_ev );
            continue;
         } else {
            // 如果是客户端
            ssize_t count;
            char buf[512];
            int count = read ( events[i].data.fd , buf , sizeof buf );
            // 要进行的客户端操作

            int rc = write ( 1 , buf , count );
        }
    }
}

        要记得当客户端断开连接时释放结构体指针。

        epoll附带指针需要注意的点就是epoll_wait函数是只将epoll_event对象拷贝到事件数组中,所以你的结构体中必须附带fd参数来判断这个事件属于哪个文件描述符。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值