【仿Muduo库项目】EventLoop模块

目录

一.EventFd

二.基本功能设计

2.1.设计思路

2.2.代码实现

2.3.测试

三.引入定时任务

3.1.TimerWheel定时器模块

3.2.整合EventLoop模块和TimerWheel模块

3.3.代码总览

3.4.代码测试

四.EventLoop模块和其他模块总结


一.EventFd

eventfd 是 Linux 系统提供的一个轻量级进程间通信与事件通知机制。它的核心设计目标是高效地传递事件信号,而不是传递大量的数据。

核心原理

  • 内核计数器:当你创建一个 eventfd 时 ,内核会为其维护一个简单的、64位的无符号**整数计数器。**这个计数器的初始值可以在创建时指定,通常为 0。
  • 文件描述符:**创建成功后,你会得到一个文件描述符。**这意味着你可以像操作普通文件一样,用 read、write 等系统调用来操作这个计数器,更重要的是,可以将其交给像 select、poll、epoll 这样的 I/O 多路复用 机制来监控。

工作流程

eventfd 的通信模型遵循"生产者-消费者"模式,其工作完全围绕对内核计数器的操作展开:

生产事件(通知) - write:

  • 当一个线程或进程(生产者)需要发出一个事件通知时,它向这个 eventfd 描述符执行 write 操作。写入的内容是一个整数(比如 1)。内核并不会存储你写的这个 1,而是将这个值加到它内部维护的计数器上。
  • 例如,连续 write 三次,每次写入 1,那么内核计数器的值就会从 0 变为 3。
  • 这个操作是原子的,意味着在多线程或多进程同时写入时,计数器的结果是累加正确的。

消费事件(等待) - read:

  • 另一个线程或进程(消费者)在等待事件。它会通过 read 来读取这个 eventfd 描述符。
  • **如果此时内核计数器大于 0,read 操作会成功返回当前计数器的值,并紧接着将计数器的值重置为 0。**这是关键的一点:read 操作会"取走"并清空所有累积的事件计数。
  • 如果此时内核计数器等于 0,默认情况下,read 操作会阻塞,直到有其他执行体通过 write 让计数器大于 0 为止。

与 I/O 多路复用配合:

  1. 这才是 eventfd 威力最大的地方。消费者不需要不停地调用 read 来查询(忙等待),而是可以把这个 eventfd 的文件描述符注册到 epoll 等机制中。
  2. 只要内核计数器从 0 变为大于 0(即发生了 write),epoll 就会检测到该描述符变为可读。
  3. 消费者线程只需要阻塞在 epoll_wait 调用上。当 epoll 返回并告知 eventfd 可读时,消费者再去 read,此时 read 会立即成功并返回计数值。

这种方式效率极高,消费者线程可以完全休眠,直到有真正的事件发生时才被唤醒。

接口介绍

复制代码
int eventfd(unsigned int initval, int flags);

函数功能

创建一个用于事件通知的文件描述符(eventfd)。

参数详解

  1. initval - 初始计数器值
  • 类型:unsigned int(无符号32位整数)
  • 作用:设置 eventfd 内部计数器的初始值
  1. flags - 控制标志

控制 eventfd 行为的标志,可以是以下一个或多个标志的按位或(|):

主要标志:

EFD_CLOEXEC

  • 作用:设置"执行时关闭"(Close-on-exec)
  • 解释:如果设置了这个标志,当进程执行 exec 系列函数(执行新程序)时,这个 eventfd 文件描述符会被自动关闭
  • 目的:防止 eventfd 被无意间继承到新程序中,造成资源泄漏或意外行为

EFD_NONBLOCK

  • 作用:设置非阻塞模式
  • 解释:
  • 如果不设置此标志(默认阻塞模式):当计数器为0时,read 操作会阻塞,直到有数据可读
  • 如果设置此标志:当计数器为0时,read 操作会立即返回 -1,并设置 errno 为 EAGAIN
  • 目的:避免在 read 时永久阻塞,适合需要轮询的场景

EFD_SEMAPHORE

  • 作用:启用信号量模式
  • 解释:
  • 默认模式(不设置此标志):read 会返回计数器的当前值,然后将计数器重置为0
  • 信号量模式(设置此标志):read 每次只返回1,并将计数器减1(如果计数器大于0)

注意了:

写入规则:

  • 每次 write() 必须写入一个64位整数(uint64_t),也就是必须是8字节

读取规则:

  • 每次 read() 会读取一个64位整数
  • 读取的缓冲区必须至少为8字节(64位)

示例

我们先看看阻塞版本的

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/eventfd.h>
#include <stdint.h>  // 为了 uint64_t

int main() {
    // 1. 创建一个eventfd,初始计数器为0,使用默认阻塞模式
    int efd = eventfd(0, 0);
    if (efd == -1) {
        perror("eventfd");
        return 1;
    }
    
    printf("创建 eventfd 成功,文件描述符: %d\n", efd);
    
    // 2. 写入事件通知
    uint64_t write_value = 1;//注意eventfd写入的数据必须是8字节的,uint64_t刚好是8字节的
    ssize_t write_result = write(efd, &write_value, sizeof(write_value));
    if (write_result != sizeof(write_value)) {
        perror("write");
        close(efd);
        return 1;
    }
    printf("写入事件通知成功,写入值: %lu\n", write_value);
    
    //现在内核计数器已经是1了

    // 3. 再写入一次
    write_result = write(efd, &write_value, sizeof(write_value));
    printf("再次写入事件通知成功,写入值: %lu\n", write_value);
    
    //现在内核计数器已经是2了

    // 4. 读取事件计数
    uint64_t read_value = 0;//注意eventfd读取的数据必须是8字节的,uint64_t刚好是8字节的
    ssize_t read_result = read(efd, &read_value, sizeof(read_value));
    if (read_result != sizeof(read_value)) {
        perror("read");
        close(efd);
        return 1;
    }
    printf("读取事件计数成功,读取值: %lu\n", read_value);
    printf("说明:读取到了 %lu 个事件通知\n", read_value);
    
    // 5. 再次读取(此时计数器应为0,会阻塞)
    printf("尝试再次读取(当前计数器应为0)...\n");
    printf("由于计数器为0,read会阻塞,需要Ctrl+C结束程序\n");
    
    read_result = read(efd, &read_value, sizeof(read_value));
    if (read_result != sizeof(read_value)) {
        perror("read");
    } else {
        printf("读取到值: %lu\n", read_value);
    }
    
    close(efd);
    return 0;
}

我们看看非阻塞版本的

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/eventfd.h>
#include <stdint.h>
#include <errno.h>

int main() {
    // 使用非阻塞模式创建eventfd
    int efd = eventfd(0, EFD_NONBLOCK);
    if (efd == -1) {
        perror("eventfd");
        return 1;
    }
    
    printf("创建非阻塞 eventfd 成功,文件描述符: %d\n", efd);
    
    // 尝试读取(计数器为0,非阻塞模式会立即返回错误)
    uint64_t read_value = 0;//注意eventfd读取的数据必须是8字节的,uint64_t刚好是8字节的
    ssize_t read_result = read(efd, &read_value, sizeof(read_value));
    
    if (read_result == -1 && errno == EAGAIN) {
        printf("计数器为0,非阻塞读取立即返回EAGAIN错误\n");
    }
    
    // 写入一个事件
    uint64_t write_value = 1;//注意eventfd写入的数据必须是8字节的,uint64_t刚好是8字节的
    write(efd, &write_value, sizeof(write_value));
    printf("写入事件通知成功\n");
    
    // 再次读取
    read_result = read(efd, &read_value, sizeof(read_value));
    if (read_result == sizeof(read_value)) {
        printf("成功读取事件计数: %lu\n", read_value);
    }
    
    // 再次尝试读取(现在计数器又为0了)
    read_result = read(efd, &read_value, sizeof(read_value));
    if (read_result == -1 && errno == EAGAIN) {
        printf("再次尝试读取,计数器为0,返回EAGAIN\n");
    }
    
    close(efd);
    return 0;
}

还是很容易理解的吧!!

二.基本功能设计

2.1.设计思路

EventLoop(事件循环)模块是一个与线程一一对应的事件监控与处理中心。

其主要职责是监控一组连接(或文件描述符)上的事件,并在事件就绪时执行相应的处理逻辑。

为了保证线程安全,该模块需要确保同一个连接上的所有操作都在其所属的EventLoop所在的线程中执行,避免因跨线程访问同一连接而引发的竞态条件。

关键设计点

  • EventLoop 与线程绑定,每个线程独立运行一个 EventLoop 实例。

  • 一个连接应始终由同一个 EventLoop(及对应线程)负责监控与处理。

  • 如果同一连接在多个线程中同时触发事件并进行处理,会引入线程安全问题。

解决方案:任务队列机制

为了确保连接的所有操作都在绑定线程中执行,我们在 EventLoop 中引入一个任务队列

**所有针对该连接的操作(如发送数据、调整监听事件等)都不直接执行,而是封装成任务并投递到该 EventLoop 的任务队列中。**EventLoop 会在每轮循环的合适阶段,顺序执行队列中的所有任务,从而保证这些操作发生在正确的线程上下文中。

EventLoop 处理流程

  1. 事件监控:通过 I/O 多路复用机制(如 epoll)监听注册的描述符。

  2. 事件处理 :当有描述符就绪时,调用预先注册的回调函数进行处理。在回调函数中,若涉及该连接的写操作或其他非即时完成的任务,并不直接执行 I/O,而是将其封装为任务插入任务队列。

  3. 执行队列任务当所有就绪事件处理完毕后,EventLoop 从任务队列中依次取出任务并执行,确保这些操作仍在当前线程中进行。

线程安全保证

  • 对任务队列的操作(添加任务、提取任务)可能发生在多个线程(例如其他线程希望向该连接发送数据),因此需要对任务队列的访问加锁,以保证其线程安全。

  • 由于事件监控、回调执行及任务执行都在同一个 EventLoop 线程中串行进行,因此对连接本身的状态修改不存在并发冲突,无需额外同步。

示例:发送数据的流程

