webserver服务器从零搭建到上线(七)|Channel通道类和Poller抽象类

TcpServer是我们整个编写服务器的入口,其中有一个很重要的类:EventLoop事件分发器。

其实我们就可以把EventLoop当做我们的epoll_wait,它主要管理类一个Poller类,我们看名字就可以知道,Poller类应该封装了Epoll本身;然后还有一个重要的类就是Channel类。

我们知道想要使用epoll必须得设置sockfd和对该fd感兴趣的事件events以及实际发生的事件revents,Channel类就是用来封装这些fd和事件的。

所以本节,我们主要来实现Channel类和Poller抽象类。

文章目录

  • Channel类
    • 私有成员属性
      • std::weak_ptr<void> tie_; 和 bool tied_;
    • 成员方法
    • 具体实现
      • void Channel::update()
      • void Channel::tie(const std::shared_ptr<void> &obj)
      • handleEvent()与handleEventWithGuard()
  • Poller类
    • 1.数据成员
    • 2.类型定义和构造函数
    • 3. 通用接口函数(纯虚函数)
    • 4.辅助方法
    • 各方法具体实现

Channel类

class Channel : noncopyable {
public:Channel(EventLoop *loop, int fd);~Channel();
};

这里为什么要在Channel的构造函数中,写EventLoop呢?其实就是因为每一个Channel都有它自己的EventLoop,这样Channel才能起作用嘛,毕竟它的sockfd和相关事件给谁来使用呢?


私有成员属性

private://当改变channel所表示fd的事件后,update负责在在poller里更改fd相应的事件//其实就像是epoll_ctl,EventLoop=>ChannelList、Poller//所以Channel想调用Poller的方法肯定要依靠eventLoop,所以写完该方法再回来写。void update();//根据Poller通知的channel发生的具体时间,由channel负责调用具体的回调操作void handleEventWithGuard(Timestamp receiveTime);//这是设置静态成员变量就是为了封装poll和epoll的宏//它们应该在源文件中进行具体定义static const int kNoneEvent;static const int kReadEvent;static const int kWriteEvent;EventLoop *loop_; //事件循环const int fd_;    //fd, Poller监听的对象int events_;      //注册fd感兴趣的事件int revents_;    //Poller返回的具体发生的事件int index_;     //由Poller使用,看本channel的fd是待添加还是已添加还是已删除std::weak_ptr<void> tie_;bool tied_;//因为channel通道里面能够获知fd最终发生的具体的事件revents,所以它负责调用具体时间的回调操作ReadEventCallBack readCallback_;EventCallback writeCallback_;EventCallback closeCallback_;EventCallback errorCallback_;
};

在这里只强调两个事情一个是智能指针的使用。

std::weak_ptr tie_; 和 bool tied_;

这两个成员变量的使用,主要是为了防止在执行回调函数时,Channel对象或其相关资源已经被销毁,从而引发未定义行为或崩溃。具体来说,两个变量的作用如下:

std::weak_ptr tie_:

tie_是一个弱引用指针(std::weak_ptr),它不影响所监控对象的生命周期。
主要用于与某个std::shared_ptr对象绑定,从而能够检查这个对象是否已经被销毁
通过使用tie_,可以在执行回调之前检测所绑定的对象是否仍然存在。如果对象已经被销毁,则不执行回调,从而避免了使用已经销毁的对象引发的问题。

关于 std::weak_ptr 的作用,可以查看这篇文章对于「多线程访问共享对象问题」的描述

bool tied_:

tied_是一个标志位,用来指示当前的Channel对象是否已经与某个对象绑定(tie)。
当tied_为true时,表示Channel已经绑定到某个对象,并且在执行回调前需要检查这个对象的状态。

成员方法

