C++网络编程之Reactor实现

背景介绍

在计算机的网络编程中,Reactor(反应器)是一种事件驱动的设计模式,它可以高效处理并发IO的操作。然而这个里面的核心理念用到的就是事件循环和多路复用机制。它可以将多个I/O事件集中管理,避免为每个线程创建独立线程,从而可以在有限资源下支持大量并发请求。

并发服务器模型介绍

在讲解Reactor模式前,我们先大致来了解一下有哪些并发服务器模型,并且区分一下他们之间都有什么差别。

循环式迭代式模型

这是一种简单的单线程的应用程序,就是一股脑的单线执行,它一般只能使用短连接而不去使用长连接,缺点在于无法充分利用多核CPU,这也就会导致它的效率比较低。

并发式服务器

上面讲的模型仅仅适用于执行时间短的服务,可要是遇到了长时间的任务就不适用了,因此并发式服务中有一个多进程模型。主进程负责创建套接字,绑定端口,监听新连接,如果有新连接进来的话,就创建一个新的子进程去处理这个客户端的要求。并且分配给这个子进程的套接字,会在子进程结束完业务处理逻辑之后关闭。因此多进程模型的并发式服务器主要依靠多进程来处理业务逻辑。

prefork服务器

prefork服务器模型相比于上的优化在于开始执行的时候预先分配多个子进程,这样的话就不需要在运行的途中再进行fork()创建子进程了。

其中每个子进程都会执行右边的操作流程。

反应式服务器

这个就和我们这篇文章的主题有关了。反应式服务器可以处理多个并发请求,不过这些请求在本质上还是在一个线程中完成的。并且它的本质上也是对多个请求进行循环监听,只不过这种循环监听用的是epoll机制,相比于普通的select来说高效许多。在反应式服务器中,它首先会建立一个epoll实例,然后监听这个epoll。对于客户端发来的请求建立连接、消息、断开连接等都可监听到变化。因此"在用户看来",这个请求是并发处理的。你发送什么请求,这个反应式服务器就做出相应的处理。

图中主要分为client、Reactor、dispatcher、Acceptor、矩形框内的一系列操作流程几个重要部分。 当有客户端刚开始发起连接的时候,Reactor会收到这个请求,之后就会由dispatcher来处理这个请求,dispatcher就会将连接建立交给Acceptor去处理,这样的话client与服务器之间的关系就建立了。之后再当这个client发送消息之后,Reactor中的dispatcher就会将这个消息分发给下面的操作流程,这样消息就会得到处理。所以这就是Reactor的大致流程。讲到这里大伙大概可能有点懵,但一般网络消息的流程总共分为I/O线程和业务逻辑处理线程,像在上面的话I/O部分的功能主要是由client发送给Reactor,再由Reactor发送给dispatcher。最后由dispatcher来选择怎么处理这些消息,也就是上面矩形框中的处理逻辑。但这个还并不是我们今天要讲的主角,今天的主角马上登场!----Reactoro模型加上线程池的应用!

反应式服务器+线程池

上面我们讲了Reactor响应消息的大体流程,接下来的话我们主要是对他进行改进,因为Reactor的并发处理很高效,但是它始终是一个单线程的服务,在处理一些比较耗时的业务逻辑就会显得相对乏力,因此我们选择将任务(业务逻辑)统一发送给线程池,来解放我们Reactor!

I/O逻辑

我们的这个模型终归是要对数据进行处理,只不过是将业务处理剥离出去了。因此数据的传送和接收仍然是在Reactor模型上面。因此我们在接收到客户端发送来的消息后,就将消息(数据)已经建立的Connection连接一同发送给线程池,让他完成对数据的处理。当线程池中对这个数据处理完成后就通知给Reactor服务器,让服务器来给这个处理完的数据接收再发送给客户端。这样的话我们Reactor只需要负责数据的接收和发送,而并不需要去处理业务逻辑,就可以更好的解放"生产力"!

数据处理逻辑

我们知道数据的一般处理过程分为decode--->compute----->encode;但是由于我们的篇幅有限,所以省略decode与encode部分,当然compute也是简单处理,但是会将架子搭建起来,这样一些逻辑可以直接往里面套。我们的数据在接收之后,连同Tcp连接一同打包发送给线程池,在这个地方我们为了之后的可扩展性,采用的是基于对象编程的思想。当然这个地方的细节我们在代码处细看!

代码部分

基础接口类

Socket类

C++ 复制代码
//Socket.hpp
class Socket
:public NonCopyable{//NonCopyable表示禁用了赋值预算和拷贝构造,具体代码会在后面给出
public:
    Socket();//创建套接字的接口
    explicit Socket(int fd);//禁止隐式转换
    ~Socket();
    int getSocket();//获取套接字
    int fd() const {return _fd;}
    void shutdownwrite();
private:
    int _fd;
};
//Socket.cpp
#include "Socket.hpp"
Socket::Socket(){//创建
    _fd=socket(AF_INET,SOCK_STREAM,0);
    if(_fd<0){
        perror("socket");
    }
}
Socket::Socket(int fd)
:_fd(fd)
{}
void Socket::shutdownwrite(){
    shutdown(_fd,SHUT_WR);//关闭套接字,只能读不能写
}
Socket::~Socket(){close(_fd);}
int Socket::getSocket(){
    return _fd;
}