当某个就绪事件的处理回调中需要向连接发送数据时:

  1. 不直接调用 send(),而是将"发送数据"这个操作封装为一个任务。

  2. 将该任务插入到当前 EventLoop 的任务队列中。

  3. 在本轮事件处理完成后,EventLoop 从任务队列取出该任务并真正执行 send() 操作。

这样,无论事件来源于哪个线程,最终对连接进行读写、修改状态等操作都集中在与其绑定的单个线程中,从而杜绝了多线程并发操作同一连接导致的线程安全问题。

问题一:任务处理不及时

在之前的设计中存在一个潜在问题:当 EventLoop 在 epoll_wait 等调用中阻塞等待 I/O 事件时,如果此时有其他线程向该 EventLoop 的任务队列中投递了新任务,这些任务将无法被及时执行,必须等到有 I/O 事件发生、线程从阻塞中唤醒后才能得到处理。这种延迟可能导致响应不及时,甚至在某些情况下引发逻辑错误或性能下降。

为了解决这一问题,我们引入 eventfd 作为线程间事件通知机制。它允许我们主动唤醒阻塞在 I/O 多路复用调用中的 EventLoop,使其能够及时处理任务队列中的待执行操作。

为什么需要唤醒机制?

  • EventLoop 的主循环通常会在 epoll_wait(或类似函数)处阻塞,直到被监控的描述符上有事件发生。
  • 如果任务队列在此时被放入新任务,而 EventLoop 线程仍在阻塞,那么这些任务必须等待下一个事件发生才能被执行。
  • 为了让任务能够被即时调度,就需要一种方式主动中断 epoll_wait 的阻塞,让 EventLoop 立刻回到循环中执行队列任务。

解决方案:使用 eventfd 进行唤醒

  1. 创建 eventfd

    在 EventLoop 初始化时,创建一个 eventfd 文件描述符。它是一个轻量级的通知机制,通过写入一个 8 字节整数来触发可读事件。

  2. 将其加入监控

    将该 eventfd 的描述符注册到 EventLoop 的 epoll 实例中,监听其可读事件。

  3. 唤醒操作

    当其他线程需要向该 EventLoop 投递任务时:

    • 先将任务加入队列(需加锁)。

    • 然后向这个 eventfd 写入一个数值(例如 1),这会立即触发该 eventfd 的可读事件。

  4. EventLoop 被唤醒后的处理

    • epoll_waiteventfd 可读而返回。这个时候主程序就不会阻塞在这里了,而是转而去处理就绪事件了。

    • EventLoop 在处理就绪事件时,会读到这个 eventfd,执行其对应的回调(通常只是读取并丢弃其中的数值,以清空状态)。这个时候EventLoop就会去执行任务队列里面的任务了

    • 随后,EventLoop 进入任务执行阶段,将队列中的所有任务取出并执行。

简短一点来说:

问题:EventLoop在等待IO事件时阻塞,导致新加入队列的任务不能及时执行。

解决方案:利用eventfd作为唤醒信号,当有任务加入队列时,向eventfd写入数据,使得eventfd变为可读,从而唤醒阻塞的EventLoop,让其立即处理任务。

问题二:任务队列的使用条件

在 EventLoop 的设计中,一个核心原则是:对于一个连接的任何操作,都必须在管理这个连接的 EventLoop 所属的线程中执行。这是保证该连接状态不被多线程并发访问、避免竞态条件的根本方法。

为了实现这一目标,我们引入了任务队列 作为操作的缓冲区。但在实际执行时,并非所有操作都需要无条件地放入队列等待。系统可以根据操作发起方所在的线程进行智能判断,以兼顾线程安全与执行效率。

操作执行策略:按线程归属进行分流

具体的执行逻辑遵循以下两条路径:

  1. 操作发起于 EventLoop 绑定线程(可直接执行)

    • 判断条件:当需要处理连接上的读写、修改状态等操作时,如果发起该操作的执行流本身就运行在关联的 EventLoop 线程内(例如,在事件就绪的回调函数中触发的后续操作),那么它已经满足了"在正确线程中"的前提条件。

    • 执行方式:此时,操作可以直接、同步地执行,无需经过任务队列中转。这避免了不必要的任务封装、入队和调度开销,提升了关键路径上的处理性能。

  2. 操作发起于其他线程(必须入队异步执行)

    • 判断条件:当操作请求来自其他线程(例如,业务逻辑线程收到指令,要求向某个连接发送数据),则发起方处于"错误"的线程上下文。

    • 执行方式 :必须立即将该操作封装为一个任务,安全地放入该 EventLoop 对应的任务队列中。随后,通过之前提到的 eventfd 等唤醒机制通知目标 EventLoop。EventLoop 将在其线程中,于完成当前轮次的事件处理后,从队列中取出并执行该任务,从而保证操作在正确的线程中安全执行。

2.2.代码实现

cpp 复制代码
class EventLoop {
private:
    // 定义任务函数类型,无参数无返回值
    using Functor = std::function<void()>;
    
    std::thread::id _thread_id;            // 记录当前EventLoop所属的线程ID
    int _event_fd;                         // eventfd文件描述符,用于线程间事件通知,唤醒阻塞的事件循环
    std::unique_ptr<Channel> _event_channel; // Channel对象,管理_event_fd的事件监听,记录
    Poller _poller;                        // 内部封装了epoll,用于监控所有描述符的事件
    std::vector<Functor> _tasks;           // 任务队列,存储需要在EventLoop线程中执行的任务
    std::mutex _mutex;                     // 互斥锁,保证任务队列操作的线程安全性

public:
    // 执行任务队列中的所有任务
    void RunAllTask();
    
    // 静态方法:创建eventfd文件描述符
    static int CreateEventFd();
    
    // 读取eventfd:读取事件通知次数,并清空计数器
    void ReadEventfd();
    
    // 向eventfd写入数据,用于唤醒事件循环
    void WeakUpEventFd();

public:
    // 构造函数:初始化EventLoop
    EventLoop();
    
    // 事件循环主函数:三步走 - 事件监控 -> 就绪事件处理 -> 执行任务
    void Start();
    
    // 判断当前线程是否是EventLoop所在的线程
    bool IsInLoop();
    
    // 断言当前线程是EventLoop所在的线程,如果不是则终止程序
    void AssertInLoop();
    
    // 运行任务:如果当前线程是EventLoop线程,直接执行;否则将任务加入队列
    void RunInLoop(const Functor &cb);
    
    // 将任务加入任务队列(线程安全)
    void QueueInLoop(const Functor &cb);
    
    // 添加或修改描述符的事件监控
    void UpdateEvent(Channel *channel);
    
    // 移除描述符的事件监控
    void RemoveEvent(Channel *channel);
};

我们很快就能写出来

cpp 复制代码
#pragma once
#include"poller.hpp"
#include <iostream>
#include <vector>
#include <string>
#include <cassert>
#include <cstring>
#include <ctime>
#include <functional>
#include <unordered_map>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <memory>
#include <typeinfo>
#include <fcntl.h>
#include <signal.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <sys/eventfd.h>
#include <sys/timerfd.h>


class EventLoop {
private:
    // 定义任务函数类型,无参数无返回值
    using Functor = std::function<void()>;
    
    std::thread::id _thread_id;            // 记录当前EventLoop所属的线程ID
    int _event_fd;                         // eventfd文件描述符,用于线程间事件通知,唤醒阻塞的事件循环
    std::unique_ptr<Channel> _event_channel; // Channel对象,管理_event_fd的事件监听,记录
    Poller _poller;                        // 内部封装了epoll,用于监控所有描述符的事件
    std::vector<Functor> _tasks;           // 任务队列,存储需要在EventLoop线程中执行的任务
    std::mutex _mutex;                     // 互斥锁,保证任务队列操作的线程安全性

public:
    // 执行任务队列中的所有任务
    void RunAllTask() {
        std::vector<Functor> functor;  // 临时vector,用于存储待执行的任务
        {
            // 加锁保护任务队列,防止并发访问
            std::unique_lock<std::mutex> _lock(_mutex);
            // 使用swap交换任务,减少锁持有时间,同时清空原任务队列
            _tasks.swap(functor);//将需要在EventLoop线程中执行的任务全部转存到临时的任务队列里面,这样子会清空原队列
        }
        // 遍历所有临时vector,然后执行所有任务
        for (auto &f : functor) {
            f();  // 调用任务函数
        }
        return;
    }
    
    // 静态方法:创建eventfd文件描述符
    static int CreateEventFd() {
        // 创建eventfd,使用EFD_CLOEXEC和EFD_NONBLOCK标志
        // EFD_CLOEXEC: 执行exec系函数时自动关闭描述符
        // EFD_NONBLOCK: 设置为非阻塞模式
        int efd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
        if (efd < 0) {
            ERR_LOG("CREATE EVENTFD FAILED!!");
            abort();  // 创建失败则终止程序
        }
        return efd;
    }
    
    //eventfd读事件回调函数
    // 读取eventfd:读取事件通知次数,并清空计数器
    void ReadEventfd() {
        uint64_t res = 0;//evectfd读取的数据大小必须是8字节,而uint64_t就是8字节
        // 读取8字节的数据,表示事件通知的次数
        int ret = read(_event_fd, &res, sizeof(res));
        if (ret < 0) {
            // EINTR: 被信号打断;EAGAIN: 表示无数据可读(非阻塞模式)
            if (errno == EINTR || errno == EAGAIN) {
                return;
            }
            ERR_LOG("READ EVENTFD FAILED!");
            abort();  // 读取失败则终止程序
        }
        // 成功读取,res包含了自上次读取以来的事件通知次数
        // 读取后eventfd的内部计数器会被重置为0,这个时候eventfd会从可读变成不可读
        return;
    }
    
