【C++/Linux实战项目】仿muduo库实现高性能Reactor模式TCP服务器(深度解析)

前言:

本文实现的是一个仿muduo库核心设计的高性能TCP服务器,基于多Reactor模式+epoll+多线程+时间轮定时器,覆盖Linux/C++网络编程核心知识点。

作为学习完C++/Linux后的实战项目,梳理底层网络框架的设计思路,也为实习求职夯实底层编程能力。

读完本文希望能帮你理解多Reactor模式的落地实现、muduo核心模块的设计逻辑,以及Linux网络编程的关键坑点。项目源码gitee连接项目源码连接

一:项目背景

muduo库是陈硕老师的开源高性能网络库,核心是:Reactor + 多线程 + 事件驱动,轻量、高效、易扩展。本项目并非直接照搬muduo的开源库,而是提取核心设计,如:EventLoop、Channel、Connection分层,实现轻量级版本,聚焦核心原理。

技术涉及:C++面向对象、模版、多态、Linux IO 多路复用、多线程同步、设计模式、类型擦除、条件变量、互斥锁、智能指针等。

二:核心设计思想

核心设计采用主从Reactor+多线程模式------主Reactor来监控服务器的listen_fd,从属Reactor用于监听每一个客户端连接的事件,每一个EventLoop一个Reactor,一个线程管理多个连接(多个Reactor),线程池采取RR轮询的方式。

什么是Reactor,举例就是Reactor像是餐厅里的服务员------服务员(EventLoop)盯着所有餐桌(fd),哪个餐桌有需求(IO事件),就喊对应的初试(Channel)处理,自己继续盯着其他的餐桌。具体执行流程就是EventLoop类启动后->循环执行epoll_wait->拿到处理就绪事件->执行任务队列中的人物,这就是Reactor的核心循环部分。

三:项目整体构架与模块划分

项目遵循高内聚,低耦合的原则,将整体架构拆解为「基础工具层、网络基础层、事件驱动核心层、高级特性层、业务核心层」五大层级,各层级自上而下依赖、自下而上支撑,形成完整的TCP服务器闭环。具体图示如下:

项目除宏日志之外总共分为13个类分别是:

|----------------|-------------------------------|
| 类 | 功能介绍 |
| Any | 类型擦除类 |
| Buffer | 用户层缓冲区类 |
| Socket | 网络接口类(就是封装的原生socket) |
| Channel | 客户端连接的管理事件监控类 |
| Poller | 管理所有客户端连接的Channel |
| EventLoop | Reactor的核心类(事件处理) |
| LoopThread | 封装EventLoop,一个线程管理一个EventLoop |
| LoopThreadPoll | 线程池类 |
| TimerTask | 定时任务类 |
| TimerWheel | 时间轮(模拟秒针跳动,去执行定时任务类) |
| Connection | 客户端(客户端fd)连接类 |
| Acceptor | 监听套接字(listen_fd)管理类 |
| TcpServer | 集成之前的所有类,真正的服务器类 |

又分为五个大模块:基础工具模块、网络基础模块、事件驱动核心、高级特性模块、业务核心模块。

四:模块详细介绍每个模块以及每个模块之间的关联和依赖关系

4.1基础工具模块

4.1.1 日志宏------LOG

日志宏的设计初衷------在测试项目时为了方便我们准确快速地找到错误信息,我们一般会使用日志帮我们快速定位,在日志中不是单纯的错误信息,我们还会额外的添加错误文件名,行号,错误时间等等,来帮助我们找出错误。具体如下:

cpp 复制代码
#define INF 0
#define DBG 1
#define ERR 2
#define LOG_LEVEL DBG

//使用strrchr去找到分割符之后的字符串
#define SHORT_FILE (strrchr(__FILE__, '/') ? strrchr(__FILE__, '/') + 1 : __FILE__)
#define LOG(level,fromat,...) do{\
    if(level<LOG_LEVEL)break;\
    time_t t = time(nullptr);\
    struct tm * ltm = localtime(&t);\
    char time_buf[32]={0};\
    strftime(time_buf,31,"%H:%M:%S",ltm);\
    fprintf(stdout , "[thread id:%p time:%s %s:%d]" fromat "\n" ,(void*)pthread_self(),time_buf , SHORT_FILE , __LINE__ , ##__VA_ARGS__);\
}while(0)


#define INF_LOG(fromat , ...) LOG(INF , fromat , ##__VA_ARGS__)
#define DBG_LOG(fromat , ...) LOG(DBG , fromat , ##__VA_ARGS__)
#define ERR_LOG(fromat , ...) LOG(ERR , fromat , ##__VA_ARGS__)

其中日志宏的涉及到了可变长参数,以及GNU扩展变长参数的使用##VA_ARGS,当变长常数为空时,自动删除参数中前面的逗号。其次是我使用了一个宏函数SHORT_FILE去截取路径中所对应这个文件的文件名,其中strrchr------从路径末尾开始找路径分隔符,三目表达式的目的是,如果__FILE__就是文件名那么就直接返回这个路径,如果路径名中有/那么就找到最后一个/分隔符的位置+1就是文件名。具体可以看代码,思想是比较简单的。

4.1.2 Any类------类型擦除

目的是为了让我的muduo服务器可以支持适配不同的协议(HTTP/HTTPS/FTP)的服务器,设计了这个类。主要用到了多态,模版类。代码如下:

cpp 复制代码
//类型擦除容器------存储任意类型的上下文
//用同一个 Any 对象就能存储任意类型的数据,而不是为 int、string、自定义类分别创建 Any<int>、Any<string> 这类绑定了具体类型的对象------用在假设我要用其他应用层协议这个项目后面写了一个http服务器所有可以直接用,我换个https也可以用------这就是Any类的意义所在
class Any{
    private:
        class holder{
            public:
                virtual ~holder(){}
                virtual const std::type_info& type()=0;
                virtual holder* clone()=0;
        };
        template<class T>
        class placeholder: public holder{
            public:
                placeholder(const T& val):_val(val){}
                virtual const std::type_info& type(){return typeid(T);}
                virtual holder* clone(){return new placeholder<T>(_val);}
                T _val;
        };
        holder* _content;
    public:
        Any():_content(nullptr){}
        
        template<class T>
        Any(const T& val){
            _content=new placeholder<T> (val);    
        }
        Any(const Any& val){
            _content=val._content?val._content->clone():nullptr;
        }
        ~Any(){delete _content;}
        
        //这里other能区访问_content私有成员变量是因为是在swap成员函数中。只有不在Any类中才不能去访问私有成员变量。
        Any& swap(Any&other){
            //swap函数要求交换两个可修改的左值
            std::swap(other._content,_content);
            return *this;
        }
        //返回子类对象保存的数据的指针
        template<class T>
        T* get(){
            assert(_content != nullptr && "Any object is empty");
            assert(_content->type() == typeid(T) && "Type mismatch");
            //这里换成static_cast更好,static_cast是编译时检查,运行零开销,而且我已经用assert断言了不会出现类型不匹配
            // return &((dynamic_cast<placeholder<T>*>(_content))->_val);
            return &((static_cast<placeholder<T>*>(_content))->_val);
        }
       
        //重载赋值函数
        template<class T>
        Any& operator=(const T& val){
            //这一步很巧妙,省去了自己写new的过程相当于一种炫技了,构造Any临时对象,然后调用swap函数将临时对象中的指针和this中的指针交换,交换完后临时对象自己销毁------此时就相当于原本的内存被释放了------相当于原本难道空间被释放了
            Any(val).swap(*this);
            return *this;
        }

        //返回值为&是因为重复赋值时避免不必要的拷贝
        Any& operator=(const Any& other){
            Any(other).swap(*this);
            return *this;
        }
};

目的是让一个Any对象就可以保存任意类型的数据。在Any类中保存一个父类指针,利用父类指针可以指向子类指针的多态特性,子类设置成一个模版类,来保存指定的数据类。这样就实习了Any类指向任意类型的数据操作了,在保存数据时,Any会根据构造函数推导存的数据类型,在读取数据时会使用get函数传入类型参数安全的读取读取数据。相比于直接使用void* 这样更加符合现代C++的设计思路:安全,抽象,易用。

Any类的设计:父类的设计:只需要设计虚虚构函数,虚类型返回函数和虚子类克隆函数(为了使用拷贝构造函数)。子类的设计:只需要子类去重写虚函数就行了。在外部类也就是Any类中初步的构造函数拷贝构造函数以及虚构函数是必要的。然后就是获取Any类中的数据的函数get,以及各种对于Any中的数据交换函数,比如swap,operator=(赋值运算符重载),其中使用的是深拷贝。

4.1.3 Buffer类------读写缓冲区