InetAddress类

C++ 复制代码
//InetAddress.hpp
class InetAddress
{
public:
    InetAddress(unsigned short port,const string & ip="0.0.0.0");
    InetAddress(const struct sockaddr_in &);
    ~InetAddress();
    string ip();//获取自己的ip地址
    unsigned short port();//获取自己的端口
    const struct sockaddr_in* get_InetAddressptr();//获取这个ip结构体
private:
    struct sockaddr_in _addr;
};
//InetAddress.cpp
#include "InetAddress.hpp"
InetAddress::InetAddress(unsigned short port,const string & ip){
    memset(&_addr,0,sizeof(_addr));
    _addr.sin_family=AF_INET;
    _addr.sin_addr.s_addr=inet_addr(ip.c_str());
    _addr.sin_port=htons(port);
}
InetAddress::InetAddress(const struct sockaddr_in & addr)
:_addr(addr)
{}
InetAddress::~InetAddress(){}
string InetAddress::ip(){
    return string(inet_ntoa(_addr.sin_addr));
}
unsigned short InetAddress::port(){
    return ntohs(_addr.sin_port);
}
const struct sockaddr_in * InetAddress::get_InetAddressptr(){
    return &_addr;
}

SocketIO类

C++ 复制代码
class SocketIO{
public:
    SocketIO(int fd);
    ~SocketIO();
    int readn(char * buf,int len);//读取n个字节
    int writen(const char * buf,int len);//发送n个字节
    int readline(char * buf,int len);//读取以换行符为结尾的数据
    int recvPeek(char *buf,int max) const;//查看缓冲区里是否收到数据
private:
    int _fd;
};

//SocketIO.cpp
SocketIO::SocketIO(int fd)
:_fd(fd)
{
}
SocketIO::~SocketIO(){}
int SocketIO::writen(const char *buf ,int len){
    int left=len;
    while(left>0){
        int ret =send(_fd,buf,left,0);
        if(ret<=0){
            return len-left;
        }
        left-=ret;
        buf+=ret;
    }
    return len;
}
int SocketIO::readn(char * buf,int len){
    int left=len;
    char *pbuf=buf;
    while(left>0){
        int ret=recv(_fd,pbuf,left,0);
        if(ret == -1 && errno ==  EINTR)
        {
            continue;//被信号影响了
        }
        else if(ret==0){//连接断开了
            return len-left;
        }else if(ret == -1){
            perror("recv");
            return len-left;
        }
        left-=ret;
        pbuf+=ret;
    }
    return len;
}
int SocketIO::readline(char * buf,int max){
    int left=max-1;
    char * tbuf=buf;
    int total=0;
    while(left>0){
        int ret=recv(_fd,tbuf,left,MSG_PEEK);
        for(int i=0;i<ret;i++){
            if(tbuf[i]=='\n'){//找到了换行符
                int sz=i+1;
                ret= readn(tbuf,sz);
                tbuf[i]='\0';
                total+=ret;
                return total;//这个数据不会丢失,因为他们各自被分配了一定的缓冲区
            }               
        }
        //没有找到换行符就将他们全部移到内核缓冲区中
        ret=readn(tbuf,ret);
        left-=ret;
        tbuf+=ret;
        total+=ret;
    }
    return max-1;
}
int SocketIO::recvPeek(char *buf ,int max) const{
    int ret=0;
    do{
        ret=recv(_fd,buf,max,MSG_PEEK);
    }while(ret==-1 && errno==EINTR);
    if(ret<0){
        perror("recv");
    }
    return ret; 
}

在这个SocketIO中我们定义了两个readn和writen,其实是由于在网络中数据并不是一次性全部给你发过来或者是接收到。他一个内核缓冲区和用户缓冲区交换的过程。 如下图:

上面这个图展示的是readn的过程,因为readn传进的时候有一个len的函数代表了这次必须得读出len个字节(前提是有len个,如果没有这么多他们他就会返回我读了多少个字节数)才能结束。这样就在一定程度上缓解我们数据漏接或者多接的情况。而我们用readline函数,则是因为我们在数据传输的过程中以'\n'换行符为一次输入的结束(客户端)。然后每次服务器读入数据的话以'\n'为一次终止。在这个过程也是需要防止数据在这个过程有丢失的情况,所以我们会在这一次读取没有换行符的情况下,将这些已经读到了的数据,送入内核缓冲区当中,以防丢失。

上图是用户态发送数据的图,有时候我们内核态所拥有的缓冲区大小可能和用户态的缓冲区大小不一样,这样的话如果数据较大的话,内核态不可能一次全部读完,这个时候我们就用writen来处理这个问题。writen中根据我要发送的数据大小len来发送数据。其中只有当我们正常发送完len个字节的数据后这个writen才会正常返回。

Acceptor类

C++ 复制代码
//Acceptor.hpp
class Acceptor{
public:
    Acceptor(unsigned short,const string & ip ="0.0.0.0");
    ~Acceptor();
    void ready();//表示这个套接字已经开始监听端口了
    void setReuseAddr(bool);//设置ip地址可重用
    void setReusePort(bool);//设置端口可重用
    void bind();//将套接字绑定端口
    void listen();//开始监听
    int accept();//建立连接
    unsigned short getPort(){return _addr.port();}//获取端口号
    string getIP(){return _addr.ip();}//获取ip地址
    int getfd(){return _sock.fd();}//获取端口号
private:
    InetAddress _addr; //ip结构体
    Socket      _sock; //端口类
};

