【webserver】第5节 epoll详解——对事件的操作

代码开源:

        GitHub - PetterZhukov/webserver_HTTP: 使用了线程池,通过epoll实现的Proctor版本的web服务器。参考了游双老师的《Linux高性能服务器编程》以及牛客网的《Linux高并发服务器开发》课程。在自己复现的基础上进行模块的整合并添加一些小更改。所有代码拥有完备的注释。

介绍:

        webserver_HTTP
        使用了线程池,通过epoll实现的Proactor版本的web服务器。参考了游双老师的《Linux高性能服务器编程》以及牛客网的《Linux高并发服务器开发》课程。在自己复现的基础上进行模块的整合并添加一些小更改。所有代码拥有完备的注释。

        访问的资源在 同级目录"resources"文件夹中

在epoll_class中,我们简单介绍了使用epoll和socket的应用场景,但是关于epoll的事件等特性,还没有详细介绍,还有本项目封装的关于epoll事件的对应函数

目录

5.1 epoll 事件

5.1.1 epoll简介

5.1.2 常用的事件类型

5.1.3 对事件进行操作

5.2 本项目中对事件的操作

5.2.1 设置文件描述符非阻塞

5.2.2 添加、删除、重置操作的封装


5.1 epoll 事件

5.1.1 epoll简介

        在之前的部分中我们介绍了epoll的使用、epoll的结构、epoll进行TCP网络通信的基本架构,但是最重要的部分:epoll事件(events)还未介绍。

        在使用epoll_ctl向内核注册事件,然后通过epoll_wait来获取监听到的事件列表,这样反复循环,就是监听部分的主要逻辑了。

5.1.2 常用的事件类型

        事件是通过位掩码的方式表示的,因此表示多个事件的时候是使用| 按位或运算符连接。

        本项目主要用到以下事件

EPOLLIN                  文件描述符是否可读

EPOLLOUT              文件描述符是否可写

EPOLLRDHUP         对端是否关闭

EPOLLONESHOT    文件描述符上的注册事件只触发一次

        即使可以使用 ET 模式,一个 socket 上的某个事件还是可能被触发多次。这在并发程序中就会引起一个 问题。比如一个线程在读取完某个 socket 上的数据后开始处理这些数据,而在数据的处理过程中该 socket 上又有新数据可读( EPOLLIN 再次被触发),此时另外一个线程被唤醒来读取这些新的数据。于 是就出现了两个线程同时操作一个 socket 的局面。一个 socket连接在任一时刻都只被一个线程处理,可 以使用 epoll EPOLLONESHOT 事件实现。
        对于注册了 EPOLLONESHOT 事件的文件描述符,操作系统最多触发其上注册的一个可读、可写或者异 常事件,且只触发一次,除非我们使用 epoll_ctl 函数重置该文件描述符上注册的 EPOLLONESHOT 事 件。这样,当一个线程在处理某个 socket 时,其他线程是不可能有机会操作该 socket 的。但反过来思 考,注册了 EPOLLONESHOT 事件的 socket 一旦被某个线程处理完毕, 该线程就应该立即重置这个 socket 上的 EPOLLONESHOT 事件,以确保这个 socket 下一次可读时,其 EPOLLIN 事件能被触发,进而让其他工作线程有机会继续处理这个 socket

5.1.3 对事件进行操作

#include <sys/epoll.h>

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    - 参数
        - epfd:epoll对应的文件描述符
        - op:options 有以下三种
            EPOLL_CTL_ADD:注册新的fd到epfd中
            EPOLL_CTL_MOD:修改已经注册的fd的监听事件
            EPOLL_CTL_DEL:从epfd中删除一个fd
        - fd:要操作的fd
        - event:要注册的事件,多个事件用 | 连接
           struct epoll_event {
               uint32_t     events;      /* Epoll events */
               epoll_data_t data;        /* User data variable */
        注册的数据结构,包含注册的事件,以及对应的数据结构
           typedef union epoll_data {
               void        *ptr;
               int          fd;
               uint32_t     u32;
               uint64_t     u64;
           } epoll_data_t;
        联合体,一般是使用其中的fd

           };
    - 返回值
        - 成功: 0
        - 失败: -1

这里摘录一段很清晰的示例代码

