最新资讯

  • 高级I/O编程:从Select、Poll到Epoll的演进与Reactor服务器实现

高级I/O编程:从Select、Poll到Epoll的演进与Reactor服务器实现

2026-01-29 04:57:49 栏目:最新资讯 4 阅读

高级I/O编程

1. 五种IO模型

I/O操作为什么慢?因为 I/O的核心是 等待 + 拷贝(比如 read 操作要先等数据再拷贝,write 同理),但访问外设(像网卡)时等待耗时太长,导致 IO 速度慢。

而高效 IO 的关键,是在单位时间里降低 IO 过程中的等待占比。

  1. 阻塞IO:在内核在将数据准备好之前,在系统调用处会一直等待,所有的套接字,默认都是阻塞方式。

  2. 非阻塞IO:如果内核还未将数据准备好,系统调用会直接返回,并且返回 EWOULDBLOCK 错误码。

    • 非阻塞的方式需要通过循环不断 轮询(反复尝试读写文件描述符),轮询会大量消耗CPU资源。
  3. 信号驱动IO:内核将数据准备好的时候,使用 SIGIO 信号通知进程进行IO操作。

    • 等待数据就绪的时间是异步的(由内核通过信号通知),但将数据从内核空间拷贝到用户空间的过程必须是同步完成的。

    • 信号驱动I/O的核心缺陷是信号可能丢失或合并。

  4. IO多路复用/多路转接:使用 select、poll、epoll 等系统调用监控多个文件描述符,当有事件就绪时,通知进程进行实际的IO操作。

  5. 异步IO:进程发起IO请求后立即返回,内核完成所有工作(等待数据 + 复制到用户空间),完成后通知进程。

    • 与信号驱动的区别:

      • 信号驱动:内核通知何时可以开始IO。

      • 异步IO:内核通知IO操作已完成。

阻塞 VS 非阻塞: 阻塞会因为IO条件不具备,而导致阻塞等待,直到条件就绪;非阻塞检测到IO条件不具备,出错返回。本质的区别是等待数据就绪的方式不同,非阻塞 I/O 相比于阻塞 I/O 的效率提升,本质上源自于其在等待数据就绪期间能够执行其他任务,而非缩短了内核与用户空间之间的数据拷贝时间实际数据拷贝量是完全相同的,这一环节在两种模型下是同等耗时且不可避免的。

为什么多路复用效率最高:

  • 多路复用的效率飞跃,源于将串行等待并行化,从而消除了等待时间的累加效应。

  • 系统调用的开销减少

  • 减少进程/线程切换上下文的开销

同步IO与异步IO只要程序参与了I/O操作的 等待数据就绪 或 数据拷贝 中的任意一个阶段,就是同步I/O。只有这两个阶段完全由内核完成,才是真正的异步I/O。

2. 非阻塞 IO 之 fcntl

文件描述符默认都是阻塞IO

fcntl系统调用: 对已打开的文件描述符进行各种控制操作。底层本质是修改内核中 struct file 的属性。

函数原型:

#include 
#include 

int fcntl(int fd, int cmd, ... /* arg */); // 传入的cmd值的不同,后面可变参数也不同

根据cmd参数的不同,功能也对应不同。

参数:

  • fd: 要操作的文件描述符。

  • cmd: 控制命令,决定函数的行为。

    • F_FUPFD: 复制文件描述符

    • F_GETFD/F_SETFD: 获得/设置文件描述符标记

    • F_GETFL/F_SETFL: 获得/设置文件状态标记

    • F_GETOWN/F_SETOWN: 获得/设置异步I/O所有权

实现将文件描述符设置为非阻塞的函数SetBlock

void SetNonBlock(int fd) {
    int fl = fcntl(fd, F_GETFL);
    if (fl < 0) {
        perror("fcntl");
        return;
    } 

    fcntl(fd, F_SETFL, fl | O_NONBLOCK); // O_NONBLOCK:将fd设置为非阻塞
}

int main()
{
    SetNonBlock(0);

    char buffer[1024];
    while (true)
    {
        // Linux中ctrl + d:标识输入结束,read返回值是0,类似读到文件结尾
        ssize_t n = read(0, buffer, sizeof(buffer) - 1);
        if (n > 0)
        {
            buffer[n - 1] = 0;    // 消除换行符的同时 + ''
            std::cout << buffer << std::endl;
        }
        else if (n < 0)
        {
            // 1. 读取出错 2. 底层没有数据准备好
            // eagain ewouldblock
            if(errno == EAGAIN || errno == EWOULDBLOCK) // 错误码
            {
                std::cout << "数据没有准备好..." << std::endl;
                sleep(1);
                // 做你的事情
                continue;
            }
            else if(errno == EINTR)    // read被信号打断
            {
                continue;
            }
            else
            {
                // 真正的read出错了
            }
        }
        else
        {
            break;
        }
    }
}
  • F_GETFL 返回值文件状态标志的位图。

  • EINTR(Error Interrupted)表示系统调用被信号中断(read被中断)。

  • EAGAIN / EWOULDBLOCK 表示资源没有就绪。

3. select

3.1 接口介绍

I/O = 等 + 拷贝,select 是 I/O 多路复用中常用的系统调用,用于同时等待多个fd的事件就绪通知机制。

  • select主要负责等。

  • 可读:底层有数据,读事件就绪。

  • 可写:底层有空间,写事件就绪。

函数原型:

#include 

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

参数:

  • nfds: 需要监听的最大文件描述符值 + 1。

  • readfds、writefds、exceptfds: 输入输出型参数,分别对应需要检测的文件描述符,可读事件的集合、可写事件的集合及异常事件的集合。

    • 设置的时候:用户告诉内核,需要监听哪些fd上面的对应事件。

    • 返回的时候:内核告诉用户,监听的fd上面的对应事件已经就绪。

    • fd_set 类型:是一个位图结构。低位 -> 高位分别代表文件描述符0、1、2…,位为1表示文件描述符在该事件集合中,位为0表示文件描述符不在该事件集合中。同时,还提供了操作该位图类型的接口

        /* fd_set 的典型实现(不同系统可能略有差异) */
      
        /* 在 /usr/include/sys/select.h 或类似位置 */
      
        /* 定义最大文件描述符数 */
        #define FD_SETSIZE 1024  /* 默认值,可通过编译时修改 */
      
        typedef long int __fd_mask;
      
        /* fd_set 结构定义 */
        #define __NFDBITS (8 * (int) sizeof (__fd_mask))
        typedef struct {
            __fd_mask __fds_bits[FD_SETSIZE / __NFDBITS];
            /* 或者在某些系统上是: */
            /* __fd_mask fds_bits[howmany(FD_SETSIZE, __NFDBITS)]; */
        } fd_set;
      
    • fd_set类型最大可以监听的文件描述符个数:sizeof(fd_set) * 8 = 1024 根据系统版本的不同,该值可能会有变化。但是是有上限的

    • fd_set 操作接口:

      // 用来清除 fd_set 类型的全部比特位
      void FD_ZERO(fd_set *set);
      
      // 用来设置 fd_set 类型中相关 fd 的比特位  
      void FD_SET(int fd, fd_set *set);
      
      // 用来清除 fd_set 类型中相关 fd 的比特位
      void FD_CLR(int fd, fd_set *set);
      
      // 用来检测 fd_set 类型中相关 fd 的比特位是否为真
      int  FD_ISSET(int fd, fd_set *set);
      
  • timeout: 设置 select 的超时时间。

    struct timeval
    {
        time_t tv_sec;  /* Seconds */
        suseconds_t tv_usec;    /* Microseconds */
    }
    
    • nullptr: select 阻塞等待,直到某个文件描述符上某个事件就绪。

    • {0 , 0}: 非阻塞等待,立即返回。

    • > 0: 时间内阻塞等待,超时非阻塞返回。