//Acceptor.cpp
#define MAX_LISTEN 20000
Acceptor::Acceptor(unsigned short port,const string & ip)
:_addr(port,ip)
,_sock()
{
}
Acceptor::~Acceptor(){}
void Acceptor::setReuseAddr(bool on){
    int one=on ? 1 : 0;
    int ret=setsockopt(_sock.fd(),SOL_SOCKET,SO_REUSEADDR,&one,sizeof(one));
    ERROR_CHECK(ret,-1,"setsockopt addr");
}
void Acceptor::setReusePort(bool on){
    int one=on?1:0;
    int ret=setsockopt(_sock.fd(),SOL_SOCKET,SO_REUSEPORT,&one,sizeof(one));
    ERROR_CHECK(ret,-1,"setsockpt port");
}
void Acceptor::ready(){
    setReuseAddr(true);
    setReusePort(true);
    bind();
    listen();
}
void Acceptor::bind(){
    int ret=::bind(_sock.fd(),(const struct sockaddr *)&_addr,sizeof(_addr));
    ERROR_CHECK(ret,-1,"bind");
}
void Acceptor::listen(){
    int ret=::listen(_sock.fd(),MAX_LISTEN);
    ERROR_CHECK(ret,-1,"listen");
}
int Acceptor::accept(){
    int ret=::accept(_sock.fd(),nullptr,nullptr);
    ERROR_CHECK(ret,-1,"accept");
    return ret;
}

上面这个Acceptor类,主要是用来封装完成服务器端口开始监听前的四个步骤(socket、bind、listen、accept)。

Tcp连接类

C++ 复制代码
//TcpConnection.hpp
class EventLoop;
class TcpConnection:
public std::enable_shared_from_this<TcpConnection>
{
    using TcpConnectionPtr=std::shared_ptr<TcpConnection>;
    using TcpConnectionCallback=std::function<void (const TcpConnectionPtr)>;
    public:
    TcpConnection(int,EventLoop*);
    ~TcpConnection();
    void send(const string &);//发送消息
    string receive();//接收消息
    string to_string();//打印ip地址信息
    void shutdown();//关闭套接字
    void setNewConnectionCallback(const TcpConnectionCallback &cb);//注册处理新连接函数
    void setMessageCallback(const TcpConnectionCallback &cb);//注册处理消息函数
    void setCloseCallback(const TcpConnectionCallback &cb);//注册处理关闭函数
    void handleNewConnectionCallback();//执行处理新链接函数
    void handleMessageCallback();//执行处理新消息函数
    void handleCloseCallback();//执行处理关闭函数
    bool isClosed() const;//查看连接是否有效
    void sendInLoop(const string &msg);//将消息发送给EventLoop中(后面细讲)
private:    
    InetAddress getLocalAddress();
    InetAddress getPeerAddress();
private:
    SocketIO _sockio;//套接字接口
    Socket _sock;//套接字类
    InetAddress _localaddr;//存储获取的自己的ip
    InetAddress _peeraddr; //存储获取的对端的ip
    TcpConnectionCallback _onNewConnection;//处理新连接的函数
    TcpConnectionCallback _onClose;//处理关闭的函数
    TcpConnectionCallback _onMessage;//处理消息的函数
    EventLoop *_loop;  //******用于注册函数使用******
};
//TcpConnection.cpp
TcpConnection::TcpConnection(int fd,EventLoop* loop)
:_sock(fd)
,_sockio(fd)
,_localaddr(getLocalAddress())
,_peeraddr(getPeerAddress())
,_loop(loop)
{}
TcpConnection::~TcpConnection(){}
InetAddress TcpConnection::getLocalAddress(){
    struct sockaddr_in local;
    memset(&local,0,sizeof(local));
    socklen_t len=sizeof(local);
    int ret=getsockname(_sock.fd(),(struct sockaddr *)&local,&len);
    ERROR_CHECK(ret,-1,"getsockname local");
    return local;
}
InetAddress TcpConnection::getPeerAddress(){
    struct sockaddr_in peer;
    memset(&peer,0,sizeof(peer));
    socklen_t len=sizeof(peer);
    int ret=getpeername(_sock.fd(),(struct sockaddr *)&peer,&len);
    ERROR_CHECK(ret,-1,"getsockname peer");
    return peer;
}
void TcpConnection::send(const string &msg){
    _sockio.writen(msg.c_str(),strlen(msg.c_str()));
}

string TcpConnection::receive(){
    char buf[65535]={0};
    int ret=_sockio.readline(buf,sizeof(buf));
    ERROR_CHECK(ret,-1,"readline");
    return string(buf,ret);
}

