最新资讯

  • 【C++仿Muduo库#3】Server 服务器模块实现上

【C++仿Muduo库#3】Server 服务器模块实现上

2026-01-30 19:39:19 栏目:最新资讯 7 阅读

📃个人主页:island1314

⛺️ 欢迎关注:👍点赞 👂🏽留言 😍收藏 💞 💞 💞

  • 生活总是不会一帆风顺,前进的道路也不会永远一马平川,如何面对挫折影响人生走向 – 《人民日报》

🔥 目录

    • 一、Buffer 模块
    • 二、日志模块
    • 三、套接字 Socket 设计
      • 1. 代码实现
      • 2. 代码检测
      • 3. 细节处理
        • 细节1:处理 Recv 函数时, errno 的来源以及 为啥不用 `EWOULDBLOCK`
        • 细节2:MSG_DONWAIT 的概述
        • 细节3:关于 ReuseAddr()
          • 📌 为什么默认不允许端口复用?
          • 🧠 举个例子:服务重启时的 `TIME_WAIT` 问题
          • 🧾小结
        • 细节4:宏污染
    • 四、Channel 类设计
      • 1. 代码实现
      • 2. 细节处理
        • 细节1:在 `HandleEvent` 函数中使用 `if-else if` 结构而非多个独立的 `if`
    • 五、Poller 模块实现
      • 1. 代码实现
      • 2. 细节处理
        • 细节1:epoll 是水平触发(LT)还是边缘触发(ET)?区别是什么?
        • 细节2:Poll 方法返回的活跃事件是如何处理的?
        • 细节3:Poller 是否支持多线程同时调用 Poll 方法?
        • 细节3:epoll_wait 的超时时间为何设置为 -1?是否合理?
      • 3. 与 Channel 的整合测试
    • 六、EventLoop 模块实现
      • 1. 关于 evenfd 函数
        • 1.1 函数概述
        • 1.2 代码示例
        • 1.3 使用场景及注意事项
      • 2. Eventloop 模块概述
      • 3. 与 TimeWheel 模块整合
        • 代码整合示例
      • 4. 代码测试
      • 5. 细节分析
        • 细节1:定时器任务中异步执行回调
        • 细节2:服务器端关闭再启动的文件描述符(fd)不变
        • 细节3:`Channel` 类中的 `Remove` 和 `Update` 方法为何调用 `EventLoop` 的接口?
        • 细节4:如何避免定时器任务的重复添加?


一、Buffer 模块

本质:缓冲区模板

功能:存储数据,取出数据

实现思想

  1. 实现缓冲区得有一块内存空间,采用 vector

    vector 底层其实使用的是一个线性的内存空间

  2. 要素

    1. 默认的空间大小
    2. 当前的读取数据位置
    3. 当前的写入数据位置
  3. 操作

    1. 写入数据:当前写入位置指向哪里,就从哪里开始写入,如果后续剩余空闲空间不够了,这个就考虑征途缓冲区空闲空间是否足够(因为 读位置也会向后偏移,也就是说前面也可能有空闲空间)

      • 足够:将数据移动到起始位置即可

      • 不够:扩容,从当前写位置开始扩容足够大小

      • 数据一旦写入成功,当前写位置,就要往后偏移

    2. 读取数据:当前读取位置指向哪里,就从哪里开始读取,前提是有数据可读

      • 可读数据大小:当前写入位置 - 当前读取位置
  4. 设计如下

    class Buffer{
    public:
        // 1. 获取当前写位置地址
        // 2. 确保可写空间足够
        // 3. 获取前沿空间大小    
        // 4. 获取后沿空间大小    
        // 5. 将写位置向后移动指定长度    
        // 6. 获取当前读位置地址    
        // 7. 获取可读数据大小
        // 8. 将读位置向后移动指定长度
        // 9. 清理功能   
    private:
        std::vector<char> _buffer;
        // 位置, 是一个相对偏移量, 而不是绝对地址
        uint64_t _read_idx;  // 相对读偏移
        uint64_t _write_idx; // 相对写偏移
    };
    

代码如下

class Buffer{
private:
    // 注意: 这里的起始地址:_buffer.data() 或者 &*_buffer.begin() 都可以
    char *Begin(){return &*_buffer.begin();}

    // 读写数据
    void ReadData(void *data, uint64_t len) {
        assert(len <= ReadableSize());
        std::copy(GetReadPos(), GetReadPos() + len, (char*)data);
        // MoveReadOffset(len);
    }
    void WriteData(const void *data, uint64_t len) {
        // 1. 确保可写空间足够 2. 拷贝数据
        if(len <= 0) return ;
        EnsureWriteSpace(len); 
        const char *d = static_cast(data);
        std::copy(d, d + len, GetWritePos()); // 把 data 复制到 缓冲区
        // MoveWriteOffset(len);
    }

    void WriteBuffer(Buffer &buf) {
        return WriteData(buf.GetReadPos(), buf.ReadableSize());
    }

    void WriteString(const std::string &str) {
        return WriteData(str.c_str(), str.size());
    }

public:
    Buffer(uint64_t size = 1024): _reader_idx(0), _writer_idx(0){
        _buffer.resize(size);
    }

    // 获取当前读写位置地址
    char *GetWritePos() {return Begin() + _writer_idx;}
    char *GetReadPos(){return Begin() + _reader_idx;}
    
    // 将读写位置向后移动指定长度
    void MoveReadOffset(uint64_t len){
        assert(len <= ReadableSize());
        _reader_idx += len;
    }
    void MoveWriteOffset(uint64_t len){
        assert(len <= BufferHeadSize() + BufferTailSize());
        _writer_idx += len;
    }

    
    // 获取一行数据
    std::string GetLine(){
        char *pos = FindCRLF();
        if (pos == nullptr) {
            return "";
        }
        return ReadAsString(pos - ReadPos() + 1);
    }

    // 获取缓冲区末尾空闲空间大小 -- 写偏移之后的空闲空间    
    uint64_t BufferTailSize() {return _buffer.size() - _writer_idx;}
    // 获取缓冲区起始空闲空间大小 -- 读偏移之前的空闲空间
    uint64_t BufferHeadSize(){return _reader_idx;}

    // 获取可读数据大小
    uint64_t ReadableSize() {return _writer_idx - _reader_idx;}

    
    // 确保可写空间足够 (移动 / 扩容)
    void EnsureWriteSpace(uint64_t len) {
        // 1. 末尾空闲空间大小足够, 直接返回
        if(BufferTailSize() >= len) return;
        // 2. 先移动读偏移
        if (len <= BufferHeadSize() + BufferTailSize()) {
            // 3. 空闲空间足够, 数据移动到起始位置
            uint64_t readable_size = ReadableSize(); // 保存当前数据大小
            // std::memmove(_buffer.data(), _buffer.data() + _reader_idx, ReadableSize());
            std::copy(GetReadPos(), GetReadPos() + readable_size, Begin()); // 将可读数据保存到起始位置
            
            // 更新读写偏移
            _writer_idx = readable_size; 
            _reader_idx = 0;
        } else {
            // 4. 扩容
            uint64_t new_size = _buffer.size() * 2; // 避免持续扩容
            while (new_size < len) {
                new_size *= 2;
            }
            _buffer.resize(new_size);
        }
    }
    void WriteAndPush(const void *data, uint64_t len) {
        WriteData(data, len);
        MoveWriteOffset(len);
    }

    void WriteStringAndPush(const std::string &str) {
        WriteString(str);
        MoveWriteOffset(str.size());
    }

    void WriteBufferAndPush(Buffer &buf) {
        WriteBuffer(buf);
        MoveWriteOffset(buf.ReadableSize());
    }

    void ReadAndPop(void *buf, uint64_t len) {
        ReadData(buf, len);
        MoveReadOffset(len);
    }

    std::string ReadAsString(uint64_t len){
        assert(len <= ReadableSize());
        std::string str;
        str.resize(len);
        ReadData(&str[0], len);
        return str;
    }

    std::string ReadAsStringAndPop(uint64_t len){
        std::string str = ReadAsString(len);
        MoveReadOffset(len);
        return str;
    }

