最新资讯

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

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

2026-01-28 21:26:30 栏目:最新资讯 2 阅读

前言:

        本文实现的是一个仿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
EventLoopReactor的核心类(事件处理)
LoopThread封装EventLoop,一个线程管理一个EventLoop
LoopThreadPoll线程池类
TimerTask定时任务类
TimerWheel时间轮(模拟秒针跳动,去执行定时任务类)
Connection客户端(客户端fd)连接类
Acceptor监听套接字(listen_fd)管理类
TcpServer集成之前的所有类,真正的服务器类

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

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

4.1基础工具模块

4.1.1 日志宏——LOG

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

#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

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

4.1.2 Any类——类型擦除

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

//类型擦除容器——存储任意类型的上下文
//用同一个 Any 对象就能存储任意类型的数据,而不是为 int、string、自定义类分别创建 Any、Any 这类绑定了具体类型的对象——用在假设我要用其他应用层协议这个项目后面写了一个http服务器所有可以直接用,我换个https也可以用——这就是Any类的意义所在
class Any{
    private:
        class holder{
            public:
                virtual ~holder(){}
                virtual const std::type_info& type()=0;
                virtual holder* clone()=0;
        };
        template
        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(_val);}
                T _val;
        };
        holder* _content;
    public:
        Any():_content(nullptr){}
        
        template
        Any(const T& val){
            _content=new placeholder (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
        T* get(){
            assert(_content != nullptr && "Any object is empty");
            assert(_content->type() == typeid(T) && "Type mismatch");
            //这里换成static_cast更好,static_cast是编译时检查,运行零开销,而且我已经用assert断言了不会出现类型不匹配
            // return &((dynamic_cast*>(_content))->_val);
            return &((static_cast*>(_content))->_val);
        }
       
        //重载赋值函数
        template
        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缓冲区拿上来做封装。代码如下:


//服务器业务缓冲区——将IO缓冲区拿到业务缓冲区做处理
#define BUFFER_DEFAULT_SIZE 1024
class Buffer{
    private:
        std::vector _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(memchr(ReadPosition(),'
',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的基础上把发送和接收数据以及创建客户端连接和服务器连接直接集成在了这个类中。代码如下:

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(&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(&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操作的封装类,功能有:启动读/写事件监控、关闭读/写事件监控、是否监控了读/写事件、以及对就绪事件的处理。代码如下:

//epoll模式LT模式-水平触发
//设置管理事件监控,写事件还是读事件,一个连接对应一个Channel类
#include   // 包含EPOLLIN等epoll事件标志的定义
class Channel{
    private:
        int _fd;
        EventLoop* _loop;
        uint32_t _events; // 当前需要监控的事件
        uint32_t _revents;// 当前监控事件的就绪事件 
        using EventCallback = std::function;
        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的哈希存储表,方便查询和对单个连接操作。

#define MAX_EPOLLEVENTS 1024
#include 
#include


//epoll管理类——对所有链接对应的所有Channel类管理——进行更新修改,开启事件监控和取消监控——对所有服务器上的连接做管理
class Poller{
    private:
        int _epfd;
        struct epoll_event _evs[MAX_EPOLLEVENTS];
        std::unordered_map _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* 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
",strerror(errno));
                abort();//退出程序——异常退出有调试信息,exit正常退出清理程序
            }
            for(int i=0;isecond->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函数返回了就不在阻塞了。代码如下:


using Functor = std::function;

#include
#include
#include
#include
//任务执行的Loop——事件循环核心是Reactor的核心
class EventLoop{
    private:
        std::thread::id _thread_id;//线程ID

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

        std::vector _tasks;//任务池
        std::mutex _mutex;//实现任务池操作的线程安全
        TimerWheel _timer_wheel;//定时器模块
    public:
        void RunAllTask(){
            std::vector functor;
            {
                std::unique_lock _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 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 _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函数——这个函数是为了我在执行定时任务的时候顺便把定时任务在时间轮管理类中的保存记录也删了目的就是方便也符合高内聚低偶尔原则。代码如下:

using TaskFunc = std::function;
using ReleaseFunc = std::function;

//单个超时任务类——主要是超时了就在他的析构函数中执行定时任务
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类就是模拟指针跳动来执行任务,已经对任务的增删查改的。代码如下:

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


        //下面是为了把定时器融合到EventLoop中所需要添加的成员
        EventLoop* _loop;
        int _timerfd;//定时器描述符--可读事件回调就是读取计数器,执行定时任务
        std::unique_ptr _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,×,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 _threads保存所有的LoopThread对象(线程池),用vector _loops保存线程池中的EventLoop对象,这里下标是对应关系,线程的下标和EventLoop的下标是一样的。_threads和_loops的大小就是线程池大小用_thread_count来设置,用_next_idx表示应该工作到哪一个线程了因为是RR轮询的方式。

        代码如下:

//先有线程再有EventLoop对象,然后EventLoop对象在线程中实例化(不然设置id就是主线程的线程id),这样才能保证id是唯一的,
#include  // 条件变量的核心头文件
#include               // 各种锁(互斥锁、递归锁等)的核心头文件
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 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 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 _threads;//保存所有的LoopThread对象
        std::vector _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。代码如下:

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

        using AcceptCallback = std::function;
        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类中回调函数的设置。

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

using PtrConnection = std::shared_ptr;

//一个连接一个Connection
//只有继承了enable_shared_from_this才能使用shared_from_this,返回一个自己的shared_ptr,enable_shared_from_this底层是封装了一个weak_ptr,所以shared_ptr计数不会+—1
class Connection: public std::enable_shared_from_this{
    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;
        using MessageCallback = std::function;
        using CloseCallback = std::function;
        using AnyEventCallback = std::function;
        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类给新客户端。代码如下:

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 _conns;//保存管理所有连接对应的shared_ptr对象
    private:
        //回调函数——Connection的回调函数
        using ConnectedCallback = std::function;
        using MessageCallback = std::function;
        using CloseCallback = std::function;
        using AnyEventCallback = std::function;
        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 的组合,避免数据残留和线程卡死,保证高并发下的稳定性。

本文地址:https://www.yitenyun.com/736.html

搜索文章

Tags

#ios面试 #ios弱网 #断点续传 #ios开发 #objective-c #ios #ios缓存 #服务器 #python #pip #conda #远程工作 香港站群服务器 多IP服务器 香港站群 站群服务器 #kubernetes #笔记 #平面 #容器 #linux #学习方法 #运维 #fastapi #html #css #进程控制 #低代码 #爬虫 #音视频 #docker #后端 #数据库 #Trae #IDE #AI 原生集成开发环境 #Trae AI #开发语言 #云原生 #iventoy #VmWare #OpenEuler #cpolar #Conda # 私有索引 # 包管理 #内网穿透 #网络 #人工智能 #node.js #MobaXterm #ubuntu #开源 #RTP over RTSP #RTP over TCP #RTSP服务器 #RTP #TCP发送RTP #android #腾讯云 #c# #kylin #vscode #mobaxterm #深度学习 #计算机视觉 #tcp/ip #多个客户端访问 #IO多路复用 #回显服务器 #TCP相关API #学习 #web安全 #安全 #数信院生信服务器 #Rstudio #生信入门 #生信云服务器 #nginx #物联网 #websocket #华为 #ModelEngine #金融 #大模型 #mcp #金融投资Agent #Agent #claude #windows #我的世界 #ssh #github #git #云计算 #n8n #本地部署 #hadoop #hbase #hive #zookeeper #spark #kafka #flink #qt #C++ #我的世界服务器搭建 #minecraft #java #jar #ide #AI编程 #性能优化 #缓存 #架构 #mvp #个人开发 #设计模式 #MCP #MCP服务器 #pycharm #单元测试 #集成测试 #DisM++ # GLM-4.6V # 系统维护 #京东云 #gpu算力 #算法 #NPU #CANN #前端 #vue.js #vue #阿里云 #JumpServer #堡垒机 #unity3d #游戏 #服务器框架 #Fantasy #YOLOFuse # Base64编码 # 多模态检测 #振镜 #振镜焊接 #http #c++ #SRS #流媒体 #直播 #智能手机 #守护进程 #复用 #screen #umeditor粘贴word #ueditor粘贴word #ueditor复制word #ueditor上传word图片 #科技 #自然语言处理 #神经网络 #udp #c语言 #网络协议 #mamba #jenkins #需求分析 #scala #测试用例 #测试工具 #压力测试 #Dell #PowerEdge620 #内存 #硬盘 #RAID5 #面试 #cpp #项目 #高并发 #todesk #unity #游戏引擎 #gemini #gemini国内访问 #gemini api #gemini中转搭建 #Cloudflare #screen 命令 #stm32 #macos #php #嵌入式硬件 #编辑器 #SAP #ebs #metaerp #oracle ebs #910B #昇腾 #AIGC #ida #大数据 #搜索引擎 #flask #DeepSeek #蓝耘智算 #pytorch #GPU服务器 #8U #硬件架构 #Anaconda配置云虚拟环境 #5G #智能路由器 #C2000 #TI #实时控制MCU #AI服务器电源 #cursor #1024程序员节 #elasticsearch #django #web3.py #RustDesk #IndexTTS 2.0 #本地化部署 #麒麟OS #信息与通信 #信号处理 #tcpdump #centos #毕业设计 #车辆排放 #ms-swift # 大模型 # 模型训练 #Android #Bluedroid #银河麒麟高级服务器操作系统安装 #银河麒麟高级服务器V11配置 #设置基础软件仓库时出错 #银河麒高级服务器系统的实操教程 #生产级部署银河麒麟服务系统教程 #Linux系统的快速上手教程 #AI #工具集 #sqlite #epoll #Nacos #web #微服务 #电气工程 #C# #PLC #单片机 #oracle #golang #rdp #libosinfo #自动化 #maven #gitlab #课程设计 #spring boot #SSH反向隧道 # Miniconda # Jupyter远程访问 #chatgpt #codex #yum #三维 #3D #三维重建 #PyTorch # Triton # 高并发部署 #react.js #java-ee #mysql #sql #Dify #ARM架构 #鲲鹏 #apache #jmeter #功能测试 #软件测试 #自动化测试 #职场和发展 #鸭科夫 #逃离鸭科夫 #鸭科夫联机 #鸭科夫异地联机 #开服 #deepseek #EMC存储 #存储维护 #NetApp存储 #risc-v #NAS #Termux #Samba #Linux #运维开发 #客户端 #shell #黑群晖 #虚拟机 #无U盘 #纯小白 #网络安全 #渗透测试 #黑客技术 #计算机 #文件上传漏洞 #支付 #mcu #银河麒麟 #系统升级 #信创 #国产化 #SSH跳板机 # Python3.11 #fpga开发 #LVDS #高速ADC #DDR #东方仙盟 #机器学习 #数据分析 #推荐算法 #API限流 # 频率限制 # 令牌桶算法 #驱动开发 #A2A #GenAI #SSE # AI翻译机 # 实时翻译 #华为云 #screen命令 #debian #Gunicorn #WSGI #Flask #并发模型 #容器化 #Python #性能调优 #蓝湖 #Axure原型发布 #机器人 #llama #opencv #语言模型 #聊天小程序 #微信小程序 #小程序 #心理健康服务平台 #心理健康系统 #心理服务平台 #心理健康小程序 #门禁 #梯控 #智能一卡通 #门禁一卡通 #消费一卡通 #智能梯控 #一卡通 #源代码管理 #超时设置 #客户端/服务器 #网络编程 #管道Pipe #system V #ai #ai编程 #nodejs #muduo库 #asp.net #javascript #uv #uvx #uv pip #npx #Ruff #pytest #react native #dynadot #域名 #esb接口 #走处理类报异常 #bug菌问答团队 #YOLO #ffmpeg #数据挖掘 #交互 #操作系统 #国产化OS #练习 #基础练习 #数组 #循环 #九九乘法表 #计算机实现 #C语言 #vivado license #银河麒麟部署 #银河麒麟部署文档 #银河麒麟linux #银河麒麟linux部署教程 #CVE-2025-68143 #CVE-2025-68144 #CVE-2025-68145 #intellij-idea #idea #intellij idea #html5 #SSH # 批量管理 #语音识别 #ASR #SenseVoice #星图GPU #中间件 #MQTT协议 #智能体 #openHiTLS #TLCP #DTLCP #密码学 #商用密码算法 #ONLYOFFICE #MCP 服务器 #laravel #ssl #tomcat #prometheus #grafana #svn #证书 #可信计算技术 #serverless #RAID #RAID技术 #磁盘 #存储 #leetcode #测评 #CCE #Dify-LLM #Flexus #AI 推理 #NV # 双因素认证 # TensorFlow #毕设 #服务器繁忙 #chrome #postgresql #连接数据库报错 #处理器 #数据结构 #jvm #adb #gitea #经验分享 #硬件工程 #智能家居 #pyqt #DNS #排序算法 # 目标检测 #SPA #单页应用 #redis #C #Spring AI #STDIO传输 #SSE传输 #WebMVC #WebFlux #bootstrap #YOLO26 #目标检测 #swagger #visual studio code #milvus #springboot #知识库 #web server #请求处理流程 #mariadb #LangGraph #CLI #JavaScript #langgraph.json #交通物流 #paddleocr #STDIO协议 #Streamable-HTTP #McpTool注解 #服务器能力 #wsl #scrapy #transformer #分布式 #rocketmq #selenium #蓝牙 #LE Audio #BAP #嵌入式编译 #ccache #distcc #numpy #链表 #puppeteer #wordpress #雨云 #LobeChat #vLLM #GPU加速 #xlwings #Excel #海外服务器安装宝塔面板 #翻译 #开源工具 #dlms #dlms协议 #逻辑设备 #逻辑设置间权限 #ansible #安全威胁分析 #仙盟创梦IDE #动态规划 #负载均衡 #spring cloud #spring #nfs #iscsi #大模型部署 #mindie #大模型推理 #创业创新 #业界资讯 #openlayers #bmap #tile #server #langchain #大模型开发 #程序员 #TCP #嵌入式 #DIY机器人工房 #prompt #大模型学习 #文件管理 #文件服务器 #CosyVoice3 # 语音合成 #树莓派4b安装系统 #x86_64 #数字人系统 #windows11 #microsoft #系统修复 #电脑 #信令服务器 #Janus #MediaSoup #其他 #rtsp #转发 #KMS激活 #计算机网络 #jdk #排序 #CVE-2025-61686 #漏洞 #路径遍历高危漏洞 #ddos #系统架构 #SQL注入主机 #webrtc #idm #数据仓库 #万悟 #联通元景 #镜像 #CSDN #ThingsBoard MCP # GPU租赁 # 自建服务器 #devops #戴尔服务器 #戴尔730 #装系统 #微信 #健身房预约系统 #健身房管理系统 #健身管理系统 #bug #sqlserver #HeyGem # 服务器IP访问 # 端口映射 #遛狗 #北京百思可瑞教育 #百思可瑞教育 #北京百思教育 #Puppet # IndexTTS2 # TTS # 一锤定音 # 大模型微调 #数据安全 #注入漏洞 #vllm #CUDA #Triton #说话人验证 #声纹识别 #CAM++ #SSH公钥认证 # PyTorch # 安全加固 #dify #部署 #企业开发 #ERP #项目实践 #.NET开发 #C#编程 #编程与数学 #昇腾300I DUO #fiddler #unix #json #vnstat #监控 #rust #Qwen3-14B # 大模型部署 # 私有化AI #结构体 #制造 #ping通服务器 #读不了内网数据库 #文心一言 #AI智能体 #vp9 #大模型教程 #AI大模型 #攻防演练 #Java web #红队 #新人首发 #汽车 #iBMC #UltraISO #可撤销IBE #服务器辅助 #私钥更新 #安全性证明 #双线性Diffie-Hellman #指针 #anaconda #虚拟环境 #Kylin-Server #国产操作系统 #服务器安装 #GB28181 #SIP信令 #SpringBoot #视频监控 #Android16 #音频性能实战 #音频进阶 #WT-2026-0001 #QVD-2026-4572 #smartermail #短剧 #短剧小程序 #短剧系统 #微剧 #hibernate #nosql # GLM-TTS # 数据安全 #xshell #host key #TTS私有化 # IndexTTS # 音色克隆 #ip #arm开发 #Modbus-TCP #VMware #VMWare Tool # ARM服务器 # 大模型推理 #系统管理 #服务 #Emby #视频 #微PE # GLM # 服务连通性 #word #azure #IO #wireshark #网络安全大赛 #信息可视化 #ceph #ambari #arm #挖矿 #Linux病毒 #turn #CNAS #CMA #程序文件 #网安应急响应 #数据恢复 #视频恢复 #视频修复 #RAID5恢复 #流媒体服务器恢复 #aws #实时检测 #卷积神经网络 #googlecloud #DAG #哈希算法 #云服务器选购 #Saas #CPU #线程 #VibeVoice #outlook #错误代码2603 #无网络连接 #2603 #LLM # 高并发 #ETL管道 #RAG #向量存储 #数据预处理 #DocumentReader #SSH跳转 #harmonyos #HarmonyOS APP #TTS #go # GPU集群 #Gateway #认证服务器集成详解 #服务器开启 TLS v1.2 #IISCrypto 使用教程 #TLS 协议配置 #IIS 安全设置 #服务器运维工具 #uniapp #合法域名校验出错 #服务器域名配置不生效 #request域名配置 #已经配置好了但还是报错 #uniapp微信小程序 #框架搭建 #具身智能 #状态模式 #AI-native #dba #Tokio #SSH密钥 # CUDA #华为od #华为机试 #Java #能源 #rtmp #Socket #套接字 #I/O多路复用 #字节序 #weston #x11 #x11显示服务器 #研发管理 #禅道 #禅道云端部署 #计算几何 #斜率 #方向归一化 #叉积 #samba #AI电商客服 #RSO #机器人操作系统 #glibc #spring ai #oauth2 #数据可视化 #后端框架 #https # 高温监控 #zabbix #fs7TF #深度优先 #DFS #集成学习 #ROS #fabric # 局域网访问 # 批量处理 #ui #winscp #cosmic #前端框架 #.net #JNI #Llama-Factory # 树莓派 # ARM架构 #pxe #uni-app #H5 #跨域 #发布上线后跨域报错 #请求接口跨域问题解决 #跨域请求代理配置 #request浏览器跨域 # 数字人系统 # 远程部署 #npu #STUN # TURN # NAT穿透 #大剑师 #nodejs面试题 #MCP服务器注解 #异步支持 #方法筛选 #声明式编程 #自动筛选机制 #rustdesk #p2p #UDP的API使用 #媒体 #ESP32 # OTA升级 # 黄山派 #内网 #Miniconda #Docker # WebUI # 网络延迟 #free #vmstat #sar #远程软件 #游戏机 #银河麒麟操作系统 #openssh #华为交换机 #信创终端 #r语言 #mybatis #代理服务器 #rsync # 数据同步 #spine #bash #TRO #TRO侵权 #TRO和解 #blender #设计师 #图像处理 #游戏美术 #技术美术 #运维工具 #网络攻击模型 #进程 #进程创建与终止 # Connection refused #Discord机器人 #云部署 #程序那些事 #teamviewer #sql注入 #移动端h5网页 #调用浏览器摄像头并拍照 #开启摄像头权限 #拍照后查看与上传服务器端 #摄像头黑屏打不开问题 #odoo #企业微信 #服务器IO模型 #非阻塞轮询模型 #多任务并发模型 #异步信号模型 #多路复用模型 #系统安全 #Apple AI #Apple 人工智能 #FoundationModel #Summarize #SwiftUI #大语言模型 #ipmitool #BMC # 黑屏模式 # TTS服务器 #多线程 #ollama #llm #领域驱动 #claudeCode #content7 #跳槽 #工作 #appche #文件IO #输入输出流 #Ubuntu #muduo #TcpServer #accept #高并发服务器 #串口服务器 #工业级串口服务器 #串口转以太网 #串口设备联网通讯模块 #串口服务器选型 #embedding #IndexTTS2 # 阿里云安骑士 # 木马查杀 #远程开发 #入侵 #日志排查 #kmeans #聚类 #OpenHarmony #版本控制 #Git入门 #开发工具 #代码托管 #人大金仓 #Kingbase #openEuler #代理模式 #Spring AOP #程序人生 #ftp #sftp #YOLO识别 #YOLO环境搭建Windows #YOLO环境搭建Ubuntu #LangFlow # 轻量化镜像 # 边缘计算 #远程连接 #jupyter #多进程 #python技巧 #蓝桥杯 #cpu #企业级存储 #网络设备 #iot #软件工程 #生信 #pdf #Smokeping #WinSCP 下载安装教程 #SFTP #FTP工具 #服务器文件传输 #Ansible # CosyVoice3 # 批量部署 #策略模式 #pve #AutoDL #GPU #租显卡 #训练推理 #java大文件上传 #java大文件秒传 #java大文件上传下载 #java文件传输解决方案 #AI写作 #bigtop #hdp #hue #kerberos #AI部署 # ms-swift #PN 结 #pencil #pencil.dev #设计 #服务器线程 # SSL通信 # 动态结构体 #zotero #WebDAV #同步失败 #轻量化 #低配服务器 #RWK35xx #语音流 #实时传输 #node #Anything-LLM #IDC服务器 #私有化部署 #超算中心 #PBS #lsf #excel #报表制作 #职场 #用数据讲故事 #大模型应用 #API调用 #PyInstaller打包运行 #服务端部署 #手机h5网页浏览器 #安卓app #苹果ios APP #手机电脑开启摄像头并排查 #raid #raid阵列 #语音生成 #PyCharm # 远程调试 # YOLOFuse #欧拉 #journalctl #lvs #Langchain-Chatchat # 国产化服务器 # 信创 #KMS #slmgr #麒麟 #Syslog #系统日志 #日志分析 #日志监控 #宝塔面板部署RustDesk #RustDesk远程控制手机 #手机远程控制 #生产服务器问题查询 #日志过滤 #铁路桥梁 #DIC技术 #箱梁试验 #裂纹监测 #四点弯曲 #Autodl私有云 #深度服务器配置 # 水冷服务器 # 风冷服务器 #可再生能源 #绿色算力 #风电 #VoxCPM-1.5-TTS # 云端GPU # PyCharm宕机 #麦克风权限 #访问麦克风并录制音频 #麦克风录制音频后在线播放 #用户拒绝访问麦克风权限怎么办 #uniapp 安卓 苹果ios #将音频保存本地或上传服务器 #儿童AI #图像生成 #Qwen #pjsip #express #cherry studio #Node.js # child_process #openresty #lua #AI生成 # outputs目录 # 自动化 #stl #漏洞修复 #IIS Crypto #AI应用编程 # 自动化运维 #SSH保活 #若依 #GLM-4.6V-Flash-WEB # AI视觉 # 本地部署 #everything #人脸识别sdk #视频编解码 #人脸识别 #esp32 arduino #决策树 #自由表达演说平台 #演说 #程序员创富 #HistoryServer #Spark #YARN #jobhistory #ZooKeeper #ZooKeeper面试题 #面试宝典 #深入解析 #ComfyUI # 推理服务器 #3d #n8n解惑 #编程助手 #前端开发 #EN4FE #elk #rabbitmq # GLM-4.6V-Flash-WEB # 显卡驱动备份 #模拟退火算法 #计算机毕业设计 #程序定制 #毕设代做 #课设 #源码 #Karalon #AI Test #简单数论 #埃氏筛法 #Hadoop #YOLOv8 # Docker镜像 #流程图 #论文阅读 #论文笔记 #图论 #国产开源制品管理工具 #Hadess #一文上手 #okhttp #内存接口 # 澜起科技 # 服务器主板 #范式 #大模型入门 #homelab #Lattepanda #Jellyfin #Plex #Kodi #yolov12 #研究生life #开关电源 #热敏电阻 #PTC热敏电阻 #文件传输 #电脑文件传输 #电脑传输文件 #电脑怎么传输文件到另一台电脑 #电脑传输文件到另一台电脑 #eureka #mongodb #广播 #组播 #并发服务器 #小艺 #鸿蒙 #搜索 #nacos #银河麒麟aarch64 #uvicorn #uvloop #asgi #event # 服务器迁移 # 回滚方案 #健康医疗 #时序数据库 #企业存储 #RustFS #对象存储 #高可用 #notepad++ #es安装 #AI应用 #图像识别 #gpu #nvcc #cuda #nvidia #高考 #gpt #TensorRT # 推理优化 #工程实践 #API #taro #log4j #Jetty # 嵌入式服务器 #wps #模块 #Linux多线程 #RXT4090显卡 #RTX4090 #深度学习服务器 #硬件选型 #群晖 #音乐 #Beidou #北斗 #SSR #IntelliJ IDEA #Spring Boot #neo4j #NoSQL #SQL #V11 #kylinos #Coturn #TURN #信息安全 #信息收集 # 代理转发 # 跳板机 #软件 #本地生活 #电商系统 #商城 #echarts #高级IO #poll # 服务器IP # 端口7860 #Playbook #AI服务器 #建筑缺陷 #红外 #数据集 #SMARC #ARM #simulink #matlab #aiohttp #asyncio #异步 #智能体来了 #传统行业 #AI赋能 # 公钥认证 #Reactor # 智能运维 # 性能瓶颈分析 #空间计算 #原型模式 # 云服务器 #无人机 #.netcore #junit #LoRA # lora-scripts # 模型微调 #web服务器 #I/O模型 #并发 #水平触发、边缘触发 #多路复用 #VMware创建虚拟机 #远程更新 #缓存更新 #多指令适配 #物料关联计划 #挖漏洞 #攻击溯源 #编程 #clickhouse #warp #代理 #数据访问 # AI部署 #材料工程 #数码相机 #智能电视 #DHCP #C++ UA Server #SDK #Windows #跨平台开发 #agent #ai大模型 #net core #kestrel #web-server #asp.net-core #m3u8 #HLS #移动端H5网页 #APP安卓苹果ios #监控画面 直播视频流 #Prometheus #eclipse #servlet #Zabbix #语音合成 #二值化 #Canny边缘检测 #轮廓检测 #透视变换 #arm64 #DooTask #Clawdbot #SSH复用 # 远程开发 #防毒面罩 #防尘面罩 #磁盘配额 #存储管理 #形考作业 #国家开放大学 #系统运维 #自动化运维 #GATT服务器 #蓝牙低功耗 #lucene #散列表 #机器视觉 #6D位姿 #UOS #海光K100 #统信 #mssql #wpf #postman #Modbus #MOXA #密码 #firefox #safari # RTX 3090 #flutter #select #身体实验室 #健康认知重构 #系统思维 #微行动 #NEAT效应 #亚健康自救 #ICT人 #云服务器 #个人电脑 # ControlMaster #KMS 激活 #硬件 #MC #MC群组服务器 #Fun-ASR # 语音识别 #SSH别名 #CS2 #debian13 #windbg分析蓝屏教程 #BoringSSL #远程控制 #云计算运维 #le audio #低功耗音频 #通信 #连接 #asp.net大文件上传 #asp.net大文件上传下载 #asp.net大文件上传源码 #ASP.NET断点续传 #asp.net上传文件夹 #asp.net上传大文件 #nmodbus4类库使用教程 #docker-compose #漏洞挖掘 #目标跟踪 #PowerBI #企业 #IFix #c++20 #ci/cd #k8s # 远程连接 #Buck #NVIDIA #算力 #交错并联 #DGX #ICE #信创国产化 #达梦数据库 #内存治理 # 鲲鹏 #http头信息 #TCP服务器 #开发实战 #全文检索 #银河麒麟服务器系统 #远程桌面 #gerrit #opc ua #opc #鸿蒙PC ##租显卡 # 环境迁移 #树莓派 #温湿度监控 #WhatsApp通知 #IoT #MySQL #matplotlib #安全架构 #SMTP # 内容安全 # Qwen3Guard #飞牛nas #fnos #X11转发 #改行学it #平板 #零售 #智能硬件 #vncdotool #链接VNC服务器 #如何隐藏光标 #CTF #Deepoc #具身模型 #开发板 #未来 #插件 #开源软件 #FHSS #服务器解析漏洞 #NFC #智能公交 #服务器计费 #FP-增长 #算力建设 #Proxmox VE #虚拟化 #smtp #smtp服务器 #PHP #声源定位 #MUSIC #mtgsig #美团医药 #美团医药mtgsig #美团医药mtgsig1.2 #网路编程 #百万并发 # 远程访问 #Streamlit #AI聊天机器人 #tensorflow #memcache #MinIO #ansys #ansys问题解决办法 #ranger #MySQL8.0 #sentinel #分布式数据库 #集中式数据库 #业务需求 #选型误 #智能体对传统行业冲击 #行业转型 #chat #HarmonyOS #雨云服务器 #Minecraft服务器 #教程 #MCSM面板 #Socket网络编程 # 服务器配置 # GPU # 串口服务器 # NPort5630 #Python办公自动化 #Python办公 #opc模拟服务器 #工程设计 #预混 #扩散 #燃烧知识 #层流 #湍流 #量子计算 #copilot #个人博客 #硬盘克隆 #DiskGenius # 键鼠锁定 #反向代理 #政务 #powerbi #个人助理 #数字员工 #docker安装seata #参数估计 #矩估计 #概率论 #adobe #系统安装 # IndexTTS 2.0 #全链路优化 #实战教程 #database #gmssh #宝塔 #1panel #Exchange #scikit-learn #随机森林 #闲置物品交易系统 #静脉曲张 #腿部健康 #运动 #IPv6 #POC #问答 #交付 #AI Agent #开发者工具 #sglang #Minecraft #PaperMC #我的世界服务器 #边缘AI # Kontron # SMARC-sAMX8 #SSH Agent Forwarding # 容器化 #jetty #kong #Kong Audio #Kong Audio3 #KongAudio3 #空音3 #空音 #中国民乐 #计算机外设 #scanf #printf #getchar #putchar #cin #cout #ET模式 #非阻塞 #性能 #优化 #RAM #凤希AI伴侣 #remote-ssh #SA-PEKS # 关键词猜测攻击 # 盲签名 # 限速机制 #产品经理 #就业 #CMake #Make #C/C++ #OpenAI #故障 #多模态 #微调 #超参 #LLamafactory #vps #AI论文写作工具 #学术写作辅助 #论文创作效率提升 #AI写论文实测 #AB包 #MinIO服务器启动与配置详解 #数字化转型 #实体经济 #商业模式 #软件开发 #数智红包 #商业变革 #创业干货 #gateway #Comate #Go并发 #高并发架构 #Goroutine #系统设计 #Tracker 服务器 #响应最快 #torrent 下载 #2026年 #Aria2 可用 #迅雷可用 #BT工具通用 #FASTMCP #eBPF #联机教程 #局域网联机 #局域网联机教程 #局域网游戏 #交换机 #三层交换机 #高斯溅射 #UEFI #BIOS #Legacy BIOS #b树 #云开发 #Harbor #AI智能棋盘 #Rock Pi S #边缘计算 #c++高并发 #memory mcp #Cursor #PTP_1588 #gPTP #uip # 权限修复 #进程等待 #wait #waitpid # HiChatBox # 离线AI