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