003 仿muduo实现高性能服务器组件
🌈个人主页:Fan_558
🔥 系列专栏:仿muduo
🌹关注我💪🏻带你学更多知识
文章目录
- 前言
- 时间轮timewheel设计
- 正则表达式介绍(了解知道怎么使用)
- 通用型any容器的实现
- 小结
前言
在正式讲解模块设计前,将会介绍模块当中的一些前置知识以及组件设计(timewheel时间轮,正则表达式、通用型容器any)
时间轮timewheel设计
由于服务器的资源是有限的,为了避免某些客户端连接上来之后一直不通信而平白浪费服务器资源的情况,我们需要对非活跃连接设置定时销毁,而实现这个功能的前提是得有一个定时器。
这个小组件将会运用到TimerQueue子模块当中
Linux当中提供给我们了一个定时器,如下:
#include int timerfd_create(int clockid, int flags); clockid: CLOCK_REALTIME-系统实时时间,如果修改了系统时间就会出问题; CLOCK_MONOTONIC-从开机到现在的时间是⼀种相对时间; flags: 0-默认阻塞属性 int timerfd_settime(int fd, int flags, struct itimerspec *new, struct itimerspec *old); fd: timerfd_create返回的⽂件描述符 flags: 0-相对时间, 1-绝对时间;默认设置为0即可. new: ⽤于设置定时器的新超时时间 old: ⽤于接收原来的超时时间 struct timespec { time_t tv_sec; /* Seconds */ long tv_nsec; /* Nanoseconds */ }; struct itimerspec { struct timespec it_interval; /* 第⼀次之后的超时间隔时间 */ struct timespec it_value; /* 第⼀次超时时间 */ }; 定时器会在每次超时时,⾃动给fd中写⼊8字节的数据,表⽰在上⼀次读取数据到当前读取数据期间超 时了多少次。
以下是timefd的使用样例,了解即可
#include #include #include #include #include int main() { //创建定时器 int timerfd = timerfd_create(CLOCK_MONOTONIC, 0); if(timerfd
上述的例⼦,存在⼀个很⼤的问题,每次超时都要将所有的连接遍历⼀遍,如果有上万个连接,效率⽆疑是较为低下的
针对这个问题,我们可以以连接最近一次通信的系统时间为基准建立一个小跟堆,堆中的元素按照连接最近一次通信的系统时间排序,使得堆顶元素始终是最近一次通信时间最早的连接,然后就只需要不断取出堆顶已超时的连接执行超时任务,直到没有超时连接即可。
我们也可以采用时间轮的方法,时间轮的思想来源于钟表,由此我们可以设计出一个数组,由一个指针指向起始位置,只需要让这个指针每秒钟往后走一步,及秒针(tick)走到哪里执行哪里的超时连接销毁任务即可
1、如果我们的超时时间很长应该怎么办呢?
比如我们的超时时间为一天,我们是不是要定义一个 60 * 60 * 60s 的数组?解决办法很简单,我们可以将时间轮分级,即分别定义秒级时间轮、分级时间轮以及时级时间轮,如下:
此时我们仅需要 3 * 60 个整形的空间就可以实现 60 小时内的定时器了 (如果使用位图来定义定时轮仅需要 4*3 个字节的空间)。
2、同一时刻的定时任务只能添加一个,需要考虑如何在同一时间时刻支持添加多个定时任务
解决方案:将时间轮的一维数组设计为二维数组(时间轮)
3、 如何实现定时任务的延时?
解决方法:类的析构函数+智能指针share_ptr,通过这两个计数可以实现定时任务 1、使用一个类,对定时任务进行封装,类实例化的每一个对象,就是一个定时任务对象,当对象被销毁的时候即是执行定时任务的时候(将定时任务的执行放到析构函数当中) 但是当一个连接建立成功后,我们给这个连接设置了一个30s后定时销毁任务,但是在10s后这个连接进行了一次通信(连接非活跃30s后则定时销毁),此时我们应该在40s时才关闭连接,那么当连接进行通信的时候,我们需要重新刷新定时任务时间,类的实例化与销毁就不太适合用于刷新定时任务了,该如何做呢? 这⾥,我们就⽤到了智能指针shared_ptr,shared_ptr有个计数器,当计数为0的时候,才会真正释放 ⼀个对象,那么如果连接在第10s进⾏了⼀次通信,则我们继续向定时任务时间轮(_wheel)中添加⼀个30s后(也就 是第40s)的任务类对象的shared_ptr,则这时候两个任务shared_ptr计数为2,则第30s的定时任务被释放的时候,计数-1,变为1,并不为0,则并不会执⾏实际的析构函数,那么就相当于这个 第30s的任务失效了,只有在第40s的时候,计数器减为0,这个任务才会被真正释放。 基于这个思想,我们可以使用share_ptr来管理定时任务对象下面是秒级时间轮和定时任务对象类的代码实现
using TaskFunc = std::function; using ReleaseFunc = std::function; //定时任务类 class TimerTask{ public: TimerTask(uint64_t id, uint32_t delay, const TaskFunc &cb) :_id(id) ,_timeout(delay) ,_task_cb(cb) {} //析构的时候执行定时任务 ~TimerTask(){ if(_canceled == false) _task_cb(); //定时任务执行 _release(); //释放定时器任务对象 } //设置release_cb回调函数 void SetRelease(const ReleaseFunc& cb) { if(_canceled == false) _release = cb; } //返回定时器任务超时时间 uint64_t TimeOut() { return _timeout; } //取消定时任务 void Cancel() { _canceled = false; } private: uint64_t _id; //定时器任务对象ID uint32_t _timeout; //定时任务的超时时间 TaskFunc _task_cb; //定时器对象要执行的定时任务 ReleaseFunc _release; //用于删除TimerWheel中保存的定时器对象信息 bool _canceled; //定时任务是否被取消 }; using TaskWeak = std::weak_ptr; //别名:指向TimerTask类的对象强引用,会增加引用计数 using TaskPtr = std::shared_ptr; //别名:指向TimerTask类的对象弱引用,不会增加引用计数 //时间轮类 class TimerWheel{ public: TimerWheel() :_capacity(60) ,_tick(0) ,_wheel(_capacity) {} //添加定时任务(将管理任务对象的智能指针添加到时间轮当中) void TimerAdd(uint64_t id, uint32_t timeout, const TaskFunc &cb) { //使用智能指针管理定时类任务 TaskPtr tp(new TimerTask(id, timeout, cb)); //添加WeakPtr与id的关联关系 _timers[id] = TaskWeak(tp); //释放定时任务对象(这样做的好处可以添加当前任务id,及各种信息) tp->SetRelease(std::bind(&TimerWheel::RemoveTimer, this, id)); uint64_t pos = (timeout + _tick) % _capacity; //添加任务 _wheel[pos].push_back(tp); } // 刷新/延迟定时任务(查看定时任务是否还存在,存在就刷新时间轮) void TimerRefresh(uint64_t id) { auto it = _timers.find(id); if(it == _timers.end()) return; //刷新定时任务 TaskPtr tp = (it->second).lock(); int pos = (tp->TimeOut() + _tick) % _capacity; _wheel[pos].push_back(tp); } //取消定时任务(如果管理任务对象的智能指针存在,则设置任务对象状态为取消状态) void TimerCancel(uint64_t id) { auto it = _timers.find(id); if(it == _timers.end()) return; //未找到定时任务 TaskPtr tp = it->second.lock(); if(tp) tp->Cancel(); } //执行定时任务,此函数一秒被执行一次,相当于秒针向后走一步(清空时间轮当中的智能指针) void RunTimerTask() { _tick = (_tick + 1) % _capacity; _wheel[_tick].clear(); //清空数组指定的位置,将所有管理定时任务对象的share_ptr释放掉 } private: //SetRelease回调函数,表示任务已经执行完,从unordered_map中将定时任务信息删除 void RemoveTimer(uint64_t id) { auto it = _timers.find(id); if(it != _timers.end()) _timers.erase(it); } int _tick; //秒针:走到哪里,执行哪里的任务 int _capacity; //表盘的最大数量---最大延迟时间 std::vector _wheel; //时间轮 std::unordered_map _timers; //定时器任务id与管理定时任务对象的weak_ptr之间的关联关系 }; class Test { public: Test() { std::cout std::cout delete t; } int main() { Test *t = new Test(); TimerWheel tw; tw.TimerAdd(1, 5, std::bind(Deltest, t)); // 刷新定时任务 for(int i = 0; i