返回值:

  • > 0: 在监控的文件描述符中,至少有一个或多个描述符的就绪事件发生。返回值数字 = 所有集合中已就绪的描述符总数。

    • 假设:描述符3读就绪、描述符4读就绪 + 写就绪,select返回值 = 3。
  • = 0: timeout 设置的时间超时或非阻塞方式等待但没有事件就绪时。

  • -1: select 出错,errno被设置。

3.2 SelectServer.hpp

#pragma once

#include "Socket.hpp"
#include 
#include 

class SelectServer{
    const static int fds_size = sizeof(fd_set) * 8;
    const static int default_fd = -1;
    public:
        SelectServer(uint16_t port)
            :_sockptr(std::make_unique<SocketModule::TcpSocket>())
            ,_is_running(false)
        {
            // 创建TCP套接字
            _sockptr->buildTcpListenSocket(port);

            // 初始化 _fds
            for(int i = 0; i < fds_size; i++)
                _fds[i] = default_fd;

            // 添加 listenfd
            _fds[0] = _sockptr->getfd();
        }

        void start() {
            _is_running = true;
            while(_is_running) {
                // rfds 需要每次重置
                fd_set rfds;
                FD_ZERO(&rfds);
                int maxfd = default_fd;
                // 根据辅助数组赋值
                for(int i = 0; i < fds_size; i++) {
                    if(_fds[i] == default_fd)
                        continue;
                    // 设置 rfds 对应比特位
                    FD_SET(_fds[i] , &rfds);
                    // 更新 maxfd
                    maxfd = std::max(maxfd , _fds[i]);
                }
                // 打印 _fds
                printFds();

                // struct timeval timeout = {0 , 0};
                // 阻塞方式 仅设置监听读事件
                int n = select(maxfd + 1 , &rfds , nullptr , nullptr , nullptr);
                if(n < 0) {
                    LogModule::LOG(LogModule::LogLevel::ERROR) << "select error";
                    break;
                } else if(n == 0) {
                    LogModule::LOG(LogModule::LogLevel::INFO) << "select timeout...";
                    continue;
                } else {
                    // 事件就绪调用事件派发器
                    dispatcher(rfds);
                }
            }
            _is_running = false;
        }

        void accepter() {
            // 获取与客户端的新连接 accept不会被阻塞
            InetAddr client;
            int sockfd = _sockptr->accept(&client);
            if(sockfd < 0) {
                return;
            }
            LogModule::LOG(LogModule::LogLevel::INFO) << "get a new link socket: " << sockfd << " - client: " << client.showIpPort();

            int i = 0; 
            for(; i < fds_size; i++) {
                if(_fds[i] == default_fd)
                    break;
            }
            // _fds 已满
            if(i == fds_size) {
                LogModule::LOG(LogModule::LogLevel::WARNING) << "select fd_set full";
                close(sockfd);
            } else {
                // 添加到辅助数组中
                _fds[i] = sockfd;
            }
        }

        void printFds() {
            std::cout << "_fds[] = ";
            for(int i = 0; i < fds_size; i++) {
                if(_fds[i] != default_fd)
                    std::cout << _fds[i] << " ";
            }
            std::cout << std::endl;
        }

        void recver(int pos) {
            char buffer[4096];
            // 这里读取有bug 读取的可能不是一个完整的报文
            ssize_t n = ::recv(_fds[pos] , buffer , sizeof(buffer) - 1, 0); //recv 不会阻塞
            if(n > 0) {
                buffer[n - 1] = 0;
                std::cout << "client echo: " << buffer << std::endl;
            } else if(n == 0) {
                // client quit
                close(_fds[pos]);
                _fds[pos] = default_fd;
            } else {
                // recv error
                LogModule::LOG(LogModule::LogLevel::ERROR) << "recv error";
                close(_fds[pos]);
                _fds[pos] = default_fd;
            }
        }

        // 事件分发器
        void dispatcher(fd_set& rfds) {
            for(int i = 0; i < fds_size; i++) {
                if(_fds[i] == default_fd)
                    continue;
                // 
                if(FD_ISSET(_fds[i] , &rfds)) {
                    if(_fds[i] == _sockptr->getfd())
                        accepter();
                    else
                        recver(i);
                }
            }
        }
    private:
        std::unique_ptr<SocketModule::Socket> _sockptr;
        bool _is_running;

        // select 辅助数组记录需要监听的文件描述符
        int _fds[fds_size];
};

3.3 select优缺点

特点:

  • 跨平台兼容性好:几乎所有操作系统都支持,代码可移植性强。

  • 需要构建辅助数组,将监视的文件描述符永久保存到辅助数组中,因为 readfds、writefds、exceptfds 是输入输出型参数, 当 select 返回后,fd_set 集合中只保留就绪的文件描述符,原始集合被破坏。

缺点:

  • 事件就绪和设置事件监听不是分离的,通过输入输出型参数同时控制,因此每次调用 select 时,都需要手动重新设置监听的 fd 集合。

  • 内核底层也会通过 nfds 和用户设置的监听文件描述符事件 遍历文件描述符数组。效率比较低。

  • select 支持的文件描述符数量有上限。

4. poll

4.1 接口介绍

poll 是 select 的改进版本,解决了 select 一些问题,但仍然有瓶颈。

函数原型:

#include 

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

参数:

  • fds: 一个结构体数组。

    • struct pollfd 类型:

      struct pollfd {
        int   fd;         /* 文件描述符 */
        short events;     /* 等待的事件 是一个位图通过按位或的方式 */
        short revents;    /* 实际发生的事件 是一个位图通过按位或的方式 */
      };
      
    • 调用的时候:fd + events 有效,用户告诉内核,监听 fd 对应的 events 事件。

    • 返回的时候:fd + revents 有效,内核告诉用户,fd 的 revents 事件就绪。

  • nfds: fds的数组长度。

  • timeout: 超时时间。

    • -1: 阻塞等待,直到有事件就绪。

    • 0: 立即返回,非阻塞模式。

    • > 0: 等待的毫秒数。

返回值:

  • > 0: 在监控的文件描述符中,至少有一个或多个描述符的就绪事件发生。返回值数字 = 已就绪的描述符总数。

    • 假设:描述符3读就绪、描述符4读就绪 + 写就绪,poll返回值 = 2。
  • = 0: timeout 设置的时间超时或非阻塞方式等待但没有事件就绪时。

  • -1: poll 出错,errno被设置。

events 与 revents 的取值:

事件描述是否可作为输入是否可作为输出
POLLIN数据(包括普通数据和优先数据)可读
POLLRDNORM普通数据可读
POLLRDBAND优先级带数据可读(Linux 不支持)
POLLPRI高优先级数据可读,比如 TCP 带外数据
POLLOUT数据(包括普通数据和优先数据)可写
POLLWRNORM普通数据可写
POLLWRBAND优先级带数据可写
POLLRDHUPTCP 连接被对方关闭,或者对方关闭了写操作(GNU 引入)
POLLERR错误
POLLHUP挂起(如管道写端关闭后,读端描述符将收到 POLLHUP 事件)
POLLNVAL文件描述符没有打开

4.2 PollServer.hpp

#pragma once

#include "Socket.hpp"
#include 
#include 

class PollServer{
    const static int default_fd = -1;
    const static int default_capacity = 1;
    public:
        PollServer(uint16_t port)
            :_sockptr(std::make_unique<SocketModule::TcpSocket>())
            ,_is_running(false)
            ,_capacity(default_capacity)
        {
            // 创建TCP套接字
            _sockptr->buildTcpListenSocket(port);

            // 初始化 _fds
            _fds = new struct pollfd[16];
            for(int i = 0; i < _capacity; i++)
                _fds[i] = {default_fd , 0 , 0};

            // 监听 listenfd 读事件
            _fds[0] = {_sockptr->getfd() , POLLIN , 0};
        }