    // 向eventfd写入数据,这个就会触发eventfd的可读事件,用于唤醒事件循环
    void WeakUpEventFd() {
        uint64_t val = 1;  // 写入值1,表示一个事件通知
        //evectfd写入的数据大小必须是8字节,而uint64_t就是8字节
        int ret = write(_event_fd, &val, sizeof(val));
        if (ret < 0) {
            // EINTR: 被信号打断
            if (errno == EINTR) {
                return;
            }
            ERR_LOG("WRITE EVENTFD FAILED!");  // 注意:这里应该是WRITE错误
            abort();  // 写入失败则终止程序
        }
        // 成功写入,eventfd内部计数器增加1,如果从0变为非0,会触发可读事件
        // 只要内核计数器从 0 变为大于 0(即发生了 write),epoll 就会检测到该描述符变为可读
        return;
    }

public:
    // 构造函数:初始化EventLoop
    EventLoop() : _thread_id(std::this_thread::get_id()),  // 记录当前线程ID
                  _event_fd(CreateEventFd()),              // 创建eventfd
                  _event_channel(new Channel(this, _event_fd)) {  // 创建Channel管理eventfd
        // 设置eventfd的可读事件回调函数
        _event_channel->SetReadCallback(std::bind(&EventLoop::ReadEventfd, this));
        // 启动eventfd的读事件监控
        _event_channel->EnableRead();
    }
    
    // 事件循环主函数:三步走 - 事件监控 -> 就绪事件处理 -> 执行任务
    void Start() {
        while (1) //死循环,一个EventLoop占据一个线程
        {
            // 1. 事件监控:等待事件就绪
            std::vector<Channel *> actives;  // 存储就绪的事件通道
            _poller.Poll(&actives);          // 阻塞等待事件发生,它会将所有就绪好的事件全部存储到actives里面
            
            // 2. 事件处理:处理所有就绪的事件
            for (auto &channel : actives) {
                channel->HandleEvent();      // 根据实际触发的事件类型调用相应的回调函数
            }
            
            // 3. 执行任务:执行任务队列中的所有任务
            RunAllTask();//将任务队列里面所有的任务执行,并清空任务队列
        }
    }
    
    // 判断当前线程是否是EventLoop所在的线程
    bool IsInLoop() {
        return (_thread_id == std::this_thread::get_id());
    }
    
    // 断言当前线程是EventLoop所在的线程,如果不是则终止程序
    void AssertInLoop() {
        assert(_thread_id == std::this_thread::get_id());
    }
    
    // 运行任务:如果当前线程是EventLoop所绑定的那个线程,就直接执行任务,无需添加到任务队列里面;
    //如果当前线程不是EventLoop所绑定的那个线程,就先不执行任务,而是将任务加入任务队列
    void RunInLoop(const Functor &cb) {
        if (IsInLoop()) {
            // 如果当前线程就是EventLoop线程,直接执行任务
            return cb();
        }
        // 否则,将任务加入任务队列,等待EventLoop线程执行
        return QueueInLoop(cb);
    }
    
    // 将任务加入任务队列(线程安全)
    void QueueInLoop(const Functor &cb) //传递进来的是一个任务
    {
        {
            // 加锁保护任务队列
            std::unique_lock<std::mutex> _lock(_mutex);
            // 将任务添加到任务队列
            _tasks.push_back(cb);
        }
        // 唤醒可能因没有事件就绪而阻塞的epoll
        //EventLoop所属线程可能在epoll_wait那里等待事件就绪,就来不及处理任务
        // 通过向eventfd写入数据,触发eventfd的可读事件,从而唤醒事件循环,这样子这个EventLoop所属线程就会来处理任务队列里面的任务
        WeakUpEventFd();
    }
    
    // 添加或修改描述符的事件监控
    void UpdateEvent(Channel *channel) { 
        return _poller.UpdateEvent(channel); 
    }
    
    // 移除描述符的事件监控
    void RemoveEvent(Channel *channel) { 
        return _poller.RemoveEvent(channel); 
    }
};


void Channel::Remove()
{
    _loop->RemoveEvent(this);
}
void Channel::Update()
{
    _loop->UpdateEvent(this);
}

2.3.测试

我们通过编写一个服务端和一个客户端来进行测试

tcp_srv.cc

cpp 复制代码
#include"../server/socket.hpp"
#include"../server/poller.hpp"
#include"../server/channel.hpp"
#include"../server/eventloop.hpp"

// 处理关闭事件的回调函数
// 参数:channel - 指向要处理的Channel对象的指针
void HandleClose(Channel *channel) {
    // 输出关闭连接的日志,显示文件描述符
    std::cout << "close:" << channel->Fd() << std::endl;
    // 从事件监控器(epoll)中移除该Channel的事件监控
    channel->Remove();
    // 释放Channel对象占用的内存
    delete channel;
}

// 处理读事件的回调函数
// 当文件描述符有数据可读时被调用
void HandleRead(Channel *channel) {
    // 从Channel对象中获取文件描述符
    int fd = channel->Fd();
    // 定义接收缓冲区,初始化为0,大小1024字节
    // 注意:实际上只能接收1023个数据字节,最后一个字节用于字符串结束符'\0'
    char buf[1024] = {0};
    
    // 从套接字接收数据
    // 参数:
    //   fd - 套接字文件描述符
    //   buf - 接收缓冲区
    //   1023 - 最大接收字节数(留一个字节给结束符)
    //   0 - 接收标志,0表示默认阻塞模式(实际上这里是非阻塞模式)
    int ret = recv(fd, buf, 1023, 0);
    
    // 检查接收结果
    if (ret <= 0) {
        return HandleClose(channel);//关闭并释放连接
    }
    // 启用可写事件监控,准备将接收到的数据发送回去
    // 注意:首先别人发了数据过来,就会触发可读事件回调函数(注意我们在前面开启了读事件监控),人家都发数据来了,你必须回应人家吧,
    //当有数据要发送时,才需要启用写事件监控
    channel->EnableWrite();
    // 打印接收到的数据到控制台
    std::cout << buf << std::endl;
}

// 处理写事件的回调函数
void HandleWrite(Channel *channel) {
    int fd=channel->Fd();
    const char* data="天气还不错";
    int ret=send(fd,data,strlen(data),0);
    if(ret<0)
    {
        return HandleClose(channel);//关闭释放
    }
    channel->DisableWrite();//关闭写事件监控
}

// 处理错误事件的回调函数
void HandleError(Channel *channel) {
    return HandleClose(channel);//发生错误,直接关闭释放好吧
}
// 处理任意事件的回调函数
void HandleEvent(Channel *channel) {
    std::cout<<"有了一个事件!!"<<std::endl;
}


//要添加事件监控,肯定需要一个Poller
// 接受新连接的函数
// 参数:poller - 事件轮询器,lst_channel - 监听套接字的Channel
void Acceptor(EventLoop*loop, Channel *lst_channel) {
    // 从监听Channel获取监听套接字文件描述符
    int fd = lst_channel->Fd();
    
    // 接受一个新的客户端连接
    // accept 函数从监听套接字的连接队列中取出一个连接
    // 参数1:监听套接字描述符
    // 参数2、3:设为NULL表示不关心客户端的地址信息
    int newfd = accept(fd, NULL, NULL);
    
    // 如果接受连接失败,直接返回
    if (newfd < 0) { 
        return; 
    }
    
    // 为新的客户端连接创建一个Channel对象
    // Channel封装了文件描述符的事件监控和回调处理
    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(HandleEvent,channel));// 为通信套接字设置任意事件的回调函数

    // 启用读事件监控,开始监听客户端发送数据
    channel->EnableRead();
    
    // 注意:这里创建了动态分配的Channel对象,需要记得在适当的时候释放内存
    // 通常在连接关闭时,在关闭回调函数中删除Channel对象
}

int main() {
    EventLoop loop;

    // 创建一个监听套接字对象,用于接受客户端连接
    Socket lst_sock;
    
    // 创建服务器,监听8500端口,默认绑定到0.0.0.0(所有网络接口)
    // 如果创建失败,程序会返回错误
    lst_sock.CreateServer(8500);

    //为监听套接字,创建一个Channel进行事件的管理,以及事件的处理
    Channel channel(&loop,lst_sock.Fd());
    channel.SetReadCallback(std::bind(Acceptor,&loop,&channel));//设置读回调函数------获取新连接,为新连接创建Channel并添加监控
    channel.EnableRead();//启动读事件监控,这样子才能监听到是否有新连接到来
    
    // 无限循环,持续接受客户端连接
    while (1) {
        loop.Start();
    }
    
    // 退出循环后,关闭监听套接字
    lst_sock.Close();
    
    // 程序结束
    return 0;
}

tcp_cli.cc

cpp 复制代码
#include "../server/socket.hpp"

int main()
{
    // 创建一个客户端套接字对象,用于连接服务器
    Socket cli_sock;

    // 创建客户端连接,连接到本地主机(127.0.0.1)的8500端口
    // 如果连接失败,程序会返回错误(假设CreateClient内部会处理)
    cli_sock.CreateClient(8500, "127.0.0.1");
    while (1)
    {
        // 准备要发送给服务器的消息内容
        std::string str = "hello bitejiuyeke!";

        // 将消息发送给服务器
        // str.c_str() 获取C风格字符串指针,str.size() 获取字符串长度
        cli_sock.Send(str.c_str(), str.size());

        // 定义缓冲区,用于接收服务器返回的数据,初始化为0
        char buf[1024] = {0};

        // 从服务器接收响应数据,最多接收1023字节(留一个位置给字符串结束符)
        cli_sock.Recv(buf, 1023);

        // 打印接收到的服务器响应
        // %s 表示以字符串格式输出
        DBG_LOG("%s", buf);

        sleep(2);
    }

    // 程序结束,返回0表示成功
    return 0;

    // 注意:cli_sock对象在main函数结束时会被自动销毁
    // Socket类的析构函数会自动调用Close()方法关闭套接字
    // 所以这里不需要显式调用cli_sock.Close()
}

没有一点问题。

三.引入定时任务

3.1.TimerWheel定时器模块

还记得我们之前写的时间轮定时器吗:【仿Muduo库项目】基础套件实现-CSDN博客

cpp 复制代码
#include <iostream>
#include <vector>
#include <unordered_map>
#include <cstdint>
#include <functional>
#include <memory>
#include <unistd.h>
 
