Muduo网络库实现 [五] - Poller模块

目录

设计思路

类的设计

模块实现

私有接口

公有接口

疑惑点


设计思路

poller 模块的意义主要体现在它对 epoll 进行了更高层次的封装,让开发者在监控文件描述符(比如 socket)的事件时,操作变得更简单、更直观。咱们可以从几个方面来看看它的价值:

简化复杂性
原生的 epoll 是 Linux 提供的一种高效 I/O 事件通知机制,但它的 API 使用起来稍微有点繁琐,比如需要手动创建 epoll 实例、注册事件、处理就绪事件等。poller 模块把这些底层操作封装成了更友好的接口,让你不用直接跟 epoll_ctl、epoll_wait 这些函数打交道,减少了出错的机会。
提高代码可读性
通过 poller,你可以更专注于业务逻辑,而不是陷入底层的事件管理细节。比如,注册一个描述符、指定关心的事件(比如可读、可写),poller 通常会提供简洁的方法,代码看起来更清晰,维护起来也更容易。
统一抽象
如果你的项目需要在不同平台上运行(比如 Linux 用 epoll,其他系统可能用 select 或 poll),poller 可以作为一个抽象层,屏蔽底层的差异,让你用一套统一的接口来处理事件监控。这对跨平台开发超级友好!

1. 设计 Poller 的核心功能

  • 创建和管理 epoll 实例
  • 添加/修改/删除文件描述符及其关心的事件
  • 等待并返回就绪的事件
  • 提供简单的事件循环接口
  • 基于这些需求,可以设计一个 Poller 类或者结构体,封装这些功能。 

2. 基本封装思路

  • 初始化:调用 epoll_create 创建一个 epoll 实例。
  • 添加描述符:用 epoll_ctl 注册文件描述符和事件。
  • 事件等待:用 epoll_wait 获取就绪的事件。
  • 销毁:关闭 epoll 实例

对于epoll模型,要怎么封装呢?

我们知道,一个EventLoop线程也就是一个从属Reactor线程需要对应一个 epoll 模型,也就是一个 Poller对象(因为Poller就是对epoll的封装),那么这个对象中要实现事件的监控,就必须要保存一个epoll操作句柄(就是钥匙,没有这个钥匙,启动不了对应的epoll),也就是一个_epfd。

对于epoll模型来说,当使用epoll_ctl(),把文件描述符添加到红黑树模型上之后,当有事件就绪了,操作系统会去红黑树上找到该就绪事件对应的文件描述符_fd。然后会把这些_fd放在就绪队列上。而我们的Poller模块就是封装的epoll,所以肯定会获取到就绪的事件。那么这些事件就需要存储到一个就绪队列中。我们使用一个 struct epoll_event 类型的数组来模拟就绪队列。但是对于Poller的上层,也就是EventLoop来说,它并不需要得到一个 struct epoll_event 的数组,他其实只需要知道哪些Channel 中的事件就绪了,然后调用 Channel中的 HandlerEvent 方法去处理就绪事件就行了,所以我们未来设置就绪事件获取的时候,返回给外界或者说外界不需要传 struct epoll_event 数组,但是我们内部调用 epoll_wait 又必须使用这个数组,那么我们其实可以在Poller对象内部定义一个 struct epoll_event 数组便于后续使用。

同时,我们需要对Poller模型中监控的文件描述符以及对应的Channel对象进行管理,一个fd对应一个Channel对象。因为未来我们在添加某个事件的监控时,有可能该文件描述符在之前我们并没有添加到 epoll 模型中,这时候我们使用的是 EPOLL_CTL_ADD 操作,而其他的时候,对监控的事件进行修改,则是EPOLL_CTL_MOD操作,仅仅是对epoll模型中的红黑树节点中的内容进行修改,并不对节点的删除与插入做管理。 

就比如在一个城市中,epfd就好比不同的饭馆_fd就对应进入饭馆的顾客,当一个顾客来吃饭,就把该顾客对应的信息注册到记录本上(比如一对情侣坐在了A桌上),然后此时服务员(Poller)对已经注册的顾客进行监控,当_fd就绪了,这时候就需要通过Channel模块了,因为Poller模块只是监听fd,并且把就绪的fd放到就绪队列上。至于这些就绪的事件接下来要干什么是要传给Channel中的回调函数处理的。就好比A顾客离开了(就绪了Close事件),然后服务员检测到了A顾客就绪了离开的事件,他就会在对讲机里跟老板说,老板知道了是A顾客离开了也就是知道了A顾客所在桌的路线(Channel),就会调用调用回调函数(保洁),去执行打扫卫生的工作。如果Poller和Channel不联系起来,那老板知道有顾客离开了,但是不知道是哪个顾客离开了。

类的设计