        void start() {
            _is_running = true;
            while(_is_running) {
                
                // 打印 _fds
                printFds();

                // 阻塞方式
                int n = poll(_fds , _capacity , -1);
                if(n < 0) {
                    LogModule::LOG(LogModule::LogLevel::ERROR) << "poll error";
                    break;
                } else if(n == 0) {
                    LogModule::LOG(LogModule::LogLevel::INFO) << "poll timeout...";
                    continue;
                } else {
                    // 事件就绪调用事件分发器
                    dispatcher();
                }
            }
            _is_running = false;
        }

        void accepter() {
            // 获取与客户端的新连接 accept不会被阻塞
            InetAddr client;
            int sockfd = _sockptr->accept(&client);
            if(sockfd < 0) {
                return;
            }
            LogModule::LOG(LogModule::LogLevel::INFO) << "get a new link socket: " << sockfd << " - client: " << client.showIpPort();
            
            int i = 0; 
            for(; i < _capacity; i++) {
                if(_fds[i].fd == default_fd)
                    break;
            }
            // _fds 已满
            if(i == _capacity) {
                LogModule::LOG(LogModule::LogLevel::WARNING) << "poll fds full";
                // 扩容
                struct pollfd* temp = new struct pollfd[_capacity * 2];
                for(int i = 0; i < _capacity; i++) {
                    temp[i] = _fds[i];
                }
                for(int i = _capacity; i < _capacity * 2; i++) {
                    temp[i] = {default_fd , 0 , 0};
                }
                delete[] _fds;
                _fds = temp;
                _capacity *= 2;
            } 
            LogModule::LOG(LogModule::LogLevel::DEBUG) << "poll relloc capaciyt: " << _capacity;
            // 添加到辅助数组中·
            _fds[i].fd = sockfd;
            _fds[i].events = POLLIN;
            _fds[i].revents = 0;
        }

        void printFds() {
            std::cout << "_fds[] = ";
            for(int i = 0; i < _capacity; i++) {
                if(_fds[i].fd != default_fd)
                    std::cout << _fds[i].fd << " ";
            }
            std::cout << std::endl;
        }

        void recver(int pos) {
            char buffer[4096];
            // 这里读取有bug 读取的可能不是一个完整的报文
            ssize_t n = ::recv(_fds[pos].fd , buffer , sizeof(buffer) - 1, 0); //recv 不会阻塞
            if(n > 0) {
                buffer[n - 1] = 0;
                std::cout << "client echo: " << buffer << std::endl;
            } else if(n == 0) {
                // client quit
                close(_fds[pos].fd);
                _fds[pos].fd = default_fd;
                _fds[pos].events = 0;
                _fds[pos].revents = 0;
            } else {
                // recv error
                LogModule::LOG(LogModule::LogLevel::ERROR) << "recv error";
                close(_fds[pos].fd);
                _fds[pos].fd = default_fd;
                _fds[pos].events = 0;
                _fds[pos].revents = 0;
            }
        }

        // 事件分发器
        void dispatcher() {
            for(int i = 0; i < _capacity; i++) {
                if(_fds[i].fd == default_fd)
                    continue;
                // 该文件描述符的读事件就绪
                if(_fds[i].revents & POLLIN) {
                    if(_fds[i].fd == _sockptr->getfd())
                        accepter(); // 获取新连接
                    else
                        recver(i);  // 某个连接读事件就绪
                }
            }
        }
    private:
        std::unique_ptr<SocketModule::Socket> _sockptr;
        bool _is_running;

        // poll 结构体数组 可以通过 new 来动态扩容
        struct pollfd* _fds;
        unsigned int _capacity;
};

4.3 poll的优缺点

优点:

  • 不同于 select 使用三个位图方式,poll 使用一个结构体指针数组实现。

  • poll 通过 events 和 revents 将设置监听与返回时事件就绪分离,无需每次调用设置。

  • poll 没有 fd 数量限制。

缺点:

  • 无论用户还是内核都需要大量的遍历操作,随着监听的文件描述符数量的增多,效率也会线性下降。

5. epoll

epoll 是 Linux 内核实现的高效 I/O 事件通知机制,用于处理大量文件描述符的 I/O 事件。它是 select/ poll 的增强版本。

它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法.

5.1 epoll_create

创建 epoll 实例,内核中创建对应的数据结构

函数原型:

#include 

int epoll_create(int size);

参数:

  • size: 该参数在现代内核中已废弃,但仍需传递大于 0 的值。

返回值:

  • 成功:返回新的 epoll 文件描述符。

  • 失败:返回 -1,并设置 errno

5.2 epoll_ctl

向 epoll 实例中添加、修改、删除要监控的文件描述符。内核中注册回调机制。

函数原型:

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

参数:

  • epfd: epoll 实例的文件描述符(由 epoll_create() 返回)。

  • op: 操作类型,必须是以下之一:

    • EPOLL_CTL_ADD: 添加新的文件描述符到 epoll 实例中。

    • EPOLL_CTL_MOD: 修改已注册的文件描述符的监听事件。

    • EPOLL_CTL_DEL: 从 epoll 实例中删除文件描述符及事件。

  • fd: 要操作的目标文件描述符。

  • event: 事件配置的结构体指针。

    • struct epoll_event:
    typedef union epoll_data {
        void        *ptr;     // 用户自定义指针
        int          fd;      // 文件描述符(常用)
        uint32_t     u32;     // 32位整数
        uint64_t     u64;     // 64位整数
    } epoll_data_t;
    
    struct epoll_event {
        uint32_t     events;  // Epoll 事件(位掩码)
        epoll_data_t data;    // 用户数据(事件触发时返回)
    };
    

events可以是如下的取值:

  • EPOLLIN: 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭);

  • EPOLLOUT: 表示对应的文件描述符可以写;

  • EPOLLPRI: 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来);

  • EPOLLERR: 表示对应的文件描述符发生错误;

  • EPOLLHUP: 表示对应的文件描述符被挂断;

  • EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,默认为水平触发(Level Triggered)。

  • EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket,需要 epoll_ctl 重新添加。

返回值:

  • 成功:返回0。

  • 失败: 返回 -1 ,并设置 errno

5.3 epoll_wait

等待被监控的文件描述符上事件就绪,并返回就绪的事件列表。

函数原型:

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

参数:

  • epfd: epoll 实例的文件描述符(由 epoll_create() 返回)。

  • events: 用于接收就绪事件的数组指针,由调用者分配内存。

  • maxevents: 每次调用最多返回的事件数量,必须大于 0,且不能超过 events 数组大小。

  • timeout: 超时时间(毫秒)

    • -1: 阻塞等待,直到有时间发生。

    • 0: 立即返回,即使没有时间就绪。

    • > 0: 等待指定毫秒数后返回。

返回值:

  • 成功:返回就绪的文件描述符数量。

  • 失败:返回 -1,并设置 errno

  • 超时:返回0。

5.4 epoll 原理

内核中的 epoll 模型:

// 内核源码中的关键结构(简化版)
struct eventpoll {
    // 红黑树根节点:存储所有监控的 fd
    struct rb_root rbr;
    
    // 双向链表:存储就绪的 fd
    struct list_head rdllist;
    
    // 等待队列:存放调用 epoll_wait 而阻塞的进程
    wait_queue_head_t wq;
    
    // 互斥锁
    struct mutex mtx;
    
    // 其他字段...
};

// 红黑树中和就绪队列中的节点
struct epitem {
    // 红黑树节点
    struct rb_node rbn;
    
    // 双向链表节点(用于就绪链表)
    struct list_head rdllink;
    
    // 监控的文件描述符
    int fd;
    
