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_;](#std::weak_ptr<void> tie_; 和 bool tied_;)
- 成员方法
- 具体实现
-
- [void Channel::update()](#void Channel::update())
- [void Channel::tie(const std::shared_ptr<void> &obj)](#void Channel::tie(const std::shared_ptr<void> &obj))
- handleEvent()与handleEventWithGuard()
- Poller类
-
- 1.数据成员
- 2.类型定义和构造函数
- [3. 通用接口函数(纯虚函数)](#3. 通用接口函数(纯虚函数))
- 4.辅助方法
- 各方法具体实现
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对象加入到事件循环之前。
- 新连接建立时:
当一个新连接被接受(例如,在服务器端接受一个客户端连接时),会创建一个新的Channel对象来管理该连接的文件描述符(fd)。在设置各种事件回调函数(如读、写、关闭、错误回调)之后,调用tie方法将Channel与连接对象绑定,以确保在处理事件时,连接对象是有效的。 - 注册事件之前 :
在将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类的主要作用
- I/O 事件多路复用:
Poller类的主要功能是使用操作系统提供的I/O复用机制(如epoll、poll等)来监控多个文件描述符上的事件。它在事件循环中起到核心作用,确保高效地处理大量并发连接。- ⭐️管理Channel对象 :
Poller类维护一个从文件描述符到Channel对象的映射(ChannelMap channels_),并通过Channel对象处理具体的I/O事件。Channel封装了文件描述符和事件处理的回调函数。- 接口定义 :
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。