【项目】仿muduo库one thread one loop式并发服务器前置知识准备
📚 博主的专栏
🐧 Linux | 🖥️ C++ | 📊 数据结构 | 💡C++ 算法 | 🅒 C 语言 | 🌐 计算机网络 |🗃️ mysql
本文介绍了一种基于muduo库实现的主从Reactor模型高并发服务器框架以及前置知识准备。框架采用OneThreadOneLoop设计思想,主Reactor负责监听连接,子Reactor处理通信,实现高效并发。前置知识包括:1. 使用timerfd_create和timerfd_settime实现秒级定时任务;2. 设计时间轮定时器管理连接超时;3. 应用正则表达式解析HTTP请求;4. 实现通用any类型容器存储不同协议上下文。测试表明,该框架可有效支持高并发场景,并提供了灵活的业务逻辑扩展接口。
目录
一、项目初了解
1.1 目标定位:One Thread One Loop主从Reactor模型高并发服务器
1.2 模块关系图:
二、前置知识技术点功能用例:
2.1 C++11中的bind:这篇文章有详细的讲解
2.2 高效实现秒级定时任务:
2.2.1 Linux系统提供了以下定时器解决方案:
示例:
2.2.2 时间轮定时器的基本思想理解以及设计完善
时间轮
2.2.3 时间轮定时器的代码设计
***智能指针的使用***
2.2.4 时间轮定时器的代码实现
2.2.5 时间轮定时器的代码测试
2.3 正则库的简单使用
2.3.1 正则表达式基本认识
正则表达式匹配函数
2.3.2 正则表达式提取 HTTP 请求方法
2.3.3 正则表达式提取 HTTP 请求路径
2.3.4 正则表达式提取 HTTP 查询字符串
2.3.5 正则表达式提取 HTTP 协议版本
2.3.6 正则表达式提取 HTTP 元素细节完善
情况1:所给字符串后边跟了\r\n,用以上的e,无法提取出字符串
情况2:所给字符串根本没有查询字符串?user=pupu&passwd=12312
2.4 实现通用的any类型:
2.4.1通用类型容器any类设计思想
2.4.2通用类型容器any类结构设计
2.4.3通用类型容器any类功能实现
2.4.4 通用类型容器any类功能测试
2.4.5 通用类型容器C++17中any的使用
一、项目初了解
基于muduo库的One Thread One Loop主从Reactor模型高并发服务器实现:
我们实现的高并发服务器组件能够快速搭建高性能服务器架构。该组件提供多种应用层协议支持,可便捷构建高性能应用服务器(项目演示中已内置HTTP协议组件支持)。
需要说明的是,本项目定位为高并发服务器组件框架,因此不包含具体业务逻辑实现。
1.1 目标定位:One Thread One Loop主从Reactor模型高并发服务器
我们将采用主从Reactor模型构建服务器,其中主Reactor线程专门负责监听连接请求,确保高效处理新连接,从而提升服务器并发性能。
当主Reactor获得新连接后,会将其分配给子Reactor进行通信。各子Reactor线程独立监控其负责的描述符,处理读写事件并完成数据传输及业务逻辑处理。
One Thread One Loop的核心思想是将所有操作集中在一个线程内完成,每个线程对应一个独立的事件处理循环。
当前实现考虑到组件使用者的多样化需求,默认仅提供主从Reactor模型,而不内置业务层工作线程池。Worker线程池的实现与否,完全由组件使用者根据实际需求自行决定。
1.2 模块关系图:
二、前置知识技术点功能用例:
2.1 C++11中的bind:这篇文章有详细的讲解
bind (Fn&& fn, Args&&... args);
见见使用:
#include <iostream>
#include <string>
#include <functional>void print(const std::string &str, int num)
{std::cout << str << num << std::endl;
}int main()
{auto func = std::bind(print, "hello", std::placeholders::_1);func(10);return 0;
}
利用bind的特性,在设计线程池或任务池时,可以将任务设置为函数类型。通过bind直接绑定任务函数的参数,任务池只需取出并执行这些预绑定好的函数即可。
这种设计的优势在于:任务池不需要关心具体任务的处理方式、函数设计或参数数量,有效降低了代码之间的耦合度。
#include <iostream>
#include <string>
#include <functional>
#include <vector>
void print(const std::string &str, int num)
{std::cout << str << num << std::endl;
}int main()
{using Task = std::function<void()>;std::vector<Task> array;array.push_back(std::bind(print, "hello", 10));array.push_back(std::bind(print, "linux", 20));array.push_back(std::bind(print, "c++", 30));array.push_back(std::bind(print, "pupu", 40));for (auto &f : array){f();}return 0;
}
2.2 高效实现秒级定时任务:
在高并发服务器环境中,连接超时管理至关重要。长时间闲置的连接会持续占用系统资源,因此需要及时关闭这些无效连接。
为此,我们需要一个精准的定时任务机制,定期清理超时连接。
2.2.1 Linux系统提供了以下定时器解决方案:
timerfd_create-创建定时器
功能:创建一个定时器
#include <sys/timerfd.h>int timerfd_create(int clockid, int flags);/** 参数说明:* clockid: * - CLOCK_REALTIME 系统实时时间(系统时间发生了改变就会出问题)* - CLOCK_MONOTONIC 系统启动后的单调时间(相对时间,定时不会随着系统时间的改变而改变)* * flags:* - 0 默认阻塞模式*/ 返回值:文件描述符
Linux下“一切皆文件”,定时器的操作也是跟文件操作并没有什么区别,而定时器的原理就是:
每隔一段时间(定时器的超时时间),系统就会给这个描述符对应的定时器写入一个8字节的数据
创建了一个定时器,定时器定立的超时时间是3s,也就是说每3s算一次超时
从启动开始,每隔3s中,系统就会给fd写入一个1,表示从上一次读取数据到现在超时了1次
假设30s之后才读取数据,则这时候就会读取到一个10,表示上一次读取数据到限制超时了10次
timerfd_settime-启动定时器
功能:启动定时器
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字节数据,该数据表示从上次读取操作到当前读取操作之间发生的超时次数。
示例:
#include <stdio.h> #include <unistd.h> #include <fcntl.h> #include <sys/timerfd.h> #include <cstdint>int main() {// 创建一个定时器int timerfd = timerfd_create(CLOCK_MONOTONIC, 0);if (timerfd < 0){perror("timerfd_create failed\n");return -1;}struct itimerspec itime;// 设置第一次超时时间为3s后itime.it_value.tv_sec = 3;itime.it_value.tv_nsec = 0;// 第一次超时后,每次超时的间隔时间itime.it_interval.tv_sec = 3;itime.it_interval.tv_nsec = 0;timerfd_settime(timerfd, 0, &itime, NULL);while (1){uint64_t times;int ret = read(timerfd, ×, 8);if (ret < 0){perror("read error\n");return -1;}printf("超时了,距离上一次超时了%ld次\n", times);}close(timerfd);return 0; }
这是一个定时器使用的示例,每3秒会触发一次超时事件,否则程序会阻塞在read数据读取操作上。基于此例,我们可以实现每3秒检测一次超时连接,并将超时的连接释放。
2.2.2 时间轮定时器的基本思想理解以及设计完善
时间轮
这个示例存在一个明显问题:每次超时都需要遍历所有连接,当连接数量达到上万时,效率会非常低下。
为此,我们可以改进方案:根据每个连接最后一次通信的系统时间建立小根堆。这样只需检查堆顶的连接,逐个释放超时的连接即可,这将显著提升处理效率。
虽然上述方法能实现定时任务,但这里要介绍另一种更优的解决方案:时间轮
时间轮的灵感来自钟表机制。就像设定3点钟闹钟后,时针走到3时就会触发铃声。
同样地,我们可以定义一个数组和一个指针,指针每秒移动一个位置。当指针移动到某个位置时,就执行该位置对应的任务。
具体实现时,如果想设定3秒后的任务,只需将任务添加到指针当前位置+3的位置。随着指针每秒移动一步,3秒后就会到达对应位置,执行该任务。
然而,在同一个时间点可能会出现大量定时任务同时触发的情况。为此,我们可以为数组的每个位置创建一个子数组(下拉数组),从而允许在相同时间点存储多个定时任务。
tick(滴答指针,指向哪里,就表示哪里的任务超时了,3s后被执行)
如果tick滴答,是以秒作为计时单位,则当前这数组有7个元素,则最大定时时间就只有7s。
如果定时器想要设置一个超大时间的定时任务就可以使用多级时间轮
多级时间轮(这里是一个天级:60s 60min 24h)
缺陷:
1.同一时刻的定时任务只能添加一个,需要考虑如何在同一时刻支持添加多个定时任务
解决方案:将时间轮的一维数组设计为二维数组(时间轮一维数组的每一个节点也是一个数组)
2.假设当前的定时任务是一个连接的非活跃销毁任务,这个任务什么时候添加到时间轮中比较合适?
一个连接30s内都没有通信,则是一个非活跃连接,这时候就销毁。但是一个连接在建立的时候添加了一个30s后销毁的任务,并且这个连接30s内人家有数据通信,在第30s的时候就不是一个非活跃连接。
思想:需要在一个连接有IO事件产生的时候,延迟定时任务的执行。
作为一个时间轮定时器,本身并不关注任务类型,只要是时间到了,就需要被执行(我们要研究的是如何绕开,并且让该任务延迟)
解决方案:类的析构函数+智能指针shared_ptr,通过这两个技术可以实现定时任务的延时
1.使用一个类,对定时任务进行封装,类实例化每一个对象,就是一个定时任务对象,当对象被销毁的时候,再去执行定时任务(将定时任务的执行,放到析构函数中)
2.shared_ptr用于对new的对象进行空间管理,当shared_ptr对一个对象进行管理的时候,内部有一个计数器,计数器为0的时候,则释放所管理的对象。
int *a = new int;
std::shared_ptr<int> pi(a); ---a对象只有在pi计数为0的时候才会被释放
std::shared_ptr<int>pi1(pi);--针对pi又构建了一个shared_ptr对象,则pi和pi1计数器为2
当pi和pi1中任意一个被释放的时候,只有计数器-1,因此他们管理的a对象并没有被释放,只有当pi和pi1都被释放了,计数器为0了,这时候才会释放管理的a对象
基于这个思想,我们可以使用shared_ptr来管理定时器任务对象,智能指针的使用详解
shared_ptr来管理定时器任务对象
在实现过程中,我们采用了智能指针shared_ptr。shared_ptr通过引用计数器管理对象生命周期,只有当计数归零时才会释放资源。假设连接在第10秒进行一次通信,我们会向定时任务队列中添加一个30秒后(即第40秒)执行的任务类对象的shared_ptr。此时两个任务shared_ptr的引用计数变为2。当第30秒的定时任务释放时,计数减1变为1,由于不为0,不会触发实际析构。这意味着第30秒的任务自动失效,而真正的资源释放会延迟到第40秒的任务执行时才完成
2.2.3 时间轮定时器的代码设计
#include <memory>
#include <functional>
#include <iostream>
#include <vector>
#include <unordered_map>
#include <cstdint>using TaskFunc = std::function<void()>; // 定时任务函数类型
using ReleaseFunc = std::function<void()>;
class TimerTask
{
private:uint64_t _id; // 定时器任务对象IDuint32_t _timeout; // 定时任务的超时时间TaskFunc _task_cb; // 定时器对象要执行的定时任务ReleaseFunc _release; // 用于删除TimerWheel中保存的定时器对象信息
public:TimerTask(uint64_t id, uint32_t delay /*延迟时间*/, const TaskFunc &cb) : _id(id), _timeout(delay), _task_cb(cb) {};~TimerTask(){_task_cb();_release();}void SetRelease(const ReleaseFunc &cb) { _release = cb; }
};class TimerWheel
{
private:using WeakTask = std::weak_ptr<TimerTask>;using PtrTask = std::shared_ptr<TimerTask>;int _tick; // tick走到哪里就释放哪里的对象,释放哪里,就相当于执行哪里的任务int _capacity; // 表盘最大数量---其实就是最大延迟时间// 当我们要二次添加同一个定时器任务对象的时候,得能够找到他们的同一个计数器,使用weak_ptr辅助shared_ptr// 保存所有定时器的weak_ptr对象,因为只有保存了WeakTask才有可能通过WeakTask构造出新的shared_ptr,// 并且他们共享计数,并且WeakTask自身不影响计数std::vector<std::vector<PtrTask>> _wheel;std::unordered_map<uint64_t, WeakTask> _timers;public:TimerWheel() : _capacity(60), _tick(0), _wheel(_capacity) {}void TimerAdd(uint64_t id, uint32_t delay, const TaskFunc &cb); // 添加定时任务void TimerRefresh(uint64_t id); // 刷新/延迟定时任务
};
这段代码实现了一个基于定时轮盘(Timer Wheel)的定时任务调度器,用于管理和执行定时任务。以下是详细的代码讲解:
1. TimerTask
类
功能 :表示一个定时任务,封装了任务的 ID、延迟时间、任务回调函数以及释放回调函数。
主要成员变量 :
_id
:定时器任务对象的唯一标识符,用于区分不同的定时任务。
_timeout
:定时任务的超时时间,即任务需要延迟执行的时间。
_task_cb
:定时任务的具体执行逻辑,当定时任务到期时,会调用这个回调函数来执行相应的操作。
_release
:用于删除TimerWheel
中保存的定时器对象信息的回调函数,在定时任务执行完毕或被取消时,会调用这个函数来清理资源。构造函数 :初始化定时任务的 ID、延迟时间和任务回调函数。
析构函数 :在定时任务对象被销毁时,执行任务回调函数
_task_cb
和释放回调函数_release
,以确保任务被执行并且相关资源被正确释放。
SetRelease
方法 :用于设置释放回调函数_release
,以便在需要时能够清理TimerWheel
中的定时任务记录。
2. TimerWheel
类
功能 :一个定时轮盘,用于管理多个定时任务,支持添加新的定时任务和刷新已存在的定时任务的延迟时间。
主要成员变量 :
_wheel
:一个二维向量,模拟定时轮盘的结构。每个元素是一个包含PtrTask
(std::shared_ptr<TimerTask>
类型)的向量,代表定时轮盘上的一个槽(slot)。定时任务根据其延迟时间被放置在相应的槽中,当轮盘的指针(_tick
)移动到该槽时,就会执行其中的定时任务。
_tick
:表示定时轮盘当前所指向的槽的位置。随着时间的推移,_tick
会不断递增,当达到_capacity
时,会重新从 0 开始循环,模拟轮盘的旋转。
_capacity
:定时轮盘的最大容量,即轮盘上槽的数量,同时也是定时任务的最大延迟时间限制。在这个例子中,初始容量被设置为 60,意味着定时任务的延迟时间不能超过 60 个时间单位(具体的时间单位可以根据实际应用场景来定义,例如秒、毫秒等)。
_timers
:一个无序映射(std::unordered_map
),用于保存所有定时任务的弱引用(std::weak_ptr<TimerTask>
)。键是定时任务的 ID,值是对应的弱引用。通过使用弱引用,可以在不增加引用计数的情况下,跟踪定时任务对象,并且可以在需要时通过弱引用来构造新的共享指针,从而访问定时任务对象。
***智能指针的使用***
使用
std::shared_ptr<TimerTask>
(PtrTask
)来管理定时任务对象的生命周期,确保多个部分可以安全地共享对定时任务对象的访问,并且对象会在所有共享指针都释放后自动被销毁。使用
std::weak_ptr<TimerTask>
(WeakTask
)来保存定时任务的弱引用,避免在_timers
映射中直接保存共享指针而导致对象的引用计数增加,从而防止定时任务对象被意外地延长生命周期。通过弱引用,可以在需要时检查定时任务对象是否仍然存在,并在存在时获取其共享指针。任务生命周期管理
2.2.4 时间轮定时器的代码实现
#include <memory> #include <functional> #include <iostream> #include <vector> #include <unordered_map> #include <cstdint>#include <unistd.h>using TaskFunc = std::function<void()>; // 定时任务函数类型 using ReleaseFunc = std::function<void()>; class TimerTask { private:uint64_t _id; // 定时器任务对象IDuint32_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; // tick走到哪里就释放哪里的对象,释放哪里,就相当于执行哪里的任务int _capacity; // 表盘最大数量---其实就是最大延迟时间// 当我们要二次添加同一个定时器任务对象的时候,得能够找到他们的同一个计数器,使用weak_ptr辅助shared_ptr// 保存所有定时器的weak_ptr对象,因为只有保存了WeakTask才有可能通过WeakTask构造出新的shared_ptr,// 并且他们共享计数,并且WeakTask自身不影响计数std::vector<std::vector<PtrTask>> _wheel;std::unordered_map<uint64_t, WeakTask> _timers;private:void RemoveTimerInfo(uint64_t id){auto it = _timers.find(id);if (it != _timers.end()){_timers.erase(it);}}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::RemoveTimerInfo, this, id));int pos = (_tick + delay) % _capacity;_wheel[pos].push_back(pt);_timers[id] = WeakTask(pt);}// 刷新/延迟定时任务void TimerRefresh(uint64_t id){// 通过保存的定时器对象的weak_ptr构造一个shared_ptr出来,添加到轮子中auto it = _timers.find(id);if (it == _timers.end()){// 没找到定时任务,没法刷新,没法延时return;}PtrTask pt = it->second.lock(); // lock获取weak_ptr所管理对象对应的的shared_ptrint delay = pt->DelayTime();int pos = (_tick + delay) % _capacity;_wheel[pos].push_back(pt);}// 取消一个定时任务void TimerCancel(uint64_t id){// 通过保存的定时器对象的weak_ptr构造一个shared_ptr出来,添加到轮子中auto it = _timers.find(id);if (it == _timers.end()){// 没找到定时任务,没法刷新,没法延时return;}PtrTask pt = it->second.lock(); // lock获取weak_ptr所管理对象对应的的shared_ptrif (pt)pt->Cancel();}// 这个函数每秒钟执行一次,相当于秒针向后走了一步void RunTimerTask(){_tick = (_tick + 1) % _capacity;_wheel[_tick].clear(); // 清空指定位置的数组,就能将数组中保存的所有管理对象的shared_ptr释放掉} };
1. 动态创建定时任务对象
在
TimerAdd
函数中,使用std::shared_ptr
动态创建TimerTask
对象:PtrTask pt(new TimerTask(id, delay, cb));
这种方式允许在代码运行时根据需要创建定时任务对象,并通过智能指针管理其生命周期。
2. 设置释放回调函数
在创建定时任务对象后,调用
SetRelease
方法将RemoveTimerInfo
函数绑定到定时任务对象的释放回调函数上:pt->SetRelease(std::bind(RemoveTimerInfo, this, id));
当定时任务对象被销毁时,会自动调用
RemoveTimerInfo
函数,从_timers
映射中移除该定时任务的记录。3. 计算定时任务在轮盘上的位置
在TimerAdd
和TimerRefresh
函数中,根据当前的_tick
值(轮盘指针位置)和任务的延迟时间delay
,计算定时任务在轮盘上的位置:int pos = (_tick + delay) % _capacity;
这种方法确保定时任务能够根据其延迟时间被正确地放置在轮盘的相应槽中。
4. 将定时任务添加到轮盘
在
TimerAdd
和TimerRefresh
函数中,将定时任务的共享指针添加到_wheel
的对应槽中:_wheel[pos].push_back(pt);
这使得定时任务能够在轮盘到达该槽时被触发。
5. 更新定时任务映射
在
TimerAdd
函数中,将定时任务的弱引用添加到_timers
映射中:_timers[id] = WeakTask(pt);
通过弱引用,可以在不增加引用计数的情况下跟踪定时任务对象,便于后续的刷新操作。
6. 刷新定时任务
在
TimerRefresh
函数中,使用弱引用来获取定时任务的共享指针,并重新计算其在轮盘上的位置:PtrTask pt = it->second.lock(); ... int pos = (_tick + delay) % _capacity; _wheel[pos].push_back(pt);
这允许在定时任务已经存在的情况下,更新其延迟时间并重新将其添加到轮盘中。
7. 执行定时任务
在
RunTimerTask
函数中,将轮盘指针向前移动,并清空当前槽中的所有定时任务:_tick = (_tick + 1) % _capacity; _wheel[_tick].clear();
清空槽中的任务会减少这些任务的引用计数,如果引用计数变为零,任务对象将被销毁,从而触发其析构函数,执行任务逻辑和释放操作。
8. 在
TimerTask
类中新增任务取消功能
新增
_canceled
成员变量 :布尔类型,默认值为false
,表示定时任务未被取消。当_canceled
为true
时,表示任务已被取消。新增
Cancel
方法 :用于将_canceled
标志设置为true
,表示取消该定时任务。在析构函数中增加条件判断 :如果任务未被取消(
_canceled == false
),则执行任务回调函数_task_cb
。否则,跳过任务的执行,直接调用释放回调函数_release
。这实现了任务取消功能,即当任务被取消时,其对应的回调函数不会被执行。9. 在
TimerWheel
类中新增TimerCancel
方法
调用定时任务的
Cancel
方法 :如果成功获取到共享指针(即定时任务对象存在),则调用该定时任务对象的Cancel
方法,将其_canceled
标志设置为true
,从而取消任务。
2.2.5 时间轮定时器的代码测试
通过以下测试代码,测试代码的正确性
// 测试类
// 通过释放过程来看任务执行情况
class Test
{
private:
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(8, 5, std::bind(DelTest, t));for (int i = 0; i < 5; i++){sleep(1);tw.TimerRefresh(8); // 刷新定时任务tw.RunTimerTask(); // 向后移动秒针std::cout << "刷新了一下定时任务,重新需要5s才会被销毁" << std::endl;}tw.TimerCancel(8);while (1){std::cout << "-----------------------" << std::endl;sleep(1);tw.RunTimerTask(); // 向后移动指针}return 0;
}
2.3 正则库的简单使用
2.3.1 正则表达式基本认识
正则表达式就是一种字符串匹配规则。正则库就是给我们提供一套接口,让我们使用正则匹配功能。正则表达式能够简化HTTP请求的解析过程(主要减轻程序员的工作负担),让开发的HTTP组件库更易使用。需要注意的是,这种方式虽然提高了开发效率,但在处理速度上会略逊于直接的字符串操作。
<regex> - C++ Reference
正则表达式匹配函数
std::regex_match
功能:精确匹配整个字符串
参数:bool regex_match(const std::string& str, // 待匹配字符串std::smatch& matches, // 存储匹配结果const std::regex& pattern, // 正则表达式std::regex_constants::match_flag_type flags = std::regex_constants::match_default );
HTTP 使用场景:匹配完整请求行(如
GET /index.html HTTP/1.1
)
std::regex_search
功能:在字符串中搜索子匹配
参数:同regex_match
HTTP 使用场景:提取请求头中的键值对(如Host: www.example.com
)示例:
#include <iostream> #include <string> #include <regex>int main() {std::string str = "/numbers/1234";// 以numbers作为起始字符串,数字是\d+(多加一个\,转义)// 匹配以/numbers/起始,后边跟了一个或多个数字字符的字符串,并且在匹配的过程中提取这个匹配到的数字字符串std::regex e("/numbers/(\\d+)");std::smatch matches;bool ret = std::regex_match(str, matches, e);if (ret == false){return -1;}for (auto &s : matches){std::cout << s << std::endl;}return 0; }
输出:
/numbers/1234 1234
2.3.2 正则表达式提取 HTTP 请求方法
HTTP请求行格式: GET /pupu's_blog/login?user=pupu&passwd=12312 HTTP/1.1\r\n
示例:请求方法的匹配
(GET|HEAD|POST|PUT|DELETE),表示匹配并提取其中任意一个字符串
#include <iostream> #include <string> #include <regex>int main() {std::string str = "GET /pupu's_blog/login?user=pupu&passwd=12312 HTTP/1.1";std::smatch matches;// 请求方法的匹配:GET HEAD POST PUT DELETEstd::regex e("(GET|HEAD|POST|PUT|DELETE) .*");bool ret = std::regex_match(str, matches, e);if (ret == false){std::cout << "未找到\n";return -1;}for (auto &s : matches){std::cout << s << std::endl;}return 0; }
输出:
GET /pupu's_blog/login?user=pupu&passwd=12312 HTTP/1.1 GET
2.3.3 正则表达式提取 HTTP 请求路径
示例:([^?]*)
[^?]表示匹配一个非?字符,后边*代表匹配0次或多次(+表示匹配1次或多次)
std::regex e("(GET|HEAD|POST|PUT|DELETE) ([^?]*) .*");
输出:
GET /pupu's_blog/login?user=pupu&passwd=12312 HTTP/1.1 GET /pupu's_blog/login
2.3.4 正则表达式提取 HTTP 查询字符串
示例:\\?(.*)
\\?表示原始的?字符,后边的(.*)表示提取?之后的任意字符0次或多次,直到遇到空格
std::regex e("(GET|HEAD|POST|PUT|DELETE) ([^?]*)\\?(.*) .*");
输出:
GET /pupu's_blog/login?user=pupu&passwd=12312 HTTP/1.1 GET /pupu's_blog/login user=pupu&passwd=12312
2.3.5 正则表达式提取 HTTP 协议版本
示例:(HTTP/1\\.[01])
[01]表示匹配的是里边的01任意一个字符
std::regex e("(GET|HEAD|POST|PUT|DELETE) ([^?]*)\\?(.*) (HTTP/1\\.[01]).*");
输出:
GET /pupu's_blog/login?user=pupu&passwd=12312 HTTP/1.1 GET /pupu's_blog/login user=pupu&passwd=12312 HTTP/1.1
2.3.6 正则表达式提取 HTTP 元素细节完善
情况1:所给字符串后边跟了\r\n,用以上的e,无法提取出字符串
std::string str = "GET /pupu's_blog/login?user=pupu&passwd=12312 HTTP/1.1\r\n";
如果:(\n|\r\n)
输出结果:
得到这样的结果并不是我们的目的,我们的目的是过滤掉\r\n:
使用:(?:\n|\r\n)
输出结果:(分析的串后边跟的是\n或者\r\n)
GET /pupu's_blog/login?user=pupu&passwd=12312 HTTP/1.1GET /pupu's_blog/login user=pupu&passwd=12312 HTTP/1.1
但如果,分析的串后边没有\r\n或者\n:就输出失败
解决办法:(?:\n|\r\n)?
(?:\n|\r\n)? : (?: ...) 表示匹配某个格式字符串,但是不提取他,最后的?表示的是匹配前边的表达式0次或1次
std::regex e("(GET|HEAD|POST|PUT|DELETE) ([^?]*)\\?(.*) (HTTP/1\\.[01])(?:\n|\r\n)?");
输出结果:
GET /pupu's_blog/login?user=pupu&passwd=12312 HTTP/1.1 GET /pupu's_blog/login user=pupu&passwd=12312 HTTP/1.1
情况2:所给字符串根本没有查询字符串?user=pupu&passwd=12312
由现有的e,无法输出
解决办法:(?:\\?(.*))?
匹配一个可选的问号(
?
),如果问号存在,则捕获问号后的所有内容(包括空内容)作为一个分组。std::regex e("(GET|HEAD|POST|PUT|DELETE) ([^?]*)(?:\\?(.*))? (HTTP/1\\.[01])(?:\n|\r\n)?");
输出:
GET /pupu's_blog/login HTTP/1.1 GET /pupu's_blog/loginHTTP/1.1
2.4 实现通用的any
类型:
- 每个Connection都需要管理网络连接,这最终都会涉及应用层协议的处理。因此,Connection中需要配置一个协议处理上下文来控制处理流程。考虑到应用层协议种类繁多,为避免耦合,这个协议解析上下文不应偏向特定协议,而应该能够容纳任意协议的上下文信息,这就需要一个通用的数据类型来存储不同的数据结构。
- 在C语言中,通用类型可通过void*实现。而在C++中,boost库和C++17都提供了any这一灵活的类型。如需提高代码可移植性并减少第三方库依赖,可以选择使用C++17的any特性或自行实现。
2.4.1通用类型容器any类设计思想
设计实现一个any类
是一个容器,容器中可以保存各种不同类型的数据
解决方案:
1.模版--不可用
template <class T> class Any { private:T _content; };
实例化对象的时候,必须指定容器保存的数据类型: Any<int> a;
我们需要的是:直接用Any a;a = 10,a = "abcd"....
2.设计嵌套类,类型擦除(Type Erasure)技术的核心思想,允许在单个容器中存储任意类型的值,
多态存储架构,基类
holder
作为抽象接口模板子类,placeholder<T>
存储具体类型值在Any类中存储了holder类的指针。当需要保存数据时,Any容器只需通过placeholder子类实例化一个特定类型的子类对象,由该子类对象来实际存储数据。
2.4.2通用类型容器any类结构设计
核心设计思想
类型擦除:通过基类指针指向模板派生类,擦除具体类型信息
多态克隆:通过虚函数实现深拷贝
类型安全访问:运行时检查类型匹配
class Any
{
private:class holder{public:virtual ~holder() {}virtual std::type_info type() = 0;virtual holder *clone() = 0;};template <class T>class placeholder : holder{public:placeholder(const T &val) _val(val) {}// 获取子类对象保存的数据类型std::type_info type() override;// 针对当前的对象自身,克隆出一个新的子类对象holder *clone() override;};holder *_content; // 在new一个子类对象的时候指定类型,让父类指向这个子类
public:Any();template <class T>Any(const T &val);Any(const Any &other);~Any();// 返回在子类对象中保存的数据的指针template <class T>T *get();// 赋值运算符的重载函数template <class T>Any &operator=(const T &val);Any &operator=(const Any &other);
};
2.4.3通用类型容器any类功能实现
#include <iostream> #include <typeinfo> #include <cassert> #include <string> 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) {}// 获取子类对象保存的数据类型const std::type_info &type() override{return typeid(T);}// 针对当前的对象自身,克隆出一个新的子类对象holder *clone() override{return new placeholder(_val);}public:T _val;};// 指向不同类型数据的通用接口holder *_content; // 在new一个子类对象的时候指定类型,让父类指向这个子类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(){if (!_content)return nullptr;// 想要获取的数据类型必须和保存的数据类型一致assert(typeid(T) == _content->type());// 向下转型获取存储的值return &static_cast<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;} };
类结构分析
注意模板派生类 placeholder派生类:
关键点:必须使用
public
继承基类 holder
2.4.4 通用类型容器any类功能测试
示例1:
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; }
运行结果:
构造 拷贝构造 析构 析构 10 nihao
示例2:
int main() {{Any a;Test t;a = t;}while (1){sleep(1);}return 0; }
运行结果:
未来可以选择使用boost库\c++17\自己实现的Any
2.4.5 通用类型容器C++17中any的使用
std::any - cppreference.cn - C++参考手册
使用:
结语:
随着这篇博客接近尾声,我衷心希望我所分享的内容能为你带来一些启发和帮助。学习和理解的过程往往充满挑战,但正是这些挑战让我们不断成长和进步。我在准备这篇文章时,也深刻体会到了学习与分享的乐趣。
在此,我要特别感谢每一位阅读到这里的你。是你的关注和支持,给予了我持续写作和分享的动力。我深知,无论我在某个领域有多少见解,都离不开大家的鼓励与指正。因此,如果你在阅读过程中有任何疑问、建议或是发现了文章中的不足之处,都欢迎你慷慨赐教。
你的每一条反馈都是我前进路上的宝贵财富。同时,我也非常期待能够得到你的点赞、收藏,关注这将是对我莫大的支持和鼓励。当然,我更期待的是能够持续为你带来有价值的内容。