Muduo网络库之Channel、EPollPoller与EventLoop类【深度解析】

文章目录

  • 前言
  • 一、Channel类
    • 1、主要成员变量以及函数
    • 2、实现原理
  • 二、EPollPoller类
    • 1、实现原理
  • 二、EventLoop类
    • 1、功能实现
    • SubReactorde的唤醒操作


前言

重新梳理一遍muduo网络库的类与知识点。
Channel、EPollPoller与EventLoop类是muduo库最重要的基础, 他们三类为一体,在底层负责事件循环。EventLoop包含Channel,Poller,EventLoop负责轮询访问Poller,得到激活Channel列表,使Channel自己根据自身情况调用相应回调。

一、Channel类

Channel类是对文件描述符(fd)以及其相关事件的封装。

在TCP网络编程中,想要IO多路复用监听某个文件描述符,就要把这个fd和该fd感兴趣的事件通过epoll_ctl注册到IO多路复用模块上。当事件监听器监听到该fd发生了某个事件。事件监听器返回 [发生事件的fd集合]以及[每个fd都发生了什么事件]

Channel类则封装了一个 [fd] 和这个 [fd感兴趣事件] 以及事件监听器监听到 [该fd实际发生的事件]。Channel类还提供了设置该fd的感兴趣事件,以及将该fd及其感兴趣事件注册到事件监听器或从事件监听器上移除,以及保存了该fd的每种事件对应的处理函数

1、主要成员变量以及函数

以下是对Channel类的各个成员变量和成员函数的解析:

  • 成员变量:

    • loop_:指向所属的事件循环(EventLoop)对象的指针。

    • fd_:表示该Channel对象关联的文件描述符。

    • events_:表示该Channel对象感兴趣的事件类型(如EPOLLINEPOLLOUT等)。

    • revents_:表示由事件分发器(Poller)返回的具体发生的事件类型。

    • index_:用于记录该Channel对象在事件分发器中的位置或状态。

    • readCallback_writeCallback_closeCallback_errorCallback_:各种回调函数,用于处理不同事件的回调操作。

  • 成员函数:

    • 构造函数和析构函数:初始化和销毁Channel对象。

    • update:在Channel对象所属的事件循环中,通过调用事件分发器的相应方法,注册或更新文件描述符的事件类型。

    • remove:在Channel对象所属的事件循环中,将当前的Channel对象从事件分发器中移除。

    • handleEvent:处理文件描述符上发生的具体事件,根据事件类型调用相应的回调函数。

    • handleEventWithGuard:在保护锁的作用下执行处理事件的具体逻辑。

2、实现原理

void setReadCallback(ReadEventCallback cb) {read_callback_ = std::move(cb);}
void setWriteCallback(Eventcallback cb) {write_callback_ = std::move(cb);}
void setCloseCallback(EventCallback cb) {close_callback_ = std::move(cb);}
void setErrorCallback(EventCallback cb) {error_callback_ = std::move(cb);} 

一个文件描述符会发生可读、可写、关闭、错误事件。当发生这些事件后,就需要调用相应的处理函数来处理。外部通过调用上面这四个函数可以将事件处理函数放进Channel类中,当需要调用的时候就可以直接拿出来调用了。

    // 设置fd相应的事件状态void enableReading() { events_ |= kReadEvent; update(); }void disableReading() { events_ &= ~kReadEvent; update(); }void enableWriting() { events_ |= kWriteEvent; update(); }void disableWriting() { events_ &= ~kWriteEvent; update(); }void disableAll() { events_ = kNoneEvent; update(); }void Channel::update()
{// 通过channel所属的EventLoop,调用poller的相应方法,注册fd的events事件loop_->updateChannel(this);
}// 在channel所属的EventLoop中, 把当前的channel删除掉
void Channel::remove()
{loop_->removeChannel(this);
}

相应的事件状态,启用或禁用读写。 当改变channel所表示fd的events事件后,update负责在poller里面更改fd相应的事件epoll_ctl