using TaskFunc = std::function<void()>;    // 定义任务函数类型,注意任务函数必须是返回值为void,然后没有参数的
using ReleaseFunc = std::function<void()>; // 定义释放函数类型,注意释放函数必须是返回值为void,然后没有参数的
 
// 定时器任务类
class TimerTask
{
private:
    uint64_t _id;         // 定时器任务对象ID
    uint32_t _timeout;    // 定时任务的超时时间(以秒为单位),其实也就是定时任务多少秒后执行
    bool _canceled;       // 任务取消标志:false-未取消,true-已取消
    TaskFunc _task_cb;    // 定时器对象要执行的定时任务回调函数,注意任务函数必须是返回值为void,然后没有参数的
    ReleaseFunc _release; // 用于删除TimerWheel中保存的定时器对象信息,注意释放函数必须是返回值为void,然后没有参数的
 
public:
    // 构造函数:初始化定时任务
    TimerTask(uint64_t id, uint32_t delay, const TaskFunc &cb) : _id(id),// 定时器任务对象ID
                                                                 _timeout(delay),// 定时任务的超时时间(以秒为单位)
                                                                 _task_cb(cb),// 定时器对象要执行的定时任务回调函数
                                                                 _canceled(false) {}
 
    // 析构函数:如果任务未被取消,则执行定时任务;总是执行释放函数
    ~TimerTask()
    {
        //析构函数需要完成两件事情
        //1.执行定时任务
        //2.将定时任务从时间轮里面删除掉
        if (_canceled == false)// 如果任务没有被取消
        {               // 将定时任务放到析构函数里面来执行
            _task_cb(); // 执行定时任务回调------由构造函数设置
        }
        _release(); // 执行释放函数,从时间轮中移除自己这个定时任务------由下面的SetRelease函数设置
    }
 
    // 取消定时任务
    void Cancel()
    {
        _canceled = true;
    }
 
    // 设置释放函数------由外界进行设定
    void SetRelease(const ReleaseFunc &cb)
    {
        _release = cb;
    }
 
    // 获取定时任务的超时时间(以秒为单位),其实也就是定时任务多少秒后执行
    uint32_t DelayTime()
    {
        return _timeout;
    }
};
 
// 时间轮类
class TimerWheel
{
private:
    using WeakTask = std::weak_ptr<TimerTask>;  // 弱指针,不增加引用计数
    using PtrTask = std::shared_ptr<TimerTask>; // 共享指针,管理TimerTask生命周期
 
    int _tick;                                      // 当前时间轮的指针位置,表示当前时间,走到哪里就执行哪里的任务
    int _capacity;                                  // 时间轮的容量,即最大延迟时间
    std::vector<std::vector<PtrTask>> _wheel;       // 时间轮数组,每个位置是一个任务列表,每个任务都是std::shared_ptr<TimerTask>
    std::unordered_map<uint64_t, WeakTask> _timers; // 查找表,存储的是<任务ID,定时任务的weak_ptr智能指针>,专门用于查找任务
 
private:
    // 从_timers中移除指定ID的定时器
    void RemoveTimer(uint64_t id)
    {
        auto it = _timers.find(id);
        if (it != _timers.end()) // 如果该定时任务存在于时间轮的查找表,就从查找表里面删除掉
        {
            _timers.erase(it);
        }
    }
 
public:
    // 构造函数:初始化时间轮,默认容量为60(表示最多支持60秒的延迟)
    TimerWheel() : _capacity(60), _tick(0), _wheel(_capacity) {}
 
    // 添加定时任务
    // id: 定时器唯一标识符
    // delay: 延迟时间(秒),定时任务多少秒后执行
    // cb: 定时任务回调函数------由外界来进行设置
    void TimerAdd(uint64_t id, uint32_t delay, const TaskFunc &cb)
    {
        // 创建定时任务对象,由shared_ptr管理
        PtrTask pt(new TimerTask(id, delay, cb));
 
        // 设置释放函数:当TimerTask被销毁时,从时间轮中移除对应ID的指定任务
        pt->SetRelease(std::bind(&TimerWheel::RemoveTimer, this, id));
 
        // 计算任务在时间轮中的位置:(当前时间 + 延迟时间) % 容量
        int pos = (_tick + delay) % _capacity;
        _wheel[pos].push_back(pt); // 将任务添加到对应位置
 
        // 将任务的弱引用存储到_timers中,便于后续查找指定ID的定时任务
        _timers[id] = WeakTask(pt);
    }
 
    // 刷新/延迟定时任务
    void TimerRefresh(uint64_t id)
    {
        // 通过ID查找定时任务
        auto it = _timers.find(id);
        if (it == _timers.end())
        {
            return; // 未找到定时任务,无法刷新
        }
 
        //寻找到了指定ID的任务
        // 使用weak_ptr的lock()方法获取shared_ptr
        PtrTask pt = it->second.lock();
        if (pt!=nullptr)// 如果任务还存在(未被销毁)
        {                                          
            int delay = pt->DelayTime();           // 获取原始延迟时间,也就是获取该任务多少秒后执行
            int pos = (_tick + delay) % _capacity; // 计算新位置
            _wheel[pos].push_back(pt);             // 将任务重新添加到时间轮
            // 注意:旧位置的任务引用仍然存在,但由于有新的引用,不会触发析构
        }
    }
 
    // 取消定时任务
    void TimerCancel(uint64_t id)
    {
        auto it = _timers.find(id);
        if (it == _timers.end())
        {
            return; // 未找到定时任务,无法取消
        }
 
        // 获取定时任务对象并取消它
        PtrTask pt = it->second.lock();
        if (pt!=nullptr)// 如果任务还存在(未被销毁)
        {
            pt->Cancel(); // 设置取消标志,防止执行回调
        }
    }
 
    // 执行定时任务:每秒钟调用一次,相当于秒针向后走一步
    void RunTimerTask()
    {
        _tick = (_tick + 1) % _capacity; // 指针向前移动一步
        // 清空当前位置的任务列表
        // 当vector被清空时,所有shared_ptr被释放
        // 如果没有其他引用,TimerTask对象会被销毁,触发析构函数执行定时任务
        _wheel[_tick].clear();
    }
};

但是这个定时器还是没有很完善

首先,我们需要往我们之前写的这个TimerWheel里面加入我们的Enevtfd相关

cpp 复制代码
#pragma once
#include <iostream>
#include <vector>
#include <unordered_map>
#include <cstdint>
#include <functional>
#include <memory>
#include <unistd.h>
#include"eventloop.hpp"
#include"channel.hpp"

// 定时器任务回调函数类型定义:无参数无返回值的函数对象,用于封装定时任务执行逻辑
using TaskFunc = std::function<void()>;
// 资源释放回调函数类型定义:用于定时器销毁时的资源清理操作
using ReleaseFunc = std::function<void()>;

// 定时器任务类:封装单个定时任务,管理任务执行和资源清理
class TimerTask{
    private:
        uint64_t _id;       // 定时器任务对象的唯一标识ID,用于区分不同定时任务
        uint32_t _timeout;  // 定时任务的超时时间(单位:秒),表示延迟多少秒执行
        bool _canceled;     // 任务取消标志:false-表示任务有效未取消,true-表示任务已取消
        TaskFunc _task_cb;  // 定时器对象要执行的定时任务回调函数,保存用户定义的任务逻辑
        ReleaseFunc _release; // 资源释放回调函数,用于从TimerWheel中删除当前定时器对象信息
    
    public:
        // 构造函数:初始化定时任务对象
        TimerTask(uint64_t id, uint32_t delay, const TaskFunc &cb): 
            _id(id), _timeout(delay), _task_cb(cb), _canceled(false) {}
        
        // 析构函数:对象销毁时自动执行任务和清理资源
        ~TimerTask() { 
            // 如果任务没有被取消,则执行用户定义的任务回调函数
            if (_canceled == false) _task_cb(); 
            // 执行资源释放回调,从TimerWheel中移除本任务
            _release(); 
        }
        
        // 取消定时任务:设置取消标志,阻止任务执行
        void Cancel() { _canceled = true; }
        
        // 设置资源释放回调函数:由TimerWheel调用,绑定清理逻辑
        void SetRelease(const ReleaseFunc &cb) { _release = cb; }
        
        // 获取任务的延迟时间:返回设定的超时秒数
        uint32_t DelayTime() { return _timeout; }
};

// 时间轮定时器类:基于时间轮算法管理多个定时任务,实现高效定时调度
class TimerWheel {
    private:
        // 弱引用智能指针类型:指向TimerTask但不增加引用计数,避免循环引用问题
        using WeakTask = std::weak_ptr<TimerTask>;
        // 共享智能指针类型:管理TimerTask生命周期,自动释放内存
        using PtrTask = std::shared_ptr<TimerTask>;
        
        int _tick;      // 时间轮当前指针位置(秒针),指向当前要处理的任务槽位
        int _capacity;  // 时间轮容量,即最大延迟时间(单位:秒)
        
        // 时间轮核心数据结构:二维向量,外层表示时间槽(秒),内层存储该秒的所有任务
        // 每个任务使用shared_ptr管理,当vector被清空时自动触发TimerTask析构
        std::vector<std::vector<PtrTask>> _wheel;
        
        // 任务查找表:哈希表存储<任务ID, 任务弱引用>,用于快速查找和操作特定任务
        // 使用weak_ptr避免增加引用计数,不影响任务自动释放
        std::unordered_map<uint64_t, WeakTask> _timers;

        EventLoop *_loop;           // 事件循环对象指针,用于集成到事件驱动框架中
        int _timerfd;               // Linux定时器文件描述符,基于timerfd实现高精度定时
        std::unique_ptr<Channel> _timer_channel; // 定时器事件通道,管理timerfd的IO事件
    
    private:
        // 从任务查找表中移除指定ID的定时器
        void RemoveTimer(uint64_t id) {
            auto it = _timers.find(id);
            if (it != _timers.end()) {
                _timers.erase(it);  // 从哈希表中删除任务记录
            }
        }
        