void TcpConnection::shutdown(){
    _sock.shutdownwrite();
}
string TcpConnection::to_string(){
    std::ostringstream oss;
    oss << "> tcp socket " << _sock.fd() 
        << ", " << _localaddr.ip() << ":" << _localaddr.port()
        << "->"      << _peeraddr.ip() << ":" << _peeraddr.port();
    return oss.str();
}
void TcpConnection::setNewConnectionCallback(const TcpConnectionCallback & cb){//不能用右值引用
    _onNewConnection=cb;
}
void TcpConnection::setMessageCallback(const TcpConnectionCallback &cb){
    _onMessage=cb;
}
void TcpConnection::setCloseCallback(const TcpConnectionCallback &cb){
    _onClose=cb;
}
void TcpConnection::handleNewConnectionCallback(){
    if(_onNewConnection){
        _onNewConnection(shared_from_this());
    }else{
        cout<<"_onNewConnection == nullptr \n"<<endl;
    }
}
void TcpConnection::handleMessageCallback(){
    if(_onMessage){
        _onMessage(shared_from_this());
    }else{
        cout<<"_onMessage == nullptr \n"<<endl;
    }
}
void TcpConnection::handleCloseCallback(){
    if(_onClose){
        _onClose(shared_from_this());
    }else{
        cout<<"_onClose == nullptr \n"<<endl;
    }
}
bool TcpConnection::isClosed() const{
    char buf[20]={0};
    return _sockio.recvPeek(buf,sizeof(buf)) == 0;
}
void TcpConnection::sendInLoop(const string &msg){
    if(_loop){
        _loop->runInLoop(std::bind(&TcpConnection::send,this,msg));
    }
}

其实,我们看到了这个类会发现它有一部分其实是对前面的再封装,还有一部分就是对一些功能函数进行注册登记,其中还有关于EventLoop类中的一些部分,我们再下面讲到了EventLoop的时候细讲!

*** EventLoop类 ***

EventLoop类是我们这个模型是现在一个最重要的部分,我们也可以将他是为前文讲到过了Reactor+dispatcher的组合。因此搞懂这个才是搞懂这个模型的关键!

C++ 复制代码
//EventLoop.hpp

class EventLoop{
public:
    EventLoop(Acceptor &accept);
    ~EventLoop();
    void Loop();//开始循环
    void unLoop();//停止循环
    void runInLoop(Functor && cb);//将TcpConnection发来的任务添加,并通知可以执行了
    void setNewConnectionCallback(TcpConnectionCallback &&cb);
    void setMessageCallback(TcpConnectionCallback &&cb);
    void setCloseCallback(TcpConnectionCallback &&cb);//这三个set是用来登记注册三个函数的
    void handleNewConnection();//执行处理新连接的函数
    void handleMessage(int fd);//执行处理消息函数
    void HandleRead();//处理eventfd的读取
    void Wakeup();//唤醒等待eventfd的事件
    void doPengdingFunctors();//对vector中的Task直接执行
private:
    void waitEpollFd();//循环等待函数会
    int CreateEpollFd();//创建epoll实体
    int CreateEventFd();//创建通知器
    int CreateTimefd();//创建计时器
    void addEpollReadFd(int fd);//将这个文件描述符添加到需要监听的红黑树当中
    void delEpoolReadFd(int fd);//在监听列表中删除这个描述符
    void BroadCast(const string &msg);//广播数据给用户
    void initTimefd();//初始化计时器
    void TimeCheck();//超时踢出检查
    
private:
    int _epfd;//epoll实体
    int _evtfd;//eventFd 可以起到通知作用
    int _timefd;//计时器
    vector<struct epoll_event> _eventList;//epoll监听事件数组
    bool _isLooping;//循环开启标志
    Acceptor & _accept;//接收器
    map<int,TcpConnectionPtr> _conns;//记录每个文件描述符与其对应的TcpConnection
    map<int,time_t>           _timecheck;//用于记录每个用户的最后发言时间(用于实现超时踢出的功能)
    TcpConnectionCallback _onNewConnection;
    TcpConnectionCallback _onMessage;
    TcpConnectionCallback _onClose;//这个地方的是用来注册TcpConnection的相关函数
    vector<Functor> _pendings;//存放Task的地方,也就是我要完成的任务,
    MutexLock _mutex;//互斥锁
};