//定义两个事件回调
using EventCallback = std::function<void()>;
using ReadEventCallBack = std::function<void(Timestamp)>;//fd得到poller通知以后,处理事件的。调用相应的回调方法来处理事件
void handleEvent(Timestamp receiveTime);//设置回调函数对象
void setReadCallback(ReadEventCallBack cb) { readCallback_ = std::move(cb); }
void setWriteCallback(EventCallback cb) { writeCallback_ = std::move(cb); }
void setCloseCallback(EventCallback cb) { closeCallback_ = std::move(cb); }
void setErrorCallback(EventCallback cb) { errorCallback_ = std::move(cb); }//防止当channel被手动remove掉,channel还在执行回调操作
void tie(const std::shared_ptr<void>&);int fd() const { return fd_; }
int events() const { return events_; }
void set_revents(int revt) { revents_ = revt; } //设置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(); }//返回fd当前的事件状态
bool isNoneEvent() const { return events_ == kNoneEvent; }
bool isWriting() const { return events_ & kWriteEvent; }
bool isReading() const { return events_ & kReadEvent; }int index() { return index_; }
void set_index(int idx) { index_ = idx; }//one loop per thread
EventLoop* ownerLoop() { return loop_; }
void remove(); //应该是删除eventloop所属的这个channel

还是比较简单,大部分还是在类里面就已经给出实现了。

具体实现

const int Channel::kNoneEvent = 0;
const int Channel::kReadEvent = EPOLLIN | EPOLLPRI;
const int Channel::kWriteEvent = EPOLLOUT;//EventLoop :包含了很多个Channel ,即ChannelList和Poller
Channel::Channel(EventLoop *loop, int fd): loop_(loop), fd_(fd) , events_(0), revents_(0), index_(-1), tied_(false) {}Channel::~Channel() {}// Channel的tie方法什么时候调用过?
void Channel::tie(const std::shared_ptr<void> &obj) {tie_ = obj;tied_ = true;
}/*
当改变channel所表示fd的events事件后,update负责在poller
里面更改fd相应的事件,也就是通过epoll_ctl
*/
void Channel::update()
{// 通过channel所属的EventLoop,调用poller的相应方法,注册fd的events事件loop_->updateChannel(this);
}// 在channel所属的EventLoop中, 把当前的channel删除掉
void Channel::remove()
{loop_->removeChannel(this);
}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", 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_();}
}

源文件具体内容入上文所示,这里主要讲解四个成员方法:void Channel::update()void Channel::tie(const std::shared_ptr<void> &obj)void Channel::handleEvent(Timestamp receiveTime)void Channel::handleEventWithGuard(Timestamp receiveTime)

void Channel::update()

这里的update,我们见名知其意,当Channel对象表示的文件描述符(fd)的事件(events)发生改变时,通知其所属的EventLoop,从而在Poller中更新对应的事件注册状态。它是将Channel对象当前感兴趣的事件状态(例如读、写事件)同步到Poller中的关键步骤。

void Channel::tie(const std::shared_ptr &obj)

tie方法用于将Channel对象与某个对象绑定,这个方法最有可能在创建Channel对象并设置回调函数之后调用,具体时机通常是在将Channel对象加入到事件循环之前。

  1. 新连接建立时:
    当一个新连接被接受(例如,在服务器端接受一个客户端连接时),会创建一个新的Channel对象来管理该连接的文件描述符(fd)。在设置各种事件回调函数(如读、写、关闭、错误回调)之后,调用tie方法将Channel与连接对象绑定,以确保在处理事件时,连接对象是有效的。
  2. 注册事件之前
    在将Channel对象注册到EventLoop的事件循环之前,调用tie方法以确保在事件处理期间,相关对象不会被销毁。通常情况下,在调用EventLoop的updateChannel方法之前,会先调用tie方法进行绑定。

综上所述,我们的TcpConnection类,他表示一个TCP连接,其中包含一个Channel对象。TcpConnection类可能在其构造函数或初始化方法中调用tie方法,将自身与Channel绑定。

handleEvent()与handleEventWithGuard()

void Channel::handleEvent(Timestamp receiveTime) {// 如果Channel对象被绑定了,即tied_为trueif (tied_) {// 尝试提升tie_(std::weak_ptr)到std::shared_ptrstd::shared_ptr<void> guard = tie_.lock();// 如果提升成功,说明绑定的对象还存在,执行handleEventWithGuardif (guard) {handleEventWithGuard(receiveTime);}} else {// 如果Channel对象没有被绑定,直接执行handleEventWithGuardhandleEventWithGuard(receiveTime);}
}

handleEvent 方法首先检查Channel对象是否绑定到某个对象(通过tied_和tie_)。如果绑定,确保绑定对象还存在才处理事件,否则直接处理事件。