为了方便我们处理数据,不使用原生的I/O缓冲区,原生I/O缓冲区首先我们不能直接操作,其次原生缓冲区大小有限,而且可以避免TCP的粘包/拆包问题。所以为了解决这些难点我们使用Buffer来做一层应用层的缓冲区,把I/O缓冲区拿上来做封装。代码如下:

cpp 复制代码
//服务器业务缓冲区------将IO缓冲区拿到业务缓冲区做处理
#define BUFFER_DEFAULT_SIZE 1024
class Buffer{
    private:
        std::vector<char> _buffer;
        uint64_t _reader_idx;
        uint64_t _writer_idx;
    public:
        Buffer():_buffer(BUFFER_DEFAULT_SIZE),_reader_idx(0),_writer_idx(0){}
        char *Begin(){return &*_buffer.begin();}
        //获取当前写入/读出的起始地址,_buffer的空间的起始地址,加上偏移量
        char* WritePosition(){return Begin()+_writer_idx;}
        char*  ReadPosition(){return Begin()+_reader_idx;}
        //获取缓冲区写位置之后/读位置之前的空闲空间大小
        // 如果发生了扩容 ,这里的容量计算就是错误的
        // uint64_t TailIdleSize(){return BUFFER_DEFAULT_SIZE-_writer_idx;}
        uint64_t TailIdleSize(){return _buffer.size() - _writer_idx;}
        uint64_t HeadIdleSize(){return _reader_idx;}
        //获取缓冲区可读数据大小
        uint64_t ReadAbleSize(){
            // //这一步是考虑环形结构是必须的,但是我这里不是环形结构,这段代码可有可无。
            // assert(_reader_idx<=_writer_idx);
            return _writer_idx-_reader_idx;
        }
        
        //将读/写偏移量后移
        void MoveReadOffset(uint64_t len){
            if(len == 0)return;
            assert(len<=ReadAbleSize());
            _reader_idx+=len;
        }
        void MoveWriteOffset(uint64_t len){
            assert(len<=TailIdleSize());
            _writer_idx+=len;
        }
        //确保可写空间足够,首先要声明的是这里的数据缓冲区不是环形结构。
        void EnsureWriteSpace(uint64_t len){
            if(TailIdleSize() >= len){return;/*此时可写空间足够*/}
            
            if(len<=TailIdleSize()+HeadIdleSize()){
                //将数据前移,用到copy函数
                uint64_t temp=ReadAbleSize();
                auto dest=ReadPosition();
                std::copy(dest,dest+temp,Begin());

                _reader_idx=0;//将读偏移置为0,从头开始
                _writer_idx=temp;//将写偏移量置为数据长度
            }
            else{
                //扩容
                _buffer.resize(_writer_idx+len);
            }
        }
        //写入数据------起始就是拷贝
        void Write(const void* data,uint64_t len){
            //1.判断空间。2.拷贝数据
            if(len == 0)return;
            EnsureWriteSpace(len);
            //void* 是没有步长的
            const char*d=(const char*)data;
            std::copy(d,d+len,WritePosition());
            //同样这里也分开写,实现两个API-写入不移动,写入移动
            // MoveWriteOffset(len);
        }
        void WriteAndPush(const void* data,uint64_t len){
            Write(data,len);
            MoveWriteOffset(len);
        }

        void WriteString(std::string &data){Write(data.c_str(),data.size());}
        void WriteStringAndPush(std::string &data){
            WriteString(data);
            MoveWriteOffset(data.size());
        }
        
        void WriteBuffer(Buffer& data){Write(data.ReadPosition(),data.ReadAbleSize());}
        void WriteBufferAndPush(Buffer& data){
            WriteBuffer(data);
            MoveWriteOffset(data.ReadAbleSize());
        }
        
        //读数据-不移动指针
        void Read(void *buf,uint64_t len){
            //确保可读
            assert(len<=ReadAbleSize());
            std::copy(ReadPosition(),ReadPosition()+len,(char*)buf);
            
            //两个函数分开写
            // MoveReadOffset(len);
        }
        //读数据-移动指针
        void ReadAndPop(void *buf,uint64_t len){
            Read(buf,len);
            MoveReadOffset(len);
        }
        std::string ReadAsStirng(uint64_t len){
            assert(len<=ReadAbleSize());
            //string::c_str()------这返回的是一个const char*的类型
            std::string s;
            s.resize(len);
            Read(&s[0],s.size());
            return s;
        }
        std::string ReadAsStirngAndPop(uint64_t len){
            assert(len<=ReadAbleSize());
            std::string s = ReadAsStirng(len);
            MoveReadOffset(len);
            return s;
        }
        //在tcp中还有一个功能就是一次读取一行数据------说白了这就是对tcp缓冲区的一次重新封装,满足我们的需求
        char* FindCRLF(){
            char * res = static_cast<char*>(memchr(ReadPosition(),'\n',ReadAbleSize()));
            return res;
        }
        
        std::string GetLine(){
            char* pos = FindCRLF();
            if(pos == nullptr){
                return "";
            }
            return ReadAsStirng(pos-ReadPosition()+1);
        }
        std::string GetLineAndPop(){
            std::string str = GetLine();
            MoveReadOffset(str.size());
            return str;
        }
        //清空缓冲区
        void Clear(){
            _reader_idx=0;
            _writer_idx=0;
        }
};

在缓冲区大小这里我默认设置成了1024,这里注意数字一定要弄成宏定义,不然就会出现一堆的魔幻数字,增大了后期维护修改的难度。在缓冲区容器中使用vector因为其线性的数据访问特性可以加大读取效率,也方便我们扩容,使用两个指针(整形下标)去保存写数据下标和读数据下标。这样就够了。接着就是对外开放的处理缓冲区的函数,比如:获取剩余缓冲区空间、获取读位置、获取写位置、写数据、读数据、扩容函数、清空缓冲区、以及为了http服务器处理参数额外设置了读取一行数据的接口等。

4.2 网络基础模块

4.2.1 Socket类------原生API封装

这个类就是对Socket的原生封装,在Socket的基础上把发送和接收数据以及创建客户端连接和服务器连接直接集成在了这个类中。代码如下:

cpp 复制代码
class Socket{
    private:
        //创建监听套接字
        int _sockfd;
    public:
        Socket():_sockfd(-1){}
        Socket(int fd):_sockfd(fd){}
        ~Socket(){Close();}
        int Fd() { return _sockfd; }
        //创建套接字
        bool Create(){
            //int socket(int domain,int type,int protocol);
            _sockfd=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
            if(_sockfd<0){
                ERR_LOG("Create 监听套接字失败!!!");
                return false;
            }
            return true;
        }
        //绑定地址信息
        bool Bind(const std::string& ip ,uint16_t port){
            //创建tcp
            struct sockaddr_in addr;
            addr.sin_family=AF_INET;
            addr.sin_port=htons(port);
            addr.sin_addr.s_addr=inet_addr(ip.c_str());
            socklen_t len=sizeof(struct sockaddr_in);
            // int ret = bind(_sockfd,reinterpret_cast<struct sockaddr*>(&addr),len);
            int ret = bind(_sockfd, (struct sockaddr*)&addr, len);
            if(ret<0){
                ERR_LOG("Bind 失败!!!!");
                return false;
            }
            return true;
        }
        //开始监听
        bool Listen(int backlog = MAX_LISTEN){
            //int listen(int backlog)
            int ret = listen(_sockfd,backlog);
            if(ret<0){
                ERR_LOG("Listen 失败!!!!");
                return false;
            }
            return true;
        }
        //向服务器发起连接
        bool Connect(const std::string&ip ,uint16_t port){
            struct sockaddr_in addr;
            addr.sin_family=AF_INET;
            addr.sin_port=htons(port);
            addr.sin_addr.s_addr=inet_addr(ip.c_str());
            socklen_t len=sizeof(struct sockaddr_in);
            // int ret = connect(_sockfd,reinterpret_cast<struct sockaddr*>(&addr),len);
            int ret = connect(_sockfd,(struct sockaddr*)&addr,len);

            if(ret<0){
                ERR_LOG("Connect 失败!!!!");
                return false;
            }
            return true;
        }
        int Accept(){
            //int accept(int sockfd,struct sockaddr* addr, socklen_t* len);
            int newfd = accept(_sockfd,nullptr,nullptr);
            if(newfd<0){
                ERR_LOG("Accept 失败!!!%s",strerror(errno));
                return -1;
            }
            return newfd;
        }
        ssize_t Recv(void*buf,size_t len,int flag=0){
            ssize_t ret = recv(_sockfd,buf,len,flag);
            if(ret<=0){
                if(errno == EAGAIN || errno == EINTR){
                    ERR_LOG("信号中断/没数据");
                    return 0;
                    //EAGAIN------非阻塞没数据了
                    //EINTR------阻塞,被信号中断了
                }
                return -1;
            }
            return ret;
        }
        ssize_t NonBlockRecv(void*buf,size_t len){return Recv(buf,len,MSG_DONTWAIT);}
        