    char *FindCRLF(){
        char *res = (char*)std::memchr(GetReadPos(), '
', ReadableSize());
        return res;
    }

    std::string GetLineAndPop(){
        std::string str = GetLine();
        MoveReadOffset(str.size());
        return str;
    }

    // 9. 清空缓冲区   
    void clear(){
        _buffer.clear();
        _reader_idx = 0;
        _writer_idx = 0;
    }

private:
    std::vector _buffer; // 使用 vector 进行内存空间管理
    // 位置, 是一个相对偏移量, 而不是绝对地址
    uint64_t _reader_idx;  // 相对读偏移
    uint64_t _writer_idx; // 相对写偏移
};

测试代码如下

int main() {
    Buffer buffer;
    
    for(int i = 0; i < 300; i++){
        std::string str = "hello world" + std::to_string(i) + "
";      
        buffer.WriteStringAndPush(str);
    }

    while(buffer.GetReadableSize() > 0){
        std::string line = buffer.GetLineAndPop();
        std::cout << "Line: " << line << std::endl;
    }

    // // std::string str = "hello world"; 
    // // buffer.WriteStringAndPush(str);
    // std::cout << "Buffer size: " << buffer.GetReadableSize() << std::endl;
    // std::cout << "Buffer content: " << buffer.ReadAsStringAndPop(buffer.GetReadableSize()) << std::endl;


    // buffer.WriteStringAndPush("hello world
");
    // std::string tmp = buffer.ReadAsStringAndPop(buffer.GetReadableSize());
    // std::cout << "tmp: " << tmp << std::endl;
    // std::cout << "Buffer size: " << buffer.GetReadableSize() << std::endl;


    return 0;
}

二、日志模块

详情可以参考我的这篇文章:

#define INF 0
#define DBG 1
#define ERR 2
#define LOG_LEVEL -1

#define LOG(level, format, ...) do{
    if(level < LOG_LEVEL) break;
    time_t t = time(nullptr);
    struct tm *tm = localtime(&t);
    char buf[64];
    strftime(buf, sizeof(buf) - 1, "%Y-%m-%d %H:%M:%S", tm);
    printf("%s [%s:%d] " format "
", buf, __FILE__, __LINE__, ##__VA_ARGS__);
}while(0)

#define LOG_INFO(format, ...) LOG(INF, format, ##__VA_ARGS__)
#define LOG_DEBUG(format, ...) LOG(DBG, format, ##__VA_ARGS__)
#define LOG_ERROR(format, ...) LOG(ERR, format, ##__VA_ARGS__)

三、套接字 Socket 设计

这个可以参考我之前的这篇文章:

1. 代码实现

// Socket 类
#define MAXLISTEN 1024
class Socket{
private:
    // 1.创建套接字
    bool Create(){
        _sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
        if(_sockfd < 0){
            LOG_ERROR("CREATE SOCKET ERROR");
            return false;
        }
        return true;
    }
    // 2.绑定地址信息
    bool Bind(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 = bind(_sockfd, (struct sockaddr*)&addr, len);
        if(ret < 0){
            LOG_ERROR("BIND SOCKET ERROR");
            return false;
        }
        return true;
    }
    // 3.监听
    bool Listen(int backlog = MAXLISTEN){
        int ret = listen(_sockfd, backlog);
        if(ret < 0){
            LOG_ERROR("LISTEN SOCKET ERROR");
            return false;
        }
        return true;
    }
    // 4.向服务器发起连接
    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, (struct sockaddr*)&addr, len);
        if(ret < 0){
            LOG_ERROR("CONNECT SOCKET ERROR");
            return false;
        }
        return true;
    }
    
    
public:
    Socket():_sockfd(-1){}
    ~Socket(){Close();}
    Socket(int sockfd): _sockfd(sockfd){}
    // 避免拷贝问题
    Socket(const Socket&) = delete;
    Socket& operator=(const Socket&) = delete;

    int Fd() const {return _sockfd;}


    // 5.获取新连接
    int Accept(){
        int newfd = accept(_sockfd, nullptr, nullptr);
        if(newfd < 0){
            LOG_ERROR("ACCEPT SOCKET ERROR");
            return -1;
        }
        return newfd; // 返回新连接的套接字
    }

    // 6.接收数据
    ssize_t Recv(void *buf, size_t len, int flag = 0){
        ssize_t ret = recv(_sockfd, buf, len, flag);
        // < 0 出错
        // = 0 连接断开
        // > 0 接收成功
        if(ret <= 0){
            // EAGAIN | EWOULDBLOCK: 当前 socket 的非阻塞缓冲区没有数据了
            // EINTR: 当前 socket 的阻塞等待被信号中断
            // ECONNRESET: 连接重置
            // ENOTCONN: 套接字未连接
            // ETIMEDOUT: 接收超时
            if(errno == EAGAIN || errno == EINTR){
                return 0; // 表示: 这次发送没有发送成功, 需重试
            }
            LOG_ERROR("Recv SOCKET %s" , strerror(errno));
            return -1; // 其他错误
        }    
        return ret; // 实际接收长度
    }
    ssize_t NonBlockRecv(void *buf, size_t len){
        return Recv(buf, len, MSG_DONTWAIT); // MSG_DONTWAIT 表示当前接收为非阻塞。
    }

    // 7.发送数据
    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){
                return 0; // 表示: 这次发送没有发送成功, 需重试
            }
            LOG_ERROR("SEND SOCKET %s" , strerror(errno));
            return -1; // 其他错误 
        }
        return ret;
    }
    ssize_t NonBlockSend(const void* buf, size_t len){
        if(len == 0) return 0;
        return Send(buf, len, MSG_DONTWAIT); // MSG_DONTWAIT 表示当前发送为非阻塞
    }

    // 8.关闭套接字
    void Close(){
        if(_sockfd != -1){
            close(_sockfd);
            _sockfd = -1;
        }
    }

    // 9.设置套接字选项 -- 开启地址端口重用
    void ReuseAddr() {
        int opt = 1;
        // SO_REUSEADDR: 允许重用本地地址和端口
        // SO_REUSEPORT: 允许重用本地端口
        setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR, (void*)&opt, sizeof(opt));
        opt = 1;
        setsockopt(_sockfd, SOL_SOCKET, SO_REUSEPORT, (void*)&opt, sizeof(opt));
    }

    // 10.设置套接字阻塞属性 -- 设置为非阻塞
    void NonBlock(){
        int flag = fcntl(_sockfd, F_GETFL, 0);
        if(flag == -1){
            LOG_ERROR("GET SOCKET FLAG ERROR");
            return;
        }
        int ret = fcntl(_sockfd, F_SETFL, flag | O_NONBLOCK);
        if(ret < 0){
            LOG_ERROR("SET SOCKET NONBLOCK ERROR");
            return;
        }
    }

    // 9. 创建一个服务器连接
    bool CreateServer(uint16_t port, const std::string &ip = "0.0.0.0", bool nonblock_flag = false){
        if(!Create()) return false;
        if(nonblock_flag) NonBlock(); // 设置非阻塞
        ReuseAddr(); // 设置地址端口重用

        if(!Bind(ip, port)) return false;
        if(!Listen()) return false;
        return true;
    }   
    // 10. 创建一个客户端连接
    bool CreateClient(uint16_t port, const std::string &ip){
        // 1. 创建套接字 2. 连接服务器
        if(!Create()) return false;
        if(!Connect(ip, port)) return false;
        return true;
    }

private:
    int _sockfd;
};

2. 代码检测

server.cpp

int main()
{
    Socket lst_sock;
    lst_sock.CreateServer(8080);

    while(1){
        int newfd = lst_sock.Accept();
        if(newfd < 0){
            continue;
        }   
        Socket cli_sock(newfd);
        char buf[1024] = {0};
        int ret = cli_sock.Recv(buf, 1023);
        if(ret < 0){
            cli_sock.Close();
            continue;
        }
        cli_sock.Send(buf, ret);
        cli_sock.Close();
    }
    lst_sock.Close();
    return 0;
}

client.cpp

int main()
{
    Socket cli_sock;
    cli_sock.CreateClient(8080, "127.0.0.1");

    cli_sock.Send("Hello IsLand", strlen("Hello IsLand"));
    char buf[1024] = {0};
    cli_sock.Recv(buf, 1023);
    LOG_DEBUG("recv data: %s", buf);
    cli_sock.Close();
    return 0;
}

结果如下:

lighthouse@VM-8-10-ubuntu:Test1$ ./client
2025-05-02 10:48:41 [tcp_cli.cc:11] recv data: Hello IsLand

3. 细节处理

细节1:处理 Recv 函数时, errno 的来源以及 为啥不用 EWOULDBLOCK