//EventLoop.cpp
EventLoop::EventLoop(Acceptor & accept)
:_epfd(CreateEpollFd())
,_eventList(1024)
,_isLooping(false)
,_accept(accept)
,_conns()
,_evtfd(CreateEventFd())
,_timefd(CreateTimefd())
{   
    addEpollReadFd(_accept.getfd());
    addEpollReadFd(STDIN_FILENO);
    addEpollReadFd(_evtfd);
    initTimefd();//初始化时钟
    addEpollReadFd(_timefd);
}
EventLoop::~EventLoop(){close(_epfd);close(_evtfd);}
int EventLoop::CreateEpollFd(){
    int ret=epoll_create1(0);
    ERROR_CHECK(ret,-1,"epoll_create");
    return ret;
}
int EventLoop::CreateEventFd(){
    int efd=eventfd(0,0);
    if(efd<0) perror("eventfd");
    return efd;
}
int EventLoop::CreateTimefd(){
    int fd=timerfd_create(CLOCK_REALTIME,0);
    if(fd<0){
        perror("createtimerfd");
        return -1;
    }
    return fd;
}
void EventLoop::initTimefd(){
    struct itimerspec new_value;
    //首次触发时间
    new_value.it_value.tv_sec=2;
    new_value.it_value.tv_nsec=0;
    //周期性触发时间
    new_value.it_interval.tv_sec=2;//每2s触发一次
    new_value.it_interval.tv_nsec=0;
    //设置
    int ret=timerfd_settime(_timefd,0,&new_value,NULL);
    ERROR_CHECK(ret,-1,"timeerfd_settime");
}
void EventLoop::waitEpollFd(){
    int nready=epoll_wait(_epfd,_eventList.data(),_eventList.size(),3000);
    if(-1== nready && errno==EINTR){
            //continue;
        printf("被信号干扰了!\n");
        return;
    }else if(nready==-1){
        perror("epoll_wait");
        return;
    }else if(nready==0){
        //printf("epoll_wait timeout\n");
        return;
    }else{
        for(size_t idx=0;idx<nready;idx++){
            int fd=_eventList[idx].data.fd;
            if(fd == _accept.getfd()){
                handleNewConnection();
            }else if(fd == STDIN_FILENO){
                char buf[1024]={0};
                read(STDIN_FILENO,buf,sizeof(buf));
                BroadCast(buf);
            }else if(fd ==_evtfd){
                    HandleRead();//读取一次evtfd
                    doPengdingFunctors();//执行数组中的任务
            }else if(fd==_timefd){
                uint64_t one=1;
                ssize_t ret= read(_timefd,&one,sizeof(uint64_t));//这两个可以处理成handleRead,但是鉴于所需代码量小就不单独写一个函数出来了
                if(ret!=sizeof(uint64_t)){
                    perror("handleRead");
                }
                TimeCheck();
            }
            else{
                time_t now=time(NULL);
                _timecheck[fd]=now;
                handleMessage(fd);
            }
        }
    }
}
void EventLoop::addEpollReadFd(int fd){
    struct epoll_event events;
    memset(&events,0,sizeof(events));
    events.events = EPOLLIN;
    events.data.fd=fd;
    int ret= epoll_ctl(_epfd,EPOLL_CTL_ADD,fd,&events);
    ERROR_CHECK(ret,-1,"epoll_ctl add");
}
void EventLoop::delEpoolReadFd(int fd){
    int ret= epoll_ctl(_epfd,EPOLL_CTL_DEL,fd,NULL);
    ERROR_CHECK(ret,-1,"epoll_ctl del");
    close(fd);
}
void EventLoop::Loop(){
    _isLooping = true;
    while(_isLooping){
        waitEpollFd();
    }
}
void EventLoop::unLoop(){
    _isLooping = false;
}
void EventLoop::setNewConnectionCallback(TcpConnectionCallback &&cb){
    _onNewConnection=std::move(cb);
}
void EventLoop::setCloseCallback(TcpConnectionCallback &&cb){
    _onClose=std::move(cb);
}
void EventLoop::setMessageCallback(TcpConnectionCallback &&cb){
    _onMessage=std::move(cb);
}
void EventLoop::handleNewConnection(){
    int connfd= _accept.accept();//获取用于通信的描述符connfd
    if(connfd<0){
        perror("handleNewConnection");
        return;
    }

    addEpollReadFd(connfd);  //给文件描述符放进去监听

    TcpConnectionPtr con(new TcpConnection(connfd,this));//建立TCP连接
    //将这三个事件传递给TcpConnection
    con->setNewConnectionCallback(_onNewConnection);
    con->setMessageCallback(_onMessage);
    con->setCloseCallback(_onClose);
    //存储到容器之中,并将时间也插入新的检查时间map中
    time_t now=time(NULL);
    _timecheck[connfd]=now;
    _conns[connfd]=con;
    con->handleNewConnectionCallback();
}
void EventLoop::handleMessage(int fd){
    auto it=_conns.find(fd);
    if(it!=_conns.end()){
        bool flag= it->second->isClosed();
        if(flag){
            //连接已断开
            it->second->handleCloseCallback();//连接断开的事件
            delEpoolReadFd(fd);
            _conns.erase(fd);
        }else{
            it->second->handleMessageCallback();//消息达到的事件
        }
    }
}
void EventLoop::BroadCast(const string &msg){
    if(msg.size()!=0){
        for(auto &it:_conns){
            it.second->send(msg);
        }
    }
    return;
}
void EventLoop::runInLoop(Functor && cb){
    {
    MutexLockGuard autolock(_mutex);
    _pendings.push_back(cb);//将任务押进vector中
    }
    Wakeup();//给eventfd增加值,去执行任务
}
void EventLoop::Wakeup(){
    uint64_t one=1;
    int ret=write(_evtfd,&one,sizeof(one));
    if(ret!=sizeof(one)){
        perror("write");
    }
}
void EventLoop::HandleRead(){
    uint64_t howmany=0;
    int ret=read(_evtfd,&howmany,sizeof(howmany));//将eventfd清零
    if(ret!=sizeof(howmany)){
        perror("read");
    }
}
void EventLoop::doPengdingFunctors(){

    vector<Functor> tmp;
    {
        MutexLockGuard autolock(_mutex);
        tmp.swap(_pendings);
    }
    for(auto &ele :tmp){
        ele();
    }
}
void EventLoop::TimeCheck(){
    time_t now=time(NULL);
    for(auto &ele:_timecheck){
        if(now-ele.second>10){
            //踢出
            auto it=_conns.find(ele.first);
            if(it!=_conns.end()){
            it->second->handleCloseCallback();//连接断开的事件
            const string msg="You haven't spoken for a long time and have thus been kicked out.";
            it->second->send(msg);
            delEpoolReadFd(ele.first);
            _conns.erase(ele.first);
            }
        }
    }
}

