Consolexin's blog Consolexin's blog
首页
  • 算法基础

    • 图论
    • 字符串
    • 动态规划
    • 二分
    • 滑动窗口
    • 排序
  • Project

    • CppServer
  • 相关书籍

    • 现代C++编程
  • 书籍

    • SQL必知必会
    • MySQL必知必会
分类
标签
归档
GitHub (opens new window)

Consolexinhun

小学生
首页
  • 算法基础

    • 图论
    • 字符串
    • 动态规划
    • 二分
    • 滑动窗口
    • 排序
  • Project

    • CppServer
  • 相关书籍

    • 现代C++编程
  • 书籍

    • SQL必知必会
    • MySQL必知必会
分类
标签
归档
GitHub (opens new window)
  • README
  • day01-从一个最简单的socket开始
  • day02-不要放过任何一个错误
  • day03-高并发还得用epoll
  • day04-来看看我们的第一个类
  • day05-epoll高级用法-Channel登场
  • day06-服务器与事件驱动核心类登场
  • day07-为我们的服务器添加一个Acceptor
  • day08-一切皆是类,连TCP连接也不例外
  • day09-缓冲区-大作用
  • day10-加入线程池到服务器
  • day11-完善线程池,加入一个简单的测试程序
  • day12-将服务器改写为主从Reactor多线程模式
  • day13-支持业务逻辑自定义、完善Connection类
  • day14-重构核心库、使用智能指针
  • day15-重构Connection、修改生命周期
  • day16-使用CMake工程化
  • day17-使用EventLoopThreadPool、移交EventLoop
  • day18-HTTP有限状态转换机
  • day19-创建HTTP响应,实现HTTP服务器
  • day20-定时器的创建使用
  • day21-服务器主动关闭连接
  • day22-初步涉及日志库,定义自己的输出流LogStream
  • day23-定义前端日志库,实现同步输出
  • day24-异步日志库
  • day25-更有效的缓冲区
  • day26-监听写事件
  • day27-处理静态文件,实现POST请求
  • day28-文件服务器的简单实现,文件的展示和下载
  • day29-文件的上传
  • day30-WebBench的测试
  • CppServer
consolexinhun
2025-04-20

day20-定时器的创建使用

# day20-定时器的创建和使用

为了在特定时间执行特定任务,定时器在服务器上扮演着非常重要的功能。使用TCP长连接需要客户端定时向服务端发送心跳请求。或者在特定时间执行特定任务,比如定点秒杀等任务。

因此,在定时器中,我们需要实现的功能主要包含以下部分:

  1. 获取一个时间
  2. 保存一个特定时间,并在抵达该时间时执行特定的任务。
  3. 由于一个项目中,不可能只存在一个定时任务,因此还需要对定时任务进行管理。

定时器功能中,最基础的就是获取相应的时间,我们封装了TimeStamp类,并且使用gettimeofday来获取当前的时间,时间精度达到了1微秒,基本满足了本项目的需求。

其定义为,在这个类中基本定义了我们所需要的函数,主要包括对当前时间的获取,已经对时间的比较。:

class TimeStamp{

    public:
        TimeStamp();
        explicit TimeStamp(int64_t micro_seconds);
        // 重载运算符用于比较
        bool operator<(const TimeStamp &rhs) const;
        bool operator==(const TimeStamp &rhs) const;
        // 获取事件的字符串表示,用于日志库
        std::string ToFormattedString() const;
        int64_t microseconds() const;
        
        // 获取当前时间
        static TimeStamp Now();
        // 获取当前时间+add_seconds后的时间
        static TimeStamp AddTime(TimeStamp timestamp, double add_seconds);
    private:
        int64_t micro_seconds_;
};

// 静态函数
inline TimeStamp TimeStamp::Now(){
    struct timeval time;
    gettimeofday(&time, NULL);
    return TimeStamp(time.tv_sec * kMicrosecond2Second + time.tv_usec);
}