        ssize_t Send(const void*buf,size_t len,int flag=0){
            ssize_t ret = send(_sockfd,buf,len,flag);
            if(ret<0){
                if(errno == EAGAIN || errno == EINTR){
                    ERR_LOG("信号中断/没数据");
                    return 0;
                    //EAGAIN------非阻塞没数据了
                    //EINTR------阻塞,被信号中断了
                }
                ERR_LOG("Send 失败!!!!");
                return -1;
            }
            return ret;
        }
        //MSG_DONTWAIT
        ssize_t NonBlockSend(const void*buf,size_t len){ if (len == 0) return 0;return Send(buf,len,MSG_DONTWAIT);}
        //关闭套接字
        void Close(){
            if(_sockfd!=-1){
                close(_sockfd);
                _sockfd=-1;
            }
        }
        //创建一个服务器连接
        bool CreateServer(uint16_t port,const std::string&ip="0.0.0.0",bool blog_flag=false){
            //1.创建监听套接字 2.设置非阻塞  3.绑定套接字  4.开始监听 5.启动地址重用
            if(!Create())return false;
            if(blog_flag)NonBlock();
            if(!Bind(ip,port))return false;
            if(!Listen())return false;
            ReuseAddress();
            return true;
        }
        //创建一个客户端连接
        bool CreateClient(uint16_t port,const std::string&ip){
            //1.创建连接套接字  2.连接服务器
            if(!Create())return false;
            if(!Connect(ip,port))return false;
            return true;
        }
        //设置套接字选项---开启地址端口的重用
        void ReuseAddress(){
            //int setsockopt(int fd,int leve , int optname , void * val,int vallen)
            int val = 1;
            setsockopt(_sockfd,SOL_SOCKET , SO_REUSEADDR , (void*)&val ,sizeof(int));
            val = 1;
            setsockopt(_sockfd,SOL_SOCKET , SO_REUSEPORT , (void*)&val ,sizeof(int));
        }
        //设置套接字阻塞属性---设置为非阻塞
        void NonBlock(){
            //int fcntl(int fd , int cmd ,...../*arg*/);
            int flag = fcntl(_sockfd,F_GETFL,0);
            fcntl(_sockfd,F_SETFL,flag | O_NONBLOCK);
        }
};

4.2.2 Channel类------事件封装

Channel类实际上就是对epoll操作的封装类,功能有:启动读/写事件监控、关闭读/写事件监控、是否监控了读/写事件、以及对就绪事件的处理。代码如下:

cpp 复制代码
//epoll模式LT模式-水平触发
//设置管理事件监控,写事件还是读事件,一个连接对应一个Channel类
#include <sys/epoll.h>  // 包含EPOLLIN等epoll事件标志的定义
class Channel{
    private:
        int _fd;
        EventLoop* _loop;
        uint32_t _events; // 当前需要监控的事件
        uint32_t _revents;// 当前监控事件的就绪事件 
        using EventCallback = std::function<void()>;
        EventCallback _read_callback; //可读事件被处罚的回调函数
        EventCallback _write_callback;//可写事件被处罚的回调函数
        EventCallback _error_callback;//错误事件被处罚的回调函数
        EventCallback _close_callback;//连接断开事件被处罚的回调函数
        EventCallback _event_callback;//任意事件被处罚的回调函数
    public:
        Channel(EventLoop* loop,int fd):_fd(fd),_loop(loop),_events( 0),_revents(0) {}
        int Fd(){return _fd;}
        uint32_t Events(){return _events;}//获取监控事件
        void SetRevents(uint32_t events){_revents=events;}//设置实际就绪的事件
        void SetReadCallback(const EventCallback& cb){_read_callback=cb;}
        void SetWriteCallback(const EventCallback& cb){_write_callback=cb;}
        void SetErrorCallback(const EventCallback& cb){_error_callback=cb;}
        void SetCloseCallback(const EventCallback& cb){_close_callback=cb;}
        void SetEventCallback(const EventCallback& cb){_event_callback=cb;}
        //当前是否监控了可读
        bool ReadAble(){return _events & EPOLLIN;/*就是看我当前监控的事件中是否监控了可读事件就是去按位与一下*/}
        //当前是否监控了可写,这里处理在LT模式下防止多次的设置可读可写导致的LoopBusy
        bool WriteAble(){return _events & EPOLLOUT;}
        //启动读事件监控 这只是设置了_event还并没有设置到epoll中去  
        void EnableRead(){_events |= EPOLLIN; Update();}
        //启动写事件监控
        void EnableWrite(){_events |= EPOLLOUT; Update();}
        //关闭读事件监控
        void DisableRead(){_events &= ~EPOLLIN; Update();}
        //关闭写事件监控
        void DisableWrite(){_events &= ~EPOLLOUT; Update();}
        //关闭所有事件监控
        void DisableAll(){_events=0;}
        //移除监控------后面调用EventLoop调用
        void Remove();
        void Update();
        //事件处理,一旦连接触发了事件,就调用这个函数,然后看_revnet触发了哪一个事件,然后内部调用不同的回调
        //自己触发了什么事件自己处理,EventLoop不关心,我只管调用HanleEvent------本质就是进行了一层封装嘛,看是封装到哪一层了。
        void HandleEvent(){
            //触发连接断开也就是半关闭连接EPOLLRDHUP(对方关闭连接),那么我们就要把缓冲区的数据读完
            if((_revents & EPOLLIN) || (_revents & EPOLLRDHUP) || (_revents & EPOLLPRI)){
                // if(_read_callback)_read_callback();
                // if(_event_callback)_event_callback();
                if(_read_callback)_read_callback();
            }
            //有可能会释放连接的事件,一次处理一个
            if(_revents & EPOLLOUT)
                {
                    //放到事件处理完毕后调用,刷新活跃度
                    // if(_write_callback)_write_callback();
                    // if(_event_callback)_event_callback();
                    if(_write_callback)_write_callback();
                }
            else if(_revents & EPOLLERR)
                {
                    //放到前面,因为出错直接关闭了
                    if(_error_callback)_error_callback();
                }
            else if(_revents & EPOLLHUP)
                {
                    if(_close_callback)_close_callback();
                }
            if(_event_callback)_event_callback();
        }
};

4.3 事件驱动模块

4.3.1 Poller类------对epoll的封装

Poller类时真正地对epoll的封装内部有epoll的创建,和对监控事件的真正修改。在成员变量中,有一个_epfd(epoll的fd操作符)以及一个epoll_event数组对所有连接的管理,还有一个连接一个fd的哈希存储表,方便查询和对单个连接操作。

cpp 复制代码
#define MAX_EPOLLEVENTS 1024
#include <sys/epoll.h>
#include<unordered_map>


//epoll管理类------对所有链接对应的所有Channel类管理------进行更新修改,开启事件监控和取消监控------对所有服务器上的连接做管理
class Poller{
    private:
        int _epfd;
        struct epoll_event _evs[MAX_EPOLLEVENTS];
        std::unordered_map<int,Channel*> _channels;
    private:
        //对epoll的直接操作
        void Update(Channel *channel,int op){
            //int epoll+ctl(int epfd , int op , int fd , struct eopll_event* ev);
            int fd = channel->Fd();
            struct epoll_event ev;
            ev.data.fd=fd;
            //要操作的事件
            ev.events=channel->Events();
            int ret = epoll_ctl(_epfd,op,fd,&ev);
            if(ret<0){
                ERR_LOG("epoll_ctl 失败!!!");
            }
            return ;
        }
        //判断一个Channel/一个连接是否已经添加了事件监控
        bool HashCHannel(Channel* channel){
            auto it = _channels.find(channel->Fd());
            if(it==_channels.end())return false;
            return true;
        }
       
    public:
        Poller(){
            _epfd = epoll_create(MAX_EPOLLEVENTS);
            if(_epfd<0){
                ERR_LOG("epoll_create 失败!!!");
                abort();
            }
        }
        //添加或修改监控事件
        void UpdateEvent(Channel *channel){
            auto it = _channels.find(channel->Fd());
            if(it==_channels.end()){
                _channels.insert({channel->Fd(),channel});
                return Update(channel,EPOLL_CTL_ADD);
            }
            Update(channel,EPOLL_CTL_MOD);
        }
        //移除监控
        void RemoveEvent(Channel *channel){
            auto it = _channels.find(channel->Fd());
            if(it!=_channels.end()) _channels.erase(it);
            Update(channel,EPOLL_CTL_DEL);
        }
        //开始监控,返回活跃连接
        void Poll(std::vector<Channel*>* active){
            //int epoll_wait(int epfd , struct epoll_event* evs , int maxevents , int timeout)
            int nfds = epoll_wait(_epfd,_evs,MAX_EPOLLEVENTS,-1);//-1代表阻塞等待,timeout>=0代表阻塞等待x秒,超时返回
            if(nfds<0){
                if(errno == EINTR){
                    //可能是由于中断打断了epoll_wait函数
                    return;//此时程序不退出
                }
                ERR_LOG("epoll_wait 失败:%s\n",strerror(errno));
                abort();//退出程序------异常退出有调试信息,exit正常退出清理程序
            }
            for(int i=0;i<nfds;++i){
                auto it = _channels.find(_evs[i].data.fd);
                assert(it!=_channels.end());
                it->second->SetRevents(_evs[i].events);
                active->push_back(it->second);
            }
        }
};