我们看上面的EventLoop,这个里面的最重要的就是事件循环也就是waitEpollFd函数这个地方,它用epoll_wait来监听各个事件的触发。对于不同的文件描述符有着不同的处理方式,接下来让我们看图说话! 我们用上图对照着waitEpollFd来细看,我们会在每次epoll_wait之后先对非正常情况进行判断,如果排除了 异常情况,就像图中,对传出的event_list进行循环遍历,并且每次遍历都会进行分支判断。判断逻辑就如上述的紫色框框中一样,符合逻辑就会执行下面的函数。那么对于这些调用的函数其实也是有一点门路的。里面的BroadCast和TimeCheck就不多说了,因为是比较简单的函数调用。而对于handleNewConnection和handleMessage则是需要注册的。而此处对于注册函数我们还有一点东西没有给出来,因此这个注册将在后面我们给出了测试案例的时候再来细讲这个地方的注册逻辑与执行逻辑。

TcpServer类

TcpServer类主要是对前面讲的一些网络数据接收的接口进行的再度封装,让它保持接口的简单特性,从而让代码变得简洁。

C++ 复制代码
//TcpServer.hpp
class TcpServer{
public:
    TcpServer(unsigned short port,const string & ip);
    ~TcpServer();
    void start();
    void stop();
    void setAllCallback(TcpConnectionCallback &&connection,TcpConnectionCallback &&Message,TcpConnectionCallback &&Close);
private:
    Acceptor _accept;
    EventLoop _loop;
};

//TcpServer.cpp
TcpServer::TcpServer(unsigned short port,const string & ip)
:_accept(port,ip)
,_loop(_accept)
{}
TcpServer::~TcpServer(){}
void TcpServer::start(){
    _accept.ready();
    _loop.Loop();
}
void TcpServer::stop(){
    _loop.unLoop();
}
void TcpServer::setAllCallback(TcpConnectionCallback &&connection,TcpConnectionCallback &&Message,TcpConnectionCallback &&Close){
    _loop.setNewConnectionCallback(std::move(connection));
    _loop.setMessageCallback(std::move(Message));
    _loop.setCloseCallback(std::move(Close));
}

对于上述一个正常Reactor模型的启动,除了需要注册的函数之外(可以将注册的函数换成自己定义的处理函数,也就是在此处进行硬编码),已经基本上可以实现Reactor模型了。就只需要将EventLoop类的成员与接收器的成员进行初始化即可正常接收连接了。

线程池部分

因为我之前写过这个地方的文章,并且线程池这块如果重新讲的话也需大费周章,因此如果这块有想仔细了解的小伙伴可以自行前往这个链接(C++线程池的简单实现)。 这个地方的代码我会在最后给出的。

测试用例部分(如何实现注册逻辑)

C++ 复制代码
//Test.cpp
class MyTask{
public:
    MyTask(const string &msg,TcpConnectionPtr conn)
    :_msg(msg)
    ,_conn(conn)
    {}
    void process(){
        string response=_msg;
        //此处是处理任务的逻辑函数
        _conn->sendInLoop(_msg);
    }
private:
    const string _msg;
    TcpConnectionPtr _conn;
};
class EchoServer{
public:
    EchoServer(size_t threadNum,size_t quesize,unsigned short port,const string & ip)
    :_pool(threadNum,quesize)
    ,_server(port,ip)
    {}
    ~EchoServer(){}
    void start(){
        _pool.start();
        using namespace std::placeholders;
        _server.setAllCallback(std::bind(&EchoServer::onNewConnection,this,_1)
                                ,std::bind(&EchoServer::onMessage,this,_1)
                            ,std::bind(&EchoServer::onClose,this,_1));
        _server.start();
    }
    void stop(){
        _pool.stop();
        _server.stop();
    }
    void onNewConnection(const TcpConnectionPtr &conn){
         cout << conn->to_string() << " has connected successfully.\n";
    }
    void onMessage(const TcpConnectionPtr &conn){
        string msg = conn->receive();//不能阻塞
        cout << "recv: " << msg << endl;
        MyTask task(msg, conn);
        _pool.addTask(std::bind(&MyTask::process, task));
        string response = msg;
        conn->send(response);//时间不宜过长
    }
    void onClose(const TcpConnectionPtr &conn){
        cout<<conn->to_string() << "has close !(EchoServer)"<<endl;
    }
private:
    ThreadPool _pool;
    TcpServer _server;
};
int main(){
    EchoServer eser(4,8,8066,"127.0.0.1");
    eser.start();
    cout<<"main is exit!\n"<<endl;
    return 0;
}

上述Test代码中大体分为了MyTask类、EchoServer类。MyTask类主要是对数据的自定义逻辑处理操作。EchoServer类主要是对线程池和TcpServer进行的一次再度封装。因为如果我们选择不使用EchoServer进行封装的话,那么我们需要以全局变量的形式创建一个线程池对象,那么这样的话就不符合良好的代码编程习惯,因此我们选择将线程池对象以数据成员的形式的存储。