    // 监控的事件
    uint32_t events;
    
    // 用户数据
    epoll_data_t data;
    
    // 指向所属的 eventpoll
    struct eventpoll *ep;
    
    // 等待队列项(用于回调)
    struct wait_queue_entry wq;
};
  • epoll 模型的核心由三部分构成:用于管理文件描述符及事件的红黑树、存放就绪事件的就绪队列、以及当事件发生时由内核触发的回调函数。

  • 每一个 epoll 对象都有一个独立的 eventpoll 结构体,其中包含了红黑树和就绪队列。

  • 红黑树的本质是允许用户主动向内核注册特定文件描述符及其关注的事件类型。

    • 这些事件会以节点的方式挂载到红黑树上,因此效率提高了。

    • 红黑树的 key 是 fd。

  • 就绪队列本质是内核告诉用户哪些文件描述符的哪些事件已就绪。

    • 用户、内核、就绪队列,可以理解为基于事件就绪的生产者消费者模型。

    • 获取就绪事件,如果缓冲区大小不够怎么办?因为是生产者消费者模型,下次继续拿。

    • 检测是否有事件就绪,时间复杂度O(1),只需要关心就绪队列是否为空即可。

  • eventpoll 中包含了互斥锁和条件变量,因此,epoll 是线程安全的。

  • 红黑树中的每一个节点都会被注册一个回调机制,当该文件描述符上的事件就绪,会自动调用回调,激活红黑树中的节点到就绪队列中。只有指针操作,时间复杂度O(1)。

    • epitem 节点既可以属于红黑树,又可以属于就绪队列。

    • 回调机制也被通过链表管理起来了,因为同一个文件描述符可能会被不同的 epoll 实例同时监控。

  • epoll_wait 中的 events,内核会严格按照下标0开始,依次拷贝,应用层不需要向 select 或 poll 一样进行非法检测。

  • epoll_create 内核中在创建 eventpoll 结构体,本质是创建内核数据结构(红黑树和就绪队列)。

  • epoll_ctl 内核中在红黑树中增加、修改、或删除节点,添加节点时向该节点注册回调方法。

  • 为什么 epoll_create 返回值是一个文件描述符,如何根据文件描述符找到 eventpoll 结构?

    • struct files_struct -> fd_array[4] -> struct file -> void* private_data -> struct eventpoll

5.5 epoll 的特点

  • 文件描述符没有数量限制。

  • 解决了用户设置监听事件与内核告诉用户哪些事件就绪的分离。

  • 通过事件回调机制,避免使用遍历。

5.6 epoll 的LT模式与ET模式

epoll 默认模式为 LT 模式。设置 ET 模式将 events 添加 EPOLLET 标志位。

LT(Level Trigger):水平触发,只要有事件就绪,就一直通知你。

  • 有10个就绪,你只读走5个,之后会一直通知

  • 如果10kb数据,只读走2kb数据,则之后会一直通知。

ET(Edge Trigger):边缘触发,只有当事件就绪状态变化时,才通知你一次。

  • 有10个就绪,只读走5个,如果没有新的事件就绪,则不会继续通知。

  • 如果10kb数据,只读走2kb数据,除非有新的数据到来,否则不会通知。

总结:

  • 使用 ET 模式必须一次性读完所有数据,否则可能会造成数据丢失。

  • 在ET模式下,用户程序无法预知底层缓冲区是否读完,因此必须使用非阻塞I/O进行循环读取,直到 recv 返回 EAGAIN 错误(表示缓冲区已空),否则剩余数据将无法再次触发事件通知。

  • ET 比 LT 更高效

    • ET通知效率更高,因为ET每次都是有效通知。

    • ET迫使上层尽快读完所有的数据,可以给对方更新一个更大的win窗口(tcp接收缓冲区大小),提高对方滑动窗口的大小,提高 TCP 的传输效率。

LT 也可以和 ET 类似循环读取,为什么还要有 ET?
增加 IO 读写方式的确定性,使用 ET 模式是 OS 在约束程序员,如果一次性不读完则代码是存在bug的。若使用 LT 模式,没有人约束程序员,存在不确定性。

6. 多路转接设置监听写事件

  • 读事件默认是不就绪的,写事件是默认就绪的。

  • 读事件监听要常设,而写事件监听是按需设置的。

  • 当发送失败时,再将写事件的监听设置到 epoll 内部。

  • 手动开启 EPOLLOUT 的监听时,默认就会触发一次写事件就绪(即使发送缓冲区是满的·)。

7. 基于 Reactor 反应堆模式的服务器

7.1 Common.hpp

#pragma once

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

enum ExitCode{
    OK = 0,
    SOCKET_ERROR,
    BIND_ERROR,
    LISTEN_ERROR,
    USAGE_ERROR,
    CONNECT_ERROR,
    FORK_ERRO
};

class NoCopy{
    public:
        NoCopy() = default;
        ~NoCopy() = default;
        NoCopy(const NoCopy&) = delete;
        NoCopy& operator=(const NoCopy&) = delete;
};

// 设置文件描述符为非阻塞
void setNonBlock(int fd) {
    int fl = fcntl(fd , F_GETFL);
    if(fl < 0) {
        perror("fcntl");
        return;
    }
    fcntl(fd, F_SETFL , fl | O_NONBLOCK);
}

7.2 Mutex.hpp

// Mutex.hpp
#pragma once

#include 

class Mutex{
    public:
        Mutex() {
            pthread_mutex_init(&_mutex , nullptr);
        }

        void lock() {
            int n = pthread_mutex_lock(&_mutex);
            (void)n;
        }

        void unlock() {
            int n = pthread_mutex_unlock(&_mutex);
            (void)n;
        }

        pthread_mutex_t* getMutex() {
            return &_mutex;
        }

        ~Mutex() {
            pthread_mutex_destroy(&_mutex);
        }
    private:
        pthread_mutex_t _mutex;
};
// 基于 RAII 的互斥锁
class MutexGuard{
    public:
        MutexGuard(Mutex& mutex)
            :_mutex(mutex)
        {
            _mutex.lock();
        }
        ~MutexGuard() {
            _mutex.unlock();
        }
    private:
        Mutex& _mutex;
};

7.3 Log.hpp

#pragma once

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "Mutex.hpp"

namespace LogModule{
    // 定义一个刷新策略的基类
    class FlushStrategy{
        public:
            virtual void synlog(const std::string&) = 0;
            FlushStrategy() = default;
            ~FlushStrategy() = default;
    };

    class ConsoleFlushStrategy : public FlushStrategy{
        public:
            virtual void synlog(const std::string& message) override {
                MutexGuard mg(_mutex);
                std::cout << message << std::endl;
            }
        
            ConsoleFlushStrategy() = default;
            ~ConsoleFlushStrategy() = default;
        private:
            Mutex _mutex;
    };

    const std::string default_path = "./log";
    const std::string default_name = "log.log";
    class FileFlushStrategy : public FlushStrategy{
        public:
            virtual void synlog(const std::string& message) override {
                MutexGuard mg(_mutex);
                std::string pathname = _path + (_path.back() != '/' ? "/" : "") + _name;
                std::ofstream file(pathname , std::ios::app);
                if(!file.is_open()) {
                    std::cerr << "文件打开失败" << std::endl;
                    return;
                }
                file << message << std::endl;
                file.close();
            }
            FileFlushStrategy(const std::string& path = default_path , const std::string& name = default_name)
                :_path(path) , _name(name)
            {
                // 这里主要负责路径不存在创建路径
                if(!std::filesystem::exists(_path)) {
                    try
                    {
                        std::filesystem::create_directory(_path);
                    }
                    catch(const std::exception& e)
                    {
                        std::cerr << e.what() << '
';
                    }
                }
            }
            ~FileFlushStrategy() = default;
        private:
            std::string _path;  // 日志文件所在的路径
            std::string _name;  // 日志文件的名字
            Mutex _mutex;
    };