handleEventWithGuard 方法根据revents_的值依次处理关闭、错误、读、写事件,并调用相应的回调函数来处理这些事件。

这样分开是有助于提高代码健壮性的,也是大型项目所必需的,我觉得这种处理事件由两个函数来完成的逻辑还是值得我们学习的。

Poller类

Poller是我们多路事件分发器EventLoop的核心IO复用模块。的主要负责在时间循环中监控多个文件描述符的事件,并在事件发生时通知响应的Channel对象,以便处理这些事件。

我们为什么要把Poller类设计为一个抽象基类呢?因为我们想让这个IO复用模块能够使用select、poll、epoll,当然了,我们现在只实现其中的epoll,我们不可能让用户去分别直接调用封装好的这三个类,这就失去OOP的武器了,我们应该把Poller定义为抽象类,通过它来完成接口的调用。

Poller类的主要作用

  1. I/O 事件多路复用:
    Poller类的主要功能是使用操作系统提供的I/O复用机制(如epoll、poll等)来监控多个文件描述符上的事件。它在事件循环中起到核心作用,确保高效地处理大量并发连接。
  2. ⭐️管理Channel对象
    Poller类维护一个从文件描述符到Channel对象的映射(ChannelMap channels_),并通过Channel对象处理具体的I/O事件。Channel封装了文件描述符和事件处理的回调函数。
  3. 接口定义
    Poller类定义了一些纯虚函数(poll、updateChannel、removeChannel),为具体的I/O复用机制提供统一的接口。这些接口由具体的子类实现。

1.数据成员

using ChannelMap = std::unordered_map<int, Channel*>;
ChannelMap channels_;
EventLoop *ownerLoop_;
  • ChannelMap channels_:一个哈希表,键是文件描述符,值是对应的Channel对象。我们之前不是说过了吗,通过Poller类来管理Channel对象,组织形式就是把他们组成一个队列。
  • EventLoop *ownerLoop_:指向Poller所属的事件循环对象。这也对应了我们在写Channel通道类时的描述,Poller和Channel都有自己的一个EventLoop

2.类型定义和构造函数

using ChannelList = std::vector<Channel*>;Poller(EventLoop *loop);
virtual ~Poller() = default;
  • ChannelList:用于存储活跃的Channel对象。
  • Poller(EventLoop *loop):构造函数,接受一个指向EventLoop对象的指针,表示Poller所属的事件循环。

3. 通用接口函数(纯虚函数)

virtual Timestamp poll(int timeoutMs, ChannelList *activeChannels) = 0;
virtual void updateChannel(Channel* channel) = 0;
virtual void removeChannel(Channel* channel) = 0;
  • poll:启动I/O复用机制,等待事件发生,并将活跃的Channel对象添加到activeChannels中。
  • updateChannel:更新或添加一个Channel对象(如注册新的事件或修改现有事件)。
  • removeChannel:从Poller中移除一个Channel对象。

4.辅助方法

//判断参数channel是否在当前Poller当中
bool hasChannel(Channel *channel) const;
//EventLoop可以通过该接口获取默认的IO复用的具体实现
static Poller* newDefaultPoller(EventLoop *Loop);

各方法具体实现

大部分方法都是纯虚函数,所以我们在派生类中会去重写这些函数,现在我们只写Poller类独有的方法

Poller::Poller(EventLoop *loop): ownerLoop_(loop)
{
}bool Poller::hasChannel(Channel *channel) const
{auto it = channels_.find(channel->fd());return it != channels_.end() && it->second == channel;
}

上面的代码也都比较简单,这里不做过多阐述,需要重点讲解的是我们的static Poller* newDefaultPoller(EventLoop *Loop);

muduo网络库中将其写入了一个单独的源文件 DefaultPoller 中,这是为什么呢?

从设计模式的角度出发,static Poller* newDefaultPoller(EventLoop *Loop);是一个工厂函数,负责根据环境变量来创建 Poller 类的具体实现(如EpollPoller或PollPoller)。这个函数与Poller类的核心功能没有直接关系,属于创建对象的逻辑。将这个工厂函数放在一个单独的源文件Default.cc中,可以实现逻辑分离。

