三、时间轮定时器&&正则表达式&&any通用类

时间轮定时器

在我们的项目中,为了让服务器能够断开掉非活跃连接,我们可以使用Linux给我们提供的定时器。

下面这段代码演示了Linux定时器的使用

#include <stdio.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/timerfd.h>

int main() {
    int timerfd = timerfd_create(CLOCK_MONOTONIC, 0);
    if (timerfd == -1) {
        perror("timerfd_create error\n");
        fflush(stdout);
        return -1;
    }

    struct itimerspec itime;
    itime.it_value.tv_sec = 3; // 第一次超时时间为3s后
    itime.it_value.tv_nsec = 0;
    itime.it_interval.tv_sec = 1; // 第一次超时时间过后每次超时时间间隔为1s
    itime.it_interval.tv_nsec = 0;

    timerfd_settime(timerfd, 0, &itime, nullptr);
    while (1) {
        uint64_t times;
        int ret = read(timerfd, &times, 8);
        if (ret < 0) {
            perror("read error\n");
            return -1;
        }
        printf("超时了,距离上一次超时了%d次\n", times);
    }
    close(timerfd);

    return 0;
}

上边例子,是⼀个定时器的使用示例,是第一次等待3s后每隔1s钟触发⼀次定时器超时,否则就会阻塞在read读取数据这里。 基于这个例子,则我们可以实现每隔1s,检测⼀下哪些连接超时了,然后将超时的连接释放掉。

时间轮思想:

上述的例子,存在⼀个很大的问题,每次超时都要将所有的连接遍历⼀遍,如果有上万个连接,效率无疑是较为低下的。 这时候大家就会想到,我们可以针对所有的连接,根据每个连接最近⼀次通信的系统时间建立⼀个小根堆,这样只需要每次针对堆顶部分的连接逐个释放,直到没有超时的连接为止,这样也可以大大提高处理的效率。

上述方法可以实现定时任务,但是这里给大家介绍另⼀种方案:时间轮。

时间轮的思想来源于钟表,如果我们定了⼀个3点钟的闹铃,则当时针走到3的时候,就代表时间到 了。

同样的道理,如果我们定义了⼀个数组,并且有⼀个指针,指向数组起始位置,这个指针每秒钟向后走动⼀步,走到哪里,则代表哪里的任务该被执行了,那么如果我们想要定⼀个3s后的任务,则只需要将任务添加到tick+3位置,则每秒中走一步,三秒钟后tick走到对应位置,这时候执行对应位置的任务即可。

但是,同⼀时间可能会有大批量的定时任务,因此我们可以给数组对应位置下拉⼀个数组,这样就可以在同⼀个时刻上添加多个定时任务了。

当然,上述操作也有⼀些缺陷,比如我们如果要定义⼀个60s后的任务,则需要将数组的元素个数设置 为60才可以,如果设置⼀小时后的定时任务,则需要定义3600个元素的数组,这样无疑是比较麻烦的。因此,可以采用多层级的时间轮,有秒针轮,分针轮,时针轮。

但是,我们也得考虑⼀个问题,当前的设计是时间到了,则主动去执行定时任务,释放连接,那能不能在时间到了后,自动执行定时任务呢,这时候我们就想到⼀个操作——类的析构函数。

⼀个类的析构函数,在对象被释放时会自动被执行,那么我们如果将⼀个定时任务作为⼀个类的析构函数内的操作,则这个定时任务在对象被释放的时候就会执行。

但是仅仅为了这个目的,而设计⼀个额外的任务类,好像有些不划算,但是,这里我们又要考虑另⼀ 个问题,那就是假如有⼀个连接建立成功了,我们给这个连接设置了⼀个30s后的定时销毁任务,但是在第10s的时候,这个连接进行了⼀次通信,那么我们应该是在第30s的时候关闭,还是第40s的时候关闭呢?无疑应该是第40s的时候。也就是说,这时候,我们需要让这个第30s的任务失效,但是我们该如何实现这个操作呢?