        // 创建Linux定时器文件描述符(timerfd)
        static int CreateTimerfd() {
            // 创建timerfd,使用单调时钟CLOCK_MONOTONIC(不受系统时间调整影响)
            int timerfd = timerfd_create(CLOCK_MONOTONIC, 0);
            if (timerfd < 0) {
                ERR_LOG("TIMERFD CREATE FAILED!"); // 记录错误日志
                abort();  // 创建失败,终止程序运行
            }
            
            // 设置定时器参数:timerfd_settime函数控制定时器行为
            // int timerfd_settime(int fd, int flags, struct itimerspec *new, struct itimerspec *old);
            struct itimerspec itime;
            itime.it_value.tv_sec = 1;     // 首次超时时间:1秒后触发
            itime.it_value.tv_nsec = 0;
            itime.it_interval.tv_sec = 1;  // 后续超时间隔:每1秒触发一次
            itime.it_interval.tv_nsec = 0;
            
            // 启动定时器,flags=0表示相对时间(相对于当前时间)
            timerfd_settime(timerfd, 0, &itime, NULL);
            return timerfd;
        }
        
        // 读取定时器文件描述符,获取自上次读取以来的超时次数
        int ReadTimefd() {
            uint64_t times;  // 存储读取的超时次数,类型为uint64_t(timerfd规范)
            // 由于事件处理延迟,可能累计多次超时,times表示未处理的超时次数
            int ret = read(_timerfd, &times, 8); // 读取8字节数据到times变量
            if (ret < 0) {
                ERR_LOG("READ TIMEFD FAILED!"); // 读取失败记录错误
                abort();  // 读取失败,终止程序运行
            }
            return times;  // 返回超时次数
        }
        
        // 执行定时任务:秒针前进一步,处理当前槽的所有任务
        void RunTimerTask() {
            // 秒针循环前进(取模实现循环时间轮)
            _tick = (_tick + 1) % _capacity;
            // 清空当前槽位的任务列表,shared_ptr释放会触发TimerTask析构,从而执行任务回调
            _wheel[_tick].clear();
        }
        
        // 定时器超时事件回调函数:每次timerfd可读时被调用
        void OnTime() {
            // 读取实际超时次数,处理可能累积的多次超时
            int times = ReadTimefd();
            // 根据超时次数,多次执行时间轮前进操作
            for (int i = 0; i < times; i++) {
                RunTimerTask();
            }
        }
        
        // 在事件循环线程中添加定时任务(线程安全版本)
        void TimerAddInLoop(uint64_t id, uint32_t delay, const TaskFunc &cb) {
            // 创建定时任务对象,使用shared_ptr进行生命周期管理
            PtrTask pt(new TimerTask(id, delay, cb));
            
            // 设置任务的资源释放回调,绑定到当前对象的RemoveTimer方法
            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) {
            // 在任务查找表中查找指定ID的任务
            auto it = _timers.find(id);
            if (it == _timers.end()) {
                return;  // 未找到对应任务,无法刷新
            }
            
            // 通过weak_ptr的lock()方法获取shared_ptr(如果对象还存在)
            PtrTask pt = it->second.lock();
            
            // 获取任务的原始延迟时间
            int delay = pt->DelayTime();
            
            // 基于当前时间指针重新计算任务位置
            int pos = (_tick + delay) % _capacity;
            
            // 将任务重新添加到时间轮的新位置(原任务会在原位置自动析构)
            _wheel[pos].push_back(pt);
        }
        
        // 在事件循环线程中取消定时任务(线程安全版本)
        void TimerCancelInLoop(uint64_t id) {
            // 在任务查找表中查找指定ID的任务
            auto it = _timers.find(id);
            if (it == _timers.end()) {
                return;  // 未找到对应任务,无法取消
            }
            
            // 获取任务共享指针,检查任务是否存在
            PtrTask pt = it->second.lock();
            
            // 如果任务对象还存在,则调用Cancel方法设置取消标志
            if (pt) pt->Cancel();
        }
    
    public:
        // 构造函数:初始化时间轮,绑定到事件循环
        TimerWheel(EventLoop *loop):_capacity(60),      // 默认容量60秒
                                    _tick(0),           // 初始时间指针为0
                                    _wheel(_capacity),  // 初始化时间轮数据结构
                                    _loop(loop),        // 绑定事件循环
                                    _timerfd(CreateTimerfd()), // 创建定时器文件描述符
                                    _timer_channel(new Channel(_loop, _timerfd)) // 创建事件通道
        {
            // 设置定时器通道的读事件回调,绑定到OnTime方法
            _timer_channel->SetReadCallback(std::bind(&TimerWheel::OnTime, this));
            
            // 启用读事件监控,开始接收定时器超时事件
            _timer_channel->EnableRead();
        }
        
        /* 定时器操作接口(需考虑线程安全)*/
        // _timers成员可能被多线程访问,需要确保线程安全
        // 如果不加锁,需要将所有定时器操作放在同一个线程(事件循环线程)中执行
        
        // 添加定时任务
        void TimerAdd(uint64_t id, uint32_t delay, const TaskFunc &cb)
        {
            _loop->RunInLoop(std::bind(&TimerWheel::TimerAddInLoop,this,id,delay,cb));
            //将这个任务转交个EventLoop里面进行处理
            // 运行任务:如果当前线程是EventLoop所绑定的那个线程,就直接执行任务,无需添加到任务队列里面;
            //如果当前线程不是EventLoop所绑定的那个线程,就先不执行任务,而是将任务加入任务队列
            //下面的TimerRefresh和TimerCancel也是一样的道理
        }
        // 刷新/延迟定时任务
        void TimerRefresh(uint64_t id)
        {
             _loop->RunInLoop(std::bind(&TimerWheel::TimerRefreshInLoop,this,id));
        }
        
        // 取消定时任务:外部接口,阻止任务的执行
        void TimerCancel(uint64_t id)
        {
            _loop->RunInLoop(std::bind(&TimerWheel::TimerCancelInLoop,this,id));
        }
        
        // 检查定时任务是否存在:注意此接口非线程安全,仅用于调试或内部使用
        // 应在对应的EventLoop线程内调用,避免多线程竞争问题
        // 这个接口只能在对应的EventLoop线程内调用,不能在其他线程里面执行
        bool HasTimer(uint64_t id) {
            auto it = _timers.find(id);
            if (it == _timers.end()) {
                return false;  // 任务不存在
            }
            return true;  // 任务存在
        }
};

3.2.整合EventLoop模块和TimerWheel模块

首先我们需要知道,我们的EventLoop模块除了普通的任务处理,我们还需要处理一些定时任务

我们需要在上一版EventLoop模块里面加入定时器模块

类成员变量:

cpp 复制代码
TimerWheel _timer_wheel;//定时器模块

构造函数初始化列表中:

cpp 复制代码
_timer_wheel(this)

公共成员函数:

cpp 复制代码
// 定时器模块相关操作
    //id:定时器任务对象的唯一标识ID
    //delay:定时任务的超时时间(单位:秒),表示延迟多少秒执行
    //cb:定时器对象要执行的定时任务回调函数,类型是void()
    void TimerAdd(uint64_t id, uint32_t delay, const TaskFunc &cb)
    {
        return _timer_wheel.TimerAdd(id, delay, cb);//添加定时任务
    }
    void TimerRefresh(uint64_t id)//id:定时器任务对象的唯一标识ID
    {
        return _timer_wheel.TimerRefresh(id);//刷新定时任务
    }
    void TimerCancel(uint64_t id)//id:定时器任务对象的唯一标识ID
    {

        return _timer_wheel.TimerCancel(id);//取消定时任务
    }
    bool HasTimer(uint64_t id)//id:定时器任务对象的唯一标识ID
    {
        return _timer_wheel.HasTimer(id);//检查定时任务是否存在
    }

注意点

我们注意到一个问题:

  • TimerWheel类中使用了EventLoop的成员函数(构造函数),因此需要包含EventLoop的头文件。
  • 但同时,EventLoop类中又使用了TimerWheel的成员函数(就上面这4个),也需要包含TimerWheel的头文件。
  • 这种相互包含形成了头文件之间的循环依赖,在编译阶段会导致错误,因为编译器无法确定类的完整定义顺序,从而难以解析类型和函数声明。

唯一的解决方法就是

  • 我们就正常的让TimerWheel类包含EventLoop的头文件,EventLoop类包含TimerWheel的头文件
  • 但是TimerWheel类只能声明,但是不能去实现EventLoop类里面调用的TimerWheel类的接口
  • 这些EventLoop类里面调用的TimerWheel类的接口的实现全部放到EventLoop.hpp里面来进行实现

我们作出下面这些修改

timerwheel.hpp

去掉下面四个函数的实现,只保留声明

cpp 复制代码
//注意下面四个接口需要在EventLoop头文件里面实现
        //因为在EventLoop类的实现里面需要使用到下面三个函数,而我们这这个类又调用了EventLoop的函数,这个就不能直接的进行头文件包含
        // 添加定时任务
        void TimerAdd(uint64_t id, uint32_t delay, const TaskFunc &cb);
        // 刷新/延迟定时任务
        void TimerRefresh(uint64_t id);
        // 取消定时任务:外部接口,阻止任务的执行
        void TimerCancel(uint64_t id);
        // 检查定时任务是否存在
        //注意此接口非线程安全,仅用于调试或内部使用
        // 这个接口只能在对应的EventLoop线程内调用,不能在其他线程里面执行
        bool HasTimer(uint64_t id);

eventloop.hpp

实现TimerWheel类里面未实现的4个接口(在文件最后面添加即可)

cpp 复制代码
// 添加定时任务
void TimerWheel::TimerAdd(uint64_t id, uint32_t delay, const TaskFunc &cb)
{
    _loop->RunInLoop(std::bind(&TimerWheel::TimerAddInLoop, this, id, delay, cb));
    // 将这个任务转交个EventLoop里面进行处理
    //  运行任务:如果当前线程是EventLoop所绑定的那个线程,就直接执行任务,无需添加到任务队列里面;
    // 如果当前线程不是EventLoop所绑定的那个线程,就先不执行任务,而是将任务加入任务队列
    // 下面的TimerRefresh和TimerCancel也是一样的道理
}
// 刷新/延迟定时任务
void TimerWheel::TimerRefresh(uint64_t id)
{
    _loop->RunInLoop(std::bind(&TimerWheel::TimerRefreshInLoop, this, id));
}