#include "Poller.h"
//#include "EPollPoller.h"#include <stdlib.h>Poller* Poller::newDefaultPoller(EventLoop *loop)
{if (::getenv("MUDUO_USE_POLL")) //获取系统的环境变量{return new PollPoller(loop); // 生成poll的实例,这里我们不做实现}else{return new EPollPoller(loop); // 生成epoll的实例}
}

如果系统中没有设置MUDUO_USE_POLL的环境变量,则默认使用epoll。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【服务器报错】Pycharm运行服务器代码提示 can‘t open file “本地文件路径“

1. 问题 Pycharm连接远程服务器&#xff0c;代码已经同步&#xff0c;运行时候报错 #模拟报错 bash: line 0: cd: G:/python/hhh/Hi: No such file or directory /home/hhh/anaconda3/envs/hard/bin/python: cant open file G:/python/hhh/hi/hei.py: [Errno 2] No such file…

顶级域名、主域名、子域名

在互联网上&#xff0c;域名系统(DNS)用于将人类可读的域名(如www.example.com映射到服务器的IP地址 域名可以分为多个部分&#xff0c;通常包括主域名(PrimaryDomain)和子域名(Subdomain)。 顶级域名 是域名系统的最高级别&#xff0c;通常代表特定的组织类型或国家 .com、…

springMVC工作流程

大家好&#xff0c;这里是教授.F 1.浏览器会先发送请求url&#xff0c;前端控制器/中央控制器/分发器&#xff08;也就是DispatcherServlet&#xff09;进行获取。 2.此时前端控制器会调用HandlerMapping处理器映射器&#xff0c;然后返回处理器执行器链&#xff08;HandlerExe…

Android Service 启动流程

在早些年学习Android的时候&#xff0c;对Service有过总结&#xff0c;但是主要是如何去使用&#xff0c;注意事项&#xff0c;startService和bindService的区别。 Android Service_public int onstartcommand(intent intent, int flags-CSDN博客 但是今天从源码来总结下fram…

论文笔记《基于深度学习模型的药物-靶标结合亲和力预测》

基于深度学习模型的药物-靶标结合亲和力预测 这是一篇二区的文章&#xff0c;算是一个综述&#xff0c;记录一下在阅读过程中遇到的问题。 文章目录 基于深度学习模型的药物-靶标结合亲和力预测前言一、蛋白质接触图谱二、为什么蛋白质图谱的准确性对DTA模型预测结果没有影响1…

使用Python类的构造函数和析构函数

1、问题背景 当使用Python类时&#xff0c;可以使用构造函数和析构函数来初始化和清理类实例。构造函数在创建类实例时自动调用&#xff0c;而析构函数在删除类实例时自动调用。 在上面的代码示例中&#xff0c;Person类具有一个构造函数__init__和一个析构函数__del__。构造…

【代码】自定义函数

你有没有听过 n! ? n!n(n-1)(n-2)21 你想不想有一个c函数 jc() 专门用来计算n!&#xff1f; 不好意思&#xff0c;没有&#xff01;c函数库说。没有咱自己可以造呀&#xff01; 哈喽大家好&#xff0c;我是学霸小羊&#xff0c;今天讲讲自定义函数。 自定义函数的定义格式…

网易面试:手撕定时器

概述&#xff1a; 本文使用STL容器-set以及Linux提供的timerfd来实现定时器组件 所谓定时器就是管理大量定时任务&#xff0c;使其能按照超时时间有序地被执行 需求分析&#xff1a; 1.数据结构的选择&#xff1a;存储定时任务 2.驱动方式&#xff1a;如何选择一个任务并执…

CSS,HTML,JS 以及Vue前端面试题八股文总结【看完你就变高手】

■ 符号说明 &#x1f498; 主题 &#x1f31f; 常见重要 &#x1f31b; 需要有印象的 &#x1f195; v3新特性 ■ 杂谈 &#x1f31b; SEO优化 合理的title、description、keywords&#xff1a;搜索对着三项的权重逐个减小&#xff0c;title值强调重点即可&#xff1b;descrip…

东软的第三个研发基地,为什么选择了武汉?

继沈阳、大连之后&#xff0c;东软集团在国内打造的第三个研发基地——武汉东软软件园&#xff0c;于2024年5月25日正式开园。 “占地面积158亩、建筑面积14万余平方米的武汉东软软件园&#xff0c;从开工到竣工仅仅用了18个月的时间。这样的建设速度&#xff0c;充分体现了武汉…