    // 日志等级
    enum class LogLevel{
        DEBUG , INFO , WARNING , ERROR , FATAL
    };

    std::string LogLevelToString(LogLevel level) {
        switch(level) {
            case LogLevel::DEBUG:
                return "DEBUG";
            case LogLevel::INFO:
                return "INFO";
            case LogLevel::WARNING:
                return "WARNING";
            case LogLevel::ERROR:
                return "ERROR";
            case LogLevel::FATAL:
                return "FATAL";
            default:
                return "UNKNOWN";
        }
    }

    std::string GetTime() {

        // struct tm {
        //     int tm_sec;    /* Seconds (0-60) */
        //     int tm_min;    /* Minutes (0-59) */
        //     int tm_hour;   /* Hours (0-23) */
        //     int tm_mday;   /* Day of the month (1-31) */
        //     int tm_mon;    /* Month (0-11) */
        //     int tm_year;   /* Year - 1900 */
        //     int tm_wday;   /* Day of the week (0-6, Sunday = 0) */
        //     int tm_yday;   /* Day in the year (0-365, 1 Jan = 0) */
        //     int tm_isdst;  /* Daylight saving time */
        // };
        
        struct tm cur_time;
        time_t timestamp = time(nullptr);
        // 可重入的函数
        localtime_r(&timestamp , &cur_time);
        char format[64];
        snprintf(format , sizeof(format) , "%02d-%02d-%02d %02d:%02d:%02d" , 
            cur_time.tm_year + 1900,
            cur_time.tm_mon + 1,
            cur_time.tm_mday,
            cur_time.tm_hour,
            cur_time.tm_min,
            cur_time.tm_sec
        );
        return format;
    }

    // 日志类
    class Logger{
        public:
            Logger() {
                enableConsoleFlushStrategy();
            }

            void enableConsoleFlushStrategy() {
                // make_unique返回的是一个右值对象,这里调用的是unique_ptr的移动赋值
                _flush_strategy = std::make_unique<ConsoleFlushStrategy>();
            }

            void enableFileFlushStrategy() {
                _flush_strategy = std::make_unique<FileFlushStrategy>();
            }
            
            // 表示一条完整的日志消息
            class LogMessage{
                public:
                    LogMessage(LogLevel level , const std::string& filename , int line , Logger& logger) 
                        :_cur_time(GetTime())
                        ,_log_level(level)
                        ,_pid(getpid())
                        ,_cur_file(filename)
                        ,_line_number(line)
                        ,_logger(logger)
                    {
                        // [2025-10-30 12:24:30][DEBUG][102939][Main.cc][10] - 
                        // 日志的左半部分
                        std::stringstream ss;
                        ss << "[" << _cur_time << "]"
                           << "[" << LogLevelToString(_log_level) << "]"
                           << "[" << _pid << "]"
                           << "[" << _cur_file << "]"
                           << "[" << _line_number << "]"
                           << " - ";
                        
                        message = ss.str();
                    }

                    // 日志的右半部分
                    template<typename T>
                    LogMessage& operator<<(const T& info) {
                        std::stringstream ss;
                        ss << info;
                        message += ss.str();
                        return *this;
                    }

                    ~LogMessage() {
                        // 智能指针不能为空
                        if(_logger._flush_strategy)
                            _logger._flush_strategy->synlog(message);
                    }
                private:
                    std::string _cur_time;
                    LogLevel _log_level;
                    pid_t _pid;
                    std::string _cur_file;
                    int _line_number;
                    std::string message;    // 保存一条完整的日志消息
                    Logger& _logger;
            };  

            // 这里故意返回临时的 LogMessage 对象
            LogMessage operator()(LogLevel level ,const std::string& filename , int line) {
                // 只会调用一次析构函数
                return LogMessage(level , filename , line , *this);
            }
        private:
            std::unique_ptr<FlushStrategy> _flush_strategy;
    };

    // 全局的日志对象
    Logger log;

    // 使用宏,简化用户操作,获取文件名和行号
    #define LOG(le)                           log(le , __FILE__ , __LINE__)
    #define ENABLE_CONSOLE_FLUSH_STRATEGY()   log.enableConsoleFlushStrategy()
    #define ENABLE_FILE_FLUSH_STRATEGY()      log.enableFileFlushStrategy()
}

7.4 InetAddr.hpp

#pragma once

#include 
#include 
#include 
#include 
#include 
#include 

const std::string any_ip = "0.0.0.0";

// 解析IP地址和端口号的类
class InetAddr{
    public:
        InetAddr() = default;
        // 这个构造函数用来将 struct sockaddr_in 结构体转换为 
        //      - 1.本地序列的字符串风格的点分十进制的IP 
        //      - 2.本地序列的整数端口
        // 网络转主机
        InetAddr(const struct sockaddr_in& addr) 
            :_addr(addr)
        {
            _port = ntohs(addr.sin_port);
            char ip_buffer[64];
            inet_ntop(AF_INET , &addr.sin_addr , ip_buffer, sizeof(ip_buffer));
            _ip = ip_buffer;
        }

        void setSockaddrIn(const struct sockaddr_in& addr) {
            _addr = addr;
            _port = ntohs(addr.sin_port);
            char ip_buffer[64];
            inet_ntop(AF_INET , &addr.sin_addr , ip_buffer, sizeof(ip_buffer));
            _ip = ip_buffer;
        }

        // 主机转网络
        // #define INADDR_ANY 0
        InetAddr(const std::string ip , u_int16_t port) 
            :_ip(ip)
            ,_port(port)
        {
            memset(&_addr , 0 , sizeof(_addr));
            _addr.sin_family = AF_INET;
            _addr.sin_port = htons(_port);
            inet_pton(AF_INET , _ip.c_str() , &_addr.sin_addr);
        }

        InetAddr(u_int16_t port) 
            :_port(port)
            ,_ip(any_ip)
        {
            memset(&_addr , 0 , sizeof(_addr));
            _addr.sin_family = AF_INET;
            _addr.sin_port = htons(_port);
            _addr.sin_addr.s_addr = INADDR_ANY; 
            // inet_pton(AF_INET , _ip.c_str() , &_addr.sin_addr);
        }

        const std::string& getIP() const { return _ip; }
        u_int16_t getPort() const { return _port; }
        const struct sockaddr_in& getSockaddrin() const { return _addr; } 
        const struct sockaddr* getSockaddr() const { return (const struct sockaddr*)&_addr; }
        struct sockaddr* getSockaddr() { return (struct sockaddr*)&_addr; }
        socklen_t getSockaddrLen() const { return sizeof(_addr); }
        
        // 格式化显示IP + Port
        std::string showIpPort() const {
            return "[" + _ip + " : " + std::to_string(_port) + "]";
        }

        bool operator==(const InetAddr& addr) const {
            return _ip == addr.getIP() && _port == addr.getPort(); 
        }
    private:
        struct sockaddr_in _addr;
        std::string _ip;
        u_int16_t _port;
};

7.5 Socket.hpp

#pragma once

#include "Common.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"

namespace SocketModule{
    const static int default_backlog = 16;
    // 设计为虚基类,TCP和UDP的模板类
    class Socket{
        public:
            virtual ~Socket() {}
            virtual void socket() = 0;
            virtual void bind(uint16_t server_port) = 0;
            virtual void listen(int backlog) = 0;
            // virtual std::shared_ptr accept(InetAddr* client_addr) = 0;
            virtual int accept(InetAddr* client_addr) = 0;
            virtual void close() = 0;
            virtual int recv(std::string* res) = 0;
            virtual int send(const std::string& message) = 0;
            virtual void connect(const InetAddr& server_addr) = 0;
            virtual int getfd() = 0;
        public:
            void buildTcpListenSocket(uint16_t server_port , int backlog = default_backlog) {
                socket();
                bind(server_port);
                listen(default_backlog);
            }
            void buildTcpClientListendSocket() {
                socket();
            }
    };