其中最关键的是Poll函数,开始监控,返回活跃连接。epoll_wait返回就绪链表中的事件数量,所以在后续的for循环中把就绪事件一一设置到每个连接的Channel中。

4.3.2 EventLoop类------Reactor核心

在EventLoop类中最主要的就是Start函数------处理就绪监控事件,执行任务队列中的任务的死循环,如果没有就绪监控事件就会卡在Poll中,但是在其他线程压入任务队列时、监控事件就绪时或者在服务器的listen_fd管理类中新连接到来时,就不在阻塞本质就是有监控事件就绪了epoll_wait函数返回了就不在阻塞了。代码如下:

cpp 复制代码
using Functor = std::function<void()>;

#include<thread>
#include<mutex>
#include<memory>
#include<sys/eventfd.h>
//任务执行的Loop------事件循环核心是Reactor的核心
class EventLoop{
    private:
        std::thread::id _thread_id;//线程ID

        int _event_fd;//eventfd唤醒IO事件监控有可能导致的阻塞,来一个任务要压入队列那么就要唤醒在epoll_wait的阻塞就要用到这个闹钟fd
        std::unique_ptr<Channel> _event_channel;
        //对所有的连接的事件做管理,因为线程池采用RR轮询的方式,一个线程会管理多个连接
        Poller _poller;//进行所有描述符的事件监控

        std::vector<Functor> _tasks;//任务池
        std::mutex _mutex;//实现任务池操作的线程安全
        TimerWheel _timer_wheel;//定时器模块
    public:
        void RunAllTask(){
            std::vector<Functor> functor;
            {
                std::unique_lock<std::mutex> _lock(_mutex);
                _tasks.swap(functor);
            }
            for(auto&f : functor)f();
            return ;
        }
        static int CreateEventFd(){
            int efd = eventfd(0,EFD_CLOEXEC | EFD_NONBLOCK);
            if(efd<0){
                ERR_LOG("eventfd failed!!!");
                abort();//让程序异常退出
            }
            return efd;
        }
        void ReadEventFd(){
            uint64_t res = 0;
            int ret =read(_event_fd,&res,sizeof(res));
            if(ret <0){
                //EAGAIN是资源占时不可用(比如读完了数据/没数据可读),EINTR是系统调用因信号中断而提前终止
                if(errno == EINTR || errno == EAGAIN){
                    return;
                }
                //这里就是严重错误
                ERR_LOG("read eventd 失败!!");
                abort();
            }
            return;
        }
        void WeakUpEventFd(){
            uint64_t val =1;
            int ret = write(_event_fd,&val,sizeof(val));
            if(ret <0){
                if(errno == EINTR){
                    return;
                }
                //这里就是严重错误
                ERR_LOG("write eventd 失败!!");
                abort();
            }
            return;
        }
    public:
        EventLoop():_thread_id(std::this_thread::get_id()),
                    _event_fd(CreateEventFd()),
                    _event_channel(new Channel(this,_event_fd)),
                    _timer_wheel(this){
            //给eventfd添加可读事件回调函数,读取eventfd事件通知次数
            _event_channel->SetReadCallback(std::bind(&EventLoop::ReadEventFd,this));
            //启动eventfd的读事件监控
            _event_channel->EnableRead();
        }
        void Start(){
            //这个循环可以加到start里面也可以在外部调用start时套上一层循环
            //Reactor的体现所在,Reactor就是一种手段和方法------监听事件,然后在监听事件中执行对应可执行事件。
            while(true){
                //分三步:1.事件监控 2.就绪事件处理 3.执行任务
                //1.事件监控
                std::vector<Channel*> actives;
                _poller.Poll(&actives);
                //这里顺序不能变通常IO是实时的,对延迟敏感,应该立即处理。
                //就绪事件的处理
                for(auto &channel:actives)channel->HandleEvent();
                //执行任务------把close(fd)关闭客户端连接放到任务池中(避免我还在处理我就直接关了)、定时器任务
                RunAllTask();
            }
        }

        //用于判断当前线程是否是EventLoop对应的线程
        bool IsInLoop(){
            return (_thread_id == std::this_thread::get_id());
        }
        
        //确保函数执行时loop中执行的
        void AssertInLoop(){
            assert(_thread_id == std::this_thread::get_id());
        }
        //判断将要执行的任务是否处于当前线程中,如果是就直接执行,如果不是就插入任务队列中。
        //这主要是为了拓展业务场景,比如连接管理线程,协议解析线程,业务逻辑线程,数据库线程,响应组装线程。多个线程的时候我一个连接一个在主线程中执行任务,连接管理线程是主线程
        void RunInLoop(const Functor & cb){
            if(IsInLoop()){
                return cb();
            }
            return QueueInLoop(cb);
        }

        //将操作压入任务池
        void QueueInLoop(const Functor &cb){
            {
                std::unique_lock<std::mutex> _lock(_mutex);
                _tasks.push_back(cb);
            }
            //唤醒哪些因为_poller.Poll()时的阻塞线程,不然任务队列半天不会被执行
            WeakUpEventFd();
        }

        //添加/修改描述符的事件监控
        void UpdateEvent(Channel* channel){
            return _poller.UpdateEvent(channel);
        }

        //移除描述符的监控
        void RemoveEvent(Channel* channel){
            return _poller.RemoveEvent(channel);
        }

        void TimerAdd(uint64_t id,uint32_t delay,const TaskFunc& cb){
            return _timer_wheel.TimerAdd(id,delay,cb);
        }
        void TimerRefresh(uint64_t id){return _timer_wheel.TimerRefresh(id);}
        void TimerCancel(uint64_t id){return _timer_wheel.TimerCancel(id);}
        //如果我要把定时器停了,然后再启用那么就需要这个接口
        bool HashTimer(uint64_t id){return _timer_wheel.HashTimer(id);}
};

在成员变量中有,线程ID、event_fd(闹钟fd)、闹钟的Channel、对所有连接的管理Poll类、任务池、定时器、锁。其中线程ID和event_fd是为了在单个连接的多线程场景下所设置的,线程ID是为了线程安全,保证在多线程场景中我压入到一个任务队列中,依次执行。最好使用锁确保数据的一致性。在单连接多线程场景中,我可能有工作线程,数据库线程,业务线程,链接管理线程,不同线程要把任务压入,同时因为我的EventLoop启动后一直在Poll阻塞等待就绪事件,所以这里为了防止我一直压入任务,任务确不能执行,我就设置eventfd这个变量,就相当于闹钟,我把闹钟挂到epoll的红黑树中,压入任务后我就调用WeakUpEventFd函数就会写入数据到eventfd中激活epoll_wait,就绪事件触发就不会阻塞了,就依次执行任务池中的任务

4.4 高级特性模块

4.4.1 TimerTask类------任务超时类

定时任务我这里的思路就是在析构函数中调自己的_task_cb任务回调,然后再时间轮定时器里超时了就直接把时间轮中的定时任务clear()自动调用自己的析构函数自动执行超时任务了。其余的就是成员变量的设置。

用一个64位的整形做定时器的ID,2^64个定时器面对,高并发的服务器也错错有余了,超时时间用一个32位的整形其实16位整型甚至uint_8都行具体看应用场景,用_canceled标记这个任务是否被取消定时了,避免重复执行,然后就是定时任务函数以及_release函数------这个函数是为了我在执行定时任务的时候顺便把定时任务在时间轮管理类中的保存记录也删了目的就是方便也符合高内聚低偶尔原则。代码如下:

cpp 复制代码
using TaskFunc = std::function<void()>;
using ReleaseFunc = std::function<void()>;

//单个超时任务类------主要是超时了就在他的析构函数中执行定时任务
class TimerTask{
    private:
        uint64_t _id;   //定时器任务对象ID
        uint32_t _timeout;//定时任务的超时时间
        bool _canceled;// false-表示没有被取消, true-表示被取消
        TaskFunc _task_cb;//定时器对象要执行的定时任务
        ReleaseFunc _release;//用于删除TimerWheel中保存的定时器对象信息
    public:
        TimerTask(uint64_t id, uint32_t delay, const TaskFunc &cb): 
            _id(id), _timeout(delay), _task_cb(cb), _canceled(false) {}
        ~TimerTask()
        { 
            if (_canceled == false) _task_cb(); 
            _release(); 
        }
        void Cancel() { _canceled = true; }
        void SetRelease(const ReleaseFunc &cb) { _release = cb; }
        uint32_t DelayTime() { return _timeout; }
};