使用XMLHttpRequest对象进行网络请求的步骤

使用XMLHttpRequest对象进行网络请求的基本步骤如下: 1&#xff1a;创建XHR对象: const xhr new XMLHttpRequest();2&#xff1a;初始化请求: xhr.open(method, url, async);method: HTTP请求方法,如 GET, POST, PUT, DELETE 等。 url: 请求的目标URL地址。 async: 是否异步…

2.开发环境介绍

开发环境介绍三种&#xff1a;第一种是在线开发环境、第二种是Windows下的开发环境、第三种是Linux下的开发环境。 1.在线开发环境 2.Windows下的开发环境 用的比较多的是Devc&#xff0c;新手适合使用&#xff0c;上手快&#xff0c;简单&#xff0c;方便。 Devc使用&#x…

蓝桥杯练习系统(算法训练)ALGO-932 低阶行列式计算

资源限制 内存限制&#xff1a;64.0MB C/C时间限制&#xff1a;1.0s Java时间限制&#xff1a;3.0s Python时间限制&#xff1a;5.0s 问题描述 给出一个n阶行列式(1<n<9)&#xff0c;求出它的值。 输入格式 第一行给出两个正整数n,p&#xff1b;   接下来n行&…

Flutter 中的 SizedOverflowBox 小部件:全面指南

Flutter 中的 SizedOverflowBox 小部件&#xff1a;全面指南 在 Flutter 的布局世界中&#xff0c;SizedOverflowBox 是一个相对独特的小部件&#xff0c;它允许子组件溢出其父组件的界限&#xff0c;同时保持父组件的尺寸不变。这在某些特定的布局场景下非常有用&#xff0c;…

软件工程作业6

问题&#xff1a;如果你要开发一个中小学生学习数学的软件你应该找谁去做用户调研&#xff1f; 开发一个针对中小学生的数学学习软件时&#xff0c;进行有效的用户调研至关重要&#xff0c;这能确保产品贴合目标用户的需求和学习习惯。以下是一些适合参与用户调研的对象&#…

简单工厂模式与策略模式的区别

相似之处&#xff1a;都用于松耦合 不同之处&#xff1a; 简单工厂模式&#xff1a;只关注传入的参数&#xff0c;对于后续的具体执行逻辑不关注(工厂会自动根据传入的参数类型生产对应的产品) 核心目的&#xff1a;隐藏创建对象的具体逻辑&#xff0c;客户端只需传入参数&a…

【JavaScript】P2 JavaScript 书写位置

本博文总结&#xff1a; JavaScript 书写位置&#xff1a; 内部外部行内 注意事项&#xff1a; 书写的位置尽量写到 </body> 之前外部 js 标签中间不写任何内容&#xff0c;因为不予以展示 正文&#xff1a; 交互效果示例 一个简单的交互效果示例&#xff1b; <…

【从零开始学习RabbitMQ | 第一篇】如何确保生产者的可靠性

目录 前言&#xff1a; 生产者重连机制&#xff1a; 生产者确认机制&#xff1a; Publisher Confirm&#xff08;生产者者确认&#xff09; Publish Return&#xff08;发布返回&#xff09; 总结&#xff1a; 前言&#xff1a; 在现代的分布式系统中&#xff0c;消息队…

【NumPy】关于numpy.divide()函数,看这一篇文章就够了

&#x1f9d1; 博主简介&#xff1a;阿里巴巴嵌入式技术专家&#xff0c;深耕嵌入式人工智能领域&#xff0c;具备多年的嵌入式硬件产品研发管理经验。 &#x1f4d2; 博客介绍&#xff1a;分享嵌入式开发领域的相关知识、经验、思考和感悟&#xff0c;欢迎关注。提供嵌入式方向…

zabbix监控mysql

一、mysql数据库监控的内容有 mysql的吞吐量 mysql的常规操作&#xff08;增删改查&#xff09; QPS&#xff08;Questions Per second:&#xff09;每秒能处理多少次请求数 TPS&#xff08;Transactions Per Second&#xff09;每秒查询处理的事务数 mysql库大小和表大小 监控…