为何在此处讲注册

因为我们的代码想在日后变得可以方便移植和进行功能的修改,如果我们不采用注册函数的形式,就需要每一次修改都要进入EventLoop函数里面去修改函数处理逻辑,显然这样是麻烦的。因此我们选择在Test即我们的主函数所在文件里面用一个EchoServer类封装我们对数据的处理过程。这样我们日后想要对数据进行不同的处理逻辑的时候就可以直接在EchoServer类里面进行修改相应的处理函数。在上述代码中也就是对应的onNewConnection、onClose、onMessage。

如何去注册

首先我们先去看这个EchoServer类中,它提供了一个setAllCallback的接口。我们调用了这个接口,这个接口的流程我们用图去看!

  1. 这个接口会再进一步去调用TcpServer类的setAllCallback函数
  2. 然后TcpServer类的setAllCallback函数会依次去执行EventLoop类中的相应的setCallback函数
  3. 然后EventLoop类的setCallback函数其实是将TcpConnectionCallback 的数据成员进行赋值。
  4. 最后当在处理逻辑中需要用到对应的handle函数的话就会先调用对应的EventLoop的handle函数,EventLoop的handle函数就会进一步的去调用TcpConncetion类里面的handleCallback函数。

经过上面的步骤我们就会发现,EventLoop类其实在这个过程中起到了一个过渡的作用,就相当于我们暴露给用户的接口是一个EventLoop类,我们要调用具体的处理函数是在TcpConnection类中完成的,但是我们的TcpConnection并不能在一开始就接触到我们自己需要处理逻辑函数,因此就需要EventLoop类将这个函数带到TcpConnection类能够接触到的地方去。而这个方式的实现就是在TcpConnection类中定义一个EventLoop数据成员,EventLoop类中定义三个TcpConnectionCallback数据成员。至此,我们自定义的处理逻辑函数从定义到注册、到实现全部完成。

流程梳理(自顶向下探究)

接下来,我们从测试用例的代码流程一步一步往下梳理。期间可能会用到线程池的代码的地方我会截取部分图,如果后面想跟着代码去研究执行流程的话,我会给出所有的源代码供大伙一块探讨

封装MyTask类和EchoServer类

先自定义一个MyTask类,这个Task类中需要包括消息和Tcp链接,并且处理逻辑放在process函数当中。

再定义一个EchoServer类用于注册函数

C++ 复制代码
class EchoServer{
public:
    EchoServer(size_t threadNum,size_t quesize,unsigned short port,const string & ip)
    :_pool(threadNum,quesize)
    ,_server(port,ip)
    {}
    ~EchoServer(){}
    void start(){
        _pool.start();
        using namespace std::placeholders;
        _server.setAllCallback(std::bind(&EchoServer::onNewConnection,this,_1)
                                ,std::bind(&EchoServer::onMessage,this,_1)
                            ,std::bind(&EchoServer::onClose,this,_1));
        _server.start();
    }
    void stop(){
        _pool.stop();
        _server.stop();
    }
    void onNewConnection(const TcpConnectionPtr &conn){
         cout << conn->to_string() << " has connected successfully.\n";
    }
    void onMessage(const TcpConnectionPtr &conn){
        string msg = conn->receive();//不能阻塞
        cout << "recv: " << msg << endl;
        MyTask task(msg, conn);
        _pool.addTask(std::bind(&MyTask::process, task));
        string response = msg;
        conn->send(response);//时间不宜过长
    }
    void onClose(const TcpConnectionPtr &conn){
        cout<<conn->to_string() << "has close !(EchoServer)"<<endl;
    }
private:
    ThreadPool _pool;
    TcpServer _server;
};

进行初始化

创建一个EchoServer实体 EchoServer eser(4,8,8066,"127.0.0.1");然后调用eser.start(); 当我们调用start后,start就会开始调用线程池和服务器的start,并在其中注册函数。

线程池的开启

线程池的开启由如下代码执行:

其中我们看到创建线程的时候有着doTask。也就是线程池开启的时候就会调用一次doTask。然后之后将些线程全部加入_threads队列中,然后将线程逐个开启。当然,在最开始的时候没有任务,这样的话线程池中的线程会处于一个等待任务添加的过程,所以接下来就需要添加任务进入线程池了。

TcpServer的开启

TcpServer的启动主要是对acceptor类成员的ready调用和EventLoop的循环开启调用。

当_loop进入循环的时候就会开始监听描述符

任务的触发

当我们将上面的步骤完成之后,再开始用客户端连接这个服务器。刚开始连接的时候,会建立新连接,也就是说会触发waitEpollFd中对handleNewConnection的调用。而在这个地方的调用就会进行如下处理:

C++ 复制代码
void EventLoop::handleNewConnection(){
    int connfd= _accept.accept();//获取用于通信的描述符connfd
    if(connfd<0){
        perror("handleNewConnection");
        return;
    }

    addEpollReadFd(connfd);  //给文件描述符放进去监听

    TcpConnectionPtr con(new TcpConnection(connfd,this));//建立TCP连接
    //将这三个事件传递给TcpConnection
    con->setNewConnectionCallback(_onNewConnection);
    con->setMessageCallback(_onMessage);
    con->setCloseCallback(_onClose);
    //存储到容器之中,并将时间也插入新的检查时间map中
    time_t now=time(NULL);
    _timecheck[connfd]=now;
    _conns[connfd]=con;
    con->handleNewConnectionCallback();
}