4.4.2 TimerWheel类------时间轮定时器

上面说了超时任务类,这里的TImerWheel类就是模拟指针跳动来执行任务,已经对任务的增删查改的。代码如下:

cpp 复制代码
//时间轮------模拟时/秒/分钟的跳动------这里是模拟秒钟的跳动。
class TimerWheel{
    private:
        using WeakPtr = std::weak_ptr<TimerTask>;
        using PtrTask = std::shared_ptr<TimerTask>;
        int _tick;//秒针的位置
        int _capacity;//表盘的刻度
        //同理这是shared_ptr表只存在一份,clear了,就直接析构,因为可能有多个任务在同一个时刻所以用vector<vector<PtrTask>>的形式
        std::vector<std::vector<PtrTask>> _wheel;
        //这里很关键,我找任务的表只能存weak_ptr不然我无法做到析构函数执行任务,因为任务表存shared_ptr的话那么就永远有一个实例在。
        std::unordered_map<uint64_t,WeakPtr> _timers;


        //下面是为了把定时器融合到EventLoop中所需要添加的成员
        EventLoop* _loop;
        int _timerfd;//定时器描述符--可读事件回调就是读取计数器,执行定时任务
        std::unique_ptr<Channel> _timer_channel;
    private:
        void RemoveTimer(uint64_t id){
            auto it = _timers.find(id);
            if(it==_timers.end())return;
            _timers.erase(it);
        }
        static int CreateTimerfd(){
            int timerfd = timerfd_create(CLOCK_MONOTONIC,0);
            if(timerfd < 0){
                ERR_LOG("TIMERFD CREATE FAILED!!!");
                abort();
            }
            //int timerfd_settime(int fd,int flags,struct itimerspc* new,struct itimerspc* old);
            struct itimerspec itime;
            itime.it_value.tv_sec=1;
            itime.it_value.tv_nsec=0;//第一次超时间为1s后
            itime.it_interval.tv_sec =1;
            itime.it_interval.tv_nsec=0;//第一次超时后,每次超时的间隔时间
            timerfd_settime(timerfd,0,&itime,nullptr);
            return timerfd;
        }
        int ReadTimefd(){
            uint64_t times;
            int ret = read(_timerfd,&times,8);
            if(ret<0){
                ERR_LOG("READ TIMEFD FAILED!!!");
                abort();
            }
            return times;
        }
        
        

        void RunTimerTask() {
            _tick = (_tick + 1) % _capacity;
            _wheel[_tick].clear();//清空指定位置的数组,就会把数组中保存的所有管理定时器对象的shared_ptr释放掉
        }
        void OnTime(){
            int times = ReadTimefd();
            for (int i = 0; i < times; i++) {
                RunTimerTask();
            }
        }
        //添加定时任务
        void TimerAddInLoop(uint64_t id , uint32_t delay,const TaskFunc&cb){
            //可能出现同时插入同一个任务的情况
            //新增:检查ID是否已存在,存在则先取消旧任务
            auto it = _timers.find(id);
            if(it != _timers.end()){
                PtrTask old_task = it->second.lock();
                if(old_task) old_task->Cancel(); // 标记旧任务取消,避免执行
            }
            PtrTask pt(new TimerTask(id,delay,cb));
            pt->SetRelease(std::bind(&TimerWheel::RemoveTimer,this,id));
            int pos = (_tick+delay)%_capacity;
            _wheel[pos].push_back(pt);
            _timers[id]=WeakPtr(pt);
        } 
        //刷新/延迟定时任务
        void TimerRefreshInLoop(uint64_t id){
            //通过保存的定时器对象的weak_ptr构造处一个shared_ptr出来,添加到轮子中
            auto it = _timers.find(id);
            if(it == _timers.end())return;
            PtrTask pt = it->second.lock();//lock获取weak_ptr的对象,并赋值给新的shared_ptr去管理此时引用计数+1
            int delay = pt->DelayTime();
            int pos = (_tick+delay)%_capacity;
            _wheel[pos].push_back(pt);
        }
        void TimerCancelInLoop(uint64_t id){
            auto it = _timers.find(id);
            if(it == _timers.end()){
                return;//没找到定时任务,没法刷新,没法延迟
            }
            PtrTask pt = it->second.lock();
            if(pt)pt->Cancel();
        }
    public:
        TimerWheel(EventLoop* loop):_capacity(60),_tick(0),_wheel(_capacity),_loop(loop),
                                    _timerfd(CreateTimerfd()),_timer_channel(new Channel(_loop,_timerfd))
        {
            _timer_channel->SetReadCallback(std::bind(&TimerWheel::OnTime,this));
            _timer_channel->EnableRead();//启动读事件监控
        }
        void TimerAdd(uint64_t id , uint32_t delay,const TaskFunc&cb);
        void TimerRefresh(uint64_t id);
        void TimerCancel(uint64_t id);
        //这个接口存在线程安全问题,不能在其他线程调用只能在对应EventLoop中调用。
        bool HashTimer(uint64_t id){
            auto it = _timers.find(id);
            if(it == _timers.end())return false;
            return true;
        }
       
};

4.4.3 LoopThread/LoopThreadPoll类------多线程类

在LoopThread类中我不能直接创建EventLoop实例对象不然线程ID就是主线程的ID/当前线程ID,所以在LoopThread中是EventLoop的指针对象,接着有一个函数ThreadEntry这个函数是传给thread的执行函数,然后再thread线程中创建loop,这样在哪个线程中创建的EventLoop中的线程id就是哪个线程的。我创建了EventLoop实例同时是要提供给外部使用的,所以我还需要一个GetLoop函数,但是我不能没有创建线程我就直接给别人获取吧,这样是错误的,所以这里我还需要加上条件变量去控制顺序,只有创建了线程后才能去调用GetLoop函数去获取EventLoop实例。

在LoopThreadPoll类中采用RR轮询的方式去一次访问线程池中的其余线程。用vector<LoopThread*> _threads保存所有的LoopThread对象(线程池),用vector<EventLoop*> _loops保存线程池中的EventLoop对象,这里下标是对应关系,线程的下标和EventLoop的下标是一样的。_threads和_loops的大小就是线程池大小用_thread_count来设置,用_next_idx表示应该工作到哪一个线程了因为是RR轮询的方式。

代码如下:

cpp 复制代码
//先有线程再有EventLoop对象,然后EventLoop对象在线程中实例化(不然设置id就是主线程的线程id),这样才能保证id是唯一的,
#include <condition_variable> // 条件变量的核心头文件
#include <mutex>              // 各种锁(互斥锁、递归锁等)的核心头文件
class LoopThread{
    private:
        //同步操作
        std::mutex _mutex;//互斥锁
        std::condition_variable _cond;//条件变量
        
        //创建一个线程一个EventLoop的实例化
        EventLoop *_loop;   //EventLoop的指针变量,这个对象需要在线程内取实例化,所以要初始化为指针(引用初始化必须赋值,所以这里还是用指针),传递我的loop对象给主线程
        std::thread _thread;//EventLoop对应的线程
    private:  
        //实例化EventLoop对象,唤醒_cond上可能use的线程,并且开始运行EventLoop模块的功能------实际上就是去运行start函数
        void ThreadEntry(){
            //但是注意这是局部变量函数退出就销毁了,所以下面的start是循环执行的
            EventLoop loop;//这里为什么不直接使用_loop呢?------首先是局部的,所以跳出作用域自动销毁了,其次这是一个EventLoop实例,直接赋值了。如果LoopThread中_loop是实例对象的话,那么线程id就是主线程也就是创建实例的线程id了,我把这个当作thread()函数的函数传入thread然后在新的线程中创建,这样id就是新线程的id了
            {
                std::unique_lock<std::mutex> lock(_mutex);//加锁
                _loop=&loop;
                _cond.notify_all();//唤醒所有等待的条件变量
            }
            loop.Start();
        }
        public:
        //在构造函数中创建线程,设定线程的入口函数------调用构造函数就自动创建线程
        LoopThread():_loop(nullptr),_thread(std::thread(std::bind(&LoopThread::ThreadEntry,this))){}
        //当外界有一个新链接来了,我得获取这个EventLoop才能去添加任务,才能将EventLoop和连接关联
        //但是我们要注意,假设此时我们还没有设置EventLoop,就坏事了所以此时要用条件变量来控制顺序,GetLoop中就阻塞等待实例化完了才可以获取EventLoop实例化对象
        EventLoop *GetLoop(){
            EventLoop *loop = nullptr;
            {
                std::unique_lock<std::mutex> lock(_mutex);//加锁-在构造函数中等待
                //使用&------避免拷贝,引用的方式获取外部所有的变量,可修改。=不可修改,还会造成拷贝
                _cond.wait(lock,[&](){return _loop !=nullptr;});//loop为nullptr就阻塞,等待被唤醒,把判断条件写到wait函数中
                loop=_loop;
            }
            return loop;
        }

};

