day6-事件驱动
问题1:什么是事件驱动?
之前的代码中,我们发现最主要的一直发生变化的就是服务器,服务器最主要的工作是监听epoll上的事件并对不同事件做出不同响应,这就是事件驱动。
问题2:事件驱动有哪些模式?(这里的回答借鉴于https://www.zhihu.com/question/26943938/answer/1856426252)
主要分为Reactor模式和Proactor模式,这里主要说Reactor,Proactor在链接里有讲。Reactor模式是在 I/O 多路复用监听事件,收到事件后,根据事件类型分配给某个进程 / 线程。
单 Reactor 单进程 / 线程(day5之前的代码都是这个样子)
Reactor 对象通过 select (IO 多路复用接口) 监听事件,收到事件后通过 dispatch 进行分发,如果是连接事件则分发到Acceptor 处理,如果是其他事件则分发到Handler 处理。
这种方案无法充分利用 多核 CPU 的性能;而且Handler 对象在业务处理时,整个进程是无法处理其他连接的事件的。
单 Reactor 多线程 / 多进程
克服「单 Reactor 单线程 / 进程」方案的缺点
Reactor 对象通过 select (IO 多路复用接口) 监听事件,收到事件后通过 dispatch 进行分发,如果是连接事件则分发到Acceptor 处理,如果是其他事件则分发到Handler 。
然而这里的Handler 不再负责处理事件,只负责数据的接收和发送,子线程里的 Processor 对象就进行业务处理,处理完后,将结果发给主线程中的 Handler 对象,接着由 Handler 通过 send 方法将响应结果发送给 client。
然而Reactor 对象承担所有事件的监听和响应,而且只在主线程中运行,在面对瞬间高并发的场景时,容易成为性能的瓶颈的地方。
多 Reactor 多进程 / 线程
主线程中的 MainReactor 对象通过 select 监控连接建立事件,如果是建立连接的事件则通过Acceptor 建立连接,SubReactor 将建立连接的事件加入select 继续进行监听,并创建一个 Handler 用于处理连接的响应事件。
主线程只负责接收新连接,子线程负责完成后续的业务处理。
主线程只需要把新连接传给子线程,子线程无须返回数据,直接就可以在子线程将处理结果发送给客户端。
有了上述基础知识,我们开始讲解如何实现事件驱动。
1、错误检测机制
没变,不谈
2、地址创建
没变,不谈
3、创建socket
没变,不谈
4、并发epoll
没变,不谈
5、channel
这部分开始发生变化了,从这里开始发生变化意味着什么呢?是不是说处理事件的方式发生了变化?原本的先从红黑树上读取事件的机制发生了变化?
那么我们来看channel的声明出现了那些变化
class EventLoop;
class Channel
{
private:
EventLoop *loop;
int fd;
uint32_t events;
uint32_t revents;
bool inEpoll;
std::function<void()> callback;
public:
Channel(EventLoop *_loop, int _fd);
~Channel();
void handleEvent();
void enableReading();
int getFd();
uint32_t getEvents();
uint32_t getRevents();
bool getInEpoll();
void setInEpoll();
// void setEvents(uint32_t);
void setRevents(uint32_t);
void setCallback(std::function<void()>);
};
首先Epoll被替换为了一个名为EventLoop 的类,还多了一个callback回调函数,多了一个handleEvent方法,多了一个setCallback回调函数的设置。
那么我们从实现来看看各位新成员都是什么做的(这里只看多出来的部分)。
//调用回调函数,说明不同通道会根据某个特性设置不同的回调函数来处理事件
void Channel::handleEvent(){
callback();
}
//设置回调函数
void Channel::setCallback(std::function<void()> _cb){
callback = _cb;
}
总结一下,channel中现在包含事件的socket、事件本身及事件的处理方式。
6、EventLoop
刚才说Epoll被替换为了一个名为EventLoop 的类,那么既然Epoll没有发生变化,那么EventLoop 想必是对Epoll做了封装并多了一些功能吧。
首先来看声明
class Epoll;
class Channel;
class EventLoop
{
private:
Epoll *ep;
bool quit;
public:
EventLoop();
~EventLoop();
void loop();
void updateChannel(Channel*);
};
从声明来看,EventLoop仅仅是多了一个名为quit的标志位,loop和updateChannel应该是调用Epoll中的方法。那么在实现上看一下是否正确。
EventLoop::EventLoop() : ep(nullptr), quit(false){
ep = new Epoll();
}
EventLoop::~EventLoop()
{
delete ep;
}
void EventLoop::loop(){
while(!quit){
std::vector<Channel*> chs;
chs = ep->poll();
for(auto it = chs.begin(); it != chs.end(); ++it){
(*it)->handleEvent();
}
}
}
void EventLoop::updateChannel(Channel *ch){
ep->updateChannel(ch);
}
从实现来看我们的推测并不完全正确,EventLoop在loop方法中竟然直接对事件进行了处理!!!思考一下,我们使用了上面那种Reactor的方法呢?按我的理解来看目前是单Reactor的多线程处理,因为服务器目前监听事件并分发事件。
7、服务器
在这一天之后服务器也被抽象成为了一个类,老规矩来看看声明
class EventLoop;
class Socket;
class Server
{
private:
EventLoop *loop;
public:
Server(EventLoop*);
~Server();
void handleReadEvent(int);
void newConnection(Socket *serv_sock);
};
根据声明推测,服务器在创建时同时包含了一个EventLoop(升级版Epoll)。并有对channel中的事件分发处理赋值。
首先是构造函数,在构造函数中自带一个红黑树,创建socket、创建地址、绑定监听无阻塞,为服务器创建一个channel,并记录socket、事件类型及回调函数。服务器的回调函数应该是accept,这里将accept封装进了newConnection,这里调用绑定函数是为了实现类似于虚函数的作用,让channel类也能调用该函数。
之后是析构函数
Server::Server(EventLoop *_loop) : loop(_loop){
Socket *serv_sock = new Socket();
InetAddress *serv_addr = new InetAddress("127.0.0.1", 8888);
serv_sock->bind(serv_addr);
serv_sock->listen();
serv_sock->setnonblocking();
Channel *servChannel = new Channel(loop, serv_sock->getFd());
std::function<void()> cb = std::bind(&Server::newConnection, this, serv_sock);
servChannel->setCallback(cb);
servChannel->enableReading();
}
Server::~Server()
{
}
这是处理读事件的方法,如果没猜错是在服务器完成连接并将客户端事件加入Epoll后重新set回调函数。
void Server::handleReadEvent(int sockfd){
char buf[READ_BUFFER];
while(true){ //由于使用非阻塞IO,读取客户端buffer,一次读取buf大小数据,直到全部读取完毕
bzero(&buf, sizeof(buf));
ssize_t bytes_read = read(sockfd, buf, sizeof(buf));
if(bytes_read > 0){
printf("message from client fd %d: %s\n", sockfd, buf);
write(sockfd, buf, sizeof(buf));
} else if(bytes_read == -1 && errno == EINTR){ //客户端正常中断、继续读取
printf("continue reading");
continue;
} else if(bytes_read == -1 && ((errno == EAGAIN) || (errno == EWOULDBLOCK))){//非阻塞IO,这个条件表示数据全部读取完毕
printf("finish reading once, errno: %d\n", errno);
break;
} else if(bytes_read == 0){ //EOF,客户端断开连接
printf("EOF, client fd %d disconnected\n", sockfd);
close(sockfd); //关闭socket会自动将文件描述符从epoll树上移除
break;
}
}
}
那么就来看看newConnection到底做了什么吧。
首先创建客户端地址,服务区accept,将客户端设置为无阻塞,创建一个channel,然后将handleReadEvent绑定回调。
void Server::newConnection(Socket *serv_sock){
InetAddress *clnt_addr = new InetAddress(); //会发生内存泄露!没有delete
Socket *clnt_sock = new Socket(serv_sock->accept(clnt_addr)); //会发生内存泄露!没有delete
printf("new client fd %d! IP: %s Port: %d\n", clnt_sock->getFd(), inet_ntoa(clnt_addr->addr.sin_addr), ntohs(clnt_addr->addr.sin_port));
clnt_sock->setnonblocking();
Channel *clntChannel = new Channel(loop, clnt_sock->getFd());
std::function<void()> cb = std::bind(&Server::handleReadEvent, this, clnt_sock->getFd());
clntChannel->setCallback(cb);
clntChannel->enableReading();
}
总结一下,服务器负责判断监听分发事件,loop负责处理事件。
主函数
那么按照逻辑,主函数仅需要三行即可完成。
EventLoop *loop = new EventLoop();
Server *server = new Server(loop);
loop->loop();
return 0;