    const static int default_sockfd = -1;
    class TcpSocket : public Socket{
        public: 
            TcpSocket() :_sockfd(default_sockfd) {}         // 初始化sockfd为listenfd
            // TcpSocket(int acceptfd) :_sockfd(acceptfd) {}   // 初始化sockfd为accpetfd

            virtual void socket() override {
                _sockfd = ::socket(AF_INET , SOCK_STREAM , 0);
                if(_sockfd < 0) {
                    LogModule::LOG(LogModule::LogLevel::FATAL) << "socket create error";
                    exit(SOCKET_ERROR);
                }
                LogModule::LOG(LogModule::LogLevel::INFO) << "socket create success - sockfd: " << _sockfd;
            }

            virtual void bind(uint16_t server_port) override {
                InetAddr server_addr(server_port);
                int n = ::bind(_sockfd , server_addr.getSockaddr() , server_addr.getSockaddrLen());
                if(n < 0) {
                    LogModule::LOG(LogModule::LogLevel::FATAL) << "bind error";
                    exit(BIND_ERROR);
                }
                LogModule::LOG(LogModule::LogLevel::INFO) << "bind success - sockfd: " << _sockfd;
            }

            virtual void listen(int backlog) override {
                int n = ::listen(_sockfd , backlog);
                if(n < 0) {
                    LogModule::LOG(LogModule::LogLevel::FATAL) << "listen error";
                    exit(LISTEN_ERROR);
                }
                LogModule::LOG(LogModule::LogLevel::INFO) << "listen success - sockfd: " << _sockfd;
            }

            // virtual std::shared_ptr accept(InetAddr* client_addr) override {
            int accept(InetAddr* client_addr) override {
                struct sockaddr_in client;
                socklen_t addrlen = sizeof(client);
                int acceptfd = ::accept(_sockfd , (struct sockaddr*)&client , &addrlen);
                if(acceptfd < 0) {
                    return -1;
                }
                client_addr->setSockaddrIn(client);
                return acceptfd;
                // client_addr->setSockaddrIn(client); // 输出型参数
                // return std::make_shared(acceptfd);   // 多个服务器可以共享一个客户端的链接
            }

            virtual void close() override {
                if(_sockfd > 0)
                    ::close(_sockfd);   // close(listenfd) 或 close(accpetfd)
            }

            // recv返回值和::recv返回值保持一致
            virtual int recv(std::string* res) override {
                char buffer[4096];
                ssize_t n = ::recv(_sockfd , buffer , sizeof(buffer) , 0);
                *res += buffer; // 读取可能会不完整
                return n;
            }

            virtual int send(const std::string& message) override {
                ssize_t n = ::send(_sockfd , message.c_str() , message.size() , 0);
                return n;
            }

            virtual void connect(const InetAddr& server_addr) override {
                int n = ::connect(_sockfd , server_addr.getSockaddr() , server_addr.getSockaddrLen());
                if(n < 0) {
                    LogModule::LOG(LogModule::LogLevel::FATAL) << "connect server error";
                    exit(CONNECT_ERROR);
                }
            }

            virtual int getfd() override { return _sockfd; }
        private:
            int _sockfd;    // listenfd 或 accpetfd 
    };
}

7.6 Epoller.hpp

#pragma once

#include 
#include 
#include "Log.hpp"

enum EpollerExit{
    EPOLL_CREATE_ERROR = 1,
    EPOLL_CTL_ERROR,
    EPOLL_WAIT_ERROR
};

class Epoller{
    const static int default_fd = -1;
    public:
        Epoller()
            :_epfd(default_fd)
        {
            _epfd = epoll_create(128);
            if(_epfd < 0) {
                LogModule::LOG(LogModule::LogLevel::FATAL) << "epoll create error";
                exit(EPOLL_CREATE_ERROR);
            }
            LogModule::LOG(LogModule::LogLevel::INFO) << "epoll create success - epfd: " << _epfd;
        }

        void addEvent(int fd , uint32_t events) {
            struct epoll_event ep;
            ep.data.fd = fd;
            ep.events = events; 
            int n = epoll_ctl(_epfd , EPOLL_CTL_ADD , fd , &ep);
            if(n < 0) {
                LogModule::LOG(LogModule::LogLevel::WARNING) << "epoll add error";
            }
        }

        void modEvent(int fd , uint32_t events) {
            struct epoll_event ep;
            ep.data.fd = fd;
            ep.events = events; 
            int n = epoll_ctl(_epfd , EPOLL_CTL_MOD , fd , &ep);
            if(n < 0) {
                LogModule::LOG(LogModule::LogLevel::WARNING) << "epoll mod error";
            }
        }

        void delEvent(int fd) {
            int n = epoll_ctl(_epfd , EPOLL_CTL_DEL , fd , nullptr);
            if(n < 0) {
                LogModule::LOG(LogModule::LogLevel::WARNING) << "epoll del error";
            }
        }

        int waitEvent(struct epoll_event* events , int maxevents , int timeout) {
            int n = epoll_wait(_epfd , events , maxevents , timeout);
            if(n < 0) {
                LogModule::LOG(LogModule::LogLevel::WARNING) << "epoll wait error";
                return -1;
            } else if(n == 0) {
                LogModule::LOG(LogModule::LogLevel::WARNING) << "epoll waitout";
                return -2;
            }
            return n;
        }

        ~Epoller() {
            if(_epfd > 0)
                close(_epfd);
        }
    private:
        int _epfd;
};

7.7 Reactor.hpp

#pragma once

#include 
#include 
#include "Epoller.hpp"
#include "Connection.hpp"

#define EPOLL_WAIT_SIZE 1024

class Reactor{
    bool connectionIsExist(int key_fd) {
        auto iter = _connect_manager.find(key_fd);
        return iter == _connect_manager.end();
    }

    void dispatcher(int n) {
        for(int i = 0; i < n; i++) {
            int ready_fd = _ep_wait_queue[i].data.fd;
            uint32_t ready_event = _ep_wait_queue[i].events;
            
            // 就绪事件发生错误或客户端断开链接,交到下层I/O事件中处理异常
            if(ready_event & EPOLLERR) {
                ready_event |= (EPOLLIN | EPOLLOUT);
            }
            if(ready_event & EPOLLHUP) {
                ready_event |= (EPOLLIN | EPOLLOUT);
            }

            // 读事件就绪
            if(ready_event & EPOLLIN) {
                // 为了提高代码的鲁棒性,发生错误时,只处理一次,避免处理多次,需判断链接Connection是否存在
                if(!connectionIsExist(ready_fd)) {
                    // 多态调用 Conncetion 的读方法 Connction 可能是 Listener 或 Channel
                    _connect_manager[ready_fd]->recver();
                }
            }

            // 写事件就绪
            if(ready_event & EPOLLOUT) {
                // 为了提高代码的鲁棒性,发生错误时,只处理一次,需判断链接Connection是否存在
                if(!connectionIsExist(ready_fd)) {
                    // 多态调用 Conncetion 的写方法 Connction 可能是 Listener 或 Channel
                    _connect_manager[ready_fd]->sender();
                }
            }
        }
    }
    public:
        Reactor()
            :_epoll_model_ptr(std::make_unique<Epoller>())
            ,_is_running(false)
        {}