//线程池
class LoopThreadPoll{
    private:
        int _thread_count;//从属线程的数量
        int _next_idx;//当前哪一个线程在运行/工作到哪一个线程了
        EventLoop* _baseloop;//主EventLoop,运行在主线程,从属线程数量为0,则所有操作都在baseloop中进行
        std::vector<LoopThread*> _threads;//保存所有的LoopThread对象
        std::vector<EventLoop*> _loops;//从属线程数量大于0则从_loops中进行线程EventLoop分配------设置这个vector是有必要的,因为我不能直接使用_threads->_loop对象,因为此时_loop可能是非线程安全的,可能没有创建,用GetLoop函数肯定是存在才能获取的。所以为了方便用数组存EventLoop对象
    public:
        LoopThreadPoll(EventLoop *baseloop):_thread_count(0),_next_idx(0),_baseloop(baseloop){}
        //设置线程数量
        void SetThreadCount(const int count){_thread_count=count;}
        //创建所有的从属线程
        void Create(){
            if(_thread_count>0){
                _threads.resize(_thread_count);
                _loops.resize(_thread_count);
                for(int i=0;i<_thread_count;++i){
                    _threads[i]=new LoopThread();
                    _loops[i]=_threads[i]->GetLoop();
                }
            }
        }
        //因为采取RR轮询的方式所以命名是返回下一个线程
        EventLoop* NextLoop(){
            if(_thread_count==0)return _baseloop;
            _next_idx=(_next_idx+1)%_thread_count;
            return _loops[_next_idx];  
        }
};

4.5 业务核心模块

4.5.1 Acceptor类------监听套接字管理类

是主Reactor的具体实现类,主Reactor的作用就是管理监听fd,来一个连接就调用回调函数去建立一个连接类,底层还是调用了我们之前封装的Channel类、Socket类、EventLoop类。这里的EventLoop我们直接使用主线程的EventLoop就行了,也就是TcpServer类的EventLoop。代码如下:

cpp 复制代码
class Acceptor{
    private:
        Socket _socket;//用于创建套接字
        EventLoop *_loop;//用于对监听套接字进行事件监控
        Channel _channel;//用于对监听套接字进行事件监控

        using AcceptCallback = std::function<void(int)>;
        AcceptCallback _accept_callback;
    private:
        //监听套接字的读事件回调处理函数--获取新连接,调用_accept_callback函数进行新链接处理
        void HandleRead(){
            int newfd = _socket.Accept();
            if(newfd<0)return;
            if(_accept_callback)_accept_callback(newfd); 
        }
        int CreateServer(uint16_t port){
            bool ret = _socket.CreateServer(port);
            assert(ret == true);
            return _socket.Fd();
        }
    public:
    
    //这里启动事件监控比设置回调早,这就会导致,立即有监控了,
    //但是回调还没被设置,什么没做就关了,fd也没被释放,所以不能把启动读事件监控放到构造函数中
    //  
    Acceptor(EventLoop* loop,uint16_t port):
            _socket(CreateServer(port)),
            _loop(loop),
            _channel(loop,_socket.Fd()){
            _channel.SetReadCallback(std::bind(&Acceptor::HandleRead,this));
        }
        void SetAcceptCallback(const AcceptCallback& cb){_accept_callback = cb;}
        //所以要设置一个listen接口。使用者手动开始监听。
        void Listen(){_channel.EnableRead();}
};

具体流程就是,在TcpServer中设置_accept_callback------实际上就是创建连接类的函数,☞后在Acceptor的构造函数中设置监听套接字,把监听套接字的fd设置到Channel中以及把callback设置给Channel,然后在TcpServer中启动对读事件监控。这样只要有客户端连接来了,读事件就会就绪,相应的就会在EventLoop中的Start函数中Poll就检测到就绪事件就会在Channel中执行就绪事件的回调函数,然后我设置的回调函数是给客户端创建新链接也就是创建一个Connection类,所以客户端也创建成功了。

4.5.2 Connection类------连接管理类/客户端连接类

这就是真正的客户端连接类,包含自定义缓冲区Buffer、连接套接字、连接事件的管理Channel类、类型擦除类(保存请求接收处理的上下文)、连接id(方便我们TcpServer类管理)、是否启动活跃销毁标志、EventLoop类、连接状态的枚举。尤其是连接状态的枚举,方便我们更加灵活的处理各种函数。其公有成员函数就是将私有函数封装到任务队列里然后调用。各种私有函数主要就是:SendInLoop发送数据、ShutdownInLoop关闭连接、关闭和开启连接的非活跃销毁、然后就是更新协议(这个可能用不到)、已经各种设置给Channel回调函数的设置读、写、错误、任意事件、关闭。最后在对外的公共接口就是一些安全的访问私有变量的函数,以及Connection类中回调函数的设置。

cpp 复制代码
//设置连接状态
typedef enum {DISCONNECTED , CONNECTED , CONNECTING , DISCONNECTING}ConnStatu;
// DISCONNECTED -- 连接关闭转态
// ONNECTED -- 连接建立成功-待处理转态
// CONNECTING -- 连接建立完成,各种设置已完成,可以通信
// DISCONNECTING -- 待关闭状态

using PtrConnection = std::shared_ptr<Connection>;

//一个连接一个Connection
//只有继承了enable_shared_from_this<Connection>才能使用shared_from_this,返回一个自己的shared_ptr,enable_shared_from_this<Connection>底层是封装了一个weak_ptr,所以shared_ptr计数不会+---1
class Connection: public std::enable_shared_from_this<Connection>{
    private:
        uint64_t _conn_id;//连接的唯一ID,便于连接的管理和查找,因为_conn_id唯一所以我们默认_time_id就是_conn_id
        int _sockfd;//连接关联的文件描述符
        bool _enable_inactive_release; //连接是否启动非活跃销毁的判断标志,默认为false
        //这里的EventLoop设置的是指针原因是这里的loop是线程池中传过来的loop。如果直接设置为对象的话就不是线程池模式了失去了线程池的优势会频繁的创建和销毁线程------这要从用户态切换成内核态耗时
        //线程池是一直存在内存中,也避免了缓存污染
        EventLoop *_loop;//关联一个EventLoop同样一个连接的函数调用应该是线程安全的,所以同样要关联一个loop,因为一个loop关联一个线程
        ConnStatu _statu;//连接的状态

        Socket _socket;//创建socket套接字,进行基础的网络通信设置
        Channel _channel;//连接的事件管理
        Buffer _in_buffer;//输入缓冲区
        Buffer _out_buffer;//输出缓冲区
        Any _context;//请求的接收处理上下文

        //下面四个回调函数,是让组件使用者(服务器模块)来设置的--其实服务器模块的处理回调也是组件使用者设置的
        using ConnectedCallback = std::function<void(const PtrConnection &)>;
        using MessageCallback = std::function<void(const PtrConnection &,Buffer *)>;
        using CloseCallback = std::function<void(const PtrConnection &)>;
        using AnyEventCallback = std::function<void(const PtrConnection &)>;
        ConnectedCallback _connected_callback;
        MessageCallback _message_callback;
        CloseCallback _closed_callback;
        AnyEventCallback _event_callback;
        //额外设置的下面这个是------组件内设置的连接关闭回调------组件内设置的,因为服务器组件内会把所有的连接管理起来,一旦某个连接要关闭
        //那么就应该从管理的地方移除掉自己的信息
        CloseCallback _server_closed_callback;

        //同时这些回调,一个连接所有的回调都要放到任务队列中去,确保线程安全
    private:
        //所以需要设置一堆的InLoop接口,把回调放入同一个loop中的同一个任务队列中

        //连接获取之后,所在的状态下要进行各种设置(给Channel设置事件回调,启动读监控)
        void EstablIsShedInLoop(){
            //1.修改连接状态  2.启动读事件监控    3.调用回调函数
            assert(_statu == CONNECTING);//当前连接一定是的半连接状态,这样我才去设置事件回调
            _statu = CONNECTED;//当前函数执行完毕,则连接进入已完成连接状态
            _channel.EnableRead();//一旦启动读事件监控就有可能会立即触发读事件,如果这时候启动了非活跃连接销毁
            if(_connected_callback)_connected_callback(shared_from_this());//有连接回调函数就触发对应的函数
        }
        