我们需要一个私有的实际进行epoll模型操作的结构,也就是上面的 Update 接口,同时也需要一个辅助接口,也就是IsInPoller ,来判断某个Channel是否已经在Poller的监控中,来决定是使用 EPOLL_CTL_ADD还是使用EPOLL_CTL_MOD操作。

class Poller
{
private:
#define REVENTS_SIZE 1024 
    int _epfd;      //epoll模型的操作句柄
    std::unordered_map<int,Channel*> _channels;  //保存管理的套接字以及对应的Channel
    struct epoll_event _revents[REVENTS_SIZE];  //用于从epoll模型中获取就绪的文件描述符及其就绪事件然后存储里面
private:
    //判断该文件描述符是否在epoll模型中
    bool HasChannel(Channel* channel); //判断文件描述符是否登记到了Channel中
    void Update(Channel* channel,int op); 更新文件描述符的事件
public:             //提供的接口
    void UpdateEvents(Channel* channel); //添加/更新事件监控
    void Remove(Channel*channel); //删除事件监控
    size_t PollCtl(vector<Channel*> *active); //就绪事件放到"就绪队列"中
};

模块实现

私有接口

    bool HasChannel(Channel* channel) //判断文件描述符是否登记到了Channel中
    {
        return _channels.find(channel->Fd()) == _channels.end() ? false : true;
    }

    void Update(Channel* channel, int op) //更新文件描述符的事件
    {
        int fd = channel->Fd();
        struct epoll_event ev;
        ev.data.fd = fd;
        ev.events = channel->Events();

        int ret = epoll_ctl(_epfd, op, fd, &ev);
        if(ret < 0)
        {
            ERR_LOG("epoll_ctl error");
        }
        return;
    }

公有接口

在公有接口中,有个函数 PollCtl 比较难理解,这里我给大家单独讲解一下。这个函数的目的是检测有哪些事件就绪(比如 socket 可读、可写),然后把对应的 Channel 塞进 actives 中,交给上层(比如 EventLoop)去处理类比epoll模型的就绪队列

    size_t PollCtl(vector<Channel*> *active)//就绪事件放到"就绪队列"中
    {
        int cnt = epoll_ctl(_epfd, _revents, REVENTS_SIZE, -1);//阻塞等待
        if(cnt < 0)
        {
            if(errno = EINTR)
            ERR_LOG("epoll_wait failed");
            abort();
        }

        for(int i = 0; i <cnt; i++)
        {
            auto it = _channels.find(_revents[i].data.fd);
            if(it == _channels.end())
            {
                ERR_LOG("find channel failed");
                abort();
            }

            it->second->SetRevents(_revents[i].events);
            active->push_back(it->second);
        }
        return cnt;
    }

完整公有接口

public:
    Poller()
    {
        _epfd = epoll_create(1);
        if(_epfd < 0)
        {
            ERR_LOG("epoll_create error");
            abort(); //退出程序
        }
    }
    void UpdateEvents(Channel* channel)//添加/更新事件监控
    {
        if(HasChannel(channel))  //检查下是否已经在红黑树模型上
        {
            Update(Channel* channel, EPOLL_CTL_MOD);
            return;
        }
        _channels.insert(std::make_pair(channel->Fd(), channel)); //添加到红黑树模型上
        Update(channel, EPOLL_CTL_ADD);
    }
    size_t PollCtl(vector<Channel*> *active)//就绪事件放到"就绪队列"中
    {
        int cnt = epoll_ctl(_epfd, _revents, REVENTS_SIZE, -1);//阻塞等待
        if(cnt < 0)
        {
            if(errno = EINTR)
            ERR_LOG("epoll_wait failed");
            abort();
        }

        for(int i = 0; i <cnt; i++)
        {
            auto it = _channels.find(_revents[i].data.fd);
            if(it == _channels.end())
            {
                ERR_LOG("find channel failed");
                abort();
            }

            it->second->SetRevents(_revents[i].events);
            active->push_back(it->second);
        }
        return cnt;
    }
    void RemoveEvent(Channel* channel)//删除事件监控
    {
        auto it = _channels.find(channel->Fd());
        if(it ! = _channels.end())
        {
            _channels.erase(it);
            Update(channel, EPOLL_CTL_DEL);
        }
    }
};

目前来说,这段代码编译起来没什么问题,等后续我们实现完EventLoop模块再来进行联合调试。实现完EventLoop之后,我们的逻辑就大概清晰了。

疑惑点

std::unordered_map<int,Channel*> _channels;为啥需要这个

 那为啥map中的值不是Channel而是Channel*呢?

auto it = _channels.find(_revents[i].data.fd); 这个返回的是什么?这个it是个unorder_map?

active->push_back(it->second);这个是干嘛的,这个不是就绪队列吗,不是应该把就绪事件的文件描述符传给这个vector队列吗?怎么把channel*对象指针给填进去?

这个channel对象里面既有文件描述符也还有该文件描述符所对应的就绪事件,以及对应的回调函数