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

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

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

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

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

文章目录

Channel类

cpp 复制代码
class Channel : noncopyable {
public:
	Channel(EventLoop *loop, int fd);
	~Channel();
};

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


私有成员属性

cpp 复制代码
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已经绑定到某个对象,并且在执行回调前需要检查这个对象的状态。

成员方法

cpp 复制代码
//定义两个事件回调
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

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

具体实现

cpp 复制代码
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()

cpp 复制代码
void Channel::handleEvent(Timestamp receiveTime) {
    // 如果Channel对象被绑定了,即tied_为true
    if (tied_) {
        // 尝试提升tie_(std::weak_ptr)到std::shared_ptr
        std::shared_ptr<void> guard = tie_.lock();
        // 如果提升成功,说明绑定的对象还存在,执行handleEventWithGuard
        if (guard) {
            handleEventWithGuard(receiveTime);
        }
    } else {
        // 如果Channel对象没有被绑定,直接执行handleEventWithGuard
        handleEventWithGuard(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.数据成员

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

2.类型定义和构造函数

cpp 复制代码
using ChannelList = std::vector<Channel*>;

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

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

cpp 复制代码
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.辅助方法

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

各方法具体实现

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

cpp 复制代码
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中,可以实现逻辑分离。

cpp 复制代码
#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。

相关推荐
2201_7611990437 分钟前
nginx 负载均衡1
linux·运维·服务器·nginx·负载均衡
suri ..42 分钟前
【Linux】进程第三弹(虚拟地址空间)
linux·运维·服务器
害羞的白菜42 分钟前
Nginx基础详解5(nginx集群、四七层的负载均衡、Jmeter工具的使用、实验验证集群的性能与单节点的性能)
linux·运维·笔记·jmeter·nginx·centos·负载均衡
纪伊路上盛名在42 分钟前
如何初步部署自己的服务器,达到生信分析的及格线
linux·运维·服务器·python·学习·r语言·github
木向1 小时前
leetcode42:接雨水
开发语言·c++·算法·leetcode
爱滑雪的码农1 小时前
快速熟悉Nginx
运维·nginx·dubbo
sukalot1 小时前
windows C++-创建基于代理的应用程序(下)
c++
labuladuo5201 小时前
AtCoder Beginner Contest 372 F题(dp)
c++·算法·动态规划
0DayHP1 小时前
HTB:Bike[WriteUP]
运维·服务器