        //实际的释放接口
        void ReleaseInLoop(){
            //1.修改连接状态,将其置为DISCONNECTED
            _statu=DISCONNECTED;
            //2.移除连接的事件监控
            _channel.Remove();
            //3.关闭描述符
            _socket.Close();
            //4.如果当前定时器队列中还有定时销毁任务,则取消任务
            if(_loop->HashTimer(_conn_id)) CancelInactiveReleaseInLoop();
            //5.调用关闭回调函数,避免先移除服务器管理的连接信息导致Connection被释放,再去处理会出错,因此先调用用户的关闭服务器回调函数
            if(_closed_callback)_closed_callback(shared_from_this());
            //6.调用组件内也就是Connection的关闭回调函数
            if(_server_closed_callback)_server_closed_callback(shared_from_this());
        }
        
        // void SendInLoop(const char* data,size_t len){
        //     if(_statu == DISCONNECTED)return ;
        //     _out_buffer.WriteAndPush(data,len);
        //     if(_channel.WriteAble()==false){
        //         _channel.EnableWrite();
        //     }
        // }
        //这个接口并不是实际的发送接口,而只是把数据放到了发送缓冲区,因为已经启动了可写事件监控
        void SendInLoop(Buffer &buf) {
            if (_statu == DISCONNECTED) return ;
            _out_buffer.WriteBufferAndPush(buf);
            //我写到发送缓冲区了,如果没有监控写缓冲区就启动监控
            if (_channel.WriteAble() == false) {
                _channel.EnableWrite();
            }
        }
        void ShutdownInLoop(){
            _statu = DISCONNECTING;//设置连接为半关闭状态
            if(_in_buffer.ReadAbleSize()>0){
                if(_message_callback)_message_callback(shared_from_this(),&_in_buffer);
            }
            //要么就是写入数据的时候出错关闭,要么就是么有待发送数据,直接关闭
            //虽然SendLoop函数中做了写事件监控的判断,但是这样更加优雅,防止代码瑕疵。更加安全
            if(_out_buffer.ReadAbleSize()>0){
                if(_channel.WriteAble() == false){
                    _channel.EnableWrite();
                }
            }
            if(_out_buffer.ReadAbleSize() == 0){
                Release();
            }
        }
        //启动非活跃连接超时释放规则
        void EnableInactiveReleaseInLoop(int sec){
            //1.将判断标志 _enable_inactive_release 置为true
            _enable_inactive_release = true;
            //2.如果当前定时销毁任务已经存在,那就刷新一下延迟即可
            if(_loop->HashTimer(_conn_id))return _loop->TimerRefresh(_conn_id);
            //3.如果不存在定时销毁任务,则新增
            _loop->TimerAdd(_conn_id,sec,std::bind(&Connection::Release,this));
        }
        //取消非活跃连接超时释放规则
        void CancelInactiveReleaseInLoop(){
            _enable_inactive_release = false;
            //存在则取消
            if(_loop->HashTimer(_conn_id))return _loop->TimerCancel(_conn_id);
        }
        //更新协议
        void UpgradeInLoop(const Any & context ,
            const ConnectedCallback& conn,
            MessageCallback& msg,
            CloseCallback& closed,
            AnyEventCallback& event){
                _context=context;
                _connected_callback=conn;
                _message_callback=msg;
                _closed_callback=closed;
                _event_callback=event;
        }
        
        //除此之外还有五个channel的事件回调函数
        //触发可读事件后调用的函数,接受socket数据放到接受缓冲区中,然后调用_message_callback处理
        void HandleRead(){
            //1.接受socket数据,放到缓冲区
            char buf[65536];
            ssize_t ret = _socket.NonBlockRecv(buf,65535);
            if(ret < 0){
                //出错了,不能直接关闭连接,要看数据还有没有要处理的或待处理的。
                return ShutdownInLoop();//然后在函数中,判断是否是待关闭连接的状态。还有就是只有连接处理完后才去定一个加时或者刷新,不然我连接业务可能没处理完就超时关闭之类的了
            }
            // else if(ret == 0){
            //     //这里的等于0表示的是没有读取到数据,而并不是连接断开了,连接断开返回的是-1
            //     //但是可以不写因为在Write函数中我们还设置了ret==0 就return的情况。
            //     return;
            // }
            //将数据放入输入缓冲区
            _in_buffer.WriteAndPush(buf,ret);
            //2.调用message_callback进行业务处理
            if(_in_buffer.ReadAbleSize()>0){
                //shared_from_this--从当前对象自身获取自身的shared_ptr管理对象
                //这就是ret ==0 的情况,不过我写了else if这里永远成立 ,有数据就进行业务处理
                return _message_callback(shared_from_this(),&_in_buffer);
            }
        }
        //描述符可写事件触发后调用的函数,将发送缓冲区中的数据进行发送。
        void HandleWrite(){
            ssize_t ret = _socket.NonBlockSend(_out_buffer.ReadPosition(),_out_buffer.ReadAbleSize());
            if(ret<0){
                //出错了要关闭
                //但是想要把缓冲区中的数据先处理了
                if(_in_buffer.ReadAbleSize()>0){
                    _message_callback(shared_from_this(),&_in_buffer);
                }
                //没有就释放
                return Release();//这时候就是实际的关闭释放操作了。因为发送完了我就不需要继续处理了。
            }
            _out_buffer.MoveReadOffset(ret);//还要将我们的读偏移向后偏移
            if(_out_buffer.ReadAbleSize() == 0){
                _channel.DisableWrite();//关闭写事件监控,缓冲区已经没有数据了,就不要监控写了(不断的触发写事件,又没有事件可写LoopBusy),LT模式下如果有数据就会一直提示-ET
                //回顾epoll的监控,接收缓冲区只要有数据可读就触发读事件,发送缓冲区只要有空闲空间就触发写事件。
                //如果当前是连接待关闭状态,则有数据,发送完连接数据释放连接,没有数据则直接释放
                if(_statu == DISCONNECTING){
                    return Release();
                }
            }
            return;
        }
        //描述符触发挂断事件
        void HandleClose(){
            //一旦连接挂断了,套接字就什么都干不了,因此有数据待处理就处理一下,完毕关闭连接
            if(_in_buffer.ReadAbleSize()>0){
                _message_callback(shared_from_this(),&_in_buffer);
            }
            return Release();
        }
        //描述符触发出错事件
        void HandleError(){
            return HandleClose();
        }
        //描述符触发任意事件。1.刷新连接的活跃度-延迟定时销毁任务。2.调用组件使用者的任意事件回调
        void HandleEvent(){
            //启动了非活跃销毁
            if(_enable_inactive_release == true){
                _loop->TimerRefresh(_conn_id);
            }
            if(_event_callback)_event_callback(shared_from_this());
        }
    public:
        Connection(EventLoop* loop,uint64_t conn_id ,int sockfd):_conn_id(conn_id),
            _sockfd(sockfd),_enable_inactive_release(false),_loop(loop),
            _statu(CONNECTING),_socket(_sockfd),_channel(loop,sockfd)
        {
            _channel.SetCloseCallback(std::bind(&Connection::HandleClose,this));
            _channel.SetEventCallback(std::bind(&Connection::HandleEvent,this));
            _channel.SetReadCallback(std::bind(&Connection::HandleRead,this));
            _channel.SetErrorCallback(std::bind(&Connection::HandleError,this));
            _channel.SetWriteCallback(std::bind(&Connection::HandleWrite,this));
        }
        ~Connection(){DBG_LOG("释放连接类:%p",this);}
        //发送数据到缓冲区,启动事件监控,因为这只是用户调用接口,我们还要处理缓冲区数据(协议,粘包,是否完整等),真正发送是epoll监控写事件后调用socket.send()发送
        void Send(const char* data,size_t len){
            // _loop->RunInLoop(std::bind(&Connection::SendInLoop,this,data,len));
            //外界传入的data,可能是个临时的空间,我们现在只是把发送操作压入了任务池,有可能并没有被立即执行
            //因此有可能执行的时候,data指向的空间有可能已经被释放了。
            Buffer buf;
            buf.WriteAndPush(data, len);
            _loop->RunInLoop(std::bind(&Connection::SendInLoop, this, std::move(buf)));
        }
        //提供给组件使用者的关闭接口--并不实际关闭,需要判断有没有数据待处理
        void Shutdown(){
            _loop->RunInLoop(std::bind(&Connection::ShutdownInLoop,this));
        }
        void Release(){
            _loop->QueueInLoop(std::bind(&Connection::ReleaseInLoop,this));
        }
        //启动非活跃销毁,并定义多长时间无通信就是非活跃,添加定时任务
        void EnableInactiveRelease(int sec){
            _loop->RunInLoop(std::bind(&Connection::EnableInactiveReleaseInLoop,this,sec));
        }
        //取消非活跃销毁
        void CancelInactiveRelease(){
            _loop->RunInLoop(std::bind(&Connection::CancelInactiveReleaseInLoop,this));
        }
        
