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…

java全套idea jdk maven历史版本下载

idea多版本 Other Versions - IntelliJ IDEA jdk&#xff0c;需要登陆 https://www.oracle.com/java/technologies/downloads/ maven多版本 Maven – Maven Releases History

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

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

语音控制系统的安全挑战与防御策略(中)

2.3 核心层 在VCS中&#xff0c;核心层负责解码预处理层处理后的音频特征向量。这种解码传统上是通过声学模型实现的&#xff0c;它将特征向量转换为音素&#xff0c;并使用语言模型预测词序列概率&#xff0c;以输出最可能的句子。目前Wav2Letter&#xff08;基于CNN&#xf…

「架构」模型驱动架构设计方法及其运用

本文通过一个实际的软件项目案例,深入探讨了模型驱动架构(MDA)在软件开发全过程中的应用。MDA是一种以模型为中心的设计方法,它通过分离计算、数据和业务逻辑,提高了软件的可维护性、可扩展性和可移植性。文章将从需求分析、架构设计、实现与测试三个阶段出发,分析MDA的应…

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;针对一些闭…

【社区投稿】给 NdArray 装上 CUDA 的轮子

Ndarry是Rust编程语言中的一个高性能多维、多类型数组库。它提供了类似 numpy 的多种多维数组的算子。与 Python 相比 Rust 生态缺乏类似 CuPy, Jax 这样利用CUDA 进行加速的开源项目。虽然 Hugging Face 开源的 candle 可以使用 CUDA backend 但是 candle 项瞄准的是大模型的相…

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

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

Redis教程(二十一):Redis怎么保证缓存一致性

传送门:Redis教程汇总篇,让你从入门到精通 Redis 的缓存一致性 Redis 的缓存一致性是指在使用 Redis 作为缓存层时,保证缓存中的数据与数据库中的数据保持一致的状态。在分布式系统中,数据一致性是一个重要的问题,因为可能存在多个客户端同时读写同一数据,或者数据在不同…

【量算分析工具-贴地面积】GeoServer改造Springboot番外系列十

【量算分析工具-概述】GeoServer改造Springboot番外系列三-CSDN博客 【量算分析工具-水平距离】GeoServer改造Springboot番外系列四-CSDN博客 【量算分析工具-水平面积】GeoServer改造Springboot番外系列五-CSDN博客 【量算分析工具-方位角】GeoServer改造Springboot番外系列…

SQL学习小记(三)

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

go语言方法之基于指针对象的方法

当调用一个函数时&#xff0c;会对其每一个参数值进行拷贝&#xff0c;如果一个函数需要更新一个变量&#xff0c;或者 函数的其中一个参数实在太大我们希望能够避免进行这种默认的拷贝&#xff0c;这种情况下我们就需 要用到指针了。对应到我们这里用来更新接收器的对象的方法…

域名服务器是什么?

所谓域名服务器&#xff08;即Domain Name Server&#xff0c;简称Name Server、DNS&#xff09;实际上就是装有域名系统的主机。它是一种分层结构数据库&#xff0c;能够执行域名解析。

Rocksdb原理简介

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

Google Benchmark库 简介

在C中&#xff0c;进行性能测试&#xff08;Benchmarking&#xff09;是一个常见的需求&#xff0c;用以测量代码块的执行时间&#xff0c;从而对代码进行优化。Google Benchmark库是一个广泛使用的C库&#xff0c;专门用于编写稳健的基准测试。以下是如何使用Google Benchmark…

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. 代…