在上一节中,我们添加了Channel类,这已经进入到开始实现Reactor模式。这时我们为添加到epoll上的文件描述符都添加了一个Channel。每个Channel都可以拥有自己的回调函数,即用户可以按照自己的想法去往epoll中注册事件,之后可以根据不同的事件类型调用指定的回调函数。
但上一章节还没有实现增添回调函数和调用回调函数,这一节来完成其实现。
EventLoop类
来看看上一章节的main函数中的关键部分。
int main(){
........
Epoll poll;
while (1){
vector<Channel*> active;
nums=poll.Epoll_wait(active);
for (int i = 0; i < active.size();; ++i) {
// 循环所有事件,并运行他们
........
}
}
}
显然,重点就是while(1)循环这部分了。这部分我们该怎么把它封装好呢?
看到是Epoll相关的,那很容易想到,把这个循环封装在Epoll类中。
假如说封装在Epoll类中,那来看个情况,这也是很常见的情况(比如redis),在epoll_wait前后处理一些事情,这些事情可以由用户设置的。要是这个封装在Epoll类中,就会让Epoll不伦不类,功能散乱。
while (1){
beforeEvent(); //在epoll_wait开始前处理某些事
nums=poll.Epoll_wait(active);
for (int i = 0; i < active.size();; ++i) {
// 循环所有事件,并运行他们
........
}
afterEvent();//在epoll_wait结束后处理某些事
}
而且Epoll就是负责处理网络IO,就只处理这部分就行。
所以我们需要再封装一个类EventLoop(事件循环),这也很符合while(1)循环内部的内容。
我们可以认为
类EventLoop是Epoll的进一层封装,
后续我们的版本中就会很少直接接触到Epoll类。
一个EventLoop类对象只有一个Epoll类对象。
class EventLoop
{
public:
using channelList=std::vector<Channel*>;
public:
EventLoop();
~EventLoop();
void loop();
void updateChannel(Channel *ch);
void removeChannel(channel* ch);
private:
Epoll* ep_;
channelList activeChannels_; //保存当前活跃事件的Channel列表
};
EvenLoop::loop()的实现
这是一个while(1)循环,也是整体的核心部分。
void EventLoop::loop()
{
while(1){
activeChannels_.clear(); //清空上一轮还剩余的channel
ep_->Epoll_wait(activeChannels_);
for (auto& active : activeChannels_) {
active->handleEvent(); //执行各自的回调函数
}
}
}
void EventLoop::updateChannel(Channel* channel)
{
ep_->updateChannel(channel);
}
void EventLoop::removeChannel(Channel* channel)
{
ep_->del(channel);
}
EventLoop::
updateChannel(Channel *ch)函数就是调用了成员变量ep_的updateChannel(Channel *ch)函数,这样是为了方便EventLoop类的使用。
Channel class的修改
一个Channel类对象对应一个客户端或者服务端(服务器的监听fd),那该Channel类对象会调用哪一种handleEvent()呢。
假如是服务器的监听fd被激活,那handleEvent()就是要进行accept(),建立连接;
若是客户端的fd被激活,那handleEvent()就是要进行read()/write()。
所以需要提前设置好,也就是要有设置回调函数setCallback(const func& cb)。
添加设置回调函数setCallback(const func& cb)和执行回调函数handleEvent()。详细的参看源代码。
class Channel
{
public:
using ReadEventCallback = std::function<void()>;
public:
//Channel(Epoll* ep, int fd);
Channel(EventLoop* loop, int fd); //构造函数和之前的不一样了,这里使用了EventLoop,不使用Epoll
void setEvents(int events);
int Event()const;
void setRevents(int events);
int Revent()const;
void enableReading();
bool isInEpoll();
void setInEpoll(bool in);
int Fd()const;
//添加的
void SetCallback(ReadEventCallback cb) { readCallback_ = std::move(cb); }
void handleEvent();//Channel的执行事件
private:
//Epoll* ep_;
EventLoop* loop_;
int fd_;
int events_;
int revents_;
bool isInEpoll_;
ReadEventCallback readCallback_; //读回调函数
};
//目前只有读回调函数
void Channel::handleEvent()
{
if (readCallback_) {
readCallback_();
}
}
最终所有的回调函数是在handleEvent()内执行,而这个handleEvent()会在EventLoop::loop()内被调用。
好了,有了 设置回调函数 后,谁去调用它呢?
可以让接下来的Server类去调用。
Server类
而在用户写代码使用层面,也不想再写bind(),listen()等操作,我们就需要再进行一个整体的抽象,可以把整个服务器抽象成Server类,里面会有一个核心EventLoop。
可以把Server类想象成一个管理员,他管理着所有的客户端连接。
class Server
{
public:
Server(const InetAddr& listenAddr,EventLoop* loop);
~Server();
void handleReadEvent(Channel* channel); //这里面就是一些read()/write()操作
void newConnection(Socket *serv_sock);
private:
EventLoop* loop_;
Socket* serv_socket_;
Channel* serv_channel_;
};
先看看我们的整体用法,用法和muduo的已经很相似了,用户使用时可以不用考虑太多细节。
EventLoop loop;
InetAddr servAddr(10000);
Server server(servAddr,&loop);
loop.loop();
Server构造函数
那些bind(),listen()操作去哪了呢,这些都在Server类的构造函数中了,用户使用的时候就不需要再写这些操作了。
Server::Server(const InetAddr& listenAddr, EventLoop* eventloop)
:loop_(eventloop) //绑定一个eventloop,也即是绑定了一个epoll实例
,serv_socket_(new Socket) //创建fd,调用了socket()
{
serv_socket_->bind(listenAddr); //bind()
serv_socket_->listen(); //listen()
serv_socket_->setNonblock(); //设置fd为非阻塞
serv_channel_=new Channel(loop_, serv_socket_->fd());//创建一个channel,也绑定了loop和fd
//设置回调函数
auto cb = [this](){newConnection(serv_socket_); };
serv_channel_->SetCallback(cb); //当监听fd被激活,就执行回调函数,即是执行newConnection()
serv_channel_->enableReading(); //把监听fd添加epoll实例中
}
这里还有个重点:是在Server构造函数内部调用了 设置回调函数。
Server的newConnection(Socket*)
当loop()中的ep->Epoll_wait(activeChannels_)返回活跃的channel,当其中有监听的channel后,就执行回调函数,即是执行newConnection()。那newConnection()会主要执行什么操作呢,很明显,那就是使用accpet()进行建立新连接。
void Server::newConnection(Socket* serv_sock)
{
InetAddr cliaddr;
Socket* cli_socket = new Socket(serv_sock->accept(&cliaddr));//这版本没有delete,会内存泄漏,之后版本会修改
cli_socket->setNonblock();
//创建一个新用户对应的channel
Channel* channel = new Channel(loop_, cli_socket->fd()); //这版本没有delete,会内存泄漏,之后版本会修改
//设置客户端的回调函数
auto cb = [this,channel]() {handleEvent(channel); };
channel->SetCallback(cb); //客户端的fd被激活,就会执行回调函数,即是执行handleEvent()
channel->enableReading();
}
Server类中设置回调函数的两处地方很重要,要理解这两个设置回调的操作。
只有先设置好的回调函数,那被激活的channel才能执行对应自己的handleEvent()函数。
Server::newConnection(Socket *serv_sock)会绑定到监听的channel(服务器端)对应的Channel上,而Server::hanleReadEvent(Channel* ch)会绑定到连接成功的客户端Channel上。(这里绑定的函数使用c++11/14的lambda表达式)
主要的逻辑流程
在当前的版本,我们只有一个EventLoop。在调用函数loop()中,当有活跃的文件描述符时,我们会拿到对应的Channel,并会调用该Channel内对应的回调函数。在我们新建Channel对象时,我们会设置好对应的回调函数。
当监听的文件描述符有可读事时,handleEvent()就会执行newConnection()进行连接,而当客户端的文件描述符有可读事件时,handleEvent()就会执行hanleReadEvent()进行通信。
有了IO多路复用,为什么还需要reactor模式?
IO多路复用与事件驱动
首先要明确一点,reactor模式就是基于IO多路复用的。事件驱动也是IO多路复用的,不是说使用了reactor模式才是使用了事件驱动。
以事件为连接点,当有IO事件准备就绪时,就会通知用户,并且告知用户返回的是什么类型的事件,进而用户可以执行相对应的任务。这样就不用在IO等待上浪费资源,这便是事件驱动的核心思想。
比如你点了两份外卖,外卖A,外卖B。之后你无需时刻打电话去问外卖到了没。外卖到的时候,外卖员会打电话通知你。这中途你就可以做自己的事,不用纯纯等待。还有可以知道是外卖A到了还是外卖B到了,外卖员会告知是哪个外卖到的。
这个就是事件驱动。IO事件准备就绪时,会自动通知用户,并会告知其事件类型。
所以应该是,IO多路复用 + 回调机制 构成了 reactor模式。
IO同步与异步的判断
还有reactor模式是同步的。因为其是使用IO多路复用的,而IO多路复用是同步的。
IO操作是同步还是异步,关键看数据在内核空间与用户空间的拷贝过程(数据读写的IO操作)
reactor模式的优点
网上很多说其可以很好处理高并发,但是我觉得IO多路复用也可以处理的。
还有说可扩展性,通过增加Reactor实例个数来充分利用CPU资源;那通过在其他线程再创建epoll也行的。
所以,我觉得一个很大的优势是:
- 应用处理请求的逻辑,与事件分发框架完全分离,即是解耦,有很高的复用性
写应用处理时,只需关注如何处理业务逻辑。Reactor框架本身与具体事件处理逻辑无关。
假如是把reactor模式做成一个网络库给用户使用。那用户就只需要关注处理请求的逻辑即可。该网络库对外开放setCallback函数。
假设,用户写服务器服务,收到客户端发来的数据(一串数字),想对数字做加法 或者想对数字做乘法都行。只要使用setCallback函数设置好处理请求的逻辑就行。这就是应用处理请求的逻辑,与事件分发框架完全分离,这是很方便的。
当前代码的需要继续改进之处
这次,我们封装了EventLoop类和Server类,也使用了回调函数。这里已经是构成了Reactor模式的核心内容。但是我们与客户端进行通信的业务逻辑还在Server类内,这就不符合我们的抽象,Server类还不能进行复用,这将在接下来中修改。
完整源代码:https://github.com/liwook/CPPServer/tree/main/code/server_v6