        //链接建立就绪后,进行Channel回调设置,并调用connect_cb
        void Established(){
            _loop->RunInLoop(std::bind(&Connection::EstablIsShedInLoop,this));
        }
        //切换协议--重置上下文以及阶段性处理函数 -- 这是非线程安全的,必须在线程当中立即执行,防止我要在A后切换协议,但是多线程可能A还没结束协议先执行了协议了这种情况。
        void Upgrade(const Any & context,
            const ConnectedCallback& conn,
            MessageCallback& msg,
            CloseCallback& closed,
            AnyEventCallback& event){
            _loop->AssertInLoop();
            _loop->RunInLoop(std::bind(&Connection::UpgradeInLoop,this,context,conn,msg,closed,event));
        }
        
        //对于Connection的其余接口
        //成员初始化接口,获取成员接口
        int Fd(){return _sockfd;}
        int Id(){return _conn_id;}
        // ConnStatu Statu();//返回一个连接的转态
        // //但是我们真正关心的是是否处于连接CONNECTED的状态
        bool Connected(){return (_statu == CONNECTED);}
        //获取上下文信息,返回的是指针
        Any* GetContext(){return &_context;}
        //设置成员初始化
        void SetContext(const Any& context){_context = context;}
        //设置上下文信息,连接建立完成阶段(???或者假设协议切换了,同样也要把之前的上下文信息修改成新的协议上下文信息)
        void SetConnectedCallback(const ConnectedCallback& cb){_connected_callback=cb;}
        void SetMessageCallback(const MessageCallback& cb){_message_callback=cb;}
        void SetClosedCallback(const CloseCallback& cb){_closed_callback=cb;}
        void SetSrvClosedCallback(const CloseCallback& cb){_server_closed_callback=cb;}
        void SetAnyEventCallback(const AnyEventCallback& cb){_event_callback=cb;}
};     

核心还是底层的Channel类epoll对就绪事件的管理,特点是这里还处理了多线程也就是单连接多线程的场景下的任务池处理。

4.5.3 TcpServer类------服务器入口类

TcpServer类就是对上面的Acceptor类和Connection以及线程池的核心封装,用_conns管理所有的连接。TcpServer类就比较简单了各种函数就是对连接的处理函数,以及添加定时任务。最核心的就是NewConnection函数设置给Acceptor然后Acceptor才可以在新连接来的时候创建Connection类给新客户端。代码如下:

cpp 复制代码
class TcpServer{
    private:
        uint64_t _next_id;      //自动增长的连接ID
        int _port;              //服务器所挂载的端口
        int _timeout;           //非活跃链接的统计时间
        //在_acceptor之前我们要把_baseloop实例化出来
        EventLoop _baseloop;    //主线程的EventLoop对象,负责监听事件的处理
        Acceptor _acceptor;     //监听套接字的管理对象
        LoopThreadPoll _pool;   //从属EventLoop的线程池
        bool _enable_inactive_release;//是否启动了非活跃链接销毁的判断标志
        std::unordered_map<uint64_t,PtrConnection> _conns;//保存管理所有连接对应的shared_ptr对象
    private:
        //回调函数------Connection的回调函数
        using ConnectedCallback = std::function<void(const PtrConnection &)>;
        using MessageCallback = std::function<void(const PtrConnection &,Buffer *)>;
        using CloseCallback = std::function<void(const PtrConnection &)>;
        using AnyEventCallback = std::function<void(const PtrConnection &)>;
        ConnectedCallback _connected_callback;
        MessageCallback _message_callback;
        CloseCallback _closed_callback;
        AnyEventCallback _event_callback;
    private:
        void RemoveConnectionInLoop(const PtrConnection &conn){
            int id=conn->Id();
            auto it=_conns.find(id);
            if(it!=_conns.end())
                _conns.erase(id);
        }
        //从管理的_conns中移除对应连接信息,这个连接才会真正的释放。这实际上就是服务器中的close也就是SrvClosedCallback
        void RemoveConnection(const PtrConnection &conn){
            _baseloop.RunInLoop(std::bind(&TcpServer::RemoveConnectionInLoop,this,conn));
        }
        //为新链接构造一个Connection进行管理
        void NewConnection(int fd){
            _next_id++;
            PtrConnection conn(new Connection(_pool.NextLoop(), _next_id,fd));
            //这里的设置顺序不能乱,因为在Established函数中会先调用Message_cb函数所以Message
            conn->SetMessageCallback(_message_callback);
            conn->SetClosedCallback(_closed_callback);
            conn->SetConnectedCallback(_connected_callback);
            conn->SetAnyEventCallback(_event_callback);
            conn->SetSrvClosedCallback(std::bind(&TcpServer::RemoveConnection,this,std::placeholders::_1));
            if(_enable_inactive_release)conn->EnableInactiveRelease(_timeout);
            conn->Established();
            _conns.insert(std::make_pair(_next_id,conn));//管理起来不然直接释放了就用不了了
        }
        
        //添加定时任务
        void RunAfterInLoop(const Functor& task,int delay){
            _next_id++;
            _baseloop.TimerAdd(_next_id,delay,task);
        }
    public:
        TcpServer(int port):_next_id(0),_port(port),_timeout(5),//默认5s
                            _acceptor(&_baseloop,_port),
                            _pool(&_baseloop),
                            _enable_inactive_release(false){
                            // _acceptor.Listen();//Listen不能挂到这里因为服务器的其他设置还没设置好
                            // _pool.Create();//创建线程池的从属线程,但是不能放到构建函数中,因为此时还没有设置SetThreadCount所以线程数量默认是0
                            _acceptor.SetAcceptCallback(std::bind(&TcpServer::NewConnection,this,std::placeholders::_1));
                            _acceptor.Listen();//将监听套接字挂到baseloop上开始监控事件
        }
        void SetThreadCount(int count){_pool.SetThreadCount(count);}
        void Start(){_pool.Create();_baseloop.Start();}
        void EnableInactiveRelease(int timeout){_timeout=timeout;_enable_inactive_release=true;}
        //用于添加一个定时任务
        void RunAfter(const Functor& task,int delay){
            _baseloop.RunInLoop(std::bind(&TcpServer::RunAfterInLoop,this,task,delay));
        }
        //设置回调函数
        void SetConnectedCallback(const ConnectedCallback& cb){_connected_callback=cb;}
        void SetMessageCallback(const MessageCallback& cb){_message_callback=cb;}
        void SetClosedCallback(const CloseCallback& cb){_closed_callback=cb;}
        void SetAnyEventCallback(const AnyEventCallback& cb){_event_callback=cb;}
};

五:项目总结

一个完整的仿muduo库的高性能服务器项目代码就是这样,整体看下来核心技术和整体的框架之间的联系还是比较通透的,最主要的核心就是Channel类和EventLoop类,Channel是epoll的核心负责对事件的监控已经返回就绪事件,EventLoop是Reactor的核心负责获取就绪事件和对就绪事件的处理已经对任务池中的任务处理。

核心技术亮点
  • 线程模型优化:LoopThreadPoll 实现连接的 RR 轮询分配,充分利用多核 CPU,解决单线程 Reactor 的性能瓶颈;
  • 定时器高效实现:基于 timerfd 的单层时间轮定时器,O (1) 时间复杂度处理短定时任务,适配连接超时场景;
  • 协议灵活适配:Any 类实现类型擦除,支持 HTTP / 自定义协议的上下文存储,无需修改框架即可扩展新协议;
  • IO 事件安全处理:ET 模式 + 非阻塞 fd 的组合,避免数据残留和线程卡死,保证高并发下的稳定性。
相关推荐
f大熊2 小时前
服务器状态监控
linux·运维·服务器·ubuntu·watchdog
我在人间贩卖青春2 小时前
Socket套接字与TCP实现框架
网络·网络协议·tcp/ip·socket
IDC02_FEIYA2 小时前
Discuz!论坛注册验证邮箱收不到邮件怎么办?邮件检测发送成功,但是收发邮箱都未看到邮件
linux·服务器·阿里云
zuoyou-HPU2 小时前
QT C++开发知识点剖析
开发语言·c++·qt
草莓熊Lotso2 小时前
Qt 按钮与显示类控件实战:从交互到展示全攻略
大数据·开发语言·c++·人工智能·qt·microsoft·交互
_OP_CHEN2 小时前
【Linux系统编程】(二十一)吃透 Linux “一切皆文件” 与缓冲区:从底层逻辑到实战封装
linux·操作系统·glibc·c/c++·缓冲区·linux文件·io库
承渊政道2 小时前
C++学习之旅【C++Stack和Queue类介绍—入门指南与核心概念解析】
c语言·数据结构·c++·学习·visual studio
爱学习的阿磊2 小时前
模板编译期排序算法
开发语言·c++·算法
Jtti2 小时前
怎么避免国外服务器的丢包问题?
运维·服务器