这里,我们就用到了智能指针shared_ptr,shared_ptr有个计数器,当计数为0的时候,才会真正释放⼀个对象,那么如果连接在第10s进行了⼀次通信,则我们继续向定时任务中,添加⼀个30s后(也就是第40s)的任务类对象的shared_ptr,则这时候两个任务shared_ptr计数为2,则第30s的定时任务被释放的时候,计数-1,变为1,并不为0,则并不会执行实际的析构函数,那么就相当于这个第30s的任务失效了,只有在第40s的时候,这个任务才会被真正释放。

上述过程就是时间轮定时任务的思想了,当然这里为了更加简便的实现,进行了⼀些小小的调整实 现。

#include <iostream>
#include <functional>
#include <vector>
#include <unordered_map>
#include <memory>
#include <unistd.h>

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();
        _release();
    }

    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>;
    int _tick; // 当前的秒针 走到哪里释放哪里 就相当于执行哪里的任务
    int _capacity; // 表盘最大数量 --- 其实就是最大延迟时间
    std::vector<std::vector<PtrTask>> _wheel;
    std::unordered_map<uint64_t, WeakTask> _timers;
private:
    void RemoveTimer(uint64_t id) {
        auto iter = _timers.find(id);
        if (iter != _timers.end()) {
            _timers.erase(iter);
        }
    }
public:
    TimerWheel() : _capacity(60), _tick(0), _wheel(_capacity) {}
    void TimerAdd(uint64_t id, uint32_t delay, const TaskFunc &cb) { // 添加定时任务
        PtrTask pt(new TimerTask(id, delay, cb));
        pt->SetRelease(std::bind(&TimerWheel::RemoveTimer, this, id));
        _timers[id] = WeakTask(pt);
        int pos = (_tick + delay) % _capacity;
        _wheel[pos].push_back(pt);
        _timers[id] = WeakTask(pt);
    }

    void TimerRefresh(uint64_t id) { // 刷新/延迟 定时任务
        // 通过保存的定时器对象的weak_ptr构造一个shared_ptr出来,添加到轮子中
        auto iter = _timers.find(id);
        if (iter == _timers.end()) {
            return; // 没找到定时任务 没法刷新 没法延迟
        }
        PtrTask pt = iter->second.lock(); // lock获取weak_ptr管理的对象对应的shared_ptr
        int delay = pt->DelayTime();
        int pos = (_tick + delay) % _capacity;
        _wheel[pos].push_back(pt);
    }

    void TimerCancel(uint64_t id) {
        auto iter = _timers.find(id);
        if (iter == _timers.end()) {
            return; // 没找到定时任务 没法刷新 没法延迟
        }
        PtrTask pt = iter->second.lock();
        if (pt) pt->Cancel();
    }

    void RunTimerTask() { // 这个函数应该每秒钟执行一次 相当于秒针向后走了一步
        _tick = (_tick + 1) % _capacity;
        _wheel[_tick].clear(); // 清空指定位置数组,就会把数组中保存的所有定时器对象的shared_ptr释放掉
    }
};

class Test {
public:
    Test() { std::cout << "构造" << std::endl; }
    ~Test() { std::cout << "析构" << std::endl; }
};

void DelTest(Test *t) { delete t; }

int main() {
    TimerWheel tw;

    Test *t = new Test();

    tw.TimerAdd(888, 5, std::bind(DelTest, t));

    for (int i = 0; i < 5; ++i) {
        tw.TimerRefresh(888); // 刷新定时任务
        tw.RunTimerTask(); // 向后移动秒针
        std::cout << "刷新了一下定时任务,需要重新5s后才会销毁" << std::endl;
        sleep(1);
    }

    // tw.TimerCancel(888);

    while (true) {
        std::cout << "------------------------------" << std::endl;
        tw.RunTimerTask(); // 向后移动秒针
        sleep(1);
    }

    return 0;
}

这段代码实现了一个简单的定时器轮(TimerWheel),以及一个定时任务对象(TimerTask)和一个测试类(Test)。让我来解释主要部分:

TimerTask 类

  • 表示一个定时任务,包含了任务的 ID(_id)、超时时间(_timeout)、是否被取消(_canceled)、任务回调函数(_task_cb)以及用于删除定时器对象的释放函数(_release)。
  • 构造函数初始化任务的各个属性,而析构函数在任务未被取消时执行回调函数,并调用释放函数。
  • Cancel 函数用于取消任务,SetRelease 函数用于设置释放函数。

TimerWheel 类

  • 使用定时器轮的概念,内部包含了一个轮(_wheel)和一个存储定时任务的映射表(_timers)。
  • TimerAdd 函数用于向轮中添加定时任务,根据任务的超时时间将任务放入相应的槽中,并设置任务的释放函数。
  • TimerRefresh 函数用于刷新/延迟定时任务,将已存在的任务移动到新的位置。
  • TimerCancel 函数用于取消定时任务,标记任务为已取消。
  • RunTimerTask 函数模拟时间的流逝,向后移动秒针,清空当前位置的任务列表。

Test 类

  • 一个简单的测试类,包含了构造函数和析构函数,用于测试定时任务的释放。

主函数(main 函数)

  • 创建了一个 TimerWheel 对象 tw
  • 创建了一个 Test 对象 t,并通过 TimerAdd 函数将一个定时任务添加到定时器轮中。该定时任务会在 5 秒后执行 DelTest 函数,用于释放 t 对象。
  • 在一个循环中,通过调用 TimerRefresh 刷新定时任务,然后调用 RunTimerTask 向后移动秒针。在每次迭代中,等待一秒钟,并输出提示信息。
  • 最后,通过 TimerCancel 取消定时任务,然后进入一个无限循环,模拟定时器轮的运行。

这段代码主要用于演示定时器轮的基本原理,通过添加、刷新、取消定时任务,模拟时间的推移和任务的执行。

这是对TimerWheel类的成员变量和成员函数的解释

当涉及到 TimerWheel 类中的成员变量时,这些变量用于实现定时器轮的基本结构和功能。以下是每个成员变量的解释:

  1. _tick

    • 表示当前时间轮的指针位置,类似于钟表的秒针。每次调用 RunTimerTask 函数时,该指针向前移动一步,模拟时间的推移。
  2. _capacity

    • 表示时间轮的总槽数量,也就是环形数组的大小。在这个示例中,_capacity 被设置为 60,表示这是一个模拟60秒钟的时间轮。
  3. _wheel

    • 一个二维向量,表示定时器轮的环形数组。外层向量的每个元素对应时间轮上的一个槽,内层向量表示在该槽中的所有定时任务。
  4. _timers

    • 一个无序映射,用于存储定时任务的 ID 到任务的弱引用的映射。弱引用用于避免循环引用,确保在任务执行完毕后,定时器轮能够正确释放任务对象。

成员函数:

  1. RemoveTimer 函数:

    • 用于从 _timers 映射中移除指定 ID 的定时任务。在定时任务执行完成后,会调用该函数以清理映射表中的对应项。
  2. TimerAdd 函数:

    • 用于向时间轮中添加新的定时任务。它创建一个定时任务对象,并将其添加到时间轮的相应位置。同时,该函数也更新 _timers 映射表,以便能够通过任务的 ID 查找任务。
  3. TimerRefresh 函数:

    • 用于刷新/延迟已存在的定时任务。它通过任务的 ID 在 _timers 中找到相应任务,然后将任务移动到新的时间槽中。
  4. TimerCancel 函数:

    • 用于取消指定 ID 的定时任务。通过任务的 ID 在 _timers 中找到相应任务,并将任务标记为已取消。
  5. RunTimerTask 函数:

    • 模拟时间流逝,向后移动秒针。每次调用该函数时,当前时间轮的指针 _tick 向前移动一步,同时清空当前位置槽内的所有定时任务,触发任务的执行和释放。

这些成员变量和函数共同实现了定时器轮的基本逻辑,使得能够添加、刷新、取消定时任务,并在每个时间槽内执行相应的任务。

正则库的简单使用