// fd得到poller通知以后,处理事件的
void Channel::handleEvent(Timestamp receiveTime)
{if (tied_){std::shared_ptr<void> guard = tie_.lock();if (guard){handleEventWithGuard(receiveTime);}}else{handleEventWithGuard(receiveTime);}
}// 根据poller通知的channel发生的具体事件, 由channel负责调用具体的回调操作
void Channel::handleEventWithGuard(Timestamp receiveTime)
{LOG_INFO("channel handleEvent revents:%d\n", revents_);if ((revents_ & EPOLLHUP) && !(revents_ & EPOLLIN)){if (closeCallback_){closeCallback_();}}if (revents_ & EPOLLERR){if (errorCallback_){errorCallback_();}}if (revents_ & (EPOLLIN | EPOLLPRI)){if (readCallback_){readCallback_(receiveTime);}}if (revents_ & EPOLLOUT){if (writeCallback_){writeCallback_();}}
}

当调用epoll_wait()后,可以得知事件监听器上哪些Channel(文件描述符)发生了哪些事件,事件发生后自然就要调用这些Channel对应的处理函数。 Channel::HandleEvent,让每个发生了事件的Channel调用自己保管的事件处理函数。每个Channel会根据自己文件描述符实际发生的事件(通过Channel中的revents_变量得知)和感兴趣的事件(通过Channel中的events_变量得知)来选择调用read_callback_和/或write_callback_和/或close_callback_和/或error_callback_。

二、EPollPoller类

EPollPoller类是对epoll的一个封装,EpollPoller是封装了用epoll方法实现的与事件监听有关的各种方法,

1、实现原理

EPollPoller::EPollPoller(EventLoop *loop): Poller(loop), epollfd_(::epoll_create1(EPOLL_CLOEXEC)), events_(kInitEventListSize)  // vector<epoll_event>
{if (epollfd_ < 0){LOG_FATAL("epoll_create error:%d \n", errno);}
}

构造函数 EPollPoller::EPollPoller(EventLoop *loop),其中调用了 epoll_create1 创建了一个 epoll 描述符,并将初始事件列表 events_ 初始化为一定大小的 vector<epoll_event>


Timestamp EPollPoller::poll(int timeoutMs, ChannelList *activeChannels)
{// 实际上应该用LOG_DEBUG输出日志更为合理// LOG_INFO("func=%s => fd total count:%lu \n", __FUNCTION__, channels_.size());int numEvents = ::epoll_wait(epollfd_, &*events_.begin(), static_cast<int>(events_.size()), timeoutMs);int saveErrno = errno;Timestamp now(Timestamp::now());if (numEvents > 0){LOG_INFO("%d events happened \n", numEvents);fillActiveChannels(numEvents, activeChannels);if (numEvents == events_.size()){events_.resize(events_.size() * 2);}}else if (numEvents == 0){LOG_DEBUG("%s timeout! \n", __FUNCTION__);}else{if (saveErrno != EINTR){errno = saveErrno;LOG_ERROR("EPollPoller::poll() err!");}}return now;
}

这个函数可以说是Poller的核心了,当外部调用poll方法的时候,该方法底层其实是通过epoll_wait获取这个事件监听器上发生事件的fd及其对应发生的事件,我们知道每个fd都是由一个Channel封装的,通过哈希表channels_可以根据fd找到封装这个fd的Channel。将事件监听器监听到该fd发生的事件写进这个Channel中的revents成员变量中。然后把这个Channel装进activeChannels中(它是一个vector<Channel>)。这样,当外界调用完poll之后就能拿到事件监听器的监听结果也就是被激活的Channe(activeChannels_),这个activeChannels就是事件监听器监听到的发生事件的fd,以及每个fd都发生了什么事件。*


