003 仿muduo实现高性能服务器组件_前置知识

​🌈个人主页: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, &times, 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、同一时刻的定时任务只能添加一个,需要考虑如何在同一时间时刻支持添加多个定时任务

解决方案:将时间轮的一维数组设计为二维数组(时间轮)

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<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模块的实现

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/18541.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

让 AI 回答更精准 来学学这些Prompt入门小技巧

&#x1f3a5;前言 最近一直在研究各种 AI 提问相关的方法&#xff0c;一顿输入后&#xff0c;get到了好多有趣又好玩的提问小技巧。今天就来和小伙伴们安利下&#xff0c;平常在向AI提问时&#xff0c;最最基础&#xff0c;且最最实用的6种提示词方法。 那废话不多说&#x…

如何使用git上传linux下的项目!---附带每一步截图

在实际项目中&#xff0c;我们需要把自己的模块递给GitHub&#xff0c;需要别人的模块的时候拉下来&#xff0c;那么我们怎么把自己的项目递给GitHub呢&#xff1f;下面做一个总结&#xff1a; 登录GitHub 创建一个仓库 填写相关信息 项目名称是必填的&#xff0c;项目描述可以…

FaceChain-FACT:开源10秒写真生成,复用海量LoRa风格,基模友好型写真应用

github开源地址&#xff1a;https://github.com/modelscope/facechain/tree/main/facechain_adapter 魔搭创空间应用体验&#xff1a;魔搭社区 一、效果演示 FaceChain FACT的代码和模型目前已经在github和modelscope创空间上同步开源。FaceChain FACT具有简单的交互式界面设…

AI图书推荐:基于ChatGPT API和Python开发应用程序的详细指南

ChatGPT已经以其革命性的能力引起了人们的关注&#xff0c;利用其API可能会成为你的游戏规则改变者。这不仅仅是关于编码&#xff1b;它是关于为您的创作添加一层智能&#xff0c;将它们提升到之前无法想象的水平。《基于ChatGPT API和Python开发应用程序的详细指南》&#xff…

[图解]企业应用架构模式2024新译本讲解03-事务脚本+表数据入口

1 00:00:00,570 --> 00:00:06,290 这里先创建一个service的对象 2 00:00:07,000 --> 00:00:12,470 然后调用对象的方法、操作 3 00:00:12,480 --> 00:00:13,750 就是事务脚本 4 00:00:14,700 --> 00:00:15,900 然后参数是1 5 00:00:16,660 --> 00:00:19,490…

《公正》孙溟㠭艺术

孙溟㠭艺术《公正》 孙溟㠭艺术《公正》

利用NewGIS平台将FME模板发布为接口

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 前言 一、模板编写 二、发布模板 三、接口获取 四、移动端调用 ​​​​​ 前言 在实际的应用生产过程中&#xff0c;尤其是移动端GIS软件的开发&#xff0c;针对一些闭…

12.2 通道-阻塞与流程控制、通道型函数、退出通道

阻塞与流程控制 通常在并发程序中要尽力避免阻塞式操作&#xff0c;但有时又需要让代码暂时处于阻塞状态&#xff0c;以等待某种条件、信号或数据&#xff0c;然后再继续运行。 对于无缓冲通道&#xff0c;试图从无人写入的通道中读取&#xff0c;或者向无人读取的通道中写入…

SQL学习小记(三)

SQL学习小记&#xff08;三&#xff09; 功能实现思路代码部分名词解释 代码打包为可执行文件 功能说明&#xff1a;使用python代码&#xff0c;将数据库database1中的表格table1同步到数据库database2中 功能实现 思路 #mermaid-svg-R1pWrEWA799M299a {font-family:"tre…

Rocksdb原理简介

100编程书屋_孔夫子旧书网 Rocksdb作为当下nosql中性能的代表被各个存储组件&#xff08;mysql、tikv、pmdk、bluestore&#xff09;作为存储引擎底座&#xff0c;其基于LSM tree的核心存储结构&#xff08;将随机写通过数据结构转化为顺序写&#xff09;来提供高性能的写吞吐时…

Visual Studio 的使用

