时间轮定时器
在我们的项目中,为了让服务器能够断开掉非活跃连接,我们可以使用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, ×, 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
类中的成员变量时,这些变量用于实现定时器轮的基本结构和功能。以下是每个成员变量的解释:
-
_tick
:- 表示当前时间轮的指针位置,类似于钟表的秒针。每次调用
RunTimerTask
函数时,该指针向前移动一步,模拟时间的推移。
- 表示当前时间轮的指针位置,类似于钟表的秒针。每次调用
-
_capacity
:- 表示时间轮的总槽数量,也就是环形数组的大小。在这个示例中,
_capacity
被设置为 60,表示这是一个模拟60秒钟的时间轮。
- 表示时间轮的总槽数量,也就是环形数组的大小。在这个示例中,
-
_wheel
:- 一个二维向量,表示定时器轮的环形数组。外层向量的每个元素对应时间轮上的一个槽,内层向量表示在该槽中的所有定时任务。
-
_timers
:- 一个无序映射,用于存储定时任务的 ID 到任务的弱引用的映射。弱引用用于避免循环引用,确保在任务执行完毕后,定时器轮能够正确释放任务对象。
成员函数:
-
RemoveTimer
函数:- 用于从
_timers
映射中移除指定 ID 的定时任务。在定时任务执行完成后,会调用该函数以清理映射表中的对应项。
- 用于从
-
TimerAdd
函数:- 用于向时间轮中添加新的定时任务。它创建一个定时任务对象,并将其添加到时间轮的相应位置。同时,该函数也更新
_timers
映射表,以便能够通过任务的 ID 查找任务。
- 用于向时间轮中添加新的定时任务。它创建一个定时任务对象,并将其添加到时间轮的相应位置。同时,该函数也更新
-
TimerRefresh
函数:- 用于刷新/延迟已存在的定时任务。它通过任务的 ID 在
_timers
中找到相应任务,然后将任务移动到新的时间槽中。
- 用于刷新/延迟已存在的定时任务。它通过任务的 ID 在
-
TimerCancel
函数:- 用于取消指定 ID 的定时任务。通过任务的 ID 在
_timers
中找到相应任务,并将任务标记为已取消。
- 用于取消指定 ID 的定时任务。通过任务的 ID 在
-
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协议版本等信息。在这里,使用正则表达式对请求行进行匹配和提取。
以下是对代码的解释:
-
std::string str
包含一个HTTP请求行的字符串,示例为 "GET /wyp/login?user=xiaoming&pass=123123 HTTP/1.1\r\n"。 -
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)?
: 匹配行尾的换行符,包括回车符和换行符,(?: ...)
表示非捕获分组,?
表示匹配前面的表达式零次或一次。
-
bool ret = std::regex_match(str, matches, e)
使用std::regex_match
函数对输入字符串进行匹配,将匹配结果存储在matches
中。如果匹配成功,返回true
。 -
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
,允许在运行时存储和访问不同类型的数据。以下是代码的主要部分解释:
-
Any
类的内部有一个私有类holder
,这是一个抽象基类,定义了两个纯虚函数:type()
用于获取存储的数据类型信息,clone()
用于在运行时克隆holder
的派生类。 -
Any
类有一个嵌套模板类placeholder
,用于存储特定类型的数据。这个类继承自holder
,实现了type()
和clone()
函数。 -
Any
类中有一个指向holder
对象的指针_content
,通过该指针实现对不同类型数据的存储。 -
Any
类有默认构造函数、带参构造函数(模板类型T
)、拷贝构造函数和析构函数。在构造函数中,使用placeholder
类来存储传入的数据。 -
swap
方法用于交换两个Any
对象的内容,这在实现赋值运算符时非常有用。 -
get
方法是一个模板函数,用于获取存储在Any
对象中的特定类型的数据的指针。在调用前会使用assert
进行类型检查,确保获取的数据类型与存储的数据类型相匹配。 -
赋值运算符的重载函数,包括对特定类型和
Any
对象的赋值。通过创建临时Any
对象,利用swap
方法实现数据的安全交换。 -
在
main
函数中,演示了如何使用这个简化的Any
类。首先创建一个Any
对象a
,然后在作用域内创建了一个test
对象t
,将其赋值给a
。接着,a
被赋值为整数和字符串,并通过get
方法获取这些数据进行输出。在程序结束时,对象的生命周期结束,相应的析构函数被调用。
这段代码实现了一个基本的类型擦除容器,允许在运行时存储和操作不同类型的数据。