这个里面,会先接收对应的连接的文件描述符,然后将之前在EchoServer中注册的函数传进来,并在此处进行setCallback处理,这样EventLoop中的对对应Tcp连接的对应函数已经在此处注册完成。并且将这个描述符加入到了红黑树中。

消息的接收

我们在上面讲到了新连接到来的处理,接下来新连接描述符被监听之后,如果这个客户端再次发来请求,就会被epoll监听到,并且进入处理handMessage的分支处理之中。

C++ 复制代码
void EventLoop::handleMessage(int fd){
    auto it=_conns.find(fd);
    if(it!=_conns.end()){
        bool flag= it->second->isClosed();
        if(flag){
            //连接已断开
            it->second->handleCloseCallback();//连接断开的事件
            delEpoolReadFd(fd);
            _conns.erase(fd);
        }else{
            it->second->handleMessageCallback();//消息达到的事件
        }
    }
}

进入该分支之后,调用这个函数,这个函数会先开始在存储描述符和Tcp连接的map中查找对应的Tcp连接 ,如果没有则就静默处理。如果找到了连接,就得先判断我们这个连接是不是断开的,这个地方用到的就是isClosed来判断的。这里的原理是如果客户端断开了之后会发送一些空数据过来,这个是后服务端recvPeek收到的数据为0,这样就表明断开连接直接走断开处理逻辑了。此处由于断开逻辑较为简单不多做讲解。

消息接收后对消息的处理

之后就会进入handleMessageCallback函数。而执行这个函数就是在TcpConnection成员调用对应的函数,在代码中表现为如下:

C++ 复制代码
void TcpConnection::handleMessageCallback(){
    if(_onMessage){
        _onMessage(shared_from_this());
    }else{
        cout<<"_onMessage == nullptr \n"<<endl;
    }
}

又因为此处注册的时候刚开始EchoServer中注册的函数,因此我们回到最开始EchoServer的注册中去。

在这里我们可以看到,Tcp连接被传入进来,然后收到了客户端发过来的数据msg,此时我们将这两个一起打包放进和MyTask的process一起添加进线程池,这样线程池终于有任务进去了。接下来我们就会按照MyTask的process逻辑去处理

任务进入线程池当中

我们接下来在看process中的处理过程。

C++ 复制代码
    void process(){
        string response=_msg;
        //此处是处理任务的逻辑函数
        _conn->sendInLoop(_msg);
    }

在这个process中调用了一个TcpConnection中的sendInLoop函数,所以我们再看这个函数:

C++ 复制代码
void TcpConnection::sendInLoop(const string &msg){
    if(_loop){
        _loop->runInLoop(std::bind(&TcpConnection::send,this,msg));
    }
}

到了这里我们再去看到发现这个sendInLoop中调用了EventLoop的runInLoop函数,因此我们要再去细探runInLoop的逻辑:

C++ 复制代码
void EventLoop::runInLoop(Functor && cb){
    {
    MutexLockGuard autolock(_mutex);
    _pendings.push_back(cb);//将任务押进vector中
    }
    Wakeup();//给eventfd增加值,去执行任务
}

runInLoop中有一个加锁的操作,并且发现会有一个MutexLockGuard成员,其实这是一个我们手动实现的智能锁。然后它会去执行将我们上面获得到的Functor(void())类函数压入_pendings中。到此,我们可以简单概要一下如下图:

结语

讲到这里,大伙应该对Reactor模型有了一个初步的认识,当然上文中是笔者所窥得的冰山一角。Reactor模型+线程池应用强调并发处理请求。因此需要着重关注他的dispatcher部分(在本文中用的是EventLoop类),才能更深一步的理解。然后上面的代码给出的只是部分,如果读者有兴趣可以自己拿取笔者的源代码。如果读者有更深一步的理解,希望能指点一二!创作不易,转载请注明出处!!! 源代码链接(通过网盘分享的知识:Reactor 链接: pan.baidu.com/s/5oEc6kFEO... )

相关推荐
MSXmiao28 分钟前
2048小游戏
数据结构·c++·算法
chian-ocean1 小时前
C++ 网络编程入门:TCP 协议下的简易计算器项目
网络·c++·tcp/ip
江公望1 小时前
ubuntu 20.04 C和C++的标准头文件都放在哪个目录?
c++·ubuntu
Antonio9151 小时前
【音视频】WebRTC 一对一通话-信令服
c++·websocket·音视频·webrtc
2301_763994712 小时前
c++11特性
数据结构·c++·算法
极客BIM工作室2 小时前
C++返回值优化(RVO):高效返回对象的艺术
java·开发语言·c++
序属秋秋秋2 小时前
《C++初阶之STL》【模板参数 + 模板特化 + 分离编译】
开发语言·c++·笔记·学习·stl
浮生卍流年3 小时前
C++模板知识点3『std::initializer_list初始化时逗号表达式的执行顺序』
开发语言·c++·qt
jllllyuz3 小时前
C++ 中 initializer_list&& 类型推导
开发语言·c++·list