        void start() {
            // 增加代码的鲁棒性,若 _connect_manager 为空则不启动服务器
            if(_connect_manager.empty()) {
                return;
            }

            _is_running = true;
            int timeout = -1;   // 阻塞等待
            while(_is_running) {
                PrintConnection();
                int n = _epoll_model_ptr->waitEvent(_ep_wait_queue , EPOLL_WAIT_SIZE , timeout);
                if(n == -1) { // 等待就绪事件失败
                    break;
                } else if(n == -2) {
                    continue; // 超时
                }
                // 就绪事件分发器
                dispatcher(n);
            }
            _is_running = false;    
        }

        void addConnection(std::shared_ptr<Connection> conn) {
            // 1. 判断 connect_manager 是否已经存在 conn
            std::unordered_map<int , std::shared_ptr<Connection>>::iterator iter = _connect_manager.find(conn->getSockfd());
            if(iter != _connect_manager.end()) {
                LogModule::LOG(LogModule::LogLevel::WARNING) << "connection have existed";
                return;
            }

            // 2. 获取 Connection 的  fd 与 events
            int fd = conn->getSockfd();
            uint16_t events = conn->getEvents();

            // 3. 将 fd 与 events 添加到 epoller 模型中
            _epoll_model_ptr->addEvent(fd , events);

            // 4. 设置 conn 的回指指针
            conn->setReactorPtr(this);

            // 5. 将 fd 与 conn 添加到 connect_manager 管理容器中
            _connect_manager[fd] = conn;
        }

        void delConnection(int fd) {
            // 1. epoll 模型中删除
            _epoll_model_ptr->delEvent(fd);

            // 2. 哈希表中删除
            _connect_manager.erase(fd);

            // 3. 关闭文件描述符
            close(fd);

            LogModule::LOG(LogModule::LogLevel::INFO) << "client quit";
        }

        void openWriteReadEvents(int fd , bool write , bool read) {
            // fd不存在
            if(connectionIsExist(fd)) {
                LogModule::LOG(LogModule::LogLevel::INFO) << "fd: " << fd << " is not exists";
                return;
            }

            // 1. 修改哈希表中该fd的事件
            uint32_t new_events = EPOLLET | (write ? EPOLLOUT : 0) | (read ? EPOLLIN : 0);
            _connect_manager[fd]->setEvents(new_events);
            
            // 2. 修改 epoll 模型中fd的事件
            _epoll_model_ptr->modEvent(fd , new_events);
        }

        void stop() {
            _is_running = false;
        }

        void PrintConnection()
        {
            std::cout << "当前Reactor正在进行管理的fd List:";
            for(auto &conn : _connect_manager)
            {
                std::cout << conn.second->getSockfd() << " ";
            }
            std::cout << "
";
        }
    private:
        // 1.epoll 模型
        std::unique_ptr<Epoller> _epoll_model_ptr;
        // 2.epoll 用户就绪队列
        struct epoll_event _ep_wait_queue[EPOLL_WAIT_SIZE];
        // 3.链接管理器 [fd : Connection]
        std::unordered_map<int , std::shared_ptr<Connection>> _connect_manager;
        // 4.是否运行标志
        bool _is_running;
};

7.8 Connection.hpp

#pragma once

#include 
#include 
#include 

class Reactor;

using callback_t = std::function<std::string(std::string&)>;

class Connection{
    public:
        Connection()
            :_reactor(nullptr)
            ,_events(0)
        {}

        // get/set方法
        void setReactorPtr(Reactor* reactor) { _reactor = reactor; }
        Reactor* getReactorPtr() { return _reactor; }
        void setEvents(uint32_t events) { _events = events; }
        uint32_t getEvents() { return _events; }

        // 纯虚方法
        virtual int getSockfd() = 0;
        virtual void recver() = 0;
        virtual void sender() = 0;
        virtual void except() = 0;

        // 公共方法
        void registerCallback(callback_t callback) {
            _callback = callback;
        }
    private:    
        // 1.回指指针
        Reactor* _reactor;
        // 2.监听的事件
        uint32_t _events;
    protected:
        // 3.回调函数
        callback_t _callback;
};

7.9 Listener.hpp

#pragma once

#include 
#include "Socket.hpp"
#include "Channel.hpp"
#include "Reactor.hpp"

class Listener : public Connection{
    const static uint16_t default_port = 8080;
    public:
        Listener(uint16_t port = default_port)
            :_port(port) 
            ,_socket_ptr(std::make_unique<SocketModule::TcpSocket>())
        {   
            // 1. 创建TCP套接字
            _socket_ptr->buildTcpListenSocket(_port); 
            // 2. 设置监听的事件 listener 只负责获取新连接 | 开启 ET 模式
            setEvents(EPOLLIN | EPOLLET);  
            // 3. 设置文件描述符为非阻塞模式
            setNonBlock(_socket_ptr->getfd());
        }

        virtual void recver() override {
            InetAddr client;
            // ET 模式 必须一次性读完所有客户端链接
            while(true) {
                int sockfd = _socket_ptr->accept(&client);
                if(sockfd < 0) {
                    // 本次读取完毕
                    if(errno == EAGAIN) {
                        break;
                    } else if(errno == EINTR) {
                        // accept 被信号打断
                        continue;
                    } else {
                        // 读取错误
                        LogModule::LOG(LogModule::LogLevel::ERROR) << "accpet error";
                        break;
                    }
                }
                // 读取成功 sockfd > 0
                // 1. 创建 Channel 对象
                std::shared_ptr<Connection> conn = std::make_shared<Channel>(sockfd , client);
                // 2. 将 listener 的回调函数设置到每个 Channel 中
                if(_callback != nullptr)
                    conn->registerCallback(_callback);
                // 3. 通过回指指针将 Channel 添加到 Reactor 链接管理容器中
                getReactorPtr()->addConnection(conn);
            }
        }

        virtual void sender() override {}
        virtual void except() override {}
        virtual int getSockfd() override {
            return _socket_ptr->getfd();
        }
    private:
        uint16_t _port;
        std::unique_ptr<SocketModule::Socket> _socket_ptr;  // socket中包含fd
};

7.10 Channel.hpp(多路转接写事件的具体写法)

#pragma once

#include "Connection.hpp"
#include "Reactor.hpp"

#define RECV_SIZE 4096

class Channel : public Connection{
    public:
        Channel(int sockfd  , const InetAddr& client)
            :_sockfd(sockfd)
            ,_client_addr(client)
        {
            // 1. 设置 Channel 监听的事件和开启 ET 模式
            setEvents(EPOLLIN | EPOLLET); // todo
            // 2. 设置文件描述符为非阻塞
            setNonBlock(_sockfd);
        }

        virtual int getSockfd() override { return _sockfd; }

        virtual void recver() override {
            char buffer[RECV_SIZE];
            while(true) {
                buffer[0] = 0;  // 清空字符串
                ssize_t n = recv(_sockfd , buffer , RECV_SIZE - 1 , 0); // 非阻塞方式读取
                if(n > 0) {
                    buffer[n] = 0;
                    _inbuffer += buffer;
                } else if(n == 0) {
                    // 客户端关闭 异常处理
                    except();
                    return;
                } else {
                    if(errno == EAGAIN) {
                        // 非阻塞读取完毕
                        break;
                    } else if(errno == EINTR) {
                        // 读取被信号打断
                        continue;
                    } else {
                        // recv error
                        LogModule::LOG(LogModule::LogLevel::ERROR) << "channel recv error";
                        except();
                        return;
                    }
                }
            }
            LogModule::LOG(LogModule::LogLevel::DEBUG) << "Channel inbuffer: " << _inbuffer;
            // ET 非阻塞读取数据完毕
            // 1. 判断是否报文是否是一个完整的报文,如果是多个报文?数据不完整问题或TCP粘包问题
            if(!_inbuffer.empty()) {
                // 将数据交付给 Protocol 层处理
                _outbuffer += _callback(_inbuffer);
            }

            // 2. 不为空发送
            if(!_outbuffer.empty()) {
                sender();
            }
        }