errno 的来源

  • errnoC标准库 中定义的全局变量(线程安全环境下由 __errno_location() 实现),用于存储系统调用或库函数失败时的错误码。

  • recv 返回 -1 时,表示发生错误,具体的错误原因会通过 errno 变量传递(如 EAGAIN, EINTR 等)

    if (ret <= 0) {
        if (errno == EAGAIN || errno == EINTR) {
            return 0; // 表示非致命错误,继续尝试接收
        }
    }
    
  • 注意recv 返回 -1 时,错误码需通过 errno 获取,而非直接从 ret 的值推断

EAGAINEWOULDBLOCK 的关系

  • Linux 系统 中,EAGAINEWOULDBLOCK 的值是相同的(均为 11),定义在 /usr/include/asm-generic/errno-base.h 中:

    #define EAGAIN          11      /* Try again */
    #define EWOULDBLOCK     EAGAIN  /* Operation would block */
    
细节2:MSG_DONWAIT 的概述

NonBlockRecvNonBlockSend函数中,使用了 MSG_DONTWAIT 标志来实现 非阻塞接收 (Non-blocking Receive),这是网络编程中一种常见的异步通信机制。以下是对其工作原理和用途的详细解析:

  • MSG_DONTWAITrecv 系统调用的一个标志位,用于 临时启用非阻塞模式
  • 它的作用是:即使当前套接字(socket)本身是阻塞模式(默认行为),也会让本次 recv 调用立即返回,而不是等待数据到达。
  • 如果此时接收缓冲区中没有数据,recv 会返回 -1,并将 errno 设置为 EAGAINEWOULDBLOCK(两者等价),表示“暂时无数据,稍后再试”
细节3:关于 ReuseAddr()
  • SO_REUSEADDR安全复用,允许同一地址和端口被多个套接字绑定(常用于快速重启服务)
  • SO_REUSEPORT多进程共享端口,允许多个套接字绑定到完全相同的地址和端口(需所有套接字均设置此选项),用于负载均衡
  • 建议 :若无需多进程/线程共享端口,仅保留 SO_REUSEADDR

那么之前 我们说过如下:

  • 在 TCP/IP 协议中,一个 IP 地址和端口组合(即 socket 地址)默认情况下只能被一个 socket 绑定 ,这是为了防止多个进程同时监听同一个端口,造成数据混乱。但在某些特殊场景下,我们确实需要“复用”端口,这就引入了 SO_REUSEADDRSO_REUSEPORT 选项。

📌 为什么默认不允许端口复用?
  • 每个 TCP/UDP 连接由五元组唯一标识:{协议, 源IP, 源端口, 目的IP, 目的端口}
  • 其中,服务器监听的 socket 地址(即 bind() 的地址)决定了它能接收哪些连接。如果多个 socket 绑定到相同的地址和端口,系统将无法判断哪个 socket 应该处理新连接,从而导致冲突。