正则表达式(regular expression)描述了⼀种字符串匹配的模式(pattern),可以用来检查⼀个串是否含有某种子串、将匹配的子串替换或者从某个串中取出符合某个条件的子串等。 正则表达式的使用,可以使得HTTP请求的解析更加简单(这里指的是程序员的工作变得的简单,这并不代表处理效率会变高,实际上效率上是低于直接的字符串处理的),使我们实现的HTTP组件库使用起来更加灵活。

#include <iostream>
#include <string>
#include <regex>

int main() {
    // HTTP请求行格式: GET /wyp/login?user=xiaoming&pass=123123 HTTP/1.1\r\n
    std::string str = "GET /wyp/login?user=xiaoming&pass=123123 HTTP/1.1\r\n";
    std::smatch matches;
    //请求方法的匹配 GET POST HEAD PUT DELETE...
    std::regex e("(GET|POST|HEAD|PUT|DELETE) ([^?]*)(?:\\?(.*))? (HTTP/1\\.[01])(?:\r|\n|\r\n)?");
    // GET|POST|HEAD|PUT|DELETE 表示匹配并提取其中任意一个字符串
    // [^?]* [^?]表示匹配非问号字符 后面的*表示0次或多次
    // \\?表示原始?字符,.*表示提取?之后的任意字符0次或多次 直到遇到空格
    // HTTP/1\\.[01] 表示匹配以HTTP/1.开始 后面有个0或1的字符串
    // (?:\n|\r\n)>? (?:......)表示匹配某个个数字符串 但是不提取 最后的?表示的是匹配前面的表达式1次或0次

    bool ret = std::regex_match(str, matches, e);
    if (!ret) { return -1; }
    for (auto &s : matches) {
        std::cout << s << std::endl;
    }

    return 0;
}

这段代码演示了如何使用 C++ 的正则表达式库 (<regex>) 来解析 HTTP 请求行。HTTP请求行通常有一定的格式,包括请求方法、请求路径、查询参数和HTTP协议版本等信息。在这里,使用正则表达式对请求行进行匹配和提取。

以下是对代码的解释:

  1. std::string str 包含一个HTTP请求行的字符串,示例为 "GET /wyp/login?user=xiaoming&pass=123123 HTTP/1.1\r\n"。

  2. std::regex e 定义了一个正则表达式,用于匹配HTTP请求行。这个正则表达式的主要部分解释如下:

    • (GET|POST|HEAD|PUT|DELETE): 匹配并提取HTTP请求方法,可以是GET、POST、HEAD、PUT或DELETE。
    • ([^?]*): 匹配并提取路径部分,直到遇到问号(?)为止,表示请求路径。
    • (?:\\?(.*))?: 匹配查询参数部分,其中 (?: ...) 是非捕获分组,匹配问号后面的任意字符,? 表示匹配前面的表达式零次或一次。
    • (HTTP/1\\.[01]): 匹配并提取HTTP协议版本,例如HTTP/1.1。
    • (?:\r|\n|\r\n)?: 匹配行尾的换行符,包括回车符和换行符,(?: ...) 表示非捕获分组,? 表示匹配前面的表达式零次或一次。
  3. bool ret = std::regex_match(str, matches, e) 使用 std::regex_match 函数对输入字符串进行匹配,将匹配结果存储在 matches 中。如果匹配成功,返回 true

  4. for (auto &s : matches) 循环遍历匹配结果,输出匹配到的子串。

在这个特定的例子中,如果匹配成功,输出的内容将是请求方法、请求路径、查询参数和HTTP协议版本,每个部分占据一行。这是一个简单的例子,实际的HTTP请求行可能包含更多信息。正则表达式在这里用于解析和提取特定格式的信息。

通用类型any类型的实现

每⼀个Connection对连接进行管理,最终都不可避免需要涉及到应用层协议的处理,因此在 Connection中需要设置协议处理的上下文来控制处理节奏。但是应用层协议千千万,为了降低耦合度,这个协议接收解析上下文就不能有明显的协议倾向,它可以是任意协议的上下文信息,因此就需要⼀个通用的类型来保存各种不同的数据结构。