// 取消定时任务:外部接口,阻止任务的执行
void TimerWheel::TimerCancel(uint64_t id)
{
    _loop->RunInLoop(std::bind(&TimerWheel::TimerCancelInLoop, this, id));
}
// 检查定时任务是否存在
// 注意此接口非线程安全,仅用于调试或内部使用
// 这个接口只能在对应的EventLoop线程内调用,不能在其他线程里面执行
bool TimerWheel::HasTimer(uint64_t id)
{
    auto it = _timers.find(id);
    if (it == _timers.end())
    {
        return false; // 任务不存在
    }
    return true; // 任务存在
}

3.3.代码总览

timerwheel.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <vector>
#include <unordered_map>
#include <cstdint>
#include <functional>
#include <memory>
#include <unistd.h>
#include"channel.hpp"
#include"eventloop.hpp"


// 定时器任务回调函数类型定义:无参数无返回值的函数对象,用于封装定时任务执行逻辑
using TaskFunc = std::function<void()>;
// 资源释放回调函数类型定义:用于定时器销毁时的资源清理操作
using ReleaseFunc = std::function<void()>;

// 定时器任务类:封装单个定时任务,管理任务执行和资源清理
class TimerTask{
    private:
        uint64_t _id;       // 定时器任务对象的唯一标识ID,用于区分不同定时任务
        uint32_t _timeout;  // 定时任务的超时时间(单位:秒),表示延迟多少秒执行
        bool _canceled;     // 任务取消标志:false-表示任务有效未取消,true-表示任务已取消
        TaskFunc _task_cb;  // 定时器对象要执行的定时任务回调函数,保存用户定义的任务逻辑
        ReleaseFunc _release; // 资源释放回调函数,用于从TimerWheel中删除当前定时器对象信息
    
    public:
        // 构造函数:初始化定时任务对象
        TimerTask(uint64_t id, uint32_t delay, const TaskFunc &cb): 
            _id(id), _timeout(delay), _task_cb(cb), _canceled(false) {}
        
        // 析构函数:对象销毁时自动执行任务和清理资源
        ~TimerTask() { 
            // 如果任务没有被取消,则执行用户定义的任务回调函数
            if (_canceled == false) _task_cb(); 
            // 执行资源释放回调,从TimerWheel中移除本任务
            _release(); 
        }
        
        // 取消定时任务:设置取消标志,阻止任务执行
        void Cancel() { _canceled = true; }
        
        // 设置资源释放回调函数:由TimerWheel调用,绑定清理逻辑
        void SetRelease(const ReleaseFunc &cb) { _release = cb; }
        
        // 获取任务的延迟时间:返回设定的超时秒数
        uint32_t DelayTime() { return _timeout; }
};

// 时间轮定时器类:基于时间轮算法管理多个定时任务,实现高效定时调度
class TimerWheel {
    private:
        // 弱引用智能指针类型:指向TimerTask但不增加引用计数,避免循环引用问题
        using WeakTask = std::weak_ptr<TimerTask>;
        // 共享智能指针类型:管理TimerTask生命周期,自动释放内存
        using PtrTask = std::shared_ptr<TimerTask>;
        
        int _tick;      // 时间轮当前指针位置(秒针),指向当前要处理的任务槽位
        int _capacity;  // 时间轮容量,即最大延迟时间(单位:秒)
        
        // 时间轮核心数据结构:二维向量,外层表示时间槽(秒),内层存储该秒的所有任务
        // 每个任务使用shared_ptr管理,当vector被清空时自动触发TimerTask析构
        std::vector<std::vector<PtrTask>> _wheel;
        
        // 任务查找表:哈希表存储<任务ID, 任务弱引用>,用于快速查找和操作特定任务
        // 使用weak_ptr避免增加引用计数,不影响任务自动释放
        std::unordered_map<uint64_t, WeakTask> _timers;

        EventLoop *_loop;           // 事件循环对象指针,用于集成到事件驱动框架中
        int _timerfd;               // Linux定时器文件描述符,基于timerfd实现高精度定时
        std::unique_ptr<Channel> _timer_channel; // 定时器事件通道,管理timerfd的IO事件
    
    private:
        // 从任务查找表中移除指定ID的定时器
        void RemoveTimer(uint64_t id) {
            auto it = _timers.find(id);
            if (it != _timers.end()) {
                _timers.erase(it);  // 从哈希表中删除任务记录
            }
        }
        
        // 创建Linux定时器文件描述符(timerfd)
        static int CreateTimerfd() {
            // 创建timerfd,使用单调时钟CLOCK_MONOTONIC(不受系统时间调整影响)
            int timerfd = timerfd_create(CLOCK_MONOTONIC, 0);
            if (timerfd < 0) {
                ERR_LOG("TIMERFD CREATE FAILED!"); // 记录错误日志
                abort();  // 创建失败,终止程序运行
            }
            
            // 设置定时器参数:timerfd_settime函数控制定时器行为
            // int timerfd_settime(int fd, int flags, struct itimerspec *new, struct itimerspec *old);
            struct itimerspec itime;
            itime.it_value.tv_sec = 1;     // 首次超时时间:1秒后触发
            itime.it_value.tv_nsec = 0;
            itime.it_interval.tv_sec = 1;  // 后续超时间隔:每1秒触发一次
            itime.it_interval.tv_nsec = 0;
            
            // 启动定时器,flags=0表示相对时间(相对于当前时间)
            timerfd_settime(timerfd, 0, &itime, NULL);
            return timerfd;
        }
        
        // 读取定时器文件描述符,获取自上次读取以来的超时次数
        int ReadTimefd() {
            uint64_t times;  // 存储读取的超时次数,类型为uint64_t(timerfd规范)
            // 由于事件处理延迟,可能累计多次超时,times表示未处理的超时次数
            int ret = read(_timerfd, &times, 8); // 读取8字节数据到times变量
            if (ret < 0) {
                ERR_LOG("READ TIMEFD FAILED!"); // 读取失败记录错误
                abort();  // 读取失败,终止程序运行
            }
            return times;  // 返回超时次数
        }
        
        // 执行定时任务:秒针前进一步,处理当前槽的所有任务
        void RunTimerTask() {
            // 秒针循环前进(取模实现循环时间轮)
            _tick = (_tick + 1) % _capacity;
            // 清空当前槽位的任务列表,shared_ptr释放会触发TimerTask析构,从而执行任务回调
            _wheel[_tick].clear();
        }
        
        // 定时器超时事件回调函数:每次timerfd可读时被调用
        void OnTime() {
            // 读取实际超时次数,处理可能累积的多次超时
            int times = ReadTimefd();
            // 根据超时次数,多次执行时间轮前进操作
            for (int i = 0; i < times; i++) {
                RunTimerTask();
            }
        }
        
        // 在事件循环线程中添加定时任务(线程安全版本)
        void TimerAddInLoop(uint64_t id, uint32_t delay, const TaskFunc &cb) {
            // 创建定时任务对象,使用shared_ptr进行生命周期管理
            PtrTask pt(new TimerTask(id, delay, cb));
            
            // 设置任务的资源释放回调,绑定到当前对象的RemoveTimer方法
            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) {
            // 在任务查找表中查找指定ID的任务
            auto it = _timers.find(id);
            if (it == _timers.end()) {
                return;  // 未找到对应任务,无法刷新
            }
            
            // 通过weak_ptr的lock()方法获取shared_ptr(如果对象还存在)
            PtrTask pt = it->second.lock();
            
            // 获取任务的原始延迟时间
            int delay = pt->DelayTime();
            
            // 基于当前时间指针重新计算任务位置
            int pos = (_tick + delay) % _capacity;
            
            // 将任务重新添加到时间轮的新位置(原任务会在原位置自动析构)
            _wheel[pos].push_back(pt);
        }
        
        // 在事件循环线程中取消定时任务(线程安全版本)
        void TimerCancelInLoop(uint64_t id) {
            // 在任务查找表中查找指定ID的任务
            auto it = _timers.find(id);
            if (it == _timers.end()) {
                return;  // 未找到对应任务,无法取消
            }
            
            // 获取任务共享指针,检查任务是否存在
            PtrTask pt = it->second.lock();
            
            // 如果任务对象还存在,则调用Cancel方法设置取消标志
            if (pt) pt->Cancel();
        }
    
    public:
        // 构造函数:初始化时间轮,绑定到事件循环
        TimerWheel(EventLoop *loop):_capacity(60),      // 默认容量60秒
                                    _tick(0),           // 初始时间指针为0
                                    _wheel(_capacity),  // 初始化时间轮数据结构
                                    _loop(loop),        // 绑定事件循环
                                    _timerfd(CreateTimerfd()), // 创建定时器文件描述符
                                    _timer_channel(new Channel(_loop, _timerfd)) // 创建事件通道
        {
            // 设置定时器通道的读事件回调,绑定到OnTime方法
            _timer_channel->SetReadCallback(std::bind(&TimerWheel::OnTime, this));
            
            // 启用读事件监控,开始接收定时器超时事件
            _timer_channel->EnableRead();
        }
        
        /* 定时器操作接口(需考虑线程安全)*/
        // _timers成员可能被多线程访问,需要确保线程安全
        // 如果不加锁,需要将所有定时器操作放在同一个线程(事件循环线程)中执行
        
        //注意下面四个接口需要在EventLoop头文件里面实现
        //因为在EventLoop类的实现里面需要使用到下面三个函数,而我们这这个类又调用了EventLoop的函数,这个就不能直接的进行头文件包含
        // 添加定时任务
        void TimerAdd(uint64_t id, uint32_t delay, const TaskFunc &cb);
        // 刷新/延迟定时任务
        void TimerRefresh(uint64_t id);
        // 取消定时任务:外部接口,阻止任务的执行
        void TimerCancel(uint64_t id);
        // 检查定时任务是否存在
        //注意此接口非线程安全,仅用于调试或内部使用
        // 这个接口只能在对应的EventLoop线程内调用,不能在其他线程里面执行
        bool HasTimer(uint64_t id);
};