        virtual void sender() override {
            while(true) {
                ssize_t n = send(_sockfd , _outbuffer.c_str() , _outbuffer.size() , 0);
                if(n >= 0) {
                    // n 表示实际发送的字节
                    _outbuffer.erase(0 , n);
                    if(_outbuffer.empty())
                        break;
                } else {
                    // 写事件这里的 errno == EAGAIN 表示的意思是发送缓冲区已满
                    if(errno == EAGAIN || errno == EWOULDBLOCK)
                        break;
                    else if(errno == EINTR)
                        continue;
                    else {
                        except();
                        return;
                    }
                }
            }

            // 1.数据发送完毕
            // 2.发送缓冲区已满,发送条件不具备
            if(!_outbuffer.empty()) {
                // 开启对写事件的关心
                getReactorPtr()->openWriteReadEvents(_sockfd , true , true);
            } else {
                // 关闭对写事件的关心
                getReactorPtr()->openWriteReadEvents(_sockfd , false , true);
            }
        }

        virtual void except() override {
            // 处理读出错、写出错、客户端关闭
            // 本质都是关闭链接
            getReactorPtr()->delConnection(_sockfd);
        }
    private:    
        int _sockfd;
        std::string _inbuffer;
        std::string _outbuffer;
        InetAddr _client_addr;
};

7.11 Protocol.hpp

#pragma once
#include 
#include 
#include "Socket.hpp"
#include   // 引入jsoncpp第三方库

using namespace SocketModule;

// 基于网络版本的计算器
class Request{
    public:
        Request() = default;

        Request(int x , int y , char op)
            :left(x)
            ,right(y)
            ,oper(op)
        {}

        std::string serialization() {
            Json::Value root;
            root["x"] = left;
            root["y"] = right;
            root["oper"] = oper;
            Json::StyledWriter writer;
            return writer.write(root);
        }

        bool deserialization(const std::string& data) {
            Json::Value root;
            Json::Reader reader;
            bool ok = reader.parse(data , root);
            if(!ok) {
                LogModule::LOG(LogModule::LogLevel::WARNING) << "request deserialization error";
                return false;
            }
            left = root["x"].asInt();
            right = root["y"].asInt();
            oper = root["oper"].asInt();
            return true;
        }

        int get_x() const { return left; }
        int get_y() const { return right; }
        char get_oper() const { return oper; }

    private:
        int left;
        int right;
        char oper;
};

class Response{
    public:
        Response(int res = 0 , bool _valid = false) 
            :result(res)
            ,valid(_valid)
        {}

        std::string serialization() {
            Json::Value root;
            root["result"] = result;
            root["valid"] = valid;
            Json::StyledWriter writer;
            return writer.write(root);
        }

        bool deserialization(const std::string& data) {
            Json::Value root;
            Json::Reader reader;
            bool ok = reader.parse(data , root);
            if(!ok) {
                LogModule::LOG(LogModule::LogLevel::WARNING) << "response deserialization error";
                return false;
            }
            result = root["result"].asInt();
            valid = root["valid"].asBool();
            return true;
        }

        void showResult() {
            std::cout << "result: " << result << " [valid:" << valid << "]" << std::endl;
        }
    private:
        int result;
        bool valid;
};

using calculate_t = std::function<Response(const Request&)>;

// 协议类,需要解决两个问题
//   1. 需要有序列化和反序列化功能
//   2. 对于Tcp必须保证读到完整的报文
const static std::string sep = "
";
// format: 10
{"x":10, "y":20, "oper":"+"}

class Protocol{
    public:
        Protocol() = default;   
        Protocol(calculate_t cal_handler) :_cal_handler(cal_handler) {}

        // 添加报头
        std::string encode(const std::string& jsonstr) {
            std::string json_len = std::to_string(jsonstr.size());
            return json_len + sep + jsonstr + sep;
        }

        // 分离报头
        bool decode(std::string& buffer_queue , std::string& res) {
            size_t pos = buffer_queue.find(sep);
            if(pos == std::string::npos) {
                return false;
            }
            std::string json_len = buffer_queue.substr(0 , pos); // 有效载荷总长度
            int packet_len = json_len.size() + std::stoi(json_len) +  2 * sep.size();
            if(packet_len > buffer_queue.size()) {
                return false;   //说明当前读取的数据不足一个完整的报文,读取失败,应该继续读取
            }
            // 来到这里,当前已经有一个完整的报文或者多个完整的报文,或者一个半报文
            res = buffer_queue.substr(json_len.size() + sep.size() , std::stoi(json_len));   //将有效载荷带回上层
            // 将整个报文从buffer_queue分离
            buffer_queue.erase(0 , packet_len);
            return true;
        }

        std::string execute(std::string& request_json) {
            // request_json 是一个完整的json报文
            // 1. 反序列化
            Request req;
            req.deserialization(request_json);  // 注意:反序列化也可能会失败

            // 2. 交付上层处理请求
            Response res = _cal_handler(req);

            // 3. 将响应序列化 + 添加报头
            return encode(res.serialization());
        }
    private:
        calculate_t _cal_handler;
};

7.12 Calculate.hpp

#pragma once

#include "Protocol.hpp"

class Calculate{
    public:
        Response execute(const Request& req) {
            switch(req.get_oper()) {
                case '+':
                    return Response(req.get_x() + req.get_y() , true);
                case '-':
                    return Response(req.get_x() - req.get_y() , true);
                case '*':
                    return Response(req.get_x() * req.get_y() , true);
                case '/':
                    {
                        if(req.get_y() == 0)
                            return Response(0, false);
                        else 
                            return Response(req.get_x() / req.get_y() , true);
                    }
                default:
                    LogModule::LOG(LogModule::LogLevel::WARNING) << "未知的操作符";
            }
            return Response(0 , false);
        }
};

7.13 Main.cc


#include "Calculate.hpp"
#include "Protocol.hpp"
#include "Listener.hpp"
#include "Reactor.hpp"

// reactorServer port
int main(int argc , char* argv[]) {

    if(argc != 2) {
        std::cout << argv[0] << " port" << std::endl;
        exit(USAGE_ERROR);
    }

    uint16_t port = std::stoi(argv[1]);

    LogModule::ENABLE_CONSOLE_FLUSH_STRATEGY();

    // 1. 应用层
    Calculate cal;

    // 2. 表示层
    Protocol protocol([&cal](const Request& req) -> Response {
        LogModule::LOG(LogModule::LogLevel::DEBUG) << "应用层处理报文";
        return cal.execute(req);
    });

    // 3. 会话层
    std::shared_ptr<Connection> listener_ptr = std::make_shared<Listener>(port);
    listener_ptr->registerCallback([&protocol](std::string& inbuffer)->std::string{
        LogModule::LOG(LogModule::LogLevel::DEBUG) << "表示层处理报文开始";
        // 1.能否读取出一个完整的报文
        // 2.能否处理多个报文
        std::string outbuffer;
        while(true) { 
            std::string request_json;
            if(!protocol.decode(inbuffer , request_json))  // 不存在完整的报文
                break;
            // 来到这里,response_json 里100%存在一个完整的报文
            outbuffer += protocol.execute(request_json);
        }
        LogModule::LOG(LogModule::LogLevel::DEBUG) << "表示层处理报文结束";
        return outbuffer;
    });
    
    std::unique_ptr<Reactor> reactor_server_ptr = std::make_unique<Reactor>();
    reactor_server_ptr->addConnection(listener_ptr);
    reactor_server_ptr->start();


    return 0;
}

7.14 Makefile

reactorServer: Main.cc
	g++ -o $@ $^ -std=c++17 -ljsoncpp
.PHONY:clean
clean:
	rm -f reactorServer

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

搜索文章

Tags

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