在C语⾔中,通用类型可以使用void*来管理,但是在C++中,boost库和C++17给我们提供了⼀个通用类型any来灵活使用,如果考虑增加代码的移植性,尽量减少第三方库的依赖,则可以使用C++17特性中的any,或者自己来实现。而这个any通用类型类的实现其实并不复杂,以下是简单的部分实现。

 

#include <iostream>
#include <typeinfo>
#include <cassert>
#include <unistd.h>

class Any {
private:
    class holder {
    public:
        virtual ~holder() {}
        virtual const std::type_info &type() = 0;
        virtual holder *clone() = 0;
    };
    template<class T>
    class placeholder : public holder {
    public:
        placeholder(const T &val) : _val(val) {}
        // 获取子类对象保存的数据类型
        virtual const std::type_info &type() { return typeid(T); }
        // 针对当前的对象自身,克隆出一个新的子类对象
        virtual holder *clone() { return new placeholder(_val); }
    public:
        T _val;
    };
    holder *_content;
public:
    Any() :_content(nullptr) {}
    template<class T>
    Any(const T &val) : _content(new placeholder<T>(val)) {}
    Any(const Any &other) :_content(other._content ? other._content->clone() : nullptr) {}
    ~Any() { delete _content; }

    Any &swap(Any &other) {
        std::swap(_content, other._content);
        return *this;
    }

    // 返回子类对象保存的数据的指针
    template<class T>
    T *get() {
        //想要获取的数据类型,必须和保存的数据类型一致
        assert(typeid(T) == _content->type());
        return &((placeholder<T>*)_content)->_val;
    }
    //赋值运算符的重载函数
    template<class T>
    Any &operator=(const T &val) {
        //为val构造一个临时的通用容器,然后与当前容器自身进行指针交换,临时对象释放的时候,原先保存的数据也就被释放
        Any(val).swap(*this);
        return *this;
    }
    Any &operator=(const Any &other) {
        Any(other).swap(*this);
        return *this;
    }
};

class test {
public:
    test() { std::cout << "构造" << std::endl; }
    test(const test &t) { std::cout << "拷贝构造" << std::endl; }
    ~test() { std::cout << "析构" << std::endl; }
};

int main() {
    Any a;
    {
        test t;
        a = t;
    }

    a = 10;
    int *pa = a.get<int>();
    std::cout << *pa << std::endl;

    a = std::string("nihao");
    std::string *ps = a.get<std::string>();
    std::cout << *ps << std::endl;

    return 0;
}

这段代码实现了一个简化版的通用类型容器 Any,允许在运行时存储和访问不同类型的数据。以下是代码的主要部分解释:

  1. Any 类的内部有一个私有类 holder,这是一个抽象基类,定义了两个纯虚函数:type() 用于获取存储的数据类型信息,clone() 用于在运行时克隆 holder 的派生类。

  2. Any 类有一个嵌套模板类 placeholder,用于存储特定类型的数据。这个类继承自 holder,实现了 type()clone() 函数。

  3. Any 类中有一个指向 holder 对象的指针 _content,通过该指针实现对不同类型数据的存储。

  4. Any 类有默认构造函数、带参构造函数(模板类型 T)、拷贝构造函数和析构函数。在构造函数中,使用 placeholder 类来存储传入的数据。

  5. swap 方法用于交换两个 Any 对象的内容,这在实现赋值运算符时非常有用。

  6. get 方法是一个模板函数,用于获取存储在 Any 对象中的特定类型的数据的指针。在调用前会使用 assert 进行类型检查,确保获取的数据类型与存储的数据类型相匹配。

  7. 赋值运算符的重载函数,包括对特定类型和 Any 对象的赋值。通过创建临时 Any 对象,利用 swap 方法实现数据的安全交换。

  8. main 函数中,演示了如何使用这个简化的 Any 类。首先创建一个 Any 对象 a,然后在作用域内创建了一个 test 对象 t,将其赋值给 a。接着,a 被赋值为整数和字符串,并通过 get 方法获取这些数据进行输出。在程序结束时,对象的生命周期结束,相应的析构函数被调用。

这段代码实现了一个基本的类型擦除容器,允许在运行时存储和操作不同类型的数据。

  • 21
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值