🌈个人主页:Fan_558
🔥 系列专栏:仿muduo
🌹关注我💪🏻带你学更多知识
文章目录
- 前言
- 时间轮timewheel设计
- 正则表达式介绍(了解知道怎么使用)
- 通用型any容器的实现
- 小结
前言
在正式讲解模块设计前,将会介绍模块当中的一些前置知识以及组件设计(timewheel时间轮,正则表达式、通用型容器any)
时间轮timewheel设计
由于服务器的资源是有限的,为了避免某些客户端连接上来之后一直不通信而平白浪费服务器资源的情况,我们需要对非活跃连接设置定时销毁,而实现这个功能的前提是得有一个定时器。
这个小组件将会运用到TimerQueue子模块当中
Linux当中提供给我们了一个定时器,如下:
#include <sys/timerfd.h>
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 <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdint.h>
#include <sys/timerfd.h>int main()
{//创建定时器int timerfd = timerfd_create(CLOCK_MONOTONIC, 0);if(timerfd < 0){perror("timefd_create error");return -1;}struct itimerspec itime;//设置首次超时时间itime.it_value.tv_sec = 1; //第一次超时在1s后itime.it_value.tv_nsec = 0;//设置后续超时时间间隔itime.it_interval.tv_sec = 3; //第一次超时后,每隔3秒间隔超时一次itime.it_interval.tv_nsec = 0;timerfd_settime(timerfd, 0, &itime, NULL);//从timefd中读取超时次数while(1){uint64_t times;int ret = read(timerfd, ×, 8);if(ret < 0){perror("read error");return -1;}printf("超时,距离上次超时了%ld次\n",times);}close(timerfd);
}
上述的例⼦,存在⼀个很⼤的问题,每次超时都要将所有的连接遍历⼀遍,如果有上万个连接,效率⽆疑是较为低下的
针对这个问题,我们可以以连接最近一次通信的系统时间为基准建立一个小跟堆,堆中的元素按照连接最近一次通信的系统时间排序,使得堆顶元素始终是最近一次通信时间最早的连接,然后就只需要不断取出堆顶已超时的连接执行超时任务,直到没有超时连接即可。
我们也可以采用时间轮的方法,时间轮的思想来源于钟表,由此我们可以设计出一个数组,由一个指针指向起始位置,只需要让这个指针每秒钟往后走一步,及秒针(tick)走到哪里执行哪里的超时连接销毁任务即可
1、如果我们的超时时间很长应该怎么办呢?
比如我们的超时时间为一天,我们是不是要定义一个 60 * 60 * 60s 的数组?解决办法很简单,我们可以将时间轮分级,即分别定义秒级时间轮、分级时间轮以及时级时间轮,如下:
此时我们仅需要 3 * 60 个整形的空间就可以实现 60 小时内的定时器了 (如果使用位图来定义定时轮仅需要 4*3 个字节的空间)。
2、同一时刻的定时任务只能添加一个,需要考虑如何在同一时间时刻支持添加多个定时任务
解决方案:将时间轮的一维数组设计为二维数组(时间轮)
解决方法:类的析构函数+智能指针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来管理定时任务对象3、 如何实现定时任务的延时?
下面是秒级时间轮和定时任务对象类的代码实现
using TaskFunc = std::function<void()>;
using ReleaseFunc = std::function<void()>;//定时任务类
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; //定时器任务对象IDuint32_t _timeout; //定时任务的超时时间TaskFunc _task_cb; //定时器对象要执行的定时任务ReleaseFunc _release; //用于删除TimerWheel中保存的定时器对象信息bool _canceled; //定时任务是否被取消};
using TaskWeak = std::weak_ptr<TimerTask>; //别名:指向TimerTask类的对象强引用,会增加引用计数
using TaskPtr = std::shared_ptr<TimerTask>; //别名:指向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<std::vector<TaskPtr>> _wheel; //时间轮std::unordered_map<uint64_t, TaskWeak> _timers; //定时器任务id与管理定时任务对象的weak_ptr之间的关联关系
}; class Test {
public:Test() { std::cout << "构造" << std::endl; }~Test() { std::cout << "析构" << std::endl; }
};void Deltest(Test *t) { delete t; }int main()
{Test *t = new Test();TimerWheel tw;tw.TimerAdd(1, 5, std::bind(Deltest, t));// 刷新定时任务for(int i = 0; i < 5; i++) {sleep(1);// 刷新定时任务tw.TimerRefresh(1); // 向后移动秒针tw.RunTimerTask();std::cout << "刷新,定时任务5s后启动" << std::endl;}while(true) {std::cout << "滴答滴答..." << std::endl;tw.RunTimerTask();sleep(1);}return 0;
}
正则表达式介绍(了解知道怎么使用)
由于我们要实现的是一个带有应用层协议 (HTTP) 支持的服务器组件,因此必然会涉及到对 HTTP 请求的解析,比如我们接收到了这样的一个 HTTP 请求
那么我们需要从 HTTP 请求中提取出以下的信息:
GET – 请求方法,我们需要针对不同的请求方法执行不同的操作
/login – 请求URL,我们需要知道客户端想要访问哪里的数据
user=Fan_558&pass=123456 – 请求数据
HTTP/1.1 – 协议版本
正则表达式是基于某种字符串匹配规则来提取字符串中的特定数据。
对于简单的 regex 使用,我们只需要掌握regex_match 函数的使用即可:
bool regex_match(string src, smatch matches, regex e);
src: 用于匹配的原始字符串;
e: 字符串的匹配规则;
matches: 用于存在根据匹配规则e对原始字符串src进行匹配得到的结果;
返回值:匹配成功返回true,匹配失败返回false
常用的正则表达式的匹配规则:
\
将下一个字符标记为一个特殊字符、或一个原义字符、或一个向后引用、或一个八进制转义符
*
匹配前面的子表达式零次或多次
+
匹配前面的子表达式一次或多次
?
匹配前面的子表达式零次或一次
.
匹配除“\n”之外的任何单个字符
x|y
匹配x或y
代码示例
bool test() {std::string str = "GET /login?user=zhangsan&pass=123456 HTTP/1.1\r\n";std::smatch matches;// 提取请求方法:(GET|POST|PUT|DELETE|HEAD) // |表示或者 ()表示提取匹配结果 (GET|POST|PUT|DELETE|HEAD)整体表示提取其中任意一个字符串// 提取请求URI:_([^?]*) // 我们用_表示空格 [^?]表示匹配除?号外的所有字符 *表示可以多次匹配// 提取数据:\\?(.*) //\\?表示普通的?字符//(.*)表示可以多次匹配任意字符并提取//外边的(?:)?表示如果没有数据则匹配但不提取//提取协议版本:_ (HTTP/1\\.[01]) // _表示空格//HTTP/1\\.表示匹配原字符串中的HTTP/1\\. 其中\\.表示普通的. 最后[01]表示匹配0或者1// \r\n处理:(?:\n|\r\n)? //(?:xxx)表示匹配xxx格式字符串但不提取 最后的?表示执行前面的表达式一次或零次std::regex e("(GET|POST|PUT|DELETE|HEAD) ([^?]*)(?:\\?(.*))? (HTTP/1\\.[01])(?:\n|\r\n)?");if(std::regex_match(str, matches, e) == false) return false;for(int i = 0; i < matches.size(); i++) {std::cout << i << ":" << matches[i] << std::endl;}return true;
}
通用型any容器的实现
由于后续将会对socket内核接收缓冲区到来的数据进行HTTP协议格式的解析处理,我们需要拿到一个完整的HTTP请求,由于 TCP
是面向字节流的,因此服务器在接收客户端数据的时候就可能出现 socket
中的数据不足一条完整请求的情况,因此我们需要为客户端连接设置一个请求处理的上下文,用来保存请求接收,解析以及处理的状态,它决定着下一次从缓冲区中取出的数据如何进行处理,从哪里开始处理等。同时对于一条完整的请求,我们还需要对其进行解析,得到各种关键的要素,比如HTTP请求中的请求方法、请求URL、HTTP版本等,这些信息都会被保存到请求处理的上下文当中。另外地,今天我们上层使用的是HTTP协议,服务器可支持的协议可能会不断增多,不同的协议,可能都会有不同的上下文结构,所以需要有一个容器结构不同结构的数据
C++中,boost库和C++17给我们提供了⼀个通⽤类型any来灵活使⽤,如果考虑增加代码的移植性,尽量减少第三⽅库的依赖,则可以使⽤C++17特性中的any,或者⾃⼰来实现。
这里我们选择自己实现
如何设计一个可以保存任意类型数据的容器呢?我们首先能够想到的起始时模板类,模板可以接收任意类型的数据,但是当实例化对象的时候,必须传入一个指定的类型
预期设计:
不带有任何的特定的类型,但是可以保存或接收各种类型的数据
Any a;
a = "Fan";
a = 1;
于是我们便可以在Any类中设计一个继承体系,子类placeHolder专门用于保存其它类型的数据,而父类Holder的指针去指向派生类,父类只是一个普通类不带有模板参数,再将父类Holder的指针作为Any类的成员变量,这样就可以达到我们的预期设计,不带有任何特定的类型(实例化对象的时候不需要指定类型),就可以保存任意数据类型的数据
class Any{
private:class Holder{}template<class T>class placeHolder : public Holder{public:T _val;};Holder* _content;
};
具体代码如下
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 ~placeHolder() {}//获取子类对象保存的数据类型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());//强转程子类对象类型从而能访问到子类的_valreturn &((placeHolder<T>*)_content)->_val;}template<class T>//赋值运算符的重载函数Any& operator=(const T &val){//为val构造一个临时的通用容器,然后与当前容器自身进行指针交换,临时释放对象的时候,原先保存的书也就被释放Any(val).swap(*this);return *this;}Any& operator=(const Any &other){//根据一个other构造一个对象Any(other).swap(*this);return *this;}
};
测试
int main()
{Any a;a = 10;int* pa = a.get<int>();std::cout << *pa << std::endl;a = std::string("Fan_558");std::string* ps = a.get<std::string>();std::cout << *ps << std::endl;return 0;
}
小结
下一篇将会向你带来Buffer模块与Socket模块的实现