eventloop.hpp

cpp 复制代码
#pragma once
#include "poller.hpp"
#include <iostream>
#include <vector>
#include <string>
#include <cassert>
#include <cstring>
#include <ctime>
#include <functional>
#include <unordered_map>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <memory>
#include <typeinfo>
#include <fcntl.h>
#include <signal.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <sys/eventfd.h>
#include <sys/timerfd.h>
#include "timerwheel.hpp"

class EventLoop
{
private:
    // 定义任务函数类型,无参数无返回值
    using Functor = std::function<void()>;

    std::thread::id _thread_id;              // 记录当前EventLoop所属的线程ID
    int _event_fd;                           // eventfd文件描述符,用于线程间事件通知,唤醒阻塞的事件循环
    std::unique_ptr<Channel> _event_channel; // Channel对象,管理_event_fd的事件监听,记录
    Poller _poller;                          // 内部封装了epoll,用于监控所有描述符的事件
    std::vector<Functor> _tasks;             // 任务队列,存储需要在EventLoop线程中执行的任务
    std::mutex _mutex;                       // 互斥锁,保证任务队列操作的线程安全性

    TimerWheel _timer_wheel; // 定时器模块
public:
    // 执行任务队列中的所有任务
    void RunAllTask()
    {
        std::vector<Functor> functor; // 临时vector,用于存储待执行的任务
        {
            // 加锁保护任务队列,防止并发访问
            std::unique_lock<std::mutex> _lock(_mutex);
            // 使用swap交换任务,减少锁持有时间,同时清空原任务队列
            _tasks.swap(functor); // 将需要在EventLoop线程中执行的任务全部转存到临时的任务队列里面,这样子会清空原队列
        }
        // 遍历所有临时vector,然后执行所有任务
        for (auto &f : functor)
        {
            f(); // 调用任务函数
        }
        return;
    }

    // 静态方法:创建eventfd文件描述符
    static int CreateEventFd()
    {
        // 创建eventfd,使用EFD_CLOEXEC和EFD_NONBLOCK标志
        // EFD_CLOEXEC: 执行exec系函数时自动关闭描述符
        // EFD_NONBLOCK: 设置为非阻塞模式
        int efd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
        if (efd < 0)
        {
            ERR_LOG("CREATE EVENTFD FAILED!!");
            abort(); // 创建失败则终止程序
        }
        return efd;
    }

    // eventfd读事件回调函数
    //  读取eventfd:读取事件通知次数,并清空计数器
    void ReadEventfd()
    {
        uint64_t res = 0; // evectfd读取的数据大小必须是8字节,而uint64_t就是8字节
        // 读取8字节的数据,表示事件通知的次数
        int ret = read(_event_fd, &res, sizeof(res));
        if (ret < 0)
        {
            // EINTR: 被信号打断;EAGAIN: 表示无数据可读(非阻塞模式)
            if (errno == EINTR || errno == EAGAIN)
            {
                return;
            }
            ERR_LOG("READ EVENTFD FAILED!");
            abort(); // 读取失败则终止程序
        }
        // 成功读取,res包含了自上次读取以来的事件通知次数
        // 读取后eventfd的内部计数器会被重置为0,这个时候eventfd会从可读变成不可读
        return;
    }

    // 向eventfd写入数据,这个就会触发eventfd的可读事件,用于唤醒事件循环
    void WeakUpEventFd()
    {
        uint64_t val = 1; // 写入值1,表示一个事件通知
        // evectfd写入的数据大小必须是8字节,而uint64_t就是8字节
        int ret = write(_event_fd, &val, sizeof(val));
        if (ret < 0)
        {
            // EINTR: 被信号打断
            if (errno == EINTR)
            {
                return;
            }
            ERR_LOG("WRITE EVENTFD FAILED!"); // 注意:这里应该是WRITE错误
            abort();                          // 写入失败则终止程序
        }
        // 成功写入,eventfd内部计数器增加1,如果从0变为非0,会触发可读事件
        // 只要内核计数器从 0 变为大于 0(即发生了 write),epoll 就会检测到该描述符变为可读
        return;
    }

public:
    // 构造函数:初始化EventLoop
    EventLoop() : _thread_id(std::this_thread::get_id()), // 记录当前线程ID
                  _event_fd(CreateEventFd()),             // 创建eventfd
                  _event_channel(new Channel(this, _event_fd)),
                  _timer_wheel(this) // 将当前EventLoop与定时器模块绑定
    {                                // 创建Channel管理eventfd
        // 设置eventfd的可读事件回调函数
        _event_channel->SetReadCallback(std::bind(&EventLoop::ReadEventfd, this));
        // 启动eventfd的读事件监控
        _event_channel->EnableRead();
    }

    // 事件循环主函数:三步走 - 事件监控 -> 就绪事件处理 -> 执行任务
    void Start()
    {
        while (1) // 死循环,一个EventLoop占据一个线程
        {
            // 1. 事件监控:等待事件就绪
            std::vector<Channel *> actives; // 存储就绪的事件通道
            _poller.Poll(&actives);         // 阻塞等待事件发生,它会将所有就绪好的事件全部存储到actives里面

            // 2. 事件处理:处理所有就绪的事件
            for (auto &channel : actives)
            {
                channel->HandleEvent(); // 根据实际触发的事件类型调用相应的回调函数
            }

            // 3. 执行任务:执行任务队列中的所有任务
            RunAllTask(); // 将任务队列里面所有的任务执行,并清空任务队列
        }
    }

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

    // 断言当前线程是EventLoop所在的线程,如果不是则终止程序
    void AssertInLoop()
    {
        assert(_thread_id == std::this_thread::get_id());
    }

    // 运行任务:如果当前线程是EventLoop所绑定的那个线程,就直接执行任务,无需添加到任务队列里面;
    // 如果当前线程不是EventLoop所绑定的那个线程,就先不执行任务,而是将任务加入任务队列
    void RunInLoop(const Functor &cb)
    {
        if (IsInLoop())
        {
            // 如果当前线程就是EventLoop线程,直接执行任务
            return cb();
        }
        // 否则,将任务加入任务队列,等待EventLoop线程执行
        return QueueInLoop(cb);
    }

    // 将任务加入任务队列(线程安全)
    void QueueInLoop(const Functor &cb) // 传递进来的是一个任务
    {
        {
            // 加锁保护任务队列
            std::unique_lock<std::mutex> _lock(_mutex);
            // 将任务添加到任务队列
            _tasks.push_back(cb);
        }
        // 唤醒可能因没有事件就绪而阻塞的epoll
        // EventLoop所属线程可能在epoll_wait那里等待事件就绪,就来不及处理任务
        // 通过向eventfd写入数据,触发eventfd的可读事件,从而唤醒事件循环,这样子这个EventLoop所属线程就会来处理任务队列里面的任务
        WeakUpEventFd();
    }

    // 添加或修改描述符的事件监控
    void UpdateEvent(Channel *channel)
    {
        return _poller.UpdateEvent(channel); // 将这个事件在epoll监控里面进行修改
    }

    // 移除描述符的事件监控
    void RemoveEvent(Channel *channel)
    {
        return _poller.RemoveEvent(channel); // 将这个事件从epoll监控里面移除
    }

    // 定时器模块相关操作
    // id:定时器任务对象的唯一标识ID
    // delay:定时任务的超时时间(单位:秒),表示延迟多少秒执行
    // cb:定时器对象要执行的定时任务回调函数,类型是void()
    void TimerAdd(uint64_t id, uint32_t delay, const TaskFunc &cb)
    {
        return _timer_wheel.TimerAdd(id, delay, cb); // 添加定时任务
    }
    void TimerRefresh(uint64_t id) // id:定时器任务对象的唯一标识ID
    {
        return _timer_wheel.TimerRefresh(id); // 刷新定时任务
    }
    void TimerCancel(uint64_t id) // id:定时器任务对象的唯一标识ID
    {

        return _timer_wheel.TimerCancel(id); // 取消定时任务
    }
    bool HasTimer(uint64_t id) // id:定时器任务对象的唯一标识ID
    {
        return _timer_wheel.HasTimer(id); // 检查定时任务是否存在
    }
};

void Channel::Remove()
{
    _loop->RemoveEvent(this);
}
void Channel::Update()
{
    _loop->UpdateEvent(this);
}

// 添加定时任务
void TimerWheel::TimerAdd(uint64_t id, uint32_t delay, const TaskFunc &cb)
{
    _loop->RunInLoop(std::bind(&TimerWheel::TimerAddInLoop, this, id, delay, cb));
    // 将这个任务转交个EventLoop里面进行处理
    //  运行任务:如果当前线程是EventLoop所绑定的那个线程,就直接执行任务,无需添加到任务队列里面;
    // 如果当前线程不是EventLoop所绑定的那个线程,就先不执行任务,而是将任务加入任务队列
    // 下面的TimerRefresh和TimerCancel也是一样的道理
}
// 刷新/延迟定时任务
void TimerWheel::TimerRefresh(uint64_t id)
{
    _loop->RunInLoop(std::bind(&TimerWheel::TimerRefreshInLoop, this, id));
}

// 取消定时任务:外部接口,阻止任务的执行
void TimerWheel::TimerCancel(uint64_t id)
{
    _loop->RunInLoop(std::bind(&TimerWheel::TimerCancelInLoop, this, id));
}
// 检查定时任务是否存在
// 注意此接口非线程安全,仅用于调试或内部使用
// 这个接口只能在对应的EventLoop线程内调用,不能在其他线程里面执行
bool TimerWheel::HasTimer(uint64_t id)
{
    auto it = _timers.find(id);
    if (it == _timers.end())
    {
        return false; // 任务不存在
    }
    return true; // 任务存在
}

3.4.代码测试

tcp_srv.cpp

cpp 复制代码
#include"../server/socket.hpp"
#include"../server/poller.hpp"
#include"../server/channel.hpp"
#include"../server/eventloop.hpp"