// 填写活跃的连接
void EPollPoller::fillActiveChannels(int numEvents, ChannelList *activeChannels) const
{for (int i=0; i < numEvents; ++i){Channel *channel = static_cast<Channel*>(events_[i].data.ptr);channel->set_revents(events_[i].events);activeChannels->push_back(channel); // EventLoop就拿到了它的poller给它返回的所有发生事件的channel列表了}
}

通过遍历 events_ 列表,其中存储了通过 epoll_wait 系统调用返回的发生事件的信息。对于每一个发生事件的索引,它首先取出对应的 Channel 对象(events_[i].data.ptr),并将该事件的事件类型(events_[i].events)设置给该通道的 revents 成员。这个函数的作用是在事件触发后,将触发事件的通道添加到活跃通道列表中,以便上层的 EventLoop 使用这个列表进行进一步的处理。


// 更新channel通道 epoll_ctl add/mod/del
void EPollPoller::update(int operation, Channel *channel)
{epoll_event event;bzero(&event, sizeof event);int fd = channel->fd();event.events = channel->events();event.data.fd = fd; event.data.ptr = channel;if (::epoll_ctl(epollfd_, operation, fd, &event) < 0){if (operation == EPOLL_CTL_DEL){LOG_ERROR("epoll_ctl del error:%d\n", errno);}else{LOG_FATAL("epoll_ctl add/mod error:%d\n", errno);}}
}

封装epoll_ctl,通过cpoll_ctl对指定的socket进行操作,如add/mod/del (muduo中由Epoller类对Channel类进行操作)

二、EventLoop类

Poller是封装了和事件监听有关的方法和成员,调用一次Poller::poll方法它就能给你返回事件监听器的监听结果(activeChannels)(发生事件的fd 及其 发生的事件)。作为一个网络服务器,需要有持续监听、持续获取监听结果、持续处理监听结果对应的事件的能力,也就是我们需要循环的去 【调用Poller:poll方法获取实际发生事件的Channel集合,然后调用这些Channel里面保管的不同类型事件的处理函数(调用Channel::HandlerEvent方法)。
在这里插入图片描述

1、功能实现


// 开启事件循环
void EventLoop::loop()
{looping_ = true;quit_ = false;LOG_INFO("EventLoop %p start looping \n", this);while(!quit_){activeChannels_.clear();// 监听两类fd   一种是client的fd,一种wakeupfdpollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_);for (Channel *channel : activeChannels_){// Poller监听哪些channel发生事件了,然后上报给EventLoop,通知channel处理相应的事件channel->handleEvent(pollReturnTime_);// std::thread t([&] {//     channel->handleEvent(pollReturnTime_);// });// 其他操作...// t.join(); // t.detach(); // Task task(OnMessage, num);// // cout << "addTask " << endl;// threadPool.addTask(task);}// 执行当前EventLoop事件循环需要处理的回调操作/*** IO线程 mainLoop accept fd《=channel subloop* mainLoop 事先注册一个回调cb(需要subloop来执行)    wakeup subloop后,执行下面的方法,执行之前mainloop注册的cb操作*/ doPendingFunctors();}LOG_INFO("EventLoop %p stop looping. \n", this);looping_ = false;
}

这个线程其实就在一直执行这个函数里面的while循环,这个while循环的大致逻辑比较简单。就是调用Poller::poll方法获取事件监听器上的监听结果。接下来在loop里面就会调用监听结果中每一个Channel的处理函数HandlerEvent( )。每一个Channel的处理函数会根据Channel类中封装的实际发生的事件,执行Channel类中封装的各事件处理函数。(比如一个Channel发生了可读事件,可写事件,则这个Channel的HandlerEvent( )就会调用提前注册在这个Channel的可读事件和可写事件处理函数,又比如另一个Channel只发生了可读事件,那么HandlerEvent( )就只会调用提前注册在这个Channel中的可读事件处理函数)

SubReactorde的唤醒操作

我们先来看看eventloop的loop是如何执行的。循环体中进行这几件事情:
1、使用poller_对象进行轮询,等待事件发生;
2、将触发事件的Channel对象存储在activeChannels_容器中。
3、遍历activeChannels_容器中的每个Channel对象;处理活跃的Channel事件:
4、调用doPendingFunctors()方法,执行当前EventLoop事件循环需要处理的回调操作。
这些回调操作通常是由其他线程通过runInLoop()函数添加到当前EventLoop的任务队列中的。
doPendingFunctors中包含了添加channel或者其他的一些操作,但是
如果没有唤醒操作,下面poll这一步就会永远阻塞,没有活跃的channel就执行不了下面的doPendingFunctors回调函数列表。

所以eventloop要进行添加channel或者其他的一些事件循环需要处理的回调操作的时候就需要唤醒,然后才能执行doPendingFunctors回调

这一部分之前已经详细介绍过了
深度解析Muduo库中的SubReatcor唤醒操作【万字解读】

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/145019.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

如何定时备份使用Docker构建的MySQL容器中的数据库

&#x1f468;&#x1f3fb;‍&#x1f4bb; 热爱摄影的程序员 &#x1f468;&#x1f3fb;‍&#x1f3a8; 喜欢编码的设计师 &#x1f9d5;&#x1f3fb; 擅长设计的剪辑师 &#x1f9d1;&#x1f3fb;‍&#x1f3eb; 一位高冷无情的编码爱好者 大家好&#xff0c;我是 DevO…

多维时序 | MATLAB实现WOA-CNN-GRU-Attention多变量时间序列预测(SE注意力机制)

多维时序 | MATLAB实现WOA-CNN-GRU-Attention多变量时间序列预测&#xff08;SE注意力机制&#xff09; 目录 多维时序 | MATLAB实现WOA-CNN-GRU-Attention多变量时间序列预测&#xff08;SE注意力机制&#xff09;预测效果基本描述模型描述程序设计参考资料 预测效果 基本描述…

【考研数学】概率论与数理统计 —— 第三章 | 二维随机变量及其分布(1,二维连续型和离散型随机变量基本概念与性质)

文章目录 引言一、二维随机变量及分布1.1 基本概念1.2 联合分布函数的性质 二、二维离散型随机变量及分布三、多维连续型随机变量及分布3.1 基本概念3.2 二维连续型随机变量的性质 写在最后 引言 隔了好长时间没看概率论了&#xff0c;上一篇文章还是 8.29 &#xff0c;快一个…

【IDEA】IDEA 单行注释开头添加空格

操作 打开 IDEA 的 Settings 对话框&#xff08;快捷键为CtrlAltS&#xff09;&#xff1b;在左侧面板中选择Editor -> Code Style -> Java&#xff1b;在右侧面板中选择Code Generation选项卡&#xff1b;将Line comment at first column选项设置为false使注释加在行开…

如何设置代理ip服务器地址

在今天的互联网环境中&#xff0c;代理服务器在保护个人隐私和规避网络限制方面扮演着重要的角色。设置代理服务器地址的方式主要取决于你使用的具体软件或编程语言。在本文中&#xff0c;我们将分别介绍如何在Python和Java中使用HTTP代理服务器、SOCKS代理服务器以及代理池。 …

数据结构:堆的简单介绍

目录 堆的介绍:(PriorityQueue) 大根堆:根节点比左右孩子节点大 小根堆:根节点比左右孩子节点小 堆的存储结构: 为什么二叉树在逻辑上用满二叉树结构,而不是普通二叉树呢? 因为如果是普通二叉树会造成资源的浪费​编辑 堆的介绍:(PriorityQueue) 堆又称优先级队列,何为优先…

RocketMQ —消费者负载均衡

消费者从 Apache RocketMQ 获取消息消费时&#xff0c;通过消费者负载均衡策略&#xff0c;可将主题内的消息分配给指定消费者分组中的多个消费者共同分担&#xff0c;提高消费并发能力和消费者的水平扩展能力。本文介绍 Apache RocketMQ 消费者的负载均衡策略。 背景信息​ …

maven中relativepath标签的含义

一 relative标签的含义 1.1 作用 这个<parent>下面的<relativePath>属性&#xff1a;parent的pom文件的路径。 relativePath 的作用是为了找到父级工程的pom.xml;因为子工程需要继承父工程的pom.xml文件中的内容。然后relativePath 标签内的值使用相对路径定位…

【切片】基础不扎实引发的问题

本次文章主要是来聊聊关于切片传值需要注意的问题&#xff0c;如果不小心&#xff0c;则很容易引发线上问题&#xff0c;如果不够理解&#xff0c;可能会出现奇奇怪怪的现象 问题情况&#xff1a; 小 A 负责一个模块功能的实现&#xff0c;在调试代码的时候可能不仔细&#x…

electron之快速上手

前一篇文章已经介绍了如何创建一个electron项目&#xff0c;没有看过的小伙伴可以去实操一下。 接下来给大家介绍一下electron项目的架构是什么样的。 electron之快速上手 electron项目一般有两个进程&#xff1a;主进程和渲染进程。 主进程&#xff1a;整个项目的唯一入口&…

【Java】复制数组的四种方式

1. System.arraycopy() 用来将一个数组的&#xff08;一部分&#xff09;内容复制到另一个数组里面去。 定义&#xff1a; void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);例&#xff1a; int[] arr1 { 1, 2, 3, 4, 5 }; int[] arr2 new…

SpringBoot全局异常处理源码

SpringBoot全局异常处理源码 一、SpringMVC执行流程二、SpringBoot源码跟踪三、自定义优雅的全局异常处理脚手架starter自定义异常国际化引入封装基础异常封装基础异常扫描器&#xff0c;并注册到ExceptionHandler中项目分享以及改进点 一、SpringMVC执行流程 今天这里叙述的全…

基于Java的药品管理系统设计与实现(源码+lw+部署文档+讲解等)

文章目录 前言具体实现截图论文参考详细视频演示为什么选择我自己的网站自己的小程序&#xff08;小蔡coding&#xff09;有保障的售后福利 代码参考源码获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作…

Java微信分享接口开发

概述 微信JS-SDK实现自定义分享功能&#xff0c;分享给朋友&#xff0c;分享到朋友圈 详细 概述 概述 微信公众平台开始支持前端网页&#xff0c;大家可能看到很多网页上都有分享到朋友圈&#xff0c;关注微信等按钮&#xff0c;点击它们都会弹出一个窗口让你分享和关注&…

前端uniapp防止页面整体滑动页面顶部以上,设置固定想要固定区域宽高

解决&#xff1a;设置固定想要固定区域宽高 目录 未改前图未改样式改后图改后样式 未改前图 未改样式 .main {display: flex;flex-direction: row;// justify-content: space-between;width: 100vw;// 防止全部移动到上面位置&#xff01;&#xff01;&#xff01;&#xff01…

【C++的OpenCV】第十三课-OpenCV基础强化(一):绝对有用!Mat相关的一系列知识(基础->进阶)

&#x1f389;&#x1f389;&#x1f389; 欢迎各位来到小白 p i a o 的学习空间&#xff01; \color{red}{欢迎各位来到小白piao的学习空间&#xff01;} 欢迎各位来到小白piao的学习空间&#xff01;&#x1f389;&#x1f389;&#x1f389; &#x1f496;&#x1f496;&…

在nodejs中如何防止ssrf攻击

在nodejs中如何防止ssrf攻击 什么是ssrf攻击 ssrf&#xff08;server-side request forgery&#xff09;是服务器端请求伪造&#xff0c;指攻击者能够从易受攻击的Web应用程序发送精心设计的请求的对其他网站进行攻击。(利用一个可发起网络请求的服务当作跳板来攻击其他服务)…

C++核心编程--继承篇

4.6、继承 继承是面向对象三大特征之一 有些类与类之间存在特殊的关系&#xff0c;例如下图中&#xff1a; ​ 我们发现&#xff0c;定义这些类的定义时&#xff0c;都拥有上一级的一些共性&#xff0c;还有一些自己的特性。那么我们遇到重复的东西时&#xff0c;就可以考虑使…

大数据Flink(八十九):Temporal Join(快照 Join)

文章目录 Temporal Join(快照 Join) Temporal Join(快照 Join) Temporal Join 定义(支持 Batch\Streaming):Temporal Join 在离线的概念中其实是没有类似的 Join 概念的,但是离线中常常会维护一种表叫做 拉链快照表,使用一个明细表去 join 这个 拉链快照表 的 join …

GD32F10x的输出模式

1. 单片机型号的识别。 2. GPIO的输出模式。 1. 开漏模式 2.推挽模式 3.复用开漏模式 4.复用推挽模式。 开漏模式&#xff1a;&#xff08;写入位设置&#xff0c;输出数据寄存器来控制MOS&#xff09; 只有N-MOS管导通。PMOS不导通。 当N-MOS的栅极为0&#xff0c;N-MOS管…