找工作小项目:day6-事件驱动

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;
  • 18
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值