// 处理关闭事件的回调函数
// 参数:channel - 指向要处理的Channel对象的指针
void HandleClose(Channel *channel) {
    // 输出关闭连接的日志,显示文件描述符
    std::cout << "close:" << channel->Fd() << std::endl;
    // 从事件监控器(epoll)中移除该Channel的事件监控
    channel->Remove();
    // 释放Channel对象占用的内存
    delete channel;
}

// 处理读事件的回调函数
// 当文件描述符有数据可读时被调用
void HandleRead(Channel *channel) {
    // 从Channel对象中获取文件描述符
    int fd = channel->Fd();
    // 定义接收缓冲区,初始化为0,大小1024字节
    // 注意:实际上只能接收1023个数据字节,最后一个字节用于字符串结束符'\0'
    char buf[1024] = {0};
    
    // 从套接字接收数据
    // 参数:
    //   fd - 套接字文件描述符
    //   buf - 接收缓冲区
    //   1023 - 最大接收字节数(留一个字节给结束符)
    //   0 - 接收标志,0表示默认阻塞模式(实际上这里是非阻塞模式)
    int ret = recv(fd, buf, 1023, 0);
    
    // 检查接收结果
    if (ret <= 0) {
        return HandleClose(channel);//关闭并释放连接
    }
    // 启用可写事件监控,准备将接收到的数据发送回去
    // 注意:首先别人发了数据过来,就会触发可读事件回调函数(注意我们在前面开启了读事件监控),人家都发数据来了,你必须回应人家吧,
    //当有数据要发送时,才需要启用写事件监控
    channel->EnableWrite();
    // 打印接收到的数据到控制台
    std::cout << buf << std::endl;
}

// 处理写事件的回调函数
void HandleWrite(Channel *channel) {
    int fd=channel->Fd();
    const char* data="天气还不错";
    int ret=send(fd,data,strlen(data),0);
    if(ret<0)
    {
        return HandleClose(channel);//关闭释放
    }
    channel->DisableWrite();//关闭写事件监控
}

// 处理错误事件的回调函数
void HandleError(Channel *channel) {
    return HandleClose(channel);//发生错误,直接关闭释放好吧
}
// 处理任意事件的回调函数
void HandleEvent(EventLoop*loop,uint64_t timerid,Channel *channel) {
    //但是我们希望一个连接在10秒以内没有任何事件发生,才去执行这个关闭连接的定时任务
    //注意,我们在任意事件的回调函数里面来进行刷新定时器
    loop->TimerRefresh(timerid);//对定时任务进行刷新
    std::cout<<"有了一个事件!!"<<std::endl;
}


//要添加事件监控,肯定需要一个Poller
// 接受新连接的函数
// 参数:poller - 事件轮询器,lst_channel - 监听套接字的Channel
void Acceptor(EventLoop*loop, Channel *lst_channel) {
    // 从监听Channel获取监听套接字文件描述符
    int fd = lst_channel->Fd();
    
    // 接受一个新的客户端连接
    // accept 函数从监听套接字的连接队列中取出一个连接
    // 参数1:监听套接字描述符
    // 参数2、3:设为NULL表示不关心客户端的地址信息
    int newfd = accept(fd, NULL, NULL);
    
    // 如果接受连接失败,直接返回
    if (newfd < 0) { 
        return; 
    }
    
    // 为新的客户端连接创建一个Channel对象
    // Channel封装了文件描述符的事件监控和回调处理
    Channel *channel = new Channel(loop, newfd);
    
    uint64_t timerid=rand()%10000;
    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(HandleEvent,loop,timerid,channel));// 为通信套接字设置任意事件的回调函数
    
    // 注意:这里创建了动态分配的Channel对象,需要记得在适当的时候释放内存
    // 通常在连接关闭时,在关闭回调函数中删除Channel对象

    //非活跃连接的超时释放工作,10秒中后自动关闭连接
    //注意:定时销毁任务,必须需要在启动读事件监控之前,因为有可能启动读事件监控后,立即就有了事件,但是还没有任务
    
    loop->TimerAdd(timerid,10,std::bind(HandleClose,channel));//添加定时任务------这个定时任务也就是关闭连接
    //但是我们希望一个连接在10秒以内没有任何事件发生,才去执行这个关闭连接的定时任务
    //注意,我们在任意事件的回调函数里面来进行刷新定时器


    // 启用读事件监控,开始监听客户端发送数据
    channel->EnableRead();
}

int main() {
    srand(time(nullptr));
    EventLoop loop;

    // 创建一个监听套接字对象,用于接受客户端连接
    Socket lst_sock;
    
    // 创建服务器,监听8500端口,默认绑定到0.0.0.0(所有网络接口)
    // 如果创建失败,程序会返回错误
    lst_sock.CreateServer(8500);

    //为监听套接字,创建一个Channel进行事件的管理,以及事件的处理
    Channel channel(&loop,lst_sock.Fd());
    channel.SetReadCallback(std::bind(Acceptor,&loop,&channel));//设置读回调函数------获取新连接,为新连接创建Channel并添加监控
    channel.EnableRead();//启动读事件监控,这样子才能监听到是否有新连接到来
    
    // 无限循环,持续接受客户端连接
    while (1) {
        loop.Start();
    }
    
    // 退出循环后,关闭监听套接字
    lst_sock.Close();
    
    // 程序结束
    return 0;
}

问题1:如何刷新定时任务

注意了,我只是在原来的测试代码的基础之上添加了一个定时任务,一个连接从建立的时候,就设定一个10秒的定时任务,如果10秒内这个任务没有产生任何事件,那么就直接关闭这个连接

那如果在这10秒之内,这个连接有新事件产生,那么就刷新定时器任务,那么具体怎么刷新呢?

其实很简单,我们知道产生任何事件都会去调用任意事件的回调函数,我们就在那里进行定时任务的刷新。

问题二:这里有一个重要的时序问题:为什么定时销毁任务必须在启动读事件监控之前添加?

当一个新的连接建立后,程序需要设置一个 10 秒后自动关闭连接的定时任务,但同时也要监控这个连接上的各种事件(比如读事件)。如果连接在 10 秒内有事件发生,就通过 TimerRefresh 刷新这个定时器,重新计时。

问题在于:

  • 如果先启动读事件监控(channel->EnableRead()),可能刚一启动,客户端就立即发送数据过来,触发读事件回调。
  • 读事件回调中会调用 HandleEvent,HandleEvent 又会调用 loop->TimerRefresh(timerid) 来刷新定时器。
  • 但如果此时定时任务(loop->TimerAdd(...))还没有被添加到定时器轮中(因为它是在读事件监控之后才执行的),TimerRefresh 就会去操作一个还不存在的定时器 ID,导致未定义行为(比如访问无效内存、找不到定时器等)。

所以必须:

  • 先调用 loop->TimerAdd 添加定时销毁任务,确保定时器已经注册并有效,再调用 channel->EnableRead() 启动事件监控。这样即使事件立即到来,TimerRefresh 也能正确刷新已经存在的定时器。

tcp_cli.hpp

cpp 复制代码
#include "../server/socket.hpp"

int main()
{
    // 创建一个客户端套接字对象,用于连接服务器
    Socket cli_sock;

    // 创建客户端连接,连接到本地主机(127.0.0.1)的8500端口
    // 如果连接失败,程序会返回错误(假设CreateClient内部会处理)
    cli_sock.CreateClient(8500, "127.0.0.1");
    for(int i=0;i<3;i++)//只发送3次消息
    {
        // 准备要发送给服务器的消息内容
        std::string str = "hello bitejiuyeke!";

        // 将消息发送给服务器
        // str.c_str() 获取C风格字符串指针,str.size() 获取字符串长度
        cli_sock.Send(str.c_str(), str.size());

        // 定义缓冲区,用于接收服务器返回的数据,初始化为0
        char buf[1024] = {0};

        // 从服务器接收响应数据,最多接收1023字节(留一个位置给字符串结束符)
        cli_sock.Recv(buf, 1023);

        // 打印接收到的服务器响应
        // %s 表示以字符串格式输出
        DBG_LOG("%s", buf);

        sleep(2);
    }
    //等待
    while(1) sleep(1);

    // 程序结束,返回0表示成功
    return 0;

    // 注意:cli_sock对象在main函数结束时会被自动销毁
    // Socket类的析构函数会自动调用Close()方法关闭套接字
    // 所以这里不需要显式调用cli_sock.Close()
}

我们这里只是发送了3次消息,然后就一直死循环了

四.EventLoop模块和其他模块总结

我给了一个html文件

cpp 复制代码
<iframe frameborder='0' style='width:100%;height:100%;' src='https://diagram-viewer.giteeusercontent.com?repo=qigezi/tcp-server&ref=master&file=image/eventloop%E7%AE%80%E5%8D%95%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%9A%84%E6%A8%A1%E5%9D%97%E5%85%B3%E7%B3%BB%E5%9B%BE.drawio' />

大家自己去搞一个

相关推荐
信码由缰15 小时前
Java 中的 AI 与机器学习:TensorFlow、DJL 与企业级 AI
java
꧁Q༒ོγ꧂16 小时前
算法详解(三)--递归与分治
开发语言·c++·算法·排序算法
沙子迷了蜗牛眼16 小时前
当展示列表使用 URL.createObjectURL 的创建临时图片、视频无法加载问题
java·前端·javascript·vue.js
ganshenml16 小时前
【Android】 开发四角版本全解析:AS、AGP、Gradle 与 JDK 的配套关系
android·java·开发语言
我命由我1234516 小时前
Kotlin 运算符 - == 运算符与 === 运算符
android·java·开发语言·java-ee·kotlin·android studio·android-studio
少云清16 小时前
【接口测试】3_Dubbo接口 _Telnet或python远程调用Dubbo接口
开发语言·python·dubbo·接口测试
盒子691016 小时前
【golang】替换 ioutil.ReadAll 为 io.ReadAll 性能会下降吗
开发语言·后端·golang
小途软件16 小时前
ssm327校园二手交易平台的设计与实现+vue
java·人工智能·pytorch·python·深度学习·语言模型
alonewolf_9916 小时前
Java类加载机制深度解析:从双亲委派到热加载实战
java·开发语言