6.添加类EventLoop,增添回调函数

在上一节中,我们添加了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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值