for( ; ; )
    {
        nfds = epoll_wait(epfd,events,20,500);
        for(i=0;i<nfds;++i)
        {
            if(events[i].data.fd==listenfd) //有新的连接
            {
                connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen); //accept这个连接
                ev.data.fd=connfd;
                ev.events=EPOLLIN|EPOLLET;
                epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); //将新的fd添加到epoll的监听队列中
            }
            else if( events[i].events&EPOLLIN ) //接收到数据,读socket
            {
                n = read(sockfd, line, MAXLINE)) < 0    //读
                ev.data.ptr = md;     //md为自定义类型,添加数据
                ev.events=EPOLLOUT|EPOLLET;
                epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);//修改标识符,等待下一个循环时发送数据,异步处理的精髓
            }
            else if(events[i].events&EPOLLOUT) //有数据待发送,写socket
            {
                struct myepoll_data* md = (myepoll_data*)events[i].data.ptr;    //取数据
                sockfd = md->fd;
                send( sockfd, md->ptr, strlen((char*)md->ptr), 0 );        //发送数据
                ev.data.fd=sockfd;
                ev.events=EPOLLIN|EPOLLET;
                epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改标识符,等待下一个循环时接收数据
            }
            else
            {
                //其他的处理
            }
        }
    }

 来自 epoll使用详解:epoll_create、epoll_ctl、epoll_wait、close - 雾穹 - 博客园

5.2 本项目中对事件的操作

5.2.1 设置文件描述符非阻塞

        文件描述符(file descriptor)是内核为了高效管理已被打开的文件所创建的索引

         在产生文件描述符的时候返回的数字是指在文件描述符表中注册后对应的索引。

        我们可以使用fcntl来对文件描述符(fd,file descriptor)进行操作,比如将其设置为非阻塞。

        API如下

    #include <unistd.h>
    #include <fcntl.h>

    int fcntl(int fd, int cmd, ...);
    参数:
        fd : 表示需要操作的文件描述符
        cmd: 表示对文件描述符进行如何操作
            - F_DUPFD : 复制文件描述符,复制的是第一个参数fd,得到一个新的文件描述符(返回值)
                int ret = fcntl(fd, F_DUPFD);

            - F_GETFL : 获取指定的文件描述符文件状态flag
              获取的flag和我们通过open函数传递的flag是一个东西。

            - F_SETFL : 设置文件描述符文件状态flag
              必选项:O_RDONLY, O_WRONLY, O_RDWR 不可以被修改
              可选性:O_APPEND, O_NONBLOCK
                O_APPEND 表示追加数据
                O_NONBLOK 设置成非阻塞
        
        阻塞和非阻塞:描述的是函数调用的行为。

         因此设置非阻塞的思路:获取fd原有的状态flag,然后对其添加O_NONBLOCK

// 设置文件描述符非阻塞
void setnonblocking(int fd)
{
    int old_flag = fcntl(fd, F_GETFL);
    int new_flag = old_flag | O_NONBLOCK;
    fcntl(fd, F_SETFL, new_flag);
}

5.2.2 添加、删除、重置操作的封装

        实际上就是调用epoll_ctl,但是在封装的函数中可以统一指定添加、删除、修改的时候添加的事件,达到统一的目的。

        比如因为注册了oneshot事件,因此修改时一定要添加oneshot使其可以再次被使用,否则下一次可读时EPOLLIN事件不会被触发。

// 添加需要监听的文件描述符到epoll
// 添加一个非阻塞的文件描述符,根据需要添加ONE_SHOT
// 添加EPOLLIN的同时,也添加了EPOLLRDHUP来判断对方是否中断
void addfd(int epollfd, int fd, bool one_shot)
{
    epoll_event event;
    event.data.fd = fd;
    // EPOLLRDHUP判断是否挂起
    event.events = EPOLLIN | EPOLLRDHUP;
    if (one_shot)
        event.events |= EPOLLONESHOT;

    epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);

    // 设置文件描述符非阻塞
    setnonblocking(fd);
}

// 从epoll中删除文件描述符,并close文件描述符
// 同时完成epoll内核中的删除和关闭文件描述符两个行为
void removefd(int epollfd, int fd)
{
    epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, NULL);
    close(fd);
}


// 修改文件描述符,重置socket上EPOLLONESHOT事件,确保下次可读时被触发
// 设定文件描述符的事件为设定事件+ oneshot + 检测中断
// 因此每次监听完后要再次调用modfd重置描述符
void modfd(int epollfd, int fd, int ev)
{
    epoll_event event;
    event.data.fd = fd;
    event.events = ev | EPOLLONESHOT | EPOLLRDHUP;
    epoll_ctl(epollfd, EPOLL_CTL_MOD, fd, &event);
}

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值