因此,默认情况下,系统禁止两个 socket 绑定到相同的地址和端口(之前在这篇【Linux网络#2】: Socket 编程 就提过一个端口号只能被一个进程占用,但是一个进程能够绑定多个端口)

SO_REUSEADDR:允许“安全”的端口复用

✅ 用途

  • 快速重启服务 :当服务意外崩溃或正常关闭后,TCP 连接可能仍处于 TIME_WAIT 状态(通常持续 2MSL,约 60 秒),此时端口仍被占用。
  • 避免“Address already in use”错误 :通过设置 SO_REUSEADDR,可以让服务在 TIME_WAIT 状态期间重新绑定到端口。

🧠 原理

  • SO_REUSEADDR 允许绑定到已被其他 socket 使用的地址,但前提是:
    • 该 socket 已关闭(即不再有活跃连接)。
    • 或者该 socket 也设置了 SO_REUSEADDR
  • 内核会检查当前是否有活跃连接,如果没有,则允许复用。

SO_REUSEPORT:允许多个 socket 同时绑定到相同地址和端口

✅ 用途

  • 负载均衡 :多个进程或线程可以同时监听相同的地址和端口,内核负责将连接均匀分配给它们。
  • 高并发场景 :适用于需要并行处理大量连接的服务(如 Web 服务器)。

🧠 原理

  • 多个 socket 可以绑定到相同的地址和端口,但所有 socket 必须都设置 SO_REUSEPORT
  • 内核会使用一种机制(如哈希算法)将连接请求分发给各个 socket。

⚠️ 注意事项

  • SO_REUSEPORT 是 Linux 3.9+ 引入的特性,旧版本系统不支持。
  • 不同 socket 之间的负载均衡策略依赖内核实现,不同系统行为可能不同。

🧩 为什么 SO_REUSEADDR 能绕过“一个端口只能被一个进程占用”?

虽然 TCP/IP 协议规定一个 socket 地址只能被一个 socket 绑定,但 SO_REUSEADDR 是一个“例外规则”,它允许在特定条件下复用地址。

✅ 条件如下:

  1. 原 socket 已关闭 :即没有活跃连接。
  2. 新 socket 设置了 SO_REUSEADDR
  3. 原 socket 也设置了 SO_REUSEADDR (可选)。

在这种情况下,内核认为复用是“安全”的,不会导致连接混乱,因此允许绑定。


🧠 举个例子:服务重启时的 TIME_WAIT 问题

这个情况之前在 【Linux网络#11】: 传输层协议 TCP 四次挥手的内容下也提过

假设你写了一个 TCP 服务器,监听在 0.0.0.0:8080。当你关闭服务器后立即重启,可能会遇到如下错误:

bind: Address already in use

这是因为:

  • 关闭连接后,TCP 连接进入 TIME_WAIT 状态(持续 2MSL,约 60 秒),以确保所有残留数据包都被丢弃。
  • 在此期间,端口仍被占用,系统不允许新 socket 绑定。

解决办法 :在 bind() 之前设置 SO_REUSEADDR,这样即使端口仍处于 TIME_WAIT 状态,也可以绑定成功。


🧾小结
特性SO_REUSEADDRSO_REUSEPORT
是否允许多个 socket否(默认)
是否需要所有 socket 设置
用途快速重启服务多进程/线程负载均衡
是否破坏唯一性否(仅在安全条件下复用)是(允许多个 socket 监听相同地址)
是否跨平台高度支持Linux 3.9+
是否影响连接分发是(内核分发连接)

✅ 推荐使用方式

  • 普通服务重启 :使用 SO_REUSEADDR,避免 TIME_WAIT 导致的绑定失败。
  • 高并发负载均衡 :使用 SO_REUSEPORT,多个进程/线程共享端口,提升性能。
  • 避免滥用 :除非明确需要,否则不要同时设置两者,防止行为不可预测。

虽然 TCP/IP 协议规定一个端口只能被一个 socket 绑定,但 SO_REUSEADDRSO_REUSEPORT 提供了“例外”机制,分别用于 安全复用多进程共享端口


细节4:宏污染

由于最开始的时候,我的日志实现代码 和 测试代码都用了相同的 局部变量 char buf

  • 日志定义了一个局部变量 char buf[64],与 server.cppclient.cpp 中的 char buf[1024] 同名但作用域不同
  • 虽然从语法上看,宏中的 buf 是局部变量,不会影响外部的 buf,但在某些编译器或特定优化条件下,栈内存的布局可能会导致 buf 被意外覆盖
// LOG_DEBUG 宏定义
char buf[64]; // 与 server.cpp 和 client.cpp 中的 char buf[1024] 同名

当客户端调用 LOG_DEBUG("recv data: %s", buf) 时,宏展开后会生成一个 char buf[64],用于存储时间戳字符串。

如果编译器在栈上分配内存时,将宏内的 buf 和外部的 buf 紧邻存放,printf 的格式化输出可能会溢出到外部的 buf ,导致接收到的数据被覆盖为时间戳字符串。

这样就导致我输出了如下的数据结果:

lighthouse@VM-8-10-ubuntu:Test1$ ./client
2025-05-02 10:45:38 [tcp_cli.cc:11] recv data: 2025-05-02 10:45:38

四、Channel 类设计

目的:对描述符的监控事件管理

功能

  1. 事件管理:描述符是否可读写,对描述符的监控可读可写,解除 事件 监控
  2. 事件触发后处理的管理
    1. 需要处理的事件:可读,可写,挂断,错误,任意
    2. 事件处理的回调函数

成员:

  • epoll 进行事件监控
    • EPoollIN:可读
    • EPoollOUT:可写
    • EPoollRDHUP:连接断开
    • EPoollPRI:优先数据
    • EPoollERR:出错
    • EPoollHUP:挂断

1. 代码实现

class Channel{
public:
    using EventCallback = std::function<void()>; // 注意: 这里不能放在 private 中, 否则会报错

    explicit Channel(int fd):_fd(fd),_events(0),_revents(0){} // 显式调用 

    ~Channel(){
        if (_fd != -1) {
            close(_fd);
            _fd = -1; // 避免重复关闭
        }
    }

    int Fd() const {return _fd;}
    uint32_t Events() const {return _events;}

    // 判断当前事件是否可读写
    bool ReadAble() const { return (_events & EPOLLIN);  }
    bool WriteAble() const { return (_events & EPOLLOUT);}
    
    // 设置回调函数
    void SetReadCallback(const EventCallback& cb){ _read_callback = cb;}
    void SetWriteCallback(const EventCallback& cb){_write_callback = cb;}
    void SetCloseCallback(const EventCallback& cb){_close_callback = cb;}
    void SetErrorCallback(const EventCallback& cb){_error_callback = cb;}
    void SetEventCallback(const EventCallback& cb){_event_callback = cb;}
    /* 监控事件开关 -- 进行事件监控连接后, 描述符就绪事件, 设置实际就绪事件*/
    void SetREvents(uint32_t events){ _revents = events;}
    

    /* 开启事件监控 */
    void EnableRead(){ _events |= EPOLLIN;}
    void EnableWrite(){ _events |= EPOLLOUT;}

    /* 关闭事件监控 */
    void DisableRead(){ _events &= ~EPOLLIN;}
    void DisableWrite(){ _events &= ~EPOLLOUT;} 
    void DisableAll(){_events = 0;} // 关闭所有事件

    /* 后面调用 Poller 和 EventLoop 接口来移除事件监控 */
    void Remove(){} // 移除事件
    void Update(){} // 更新事件
    
    void HandleEvent(){ // 处理事件, 判断连接触发了什么事件  
        // 实际项目中, 连接断开并不会直接先断开, 还是看到是否有数据可读, 先读完数据或者发送出错再断开
        if((_revents & EPOLLIN) || (_revents & EPOLLRDHUP) || (_revents & EPOLLPRI)){
            // 不管任何事件, 都会调用的回调函数
            if(_event_callback) _event_callback();
            if(_read_callback) _read_callback();
        }
        else if(_revents & EPOLLOUT){
            // 不管任何事件, 都会调用的回调函数
            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();
       	}
    }
private:
    int _fd; // 事件对应的文件描述符
    uint32_t _events; // 当前需要监控的事件
    uint32_t _revents; // 当前连接触发的事件

    EventCallback _read_callback;   // 读事件回调
    EventCallback _write_callback;  // 写事件回调
    EventCallback _close_callback;  // 关闭事件回调
    EventCallback _error_callback;  // 错误事件回调
    EventCallback _event_callback;  // 任意事件回调
};

2. 细节处理

细节1:在 HandleEvent 函数中使用 if-else if 结构而非多个独立的 if

使用 if-else if 是为了保障资源安全、明确事件优先级 ,并避免因同时处理多个事件导致的未定义行为

① 事件优先级和互斥性

  • 错误和挂起事件的优先级更高
    EPOLLERR(错误)和 EPOLLHUP(挂起)通常表示连接出现严重问题(如对端关闭、网络中断),需要立即处理
    如果这些事件与读写事件同时发生,应优先处理错误/挂起事件,避免在无效连接上继续执行读写操作
  • 互斥处理:某些事件的处理逻辑可能互斥。例如:
    • 写事件(EPOLLOUT)处理可能触发连接关闭,导致后续的错误/挂起事件处理访问已释放的对象
    • 错误/挂起事件处理通常直接终止连接,无需再处理读写

② 资源安全:避免访问已释放对象

  • 写事件处理可能释放连接
    在注释中明确指出:“有可能会释放连接的操作事件,一次只处理一个”
    若写事件的回调(如 _write_callback)中关闭了连接(如 close(fd) 或删除 Channel 对象),后续的错误/挂起事件处理若继续执行,可能导致访问已释放资源 (如空指针、无效文件描述符)
  • 使用 else if 阻断后续逻辑
    通过 else if 结构,确保一旦处理了写事件,错误/挂起事件将不再被处理,从而避免潜在的资源管理问题

③ 逻辑清晰和可维护

  • 明确事件处理顺序if-else if 结构清晰表达了事件处理的优先级顺序 ,使代码更易理解和维护
  • 避免冗余判断:若多个事件同时发生(如 EPOLLOUT | EPOLLERR),使用 else if 可避免重复判断和执行无关逻辑,提升效率。

那么什么时候可以使用独立的 if 呢 ???

若多个事件的处理逻辑相互独立且不会导致对象销毁或状态变化 ,可以使用独立的 if 语句,例如

if (_revents & EPOLLIN)  { /* 读事件 */ }
if (_revents & EPOLLOUT) { /* 写事件 */ }

此时,即使同时触发读和写事件,两者都会被处理,且不会互相干扰

五、Poller 模块实现

意义:通过 epoll 实现对描述符的 IO 事件监控

功能

  1. 添加 / 修改描述符的事件监控(不存在则添加,存在则修改)
  2. 移除描述符的事件监控

封装思想

  1. 必须拥有一个 epoll 的操作句柄
  2. 拥有一个 struct epoll_event 的结构数组,监控时保存所有的活跃事件
  3. 使用 hash 表管理描述符与描述符对应的事件管理 Channel 对象
  4. 逻辑流程
    • 对描述符进行监控,通过 Channel 才能知道描述符需要监控的事件
    • 当描述符就绪了,通过描述符在 hash 表中找到对应的 Channel(得到了 Channel 才能什么事件如何处理)
    • 当描述符就绪了,返回就绪描述符对应的 Channel

1. 代码实现

#define MAX_EPOLLEREVENTS 1024
class Poller{
private:
    // 对 epoll 的之间操作
    void Update(Channel* channel, int op){
        // int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
        int fd = channel->Fd();
        struct epoll_event event;
        event.data.fd = fd; // 事件对应的文件描述符
        event.events = channel->Events(); // 需要监控的事件
        int ret = epoll_ctl(_epfd, op, fd, &event);
        if(ret < 0){
            LOG_ERROR("EPOLL_CTL ERROR");
        }
        return ;
    }

    // 判断一个 Channel 是否已经添加了 事件监控
    bool HasChannel(Channel* channel){
        return _channels.find(channel->Fd()) != _channels.end();
    }

public:
    Poller(){
        // _epfd = epoll_create(MAX_EPOLLEREVENTS); // 这个已经过时
        _epfd = epoll_create1(EPOLL_CLOEXEC); // 创建 epoll 文件描述符
        if(_epfd < 0){
            LOG_ERROR("EPOLL_CREATE ERROR");
            abort(); // 退出程序
        }
    }

    Poller(const Poller&) = delete;
    Poller& operator=(const Poller&) = delete;

    // 添加 / 修改 监控事件
    void UpdateEvent(Channel* channel){
        bool ret = HasChannel(channel);
        if(!ret){
            // 不存在, 添加事件
            _channels.insert(std::make_pair(channel->Fd(), channel)); // 添加到映射表
            return Update(channel, EPOLL_CTL_ADD); 
        }
        return Update(channel, EPOLL_CTL_MOD); // 已经存在, 修改事件
    }

    void RemoveEvent(Channel* channel){
        auto it = _channels.find(channel->Fd());
        if(it != _channels.end()){
            _channels.erase(it); // 从映射表中删除
        }
        Update(channel, EPOLL_CTL_DEL); // 删除事件
    }

    // 开始监控, 返回活跃连接
    void Poll(std::vector<Channel*> *active){
        // int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
        int nfds = epoll_wait(_epfd, _events, MAX_EPOLLEREVENTS, -1);
        if(nfds < 0){
            if(errno == EINTR){
                return; // 被信号中断
            }
            LOG_ERROR("EPOLL_WAIT ERROR: %s
", strerror(errno));
            abort(); // 退出程序
        }
        // 遍历活跃连接
        for(int i = 0; i < nfds; ++i){ 
            int fd = _events[i].data.fd;
            auto it = _channels.find(fd);
            assert(it != _channels.end()); // 断言: 事件一定存在

            Channel* channel = it->second;
            channel->SetREvents(_events[i].events); // 设置当前事件
            active->push_back(channel); // 添加到活跃连接列表
        }
    }

private:
    int _epfd; // epoll 文件描述符
    struct epoll_event _events[MAX_EPOLLEREVENTS]; // epoll 事件数组
    std::unordered_map<int, Channel*> _channels; // fd -> Channel 映射表
};

2. 细节处理

细节1:epoll 是水平触发(LT)还是边缘触发(ET)?区别是什么?
  • 答案 :默认是 LT,LT 在数据未处理完时会持续通知;ET 仅在状态变化时通知一次,需配合非阻塞 I/O 使用
细节2:Poll 方法返回的活跃事件是如何处理的?
  • 答案 :遍历 epoll_wait 返回的事件,填充到 active 列表中,并设置 Channel 的 _revents
细节3:Poller 是否支持多线程同时调用 Poll 方法?
  • 答案 :不支持,需通过锁或每个线程使用独立的 epoll 实例(如 Reactor 模式)
细节3:epoll_wait 的超时时间为何设置为 -1?是否合理?
  • 答案 :-1 表示无限等待,适合服务器模型;但需根据业务需求调整,如设置超时处理定时任务

3. 与 Channel 的整合测试

由于我们有些东西,需要 Poller 来结合测试,所以对 Channel 模块也需要做一些修改,如下:

class Poller;

class Channel{
public:
    using EventCallback = std::function<void()>; // 注意: 这里不能放在 private 中, 否则会报错
    explicit Channel(Poller*poller, int fd):_fd(fd),_events(0),_revents(0),_poller(poller){} // 显式调用 

    ~Channel(){
        if (_fd != -1) {
            close(_fd);
            _fd = -1; // 避免重复关闭
        }
    }

    int Fd() const {return _fd;}
    uint32_t Events() const {return _events;}

    // 判断当前事件是否可读写
    bool ReadAble() const { return (_events & EPOLLIN);  }
    bool WriteAble() const { return (_events & EPOLLOUT);}
    
    // 设置回调函数
    void SetReadCallback(const EventCallback& cb) {_read_callback = cb;}
    void SetWriteCallback(const EventCallback& cb){_write_callback = cb;}
    void SetCloseCallback(const EventCallback& cb){_close_callback = cb;}
    void SetErrorCallback(const EventCallback& cb){_error_callback = cb;}
    void SetEventCallback(const EventCallback& cb){_event_callback = cb;}
    /* 监控事件开关 -- 进行事件监控连接后, 描述符就绪事件, 设置实际就绪事件*/
    void SetREvents(uint32_t events){ _revents = events;}
    

    /* 开启事件监控 */
    void EnableRead(){ _events |= EPOLLIN; Update();}
    void EnableWrite(){ _events |= EPOLLOUT; Update();}

    /* 关闭事件监控 */
    void DisableRead(){ _events &= ~EPOLLIN; Update();}
    void DisableWrite(){ _events &= ~EPOLLOUT; Update();} 
    void DisableAll(){_events = 0; Update();} // 关闭所有事件

    /* 后面调用 EventLoop 接口来移除事件监控 */
    void Remove(); // 移除事件
    void Update(); // 更新事件
    
    void HandleEvent(){ // 处理事件, 判断连接触发了什么事件  
        // 实际项目中, 连接断开并不会直接先断开, 还是看到是否有数据可读, 先读完数据或者发送出错再断开
        if((_revents & EPOLLIN) || (_revents & EPOLLRDHUP) || (_revents & EPOLLPRI)){
            // 不管任何事件, 都会调用的回调函数
            if(_event_callback) _event_callback();
            if(_read_callback) _read_callback();
        }
        else if(_revents & EPOLLOUT){
            // 不管任何事件, 都会调用的回调函数
            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();
        }
    }
private:
    int _fd; // 事件对应的文件描述符
    uint32_t _events; // 当前需要监控的事件
    uint32_t _revents; // 当前连接触发的事件
    Poller* _poller; // 事件循环

    EventCallback _read_callback;   // 读事件回调
    EventCallback _write_callback;  // 写事件回调
    EventCallback _close_callback;  // 关闭事件回调
    EventCallback _error_callback;  // 错误事件回调
    EventCallback _event_callback;  // 任意事件回调
};


void Channel::Remove(){return _poller->RemoveEvent(this); } // 移除事件
void Channel::Update(){return _poller->UpdateEvent(this);} // 更新事件

测试代码如下:

server.cpp

#include "../../source/server.hpp"

void HandleClose(Channel *channel){
    std::cout << "HandleClose: " << channel->Fd() << std::endl;
    channel->Remove(); // 移除事件
    delete channel; // 释放内存
}

void HandleRead(Channel *channel){
    int fd = channel->Fd();
    char buf[1024] = {0};
    ssize_t ret = recv(fd, buf, 1023, 0);
    if(ret <= 0){
        return HandleClose(channel); // 关闭事件
    }
    std::cout << buf << std::endl;
    channel->EnableWrite(); // 启动可写事件
}

void HandleWrite(Channel *channel){
    int fd = channel->Fd();
    const char *data = "I miss You";
    ssize_t ret = send(fd, data, strlen(data), 0);
    if(ret < 0){
        return HandleClose(channel); // 关闭事件
    }
    channel->DisableWrite(); // 关闭可写事件
}

void HandleError(Channel *channel){
    return HandleClose(channel); 
}
void HandlEvent(Channel *channel){
    std::cout << "有了一个事件" << std::endl;
}

void Acceptor(Poller* poller, Channel *lst_channel)
{
    int fd = lst_channel->Fd();
    int newfd = accept(fd, nullptr, nullptr);
    if(newfd < 0) return;

    Channel *channel = new Channel(poller, newfd);
    channel->SetReadCallback(std::bind(HandleRead, channel));   // 为通信套接字设置可读事件回调函数
    channel->SetWriteCallback(std::bind(HandleWrite, channel)); // 可写事件的回调函数
    channel->SetCloseCallback(std::bind(HandleClose, channel)); // 关闭事件的回调函数
    channel->SetErrorCallback(std::bind(HandleError, channel)); // 错误事件的回调函数
    channel->SetEventCallback(std::bind(HandlEvent, channel));  // 任意事件的回调函数

    channel->EnableRead(); // 监听读事件 
}

int main()
{
    Poller poller;
    Socket lst_sock;
    lst_sock.CreateServer(8080);

    // 为监听套接字, 创建一个 Channel 进行事件的管理及处理
    Channel channel(&poller, lst_sock.Fd());
    channel.SetReadCallback(std::bind(Acceptor, &poller, &channel)); // 设置监听套接字的可读事件回调函数
    channel.EnableRead();

    while(1){
        std::vector<Channel*> actives;
        poller.Poll(&actives); // 开始监控, 返回活跃连接
        for(auto& a: actives){
            a->HandleEvent(); // 处理事件
        }
    }
    lst_sock.Close();
    return 0;
}

client.cpp

#include "../../source/server.hpp"

int main()
{
    Socket cli_sock;
    cli_sock.CreateClient(8080, "127.0.0.1");

    while(1){
        std::string str = "Hello IsLand";
        cli_sock.Send(str.c_str(), str.size());
        char buf[1024] = {0};
        cli_sock.Recv(buf, 1023);
        LOG_DEBUG("%s", buf);
        sleep(1);
    }
    return 0;
}

结果如下

lighthouse@VM-8-10-ubuntu:Test2$ ./client
2025-05-04 21:54:10 [tcp_cli.cc:13] I miss You
2025-05-04 21:54:11 [tcp_cli.cc:13] I miss You
^C

lighthouse@VM-8-10-ubuntu:Test2$ ./server
有了一个事件
Hello IsLand
有了一个事件
有了一个事件
Hello IsLand
有了一个事件
有了一个事件
HandleClose: 5

六、EventLoop 模块实现

1. 关于 evenfd 函数

eventfd 是 Linux 提供的一种轻量级的进程间通信(IPC)机制,用于在进程或线程之间传递事件通知。它通过一个文件描述符来实现计数器的功能,支持读写操作,适合用于事件通知或信号量的实现

1.1 函数概述
#include 

int eventfd(unsigned int initval, int flags);
  • initval: 初始化计数器的值(uint64_t 类型)。这是 eventfd 的初始计数值。

  • flags:
    用于设置文件描述符的行为,常见的标志包括:

    • EFD_CLOEXEC: 设置 close-on-exec 标志,表示在调用 exec 系列函数时自动关闭该文件描述符,禁止进程复制
    • EFD_NONBLOCK: 设置非阻塞模式,读写操作不会阻塞。
    • EFD_SEMAPHORE: 启用信号量语义(每次读取时计数器减 1,而不是清零)

返回值

  • 成功时返回一个文件描述符(efd),可以通过 readwrite 操作与 eventfd 交互(注意read&write 进行 IO 的时候数据只能是 一个 8 字节数据 )

  • 失败时返回 -1,并设置 errno 以指示错误原因。

功能

  1. 写入计数器

    • 使用 writeeventfd 写入一个 uint64_t 值,计数器会累加该值。
    • 如果计数器的值超过 UINT64_MAX,会返回错误 EOVERFLOW
  2. 读取计数器

    • 使用 readeventfd 读取一个 uint64_t 值:
      • 如果未设置 EFD_SEMAPHORE,读取操作会返回计数器的当前值,并将计数器清零。
      • 如果设置了 EFD_SEMAPHORE,每次读取会返回 1,并将计数器减 1。
    • 如果计数器为 0 且未设置 EFD_NONBLOCKread 会阻塞;如果设置了 EFD_NONBLOCK,则返回 -1 并设置 errnoEAGAIN

1.2 代码示例
#include 
#include 
#include 
#include 
#include 

int main()
{
    int efd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
    if(efd < 0){
        perror("eventfd");
        return -1;
    }

    uint64_t val = 1;
    write(efd, &val, sizeof(val));
    write(efd, &val, sizeof(val));
    write(efd, &val, sizeof(val));

    uint64_t res = 0;
    read(efd, &res, sizeof(res));
    printf("res = %lu
", res);

    close(efd);
    return 0;
}

// 结果如下:
lighthouse@VM-8-10-ubuntu:eventfd$ ./ev
res = 3

分析如下

int efd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
  • 创建一个 eventfd,初始计数器值为 0
  • 设置了 EFD_CLOEXECEFD_NONBLOCK
    • EFD_CLOEXEC: 在调用 exec 系列函数时自动关闭文件描述符。
    • EFD_NONBLOCK: 使读写操作非阻塞。

uint64_t val = 1;
write(efd, &val, sizeof(val));
write(efd, &val, sizeof(val));
write(efd, &val, sizeof(val));
  • eventfd 写入 1 三次,计数器的值累加为 3

uint64_t res = 0;
read(efd, &res, sizeof(res));
printf("res = %lu
", res);
  • eventfd 读取计数器的值,res 被设置为 3,同时计数器清零。

  • 输出结果为:

    res = 3
    

close(efd);
  • 关闭 eventfd 文件描述符,释放资源。
1.3 使用场景及注意事项

常见用途

  1. 线程间同步

    • 使用 eventfd 实现生产者-消费者模型或信号量机制。
  2. 事件通知

    • 在多线程或多进程环境中,用于通知某些事件的发生。
  3. epoll 配合

    • eventfd 文件描述符加入 epoll,用于事件驱动的程序中。

注意事项

  1. 计数器溢出

    • 如果计数器的值超过 UINT64_MAXwrite 会返回错误 EOVERFLOW
  2. 非阻塞模式

    • 如果设置了 EFD_NONBLOCK,在计数器为 0 时调用 read 会返回 -1 并设置 errnoEAGAIN
  3. 信号量模式

    • 如果设置了 EFD_SEMAPHORE,每次读取会返回 1,并将计数器减 1,而不是清零

2. Eventloop 模块概述

Eventloop:进行事件监控,以及事件处理的模块(关键点:这个模块和线程是一一对应的)

  • 监控了一个连接,而这个连接一旦就绪,就要进行事件处理。但是如果这个描述符,在多个线程中都触发了事件,进行处理,就会存在线程安全问题
  • 因此我们需要将一个连接的事件监控,以及连接事件处理,以及其他操作都放在同一个线程中进行

如何保证一个连接的所有操作都在 eventloop 对应的线程中

  • 解决方案:给 eventloop 模块中,添加一个任务队列,对连接的所有操作,都进行一次封装,将对连接的操作并不直接执行,而是当作任务添加到任务队列中

eventloop 处理流程:

  1. 在线程中对描述符进行事件监控
  2. 有描述符就绪则对描述符进行事件处理(如何保证处理回调函数中的操作都在线程中)
  3. 所有的就绪事件处理完了,这时候再去将任务队列中的所有任务–执行

事件监控

  1. 事件监控:使用 Poller 模块,有事件就绪则进行事件处理
  2. 执行任务队列中的任务:一个线程安全的任务队列

注意:由于有可能因为等待描述符 IO 事件就绪,导致执行流流程阻塞,这时候任务队列中的任务得不到指向

  • 因此需要需要有一个事件通知的东西,能够唤醒事件监控的阻塞

代码实现

class EventLoop{
public:
    void RunAllTask(){
        // 在加锁期间取出所有任务, 给锁限定作用域
        std::vector<Functor> tasks;
        {
            std::lock_guard<std::mutex> lock(_mutex); // 加锁
            tasks.swap(_tasks); // 交换任务池, 取出所有任务
        }
        for(auto &t: tasks){
            t(); // 执行任务
        }
        return ;
    }

    
    static int CreateEventfd(){
        int efd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
        if(efd < 0){
            LOG_ERROR("CREATE EVENTFD ERROR");
            abort(); // 退出程序
        }
        return efd;
    }

    void ReadEventFd(){
        uint64_t data = 0;
        ssize_t ret = read(_event_fd, &data, sizeof(data));
        if(ret < 0){
            if(errno == EAGAIN || errno == EINTR){
                return; // 没有数据可读
            }
            LOG_ERROR("READ EVENTFD ERROR");
            abort(); // 退出程序
        }
        return ;
    }
    // 唤醒事件循环
    void WakeupEventFd(){
        uint64_t data = 1;
        ssize_t ret = write(_event_fd, &data, sizeof(data));
        if(ret < 0){
            if(errno == EAGAIN || errno == EINTR){
                return; // 没有数据可读
            }
            LOG_ERROR("WRITE EVENTFD ERROR");
            abort(); // 退出程序
        }
        return ;
    }
public:
    using Functor = std::function<void()>;
    
    EventLoop()
    :   _thread_id(std::this_thread::get_id()), // 获取当前线程 ID
        _event_fd(CreateEventfd()), // 创建 eventfd 唤醒 IO 事件监控
        _event_channel(new Channel(this, _event_fd)) // 创建事件循环的 Channel
    {
        //给eventfd添加可读事件回调函数,读取eventfd事件通知次数
        _event_channel->SetReadCallback(std::bind(&EventLoop::ReadEventFd, this));
        _event_channel->EnableRead(); // 设置可读事件
    }

    // 判断当前线程是否是 EventLoop 中对应线程
    bool IsInLoop() {return _thread_id == std::this_thread::get_id();}

    // 修改/添加 描述符的事件监控
    void UpdateEvent(Channel* channel){
        assert(IsInLoop()); // 断言: 当前线程是事件循环线程
        _poller.UpdateEvent(channel); // 修改/添加事件监控
    }

    void RemoveEvent(Channel* channel){
        assert(IsInLoop()); 
        _poller.RemoveEvent(channel); // 移除事件监控
    }

    // 事件监控->就绪事件处理->执行任务
    void Start(){
        // 1, 事件监控
        std::vector<Channel*> actives; // 活跃连接
        _poller.Poll(&actives); // 进行事件监控
        // 2, 事件处理
        for(auto &channel: actives){
            channel->HandleEvent(); // 处理事件
        }
        // 3, 执行任务
        RunAllTask(); // 执行任务
    }

    // 压入任务队列
    void QueueInLoop(const Functor& cb){
        {
            std::lock_guard<std::mutex> lock(_mutex); // 加锁
            _tasks.emplace_back(cb); // 压入任务
        }
        // 唤醒事件循环 -- 由于没有事件就绪 导致的 epoll 阻塞
        // 其实就是给 eventfd 写入一个数据, 使得 epoll 事件就绪
        WakeupEventFd();
    }

    // 判断要执行任务是否处于当前线程, 如果是则执行, 不是则压入队列
    void RunInLoop(const Functor& cb){
        if(IsInLoop()){
            cb();
        }else{
            QueueInLoop(cb);
        }
    }


private:    
    std::thread::id _thread_id; // 事件循环线程 ID
    int _event_fd; // eventfd 唤醒 IO 事件监控可能导致的阻塞
    // 注意: 这里的 Channel用智能指针进行管理, Poller 使用的对象
    std::unique_ptr<Channel> _event_channel; // 事件循环的 Channel
    Poller _poller; // 进行所有描述符的事件监控

    std::vector<Functor> _tasks; // 任务池
    std::mutex _mutex; // 互斥锁
};

// 注意: 这里的 Channel 类也要做一些改变, 类似于 Poller 模块的处理改变
void Channel::Remove(){return _loop->RemoveEvent(this); } // 移除事件
void Channel::Update(){return _loop->UpdateEvent(this);} // 更新事件

3. 与 TimeWheel 模块整合

由于我们需要用到我们之前所说的 TimeWheel 模块,并且对其做一些改变

  • 将定时器任务与事件循环绑定 :确保定时器回调在 EventLoop 线程中执行,避免线程安全问题
  • 利用事件驱动机制:通过 timerfd 触发定时任务,与 epoll 事件监控无缝结合
  • 支持任务的添加、刷新、取消和周期性执行

① TimerWheel 与 EventLoop 的绑定

  • TimerWheel 构造函数

    TimerWheel(EventLoop *loop)
        : _capacity(60), _tick(0), _loop(loop),
          _timerfd(CreateTimerfd()), 
          _timer_channel(new Channel(_loop, _timerfd)) {
        _wheel.resize(_capacity);
        _timer_channel->SetReadCallback(std::bind(&TimerWheel::OnTime, this));
        _timer_channel->EnableRead(); // 启动读事件监控
    }
    
    • 绑定关系 :TimerWheel 依赖于一个 EventLoop 实例,所有定时任务的执行都通过该 EventLoop 的线程完成
    • 事件驱动 :通过 timerfd 的可读事件触发定时任务处理(OnTime

② 定时器任务的执行流程

  • OnTime 函数

    void RunTimerTask() {
        _tick = (_tick + 1) % _capacity;
        auto& tasks = _wheel[_tick];
        for (auto& task : tasks) {
            if (!task->_canceled) {
                task->_cb(); // 执行回调
            }
            task->_release(); // 释放资源
        }
        tasks.clear(); // 清空当前 tick 的任务
    }
    
    • 触发机制 :当 timerfd 被触发时,OnTime 会被调用,读取超时次数并依次处理每个 tick 的任务。

    • 线程安全 :OnTimeChannel 的读事件回调,由 EventLoop 的线程调用,确保所有定时任务在事件循环线程中执行。

  • RunTimerTask 函数

    void RunTimerTask() {
        _tick = (_tick + 1) % _capacity;
        auto& tasks = _wheel[_tick];
        for (auto& task : tasks) {
            if (!task->_canceled) {
                task->_cb(); // 执行回调
            }
            task->_release(); // 释放资源
        }
        tasks.clear(); // 清空当前 tick 的任务
    }
    
    • 任务执行 :遍历当前 tick 的所有任务,执行回调函数。
    • 资源管理 :通过 _release 删除 TimerWheel 中保存的任务映射,避免内存泄漏。

③ 定时器任务的添加与刷新

  • 添加任务

    void TimerAddInLoop(uint64_t id, uint32_t delay, const TaskFunc& cb) {
        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] = WeakTask(pt); // 保存任务映射
    }
    
    • 任务封装 :使用 shared_ptr 管理任务生命周期,weak_ptr 避免循环引用。
    • 位置计算 :根据当前 tick 和延迟时间 delay 计算任务在轮子中的位置。
  • 刷新任务

    void TimerRefreshInLoop(uint64_t id) {
        auto it = _timers.find(id);
        if (it == _timers.end()) return;
        PtrTask pt = it->second.lock();
        if (!pt) return;
        int remaining = pt->DelayTime();
        int pos = (_tick + remaining) % _capacity;
        _wheel[pos].push_back(pt); // 重新插入任务
    }
    
  • 重新插入 :将任务移动到新的 tick 位置,实现延迟效果。

④ 线程安全保证

  • 任务队列机制

    • QueueInLoop 方法 :所有对 Channel 或定时器的操作都通过 EventLoop::QueueInLoop 提交到任务队列。
    • RunInLoop 方法 :确保操作在事件循环线程中执行。
    • WakeupEventFd :通过写入 eventfd 唤醒阻塞的 epoll_wait,及时处理任务队列。
  • 定时器回调的线程一致性

    • TimerTask 析构函数 :

      ~TimerTask() {
          if (!_canceled) _cb(); // 直接执行回调
          _release();
      }
      

      关键点_cb() 的执行必须在 EventLoop 线程中,通过 TimerWheel::OnTime 触发,无需额外线程同步

代码整合示例
class EventLoop {
public:
    // 添加定时器任务
    void AddTimer(uint64_t id, uint32_t delay, const TaskFunc &cb) {
        return _timer_wheel.AddTimer(id, delay, cb);
    }

    // 刷新定时器
    void RefreshTimer(uint64_t id) {
        return _timer_wheel.RefreshTimer(id);
    }

    // 取消定时器
    void CancelTimer(uint64_t id) {
        return _timer_wheel.CancelTimer(id);
    }

private:
    TimerWheel _timer_wheel; // 定时器轮
};

TimerWheel 类关键函数

class TimerWheel {
public:
    // 添加定时任务(供 EventLoop 调用)
    void AddTimer(uint64_t id, uint32_t delay, const TaskFunc& cb) {
        _loop->RunInLoop(std::bind(&TimerWheel::TimerAddInLoop, this, id, delay, cb));
    }

    // 刷新定时任务
    void RefreshTimer(uint64_t id) {
        _loop->RunInLoop(std::bind(&TimerWheel::TimerRefreshInLoop, this, id));
    }

    // 取消定时任务
    void CancelTimer(uint64_t id) {
        _loop->RunInLoop(std::bind(&TimerWheel::TimerCanceInLoop, this, id));
    }

private:
    // 实际添加任务的逻辑
    void TimerAddInLoop(uint64_t id, uint32_t delay, const TaskFunc& cb) {
        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] = WeakTask(pt);
    }

    // 实际刷新任务的逻辑
    void TimerRefreshInLoop(uint64_t id) {
        auto it = _timers.find(id);
        if (it == _timers.end()) return;
        PtrTask pt = it->second.lock();
        if (!pt) return;
        int remaining = pt->DelayTime();
        int pos = (_tick + remaining) % _capacity;
        _wheel[pos].push_back(pt);
    }

    // 实际取消任务的逻辑
    void TimerCanceInLoop(uint64_t id) {
        auto it = _timers.find(id);
        if (it == _timers.end()) return;
        PtrTask pt = it->second.lock();
        if (pt) pt->Cancel();
    }

    // 移除任务
    void RemoveTimer(uint64_t id) {
        auto it = _timers.find(id);
        if (it != _timers.end()) _timers.erase(it);
    }

    // 执行定时任务
    void RunTimerTask() {
        _tick = (_tick + 1) % _capacity;
        auto& tasks = _wheel[_tick];
        for (auto& task : tasks) {
            if (!task->_canceled) task->_cb(); // 执行回调
            task->_release(); // 释放资源
        }
        tasks.clear(); // 清空当前 tick 的任务
    }

    // 定时器事件回调
    void OnTime() {
        int times = ReadTimerfd();
        for (int i = 0; i < times; ++i) {
            RunTimerTask(); // 执行定时任务
        }
    }

    // 创建 timerfd
    static int CreateTimerfd() {
        int timerfd = timerfd_create(CLOCK_MONOTONIC, 0);
        if (timerfd < 0) {
            LOG_ERROR("Create timerfd error");
            abort();
        }
        struct itimerspec itime;
        itime.it_value.tv_sec = 1;
        itime.it_value.tv_nsec = 0;
        itime.it_interval.tv_sec = 1;
        itime.it_interval.tv_nsec = 0;
        timerfd_settime(timerfd, 0, &itime, nullptr);
        return timerfd;
    }

    // 读取 timerfd
    int ReadTimerfd() {
        uint64_t times = 0;
        ssize_t ret = read(_timerfd, &times, sizeof(times));
        if (ret < 0) {
            LOG_ERROR("READ TIMERFD ERROR");
            abort();
        }
        return times;
    }

private:
    using WeakTask = std::weak_ptr<TimerTask>;
    using PtrTask = std::shared_ptr<TimerTask>;
    const int _capacity;
    int _tick;
    int _timerfd;
    std::unique_ptr<Channel> _timer_channel;
    EventLoop* _loop;
    std::vector<std::vector<PtrTask>> _wheel; // 定时器轮
    std::unordered_map<uint64_t, WeakTask> _timers; // ID 映射
};

4. 代码测试

server.cpp

#include "../../source/server.hpp"

void HandleClose(Channel *channel){
    LOG_DEBUG("close fd: %d", channel->Fd());
    channel->Remove(); // 移除事件
    delete channel; // 释放内存
}

void HandleRead(Channel *channel){
    int fd = channel->Fd();
    char buf[1024] = {0};
    ssize_t ret = recv(fd, buf, 1023, 0);
    if(ret <= 0){
        return HandleClose(channel); // 关闭事件
    }
    LOG_DEBUG("Read: %s", buf);
    channel->EnableWrite(); // 启动可写事件
}

void HandleWrite(Channel *channel){
    int fd = channel->Fd();
    const char *data = "I miss You";
    ssize_t ret = send(fd, data, strlen(data), 0);
    if(ret < 0){
        return HandleClose(channel); // 关闭事件
    }
    channel->DisableWrite(); // 关闭可写事件
}

void HandleError(Channel *channel){
    return HandleClose(channel); 
}
void HandlEvent(EventLoop* loop, Channel *channel, uint64_t timerid){
    loop->RefreshTimer(timerid); // 刷新定时器
}

void Acceptor(EventLoop* loop, Channel *lst_channel)
{
    int fd = lst_channel->Fd();
    int newfd = accept(fd, nullptr, nullptr);
    if(newfd < 0) return;

    uint64_t timerid = rand() % 1000; 
    Channel *channel = new Channel(loop, newfd);
    channel->SetReadCallback(std::bind(HandleRead, channel));   // 为通信套接字设置可读事件回调函数
    channel->SetWriteCallback(std::bind(HandleWrite, channel)); // 可写事件的回调函数
    channel->SetCloseCallback(std::bind(HandleClose, channel)); // 关闭事件的回调函数
    channel->SetErrorCallback(std::bind(HandleError, channel)); // 错误事件的回调函数
    channel->SetEventCallback(std::bind(HandlEvent, loop, channel, timerid));  // 任意事件的回调函数


    // 非活跃连接的超时释放操作 -- 5s 后关闭
    // 注意: 定时销毁任务必须在启动读事件之前, 因为读事件会启动可写事件, 但这个时候还没有任务
    loop->AddTimer(timerid, 5, std::bind(HandleClose, channel));
    channel->EnableRead(); // 监听读事件 

}

int main()
{
    srand(time(nullptr)); // 随机数种子
    EventLoop loop;
    Socket lst_sock;
    lst_sock.CreateServer(8080);

    // 为监听套接字, 创建一个 Channel 进行事件的管理及处理
    Channel channel(&loop, lst_sock.Fd());
    channel.SetReadCallback(std::bind(Acceptor, &loop, &channel)); // 设置监听套接字的可读事件回调函数
    channel.EnableRead();

    while(1){
        loop.Start(); // 事件循环
    }
    lst_sock.Close();
    return 0;
}

client.cpp

#include "../../source/server.hpp"

int main()
{
    Socket cli_sock;
    cli_sock.CreateClient(8080, "127.0.0.1");

    for(int i = 0; i < 3; ++i){
        std::string str = "Hello IsLand";
        cli_sock.Send(str.c_str(), str.size());
        char buf[1024] = {0};
        cli_sock.Recv(buf, 1023);
        LOG_DEBUG("%s", buf);
        sleep(1);
    }
    while(1) sleep(1);
    return 0;
}

结果如下

lighthouse@VM-8-10-ubuntu:Test4$ ./client
2025-05-05 22:53:49 [tcp_cli.cc:13] I miss You
2025-05-05 22:53:50 [tcp_cli.cc:13] I miss You
2025-05-05 22:53:51 [tcp_cli.cc:13] I miss You
^C
lighthouse@VM-8-10-ubuntu:Test4$ ./client
2025-05-05 22:54:00 [tcp_cli.cc:13] I miss You
2025-05-05 22:54:01 [tcp_cli.cc:13] I miss You
2025-05-05 22:54:02 [tcp_cli.cc:13] I miss You
^C
    
lighthouse@VM-8-10-ubuntu:Test4$ ./server
2025-05-05 22:53:49 [tcp_srv.cc:16] Read: Hello IsLand
2025-05-05 22:53:50 [tcp_srv.cc:16] Read: Hello IsLand
2025-05-05 22:53:51 [tcp_srv.cc:16] Read: Hello IsLand
2025-05-05 22:53:55 [tcp_srv.cc:4] close fd: 7
2025-05-05 22:54:00 [tcp_srv.cc:16] Read: Hello IsLand
2025-05-05 22:54:01 [tcp_srv.cc:16] Read: Hello IsLand
2025-05-05 22:54:02 [tcp_srv.cc:16] Read: Hello IsLand
2025-05-05 22:54:04 [tcp_srv.cc:4] close fd: 7
^C

5. 细节分析

细节1:定时器任务中异步执行回调

由于我之前在实现 TimeWheel 代码是这样写的,如下:

~TimerTask() { 
    if (!_canceled) {
        std::thread(_cb).detach(); // ❌ 异步执行回调
    }
    _release(); 
}

这个写法会导致定时器回调(如 HandleClose在子线程中执行 ,而不是在 EventLoop 所属的线程中执行,然后就出现了如下的问题:

Assertion `IsInLoop()' failed.
Aborted (core dumped)

根本原因 是:在非事件循环线程中调用了 RemoveEventChannel::Remove() ,而 EventLoop 的所有操作都要求必须在事件循环线程中执行(通过 assert(IsInLoop()) 检查)

分析

  1. HandleClose 函数内部调用了:channel->Remove(); // 会调用 EventLoop::RemoveEvent
  2. 此时 RemoveEvent 中有断言:assert(IsInLoop()); // 检查当前线程是否是事件循环线程
  3. 由于回调在子线程中执行,断言失败,程序崩溃
细节2:服务器端关闭再启动的文件描述符(fd)不变

上面我们演示的时候,可以发现,当我们服务器端关闭再启动之后 fd 并没有发生改变,如下:

2025-05-05 22:53:55 [tcp_srv.cc:4] close fd: 7
2025-05-05 22:54:04 [tcp_srv.cc:4] close fd: 7

Linux 系统中,文件描述符的分配遵循 “最小可用原则” ,即:

  • 总是分配当前进程中最小的未被占用的整数 fd
  • 当一个 fd 被关闭后,它会被标记为“可重用”,下次分配新文件或 socket 时会优先使用这些被释放的 fd

分析

  1. 客户端连接流程
  • 每次运行 client,都会创建一个新的 socket,系统返回一个可用的 fd。
  • 由于服务器端在连接关闭时 主动关闭了 socket 并释放了 fd ,因此下一次客户端连接时,系统会优先使用刚刚释放的 fd(例如 7)。
  1. 服务器端处理连接的方式
  • Acceptor 函数中,每当有新连接到来时:

    int newfd = accept(fd, nullptr, nullptr);
    Channel *channel = new Channel(loop, newfd);
    
  • newfd 是系统分配的文件描述符

  • 如果前一个连接的 newfd 刚好是 7,并且已经被关闭(close(7)),那么下一个新连接就会再次分配 7

  1. Channel 的析构行为
  • 你定义的 Channel 类析构函数会 主动关闭 fd

    ~Channel(){
        if (_fd != -1) {
            close(_fd);
            _fd = -1;
        }
    }
    
  • 这意味着每次连接关闭时,Channel 对象被销毁时会调用 close(fd),从而释放该 fd

  • 释放后,系统可以再次分配该 fd 给新的连接

补充(关于这种做法的意义)

  • 无需担心 fd 复用问题 :只要每次连接关闭时正确调用 close(fd),系统会安全地回收和复用 fd
  • 避免 fd 泄漏 :确保所有连接关闭时都正确删除 Channel 对象,防止 fd 被占用不释放
细节3:Channel 类中的 RemoveUpdate 方法为何调用 EventLoop 的接口?

问题本质:模块职责分离
回答要点:

  • 职责分离Channel 仅负责事件注册,Poller 负责底层 I/O 事件监控。

  • 统一管理 :通过 EventLoop 统一管理事件增删改,确保事件状态一致性。

  • 示例代码

    void Channel::Remove() { return _loop->RemoveEvent(this); }
    void Channel::Update() { return _loop->UpdateEvent(this); }
    
细节4:如何避免定时器任务的重复添加?

问题本质:资源泄漏与逻辑错误
回答要点:

  • HasTimer 检查 :在添加定时任务前调用 HasTimer(id) 避免重复。
  • 刷新替代新增 :若定时任务已存在,调用 RefreshTimer(id) 延迟销毁时间。
  • 线程安全 :所有定时器操作通过 EventLoop 串行化执行。

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

搜索文章

Tags

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