目录 1. 引言 2. 安装和配置 2.1 系统要求 2.2 安装步骤 2.3 初次配置 3. 界面介绍 3.1 菜单栏和工具栏 3.2 解决方案资源管理器 3.3 编辑器窗口 3.4 输出窗口 3.5 错误列表 3.6 属性窗口 4. 项目管理 4.1 创建新项目 4.2 导入现有项目 4.3 项目属性配置 5. 代…

当传统文化遇上数字化,等级保护测评的必要性

第二十届中国&#xff08;深圳&#xff09;国际文化产业博览交易会5月23日在深圳开幕。本届文博会以创办20年为契机&#xff0c;加大创新力度&#xff0c;加快转型升级&#xff0c;着力提升国际化、市场化、专业化和数字化水平&#xff0c;不断强化交易功能&#xff0c;打造富有…

《软件方法(下)》8.3.4.6 DDD话语“聚合”中的伪创新(1)

DDD领域驱动设计批评文集 做强化自测题获得“软件方法建模师”称号 《软件方法》各章合集 8.3 建模步骤C-2 识别类的关系 8.3.4 识别关联关系 8.3.4.6 DDD话语“聚合”中的伪创新 DDD话语中也有“聚合”。Eric Evans的“Domain-Driven Design: Tackling Complexity in the…

在今日头条上写文章:ChatGPT完整使用教程

了解如何充分运用ChatGPT进行创作 简介 在今日头条上发布文章变得越来越方便。本文旨在详细解析如何运用ChatGPT来创作文章&#xff0c;并提供全方位的使用指南及常见问题的答疑。 第一步&#xff1a;基础准备 确保你已注册今日头条账号。 登录ChatGPT并与你的今日头条账号进…

软件测试经理工作日常随记【6】-利用python连接禅道数据库并自动统计bug数据到钉钉群

测试管理_利用python连接禅道数据库并统计bug数据到钉钉 这篇不多赘述&#xff0c;直接上代码文件。 另文章基础参考博文&#xff1a;参考博文 加以我自己的需求优化而成。 统计的前提 以下代码统计的前提是禅道的提bug流程应规范化 bug未解决不删除bug未关闭不删除 db_…

LuatOS学习

开发顺序 Lua是脚本语言中运行速度最快的语言 资源占用极低 脚本语言运行方式 脚本语言是从上往下一行一行运行的 变量 coun 123456 a,b,c 1,2,3交换 a,b b,a在测试环境中&#xff0c;用print(a,b)打印 nil类型 未声明的变量就是nil&#xff0c;nil用来表示此变量为空…

STM32高级控制定时器(STM32F103):检测输入PWM周期和占空比

目录 概述 1 PWM 输入模式 1.1 原理介绍 1.2 应用实例 1.3 示例时序图 2 使用STM32Cube配置工程 2.1 软件环境 2.2 配置参数 2.3 生成项目文件 3 功能实现 3.1 PWM占空比函数 3.2 输入捕捉回调函数 4 功能测试 4.1 测试软件框架结构 4.2 实验实现 4.2.1 测试实…

视觉语音识别挑战赛 CNVSRC 2024

CNVSRC 2024由NCMMSC 2024组委会发起&#xff0c;清华大学、北京邮电大学、海天瑞声、语音之家共同主办。竞赛的目标是通过口唇动作来推断发音内容&#xff0c;进一步推动视觉语音识别技术的发展。视觉语音识别&#xff08;也称为读唇技术&#xff09;是一种通过观察唇部动作推…

二叉树顺序结构实现【堆的实现】【详细图解】

P. S.&#xff1a;以下代码均在VS2019环境下测试&#xff0c;不代表所有编译器均可通过。 P. S.&#xff1a;测试代码均未展示头文件stdio.h的声明&#xff0c;使用时请自行添加。 目录 1、二叉树的顺序结构2、堆的概念3、堆的实现3.1 堆实现的前提3.1.1 向上调整3.1.2 向下调…

采用java语言+B/S架构+后端SpringBoot前端Vue开发的ADR药品不良反应智能监测系统源码

采用java语言&#xff0b;B/S架构&#xff0b;后端SpringBoot前端Vue开发的ADR药品不良反应智能监测系统源码 ADR监测引擎每日主动获取检验数据、病历内容&#xff08;可拓展&#xff09;、以及其他临床数据&#xff0c;根据知识库内容自动判定患者是否有不良反应迹象&#xf…