inline TimeStamp TimeStamp::AddTime(TimeStamp timestamp, double add_seconds){
    int64_t add_microseconds = static_cast<int64_t>(add_seconds) * kMicrosecond2Second;   
    return TimeStamp(timestamp.microseconds() + add_microseconds);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

上述只是封装了对时间的获取,而我们实现定时器,还需要保存对应的任务,因此,我们还需要定义一个Timer,其内部有两个关键的成员变量时间戳TimeStamp expiration_和对应需要执行的函数std::function<void()> callback_。

除此之外,我们可能会需要重复的执行某些任务,因此还定义了double interval_表示重复的时间间隔。

具体的定义如下:

class Timer
{
public:
    DISALLOW_COPY_AND_MOVE(Timer);
    Timer(TimeStamp timestamp, std::function<void()>const &cb, double interval);

    TimeStamp expiration() const;

    // 抵达时间,运行
    void run() const;
    
    // 如果是重复任务
    void ReStart(TimeStamp now);
    bool repeat() const;

private:
    TimeStamp expiration_; // 定时器的绝对时间
    std::function<void()> callback_; // 到达时间后进行回调
    double interval_; // 如果重复,则重复间隔
    bool repeat_;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

TimeStamp和Timer只是作为定时器的最基础的组件。对于多个定时器任务。我们采用了c++中set数据结构对Timer进行存储,并以时间戳进行排序。以方便获取所有超过当前时间的定时任务。

而对于获得当前超时的定时任务。我们采用timerfd将定时计时的任务交给了系统,由系统负责,当到达时间后,timerfd变成了可读,此时就可以执行相应的定时器任务,这样就可以将其加入到Epoll中由Epoll监控。

TimerQueue::TimerQueue(EventLoop *loop)
    : loop_(loop)
{
    CreateTimerfd();
    channel_ = std::make_unique<Channel>(timerfd_, loop_);
    channel_->set_read_callback(std::bind(&TimerQueue::HandleRead, this));
    channel_->EnableRead();
}
1
2
3
4
5
6
7
8

当超时时timerfd会变得可读,此时调用HandleRead操作,获取所有可执行的定时任务,并进行执行。

void TimerQueue::HandleRead(){
    ReadTimerFd(); // 将事件读出来,防止loop陷入忙碌状态
    active_timers_.clear(); 
    
    auto end = timers_.lower_bound(Entry(TimeStamp::Now(), reinterpret_cast<Timer *>(UINTPTR_MAX)));
    active_timers_.insert(active_timers_.end(), timers_.begin(), end); // 将所有超时事件放入到激活序列中
    timers_.erase(timers_.begin(), end);

    // 执行
    for (const auto &entry : active_timers_)
    {
        entry.second->run();
    }
    ResetTimers(); // 对于部分事件,可能存在重复属性,需要将其重新加入到定时器队列中,并且需要重新设定`timerfd`超时时间
}

void TimerQueue::ResetTimers() {
    // 将带有重复属性的定时任务重新加入到set中
    for (auto& entry: active_timers_) {
        if ((entry.second)->repeat()) {
            auto timer = entry.second;
            timer->ReStart(TimeStamp::Now());
            Insert(timer);
        } else {
            delete entry.second;
        }
    } 

    if (!timers_.empty()) {
        // 重新设定`timerfd`超时时间
        ResetTimerFd(timers_.begin()->second);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

当我们向定时器加入一个定时任务时,我们需要判断是否将该任务加入到了队首,如果加入到了队首,那么就需要更新timerfd的状态,设置一个新的超时时间。

void TimerQueue::AddTimer(TimeStamp timestamp, std::function<void()> const &cb, double interval){
    Timer * timer = new Timer(timestamp, cb, interval);

    if (Insert(timer))
    {
        ResetTimerFd(timer);
    }
}

bool TimerQueue::Insert(Timer * timer){
    bool reset_instantly = false;

    // 如果时间比对首要早,就更新。
    if(timers_.empty() || timer->expiration() < timers_.begin()->first){
        reset_instantly = true;
    }
    timers_.emplace(std::move(Entry(timer->expiration(), timer)));
    return reset_instantl;
}

void TimerQueue::ResetTimerFd(Timer *timer){
    struct itimerspec new_;
    struct itimerspec old_;
    memset(&new_, '\0', sizeof(new_));
    memset(&old_, '\0', sizeof(old_));

    int64_t micro_seconds_dif = timer->expiration().microseconds() - TimeStamp::Now().microseconds();
    if (micro_seconds_dif < 100){
        micro_seconds_dif = 100;
    }

    new_.it_value.tv_sec = static_cast<time_t>(
        micro_seconds_dif / kMicrosecond2Second);
    new_.it_value.tv_nsec = static_cast<long>((
        micro_seconds_dif % kMicrosecond2Second) * 1000);
    // 获取监控的事件的时间距离当前有多久。

    int ret = ::timerfd_settime(timerfd_, 0, &new_, &old_);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

至此,一个基础的定时器任务就完成了,我们在EventLoop保存一个成员变量, 并创建三个回调函数以方便在最外层定义相应的定时任务。

class EventLoop
{
public: 
    void RunAt(TimeStamp timestamp, std::function<void()> const & cb);
    void RunAfter(double wait_time, std::function < void()>const & cb);
    void RunEvery(double interval, std::function<void()> const & cb);
private:
    std::unique_ptr<TimerQueue> timer_queue_;
};
1
2
3
4
5
6
7
8
9

为了对定时器进行测试,在HttpServer定义了一些函数进行了简单的测试,更加复杂的操作还需要进一步的探索。

进入build,运行./test/webserver以观察程序的运行效果。

编辑 (opens new window)
上次更新: 2025/05/21, 06:42:57
day19-创建HTTP响应,实现HTTP服务器
day21-服务器主动关闭连接

← day19-创建HTTP响应,实现HTTP服务器 day21-服务器主动关闭连接→

最近更新
01
6-其他操作
05-20
02
4-联结
05-20
03
7-管理
05-20
更多文章>
Theme by Vdoing | Copyright © 2019-2025 Consolexinhun | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×