目录
3.2.整合EventLoop模块和TimerWheel模块
一.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 多路复用配合:
- 这才是 eventfd 威力最大的地方。消费者不需要不停地调用 read 来查询(忙等待),而是可以把这个 eventfd 的文件描述符注册到 epoll 等机制中。
- 只要内核计数器从 0 变为大于 0(即发生了 write),epoll 就会检测到该描述符变为可读。
- 消费者线程只需要阻塞在 epoll_wait 调用上。当 epoll 返回并告知 eventfd 可读时,消费者再去 read,此时 read 会立即成功并返回计数值。
这种方式效率极高,消费者线程可以完全休眠,直到有真正的事件发生时才被唤醒。
接口介绍
int eventfd(unsigned int initval, int flags);
函数功能
创建一个用于事件通知的文件描述符(eventfd)。
参数详解
- initval - 初始计数器值
- 类型:unsigned int(无符号32位整数)
- 作用:设置 eventfd 内部计数器的初始值
- 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 处理流程
-
事件监控:通过 I/O 多路复用机制(如 epoll)监听注册的描述符。
-
事件处理 :当有描述符就绪时,调用预先注册的回调函数进行处理。在回调函数中,若涉及该连接的写操作或其他非即时完成的任务,并不直接执行 I/O,而是将其封装为任务插入任务队列。
-
执行队列任务 :当所有就绪事件处理完毕后,EventLoop 从任务队列中依次取出任务并执行,确保这些操作仍在当前线程中进行。
线程安全保证
-
对任务队列的操作(添加任务、提取任务)可能发生在多个线程(例如其他线程希望向该连接发送数据),因此需要对任务队列的访问加锁,以保证其线程安全。
-
由于事件监控、回调执行及任务执行都在同一个 EventLoop 线程中串行进行,因此对连接本身的状态修改不存在并发冲突,无需额外同步。
示例:发送数据的流程
当某个就绪事件的处理回调中需要向连接发送数据时:
-
不直接调用
send(),而是将"发送数据"这个操作封装为一个任务。 -
将该任务插入到当前 EventLoop 的任务队列中。
-
在本轮事件处理完成后,EventLoop 从任务队列取出该任务并真正执行
send()操作。
这样,无论事件来源于哪个线程,最终对连接进行读写、修改状态等操作都集中在与其绑定的单个线程中,从而杜绝了多线程并发操作同一连接导致的线程安全问题。
问题一:任务处理不及时
在之前的设计中存在一个潜在问题:当 EventLoop 在 epoll_wait 等调用中阻塞等待 I/O 事件时,如果此时有其他线程向该 EventLoop 的任务队列中投递了新任务,这些任务将无法被及时执行,必须等到有 I/O 事件发生、线程从阻塞中唤醒后才能得到处理。这种延迟可能导致响应不及时,甚至在某些情况下引发逻辑错误或性能下降。
为了解决这一问题,我们引入 eventfd 作为线程间事件通知机制。它允许我们主动唤醒阻塞在 I/O 多路复用调用中的 EventLoop,使其能够及时处理任务队列中的待执行操作。
为什么需要唤醒机制?
- EventLoop 的主循环通常会在 epoll_wait(或类似函数)处阻塞,直到被监控的描述符上有事件发生。
- 如果任务队列在此时被放入新任务,而 EventLoop 线程仍在阻塞,那么这些任务必须等待下一个事件发生才能被执行。
- 为了让任务能够被即时调度,就需要一种方式主动中断 epoll_wait 的阻塞,让 EventLoop 立刻回到循环中执行队列任务。
解决方案:使用 eventfd 进行唤醒
-
创建 eventfd
在 EventLoop 初始化时,创建一个
eventfd文件描述符。它是一个轻量级的通知机制,通过写入一个 8 字节整数来触发可读事件。 -
将其加入监控
将该
eventfd的描述符注册到 EventLoop 的 epoll 实例中,监听其可读事件。 -
唤醒操作
当其他线程需要向该 EventLoop 投递任务时:
-
先将任务加入队列(需加锁)。
-
然后向这个
eventfd写入一个数值(例如1),这会立即触发该 eventfd 的可读事件。
-
-
EventLoop 被唤醒后的处理
-
epoll_wait因eventfd可读而返回。这个时候主程序就不会阻塞在这里了,而是转而去处理就绪事件了。 -
EventLoop 在处理就绪事件时,会读到这个
eventfd,执行其对应的回调(通常只是读取并丢弃其中的数值,以清空状态)。这个时候EventLoop就会去执行任务队列里面的任务了 -
随后,EventLoop 进入任务执行阶段,将队列中的所有任务取出并执行。
-
简短一点来说:
问题:EventLoop在等待IO事件时阻塞,导致新加入队列的任务不能及时执行。
解决方案:利用eventfd作为唤醒信号,当有任务加入队列时,向eventfd写入数据,使得eventfd变为可读,从而唤醒阻塞的EventLoop,让其立即处理任务。
问题二:任务队列的使用条件
在 EventLoop 的设计中,一个核心原则是:对于一个连接的任何操作,都必须在管理这个连接的 EventLoop 所属的线程中执行。这是保证该连接状态不被多线程并发访问、避免竞态条件的根本方法。
为了实现这一目标,我们引入了任务队列 作为操作的缓冲区。但在实际执行时,并非所有操作都需要无条件地放入队列等待。系统可以根据操作发起方所在的线程进行智能判断,以兼顾线程安全与执行效率。
操作执行策略:按线程归属进行分流
具体的执行逻辑遵循以下两条路径:
-
操作发起于 EventLoop 绑定线程(可直接执行)
-
判断条件:当需要处理连接上的读写、修改状态等操作时,如果发起该操作的执行流本身就运行在关联的 EventLoop 线程内(例如,在事件就绪的回调函数中触发的后续操作),那么它已经满足了"在正确线程中"的前提条件。
-
执行方式:此时,操作可以直接、同步地执行,无需经过任务队列中转。这避免了不必要的任务封装、入队和调度开销,提升了关键路径上的处理性能。
-
-
操作发起于其他线程(必须入队异步执行)
-
判断条件:当操作请求来自其他线程(例如,业务逻辑线程收到指令,要求向某个连接发送数据),则发起方处于"错误"的线程上下文。
-
执行方式 :必须立即将该操作封装为一个任务,安全地放入该 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, ×, 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, ×, 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' />
大家自己去搞一个
