首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >【C++仿Muduo 库 #2】前置知识技术点功能用例

【C++仿Muduo 库 #2】前置知识技术点功能用例

作者头像
IsLand1314
发布2025-06-08 12:04:08
发布2025-06-08 12:04:08
12700
代码可运行
举报
文章被收录于专栏:学习之路学习之路
运行总次数:0
代码可运行

一、C++11 中的 bind

std::bind 是C++11引入的函数适配工具,用于绑定函数参数或调整参数顺序,生成新的可调用对象

代码语言:javascript
代码运行次数:0
运行
复制
bind(Fn&& fn, Args&&... args);
  • 接收一个函数对象,以及函数的各项参数,然后返回一个新的函数对象
  • 但是由于这个函数对象的参数已经被绑定为设置的参数,运行的时候相当于总是调用传入固定参数的原函数
  • 如果进行绑定的时候,给与的参数为 std::placeholders::_1, _2... 则相当于为新适配生成的函数对象预留一个参数进行传递

想了解更多详情可以参考我的这篇博客:【C++11】 函数适配:深入理解std::bind与占位符_c++中占位符用法

代码示例如下

代码语言:javascript
代码运行次数:0
运行
复制
#include <iostream>
#include <functional>
class Test{
public:
    Test(){std::cout << "构造 Test()" << std::endl;}
    ~Test() {std::cout << "析构 ~Test()" << std::endl;}
};

void del(const Test *t, int num){
    std::cout << num << std::endl;
    delete t;
}

int main(){
    Test* t = new Test();
    /*bind作⽤也可以简单理解为给⼀个函数绑定好参数,然后返回⼀个参数已经设定好或者预留好的函数, 可以在合适的时候进行调用*/ 
 
    /*比如:del函数,要求有两个参数,⼀个Test*, ⼀个int, 想要基于del函数,适配生成⼀个新的函数,
 这个函数固定第1个参数传递t变量, 第二个参数预留出来,在调用的时候进⾏设置 */
    std::function<void(int)> f = std::bind(del, t, std::placeholders::_1);
    f(10);
    return 0;
}

运行结果如下:

代码语言:javascript
代码运行次数:0
运行
复制
lighthouse@VM-8-10-ubuntu:bind$ ./test
构造 Test()
10
析构 ~Test()

了解了bind的作用,那么当我们在设计一些线程池,或者任务池的时候,就可以基于 bind ,将任务池中的任务设置为函数类型,函数的参数由添加任务者直接使用bind进行适配绑定设置,而任务池中的任务被处理,只需要取出一个个的函数进行执行即可。

  • 这样做有个好处就是:这种任务池在设计的时候,不用考虑都有哪些任务处理方式了,处理函数该如何设计,有多少个什么样的参数,这些都不用考虑了,降低了代码之间的耦合度。

代码如下

代码语言:javascript
代码运行次数:0
运行
复制
#include <iostream>
#include <functional>
#include <string>
#include <vector>

// 下面代码就相当于把数组当成了线程池, 遍历每个数组就相当于从线程池中取出一个线程
void Print(const std::string& str){
    std::cout << str << std::endl;
}

// 注意: 这里每个bind的类型都是 std::function<void()>
int main()
{
    std::vector<std::function<void()>> task_pool;

    task_pool.emplace_back(std::bind(Print, "Hello"));
    task_pool.emplace_back(std::bind(Print, "World"));
    task_pool.emplace_back(std::bind(Print, "I Miss You (Island1314)"));

    for(auto &functor: task_pool){
        // 这里的调用就相当于从线程池中取出一个线程, 然后执行
        functor();
    }
    return 0;
}

二、简单的秒级定时任务实现

1. timerfd

📟 在当前的高并发服务器中,我们不得不考虑一个问题,那就是连接的超时关闭问题。我们需要避免-个连接长时间不通信,但是也不关闭,空耗资源的情况。

这时候我们就需要一个定时任务,定时的将超时过期的连接进行释放

创建一个定时器

代码语言:javascript
代码运行次数:0
运行
复制
#include <sys/timerfd.h>
int timerfd_create(int clockid, int flags);
  • clockid
    • CLOCK_REALTIME:以系统时间作为计时基准值(如果系统时间发生改变就会有问题)
    • CLOCK_MONOTONIC:以系统启动时间进行递增的一个基准值(定时器不会随着系统时间而改变)
  • flags:0—默认阻塞属性

返回值:文件描述符 fd

启动定时器

代码语言:javascript
代码运行次数:0
运行
复制
int timerfd_settime(int fd, int flags,const struct itimerspec *new_value,struct itimerspec *old_value);

struct timespec {
   time_t tv_sec;                /* Seconds */
   long   tv_nsec;               /* Nanoseconds */
};

struct itimerspec {
   struct timespec it_interval;  /* 第一次之后的超时间隔时间 */ 
   struct timespec it_value;     /* 第一次超时时间 */
};
  • fdtimerfd_create 函数的返回值,文件描述符 – 创建的定时器的标识符
  • flags:0—相对时间,1—绝对时间;默认设置为 0 即可
  • new:用于设置定时器的新超时时间
  • old:用于接收原来的超时时间

由于 Linux 下一切皆文件,定时器的操作也是和文件操作类似,而定时器定时的原理每隔一段时间(定时器的超时时间),系统就会给这个描述符对应的定时器写入一个 8 字节数据

注意:定时器在每次超时后,并不会立即单独写入一个 1,而是累积超时次数

  • 当应用程序调用 read 读取定时器对应的文件描述符时,内核会返回一个 uint64_t 类型的值(8 字节),表示自上次读取后累积的超时次数。
  • 读取操作会自动重置计数器,后续超时次数会重新累积

示例如下:

  • 若定时器每 3 秒触发一次,且在 6 秒后读取,则 read 会返回 2(表示触发了 2 次超时),而非每次超时写入
  • 若超时后未及时读取,超时次数会持续累积,直到下一次 read 操作

案例代码如下

代码语言:javascript
代码运行次数:0
运行
复制
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <sys/select.h>
#include <sys/timerfd.h>

int main()
{
    int timerfd = timerfd_create(CLOCK_MONOTONIC, 0);
    if(timerfd < 0){
        std::cerr << "timerfd_create failed" << std::endl;
        return -1;
    }

    struct itimerspec itime;
    // 设置第一次超时间隔时间为 1 s
    itime.it_value.tv_sec = 1;
    itime.it_value.tv_nsec = 0;

    // 设置第一次超时后 每次超时时间间隔为 1 s
    itime.it_interval.tv_sec = 1;
    itime.it_interval.tv_nsec = 0;

    // 设置定时器
    int ret = timerfd_settime(timerfd, 0, &itime, NULL);
    if(ret < 0){
        std::cerr << "timer_settime failed" << std::endl;
        return -1;
    }
    
    // 当前这个定时器描述符每隔 1 s触发可读事件
    time_t start = time(NULL);

    while(1){
        uint64_t times;
        int ret = read(timerfd, &times, 8);
        if(ret < 0){
            std::cerr << "read failed" << std::endl;
            return -1;
        }
        std::cout << "当前时间: " << time(NULL) - start << std::endl;
    }
    close(timerfd);
    return 0;
}

结果如下

代码语言:javascript
代码运行次数:0
运行
复制
lighthouse@VM-8-10-ubuntu:timerfd$ ./timerfd
超时次数1 , 当前时间: 1
超时次数1 , 当前时间: 2
  • 上边实现了每隔 1s 触发一次定时器超时,否则就会阻塞在read读取数据这
  • 基于这个例子,则我们可以实现每隔1s,检测一下哪些连接超时了,然后将超时的连接释放掉
2. 时间轮

上述的例子,存在一个很大的问题,每次超时都要将所有的连接遍历一遍,如果有上万个连接,效率无疑是较为低下的。

这时候大家就会想到,我们可以针对所有的连接,根据每个连接最近一次通信的系统时间建立一个小根堆,这样只需要每次针对堆顶部分的连接逐个释放,直到没有超时的连接为止,这样也可以大大提高处理的效率。

上述方法可以实现定时任务,但是这里给大家介绍另一种方案:时间轮

  • 时间轮的思想来源于钟表,如果我们定了一个3点钟的闹铃,则当时针走到3的时候,就代表时间到了。

同样的道理,如果我们定义了一个数组,并且有一个指针,指向数组起始位置,这个指针每秒钟向后走动一步,走到哪里,则代表哪里的任务该被执行了,那么如果我们想要定一个3s后的任务,则只需要将任务添加到 tick+3 位置,则每秒中走一步,三秒钟后 tick 走到对应位置,这时候执行对应位置的任务即可。

但是,同一时间可能会有大批量的定时任务,因此我们可以给数组对应位置下拉一个数组,这样就可以在同一个时刻上添加多个定时任务了。

当然,上述操作也有一些缺陷,比如我们如果要定义一个60s后的任务,则需要将数组的元素个数设置为60才可以,如果设置一小时后的定时任务,则需要定义3600个元素的数组,这样无疑是比较麻烦的。

  • 因此,可以采用多层级的时间轮,有秒针轮,分针轮,时针轮,60<time<3600则time/60就是分针轮对应存储的位置,当tick/3600等于对应位置的时候,将其位置的任务向分针,秒针轮进行移动

一些思考

  1. 同一时刻的定时任务只能添加一个,需要考虑如何在同一个时刻添加多个任务 解决方案:将时间轮的一维数组设计为二维数组(时间轮一维数组的每个节点也是一个数组)
  2. 假设当前的定时任务是一个连接的非活跃销毁任务,这个任务应该什么时候添加到时间轮中比较合适?
    • 如果一个连接 30s 内都没有通信,则是一个非活跃连接,这时候就销毁
    • 但是一个连接如果在建立的时候添加了一个 30s 后销毁的任务,但是这个连接 30s 内人家有数据通信,在第 30 s的时候不是一个非活跃连接
    • 思想:需要在一个连接有 IO 事件产生的时候,延迟定时任务的执行

作为一个时间轮定时器,本身并不关注任务类型,只要是时间到了就需要被执行

  • 当前的设计是时间到了,则主动去执行定时任务,释放连接,那能不能在时间到了后,自动执行定时任务呢,这时候我们就想到一个操作—— 类的析构函数
  • 类的析构函数:在对象被释放时会自动被执行,那么我们如果 将一个定时任务作为一个类的析构函数内的操作,则这个定时任务在对象被释放的时候就会执行

此时解决方案:类的析构函数 + 智能指针shared_ptr ,通过这两个技术实现定时任务延时

但是,这里我们又要考虑另一个问题,那就是假如有一个连接建立成功了,我们给这个连接设置了一个30s后的定时销毁任务,但是在第10s的时候,这个连接进行了一次通信,那么我们应该时在第30s的时候关闭,还是第40s的时候关闭呢? 应该是第40s的时候

  1. 使用一个类,对定时任务进行封装,类实例化的每个对象,就是一个定时任务对象,当对象被销毁的时候,再去执行定时任务(将定时任务的执行,放到析构函数中)
  2. shared_ptr 用于对 new 对象进行空间管理,当 shared_ptr 对一个对象进行管理的时候,内部有个计数器,计数为0时释放所管理对象
    • 比如:那么如果连接在第10s进行了一次通信,则我们继续向定时任务中,添加一个30s后(也就是第40s)的任务类对象的 shared ptr,则这时候两个任务 shared_ptr 计数为2,则第 30s 的定时任务被释放的时候,计数-1,变为1,并不为0,则并不会执行实际的析构函数,那么就相当于这个第30s的任务失效了,只有在第40s的时候,这个任务才会被真正释放。

代码如下

代码语言:javascript
代码运行次数:0
运行
复制
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <vector>
#include <unordered_map>
#include <memory>
#include <thread>
#include <unistd.h>

using TaskFunc = std::function<void()>;
using ReleaserFunc = std::function<void()>;

// 定时器任务
class TimerTask{
public:
    TimerTask(uint64_t id, uint32_t delay, const TaskFunc& cb, int start_tick = 0):
    _id(id),
    _timeout(delay),
    _cb(cb),
    _start_tick(start_tick),
    _canceled(false)
    {}

    ~TimerTask() { 
        if (!_canceled) {
            std::thread(_cb).detach(); // 异步执行
        }
        // if (!_canceled) _cb();  // 用这个的话 _cb 就会先执行, 否则后执行
        _release(); 
    }

    void SetReleaser(const ReleaserFunc& release){_release = release;}
    ReleaserFunc GetReleaser() const{return _release;}
    uint64_t GetId() const {return _id;}
    void Cancel() {_canceled = true;} // 取消定时任务
    uint32_t GetDelayTime() const { return _timeout; }

    int GetStartTick() const { return _start_tick; }
    void SetStartTick(int tick) { _start_tick = tick; } // 设置定时器任务的起始 tick

private:
    int _start_tick; // 记录定时器任务的起始 tick
    uint64_t _id;           // 定时器任务 id
    uint32_t _timeout;      // 定时任务超时时间
    bool _canceled;         // 是否取消定时任务(true: 取消)
    TaskFunc _cb;           // 定时器对象要执行的定时任务
    ReleaserFunc _release;  // 用于删除 TimerWheel 中保存的定时器对象信息
};

// 定时器轮
class TimerWheel{
private:
    using WeakTask = std::weak_ptr<TimerTask>; // 辅助 shared_ptr 解决循环引用问题
    using PtrTask = std::shared_ptr<TimerTask>;
public:
    TimerWheel(int capacity = 60):
    _capacity(capacity),
    _tick(0)
    {
        _wheel.resize(_capacity);
    }

    // 1. 添加定时任务
    void AddTimer(uint64_t id, uint32_t delay, const TaskFunc& cb){  
        if (delay > _capacity) {
            std::cerr << "Timer delay exceeds capacity" << std::endl;
            return;
        }
        PtrTask pt(new TimerTask(id, delay, cb, _tick));
        pt->SetReleaser(std::bind(&TimerWheel::RemoveTimer, this, id));
        
        int pos = (_tick + delay) % _capacity; // 计算定时器任务在轮子上的位置
        _wheel[pos].push_back(pt); // 将定时器任务添加到轮子上
        _timers[id] = WeakTask(pt); // 不能用 shared_ptr, 否则永久有shared_ptr 来管理, 计数不归0         
    }   
    
    // 2. 删除定时任务
    void RemoveTimer(uint64_t id){
        auto it = _timers.find(id);
        if(it == _timers.end()) return ;
        _timers.erase(it);
    }
    // 3. 执行定时任务 
    void RunTimerTask(){
        _tick = (_tick + 1) % _capacity;
        _wheel[_tick].clear(); // 清空当前 tick 的任务
    }

    // 4. 刷新/延迟 定时任务
    void RefreshTimer(uint64_t id) {
        // 1, 通过保存定时器对象的 weak_ptr 来获取定时器对象 添加到轮子中
        auto it = _timers.find(id);
        if (it == _timers.end())  // 没找到定时任务, 无法刷新和延迟
            return;
        
        PtrTask pt = it->second.lock(); // lock获取weak_ptr管理的对象对应的shared_ptr
        if (!pt) return;
	
        // 方法1:下面两行处理的刷新之后定时时间也重新计时
        int remaining = pt->GetDelayTime();
        
        // 由于上面这里的刷新把延迟时间也会重新计算, 因此任务原定5秒后执行,已过去3秒,刷新后仍按5秒计算,导致实际延迟8秒
        // 解决办法: 记录起始 tick
        
        // // 方法2:刷新不延迟计时
        // // 计算剩余时间 = 原始延迟 - (当前 tick - 起始 tick)
        // int elapsed = (_tick - pt->GetStartTick() + _capacity) % _capacity; // 防止负数
        // int remaining = pt->GetDelayTime() - elapsed;
        // remaining = std::max(remaining, 1); // 确保至少延迟1秒
        // // 更新任务的起始 tick 为当前 tick
        // pt->SetStartTick(_tick);

        int pos = (_tick + remaining) % _capacity;
        _wheel[pos].push_back(pt);
    }

    // 5. 取消定时任务
    void CancelTimer(uint64_t id) {
        auto it = _timers.find(id);
        if(it == _timers.end()) return ;

        PtrTask pt = it->second.lock(); // lock获取weak_ptr管理的对象对应的shared_ptr
        // 如果weak_ptr管理的对象已经被销毁(即引用计数为0),lock()会返回一个空的shared_ptr
        // 如果对象仍然存在,lock()会返回一个有效的shared_ptr,指向该对象

        if(pt) pt->Cancel(); // 取消定时任务
    }
private:
    int _tick; // 当前指针, 走到哪释放哪(相当于执行哪里任务)
    int _capacity; // 定时器轮的大小 -- 最大延迟时间
    std::vector<std::vector<PtrTask>> _wheel; // 定时器轮
    std::unordered_map<uint64_t, WeakTask> _timers; // 定时器任务 id 和定时器对象的映射关系
};

struct Test{
    Test(){std::cout << "构造" << std::endl;}
    ~Test(){std::cout << "析构" << std::endl;}
};

void DelTest(Test *t){
    std::cout << "删除" << std::endl;
    delete t;
    t = nullptr; 
}

int main() {
    TimerWheel tw(10); // 假设时间轮容量为10

    // 添加一个5秒后执行的任务 -- 保证刷新任务不影响执行
    tw.AddTimer(1, 3, [](){ 
        std::cout << "任务执行时间:" << time(nullptr) << std::endl; 
    });

    // 模拟时间推进(单位:秒)
    for (int i = 0; i < 8; ++i) {
        std::cout << "当前时间:" << time(nullptr) << " tick: " << i << std::endl;
        
        // 在第2秒时刷新任务
        if (i == 2) {
            std::cout << "刷新任务" << std::endl;
            tw.RefreshTimer(1);
        }
        
        sleep(1);
        tw.RunTimerTask();
    }

    std::cout << "------------------" << std::endl;
    Test *t = new Test();
    tw.AddTimer(2,2, std::bind(DelTest, t));
    for(int i = 0; i < 4; i++){
        std::cout << "tick: " << i << " , 刷新定时器任务" << std::endl;
        sleep(1);
        if(i == 1){
            tw.CancelTimer(2); // 取消任务
        }
        tw.RefreshTimer(2); // 刷新定时器任务1
        tw.RunTimerTask(); // 向后移动指针
    }
    return 0;
}

结果如下

代码语言:javascript
代码运行次数:0
运行
复制
lighthouse@VM-8-10-ubuntu:timewheel$ ./timewheel
当前时间:1745495736 tick: 0
当前时间:1745495737 tick: 1
当前时间:1745495738 tick: 2
刷新任务
当前时间:1745495739 tick: 3
当前时间:1745495740 tick: 4
当前时间:1745495741 tick: 5
任务执行时间:1745495741
当前时间:1745495742 tick: 6
当前时间:1745495743 tick: 7
------------------
构造
tick: 0 , 刷新定时器任务
tick: 1 , 刷新定时器任务
tick: 2 , 刷新定时器任务
tick: 3 , 刷新定时器任务
tick: 4 , 刷新定时器任务

三、正则表达式字符串匹配

正则表达式(regular expression)描述了一种字符串匹配的模式(pattern),可以用来检查一个串是否含有某种子串、将匹配的子串替换或者从某个串中取出符合某个条件的子串等

  • 正则表达式的使用,可以使得HTTP请求的解析更加简单(这里指的时程序员的工作变得的简单,这并不代表处理效率会变高,实际上效率上是低于直接的字符串处理的),使我们实现的HTTP组件库使用起来更加灵活
代码语言:javascript
代码运行次数:0
运行
复制
bool std::regex_match(const std::string& src, std::mismatch &matches, std::regex &e)
    src: 原始字符串
    matches: 正则表达式可以从原始字符串中匹配并提取符合某种规则的数据, 提取数据就放在 mathces 中, 是一个类似于数组的容器
    e: 正则表达式的匹配规则
返回值: 用于确定匹配是否成功
1. smatch

在 C++ 中,std::smatch<regex> 标准库中用于存储正则表达式匹配结果 的类模板。它专门用于处理 std::string 类型的字符串匹配,并能够保存完整的匹配结果以及每个子表达式的匹配内容。


1.1 std::smatch 是什么?🧠

std::smatchstd::match_results<std::string::const_iterator> 的别名。

功能

  • 判断是否匹配成功(bool 类型转换)
  • 获取整个匹配的字符串(str()
  • 获取每个子表达式(捕获组)的匹配内容([i]
  • 获取匹配的位置(position(i))和长度(length(i)

代码示例

代码语言:javascript
代码运行次数:0
运行
复制
#include <iostream>
#include <regex>
#include <string>

int main() {
    std::string text = "Contact us at support@example.com";
    std::regex pattern(R"((\w+)@(\w+\.\w+))");    // 匹配 邮箱 分组: 用户名 + 域名
    std::smatch match;
    if (std::regex_search(text, match, pattern)) {
        std::cout << "完整匹配: " << match.str() << std::endl;
        std::cout << "用户名: " << match[1].str() << std::endl;
        std::cout << "域名: " << match[2].str() << std::endl;
    }
    else {
        std::cout << "未找到匹配。" << std::endl;
    }

    return 0;
}

// 输出
完整匹配: support@example.com
用户名: support
域名: example.com
1.2 常用方法和属性🧩

方法/属性

说明

match.size()

返回匹配的子表达式数量(包括整个匹配)

match[i]

获取第 i 个子表达式的匹配结果(std::sub_match)

match.str(i)

获取第 i 个子表达式的匹配字符串

match.position(i)

获取第 i 个子表达式在原字符串中的起始位置

match.length(i)

获取第 i 个子表达式的匹配长度

match.empty()

判断是否匹配成功(返回false表示匹配成功)

1.3 示例:提取 URL 中的协议和域名🧪
代码语言:javascript
代码运行次数:0
运行
复制
#include <iostream>
#include <regex>
#include <string>

int main() {
    std::string url = "https://www.example.com/path/to/page.html ";
    std::regex pattern(R"(^(https?|ftp):\/\/([^\/\?:]+))"); // 匹配协议和域名

    std::smatch match;
    if (std::regex_search(url, match, pattern)) {
        std::cout << "协议: " << match[1] << std::endl;
        std::cout << "域名: " << match[2] << std::endl;
    } else {
        std::cout << "URL 格式不正确。" << std::endl;
    }

    return 0;
}

// 输出
协议: https
域名: www.example.com
2. 代码实现
代码语言:javascript
代码运行次数:0
运行
复制
#include <iostream>
#include <string>
#include <regex>

int main()
{
    // HTTP 请求行格式: GET /path HTTP/1.1\r\n
    std::string str = "GET /Island/login?user=xiao&pass=123456 HTTP/1.1\r\n";
    std::smatch matches;

    // 请求方法匹配: GET|POST|PUT|DELETE|HEAD|OPTIONS
    std::regex method_regex("(GET|HEAD|POST|PUT|DELETE) ([^?]*)(?:\\?(.*))? (HTTP/1\\.[01])(?:\n|\r\n)?");
    // GET|HEAD|POST|PUT|DELETE   表示匹配并提取其中任意一个字符串
    // [^?]*     [^?]匹配非问号字符, 后边的*表示0次或多次
    // \\?(.*)   \\?  表示原始的?字符 (.*)表示提取?之后的任意字符0次或多次,知道遇到空格
    // HTTP/1\\.[01]  表示匹配以HTTP/1.开始,后边有个0或1的字符串
    // (?:\n|\r\n)?   (?: ...) 表示匹配某个格式字符串,但是不提取, 最后的?表示的是匹配前边的表达式0次或1次
    
    bool ret = std::regex_match(str, matches, method_regex);
    if(!ret) return -1;

    std::cout << "完整匹配: " << matches[0] << std::endl;
    std::cout << "HTTP 方法: " << matches[1] << std::endl;
    std::cout << "路径: " << matches[2] << std::endl;
    std::cout << "查询参数: " << (matches[3].matched ? matches[3].str() : "") << std::endl;
    std::cout << "协议版本: " << matches[4] << std::endl;

    return 0;
}

输出如下

代码语言:javascript
代码运行次数:0
运行
复制
完整匹配: GET /Island/login?user=xiao&pass=123456 HTTP/1.1

HTTP 方法: GET
路径: /Island/login
查询参数: user=xiao&pass=123456
协议版本: HTTP/1.1
3. 分析

🧠 ① 正则表达式逻辑解析

代码语言:javascript
代码运行次数:0
运行
复制
std::regex method_regex("(GET|HEAD|POST|PUT|DELETE) ([^?]*)(?:\\?(.*))? (HTTP/1\\.[01])(?:\n|\r\n)?");

🔍 正则表达式各部分含义:

正则片段

匹配内容

(GET

HEAD

([^?]*)

捕获路径(不包含问号?的部分)

(?:\\?(.*))?

可选捕获查询字符串(?后的内容)

(HTTP/1\\.[01])

捕获协议版本(HTTP/1.0 或 HTTP/1.1)

(?:\n

\r\n)?


📌 ② 测试字符串

代码语言:javascript
代码运行次数:0
运行
复制
std::string str = "GET /Island/login?user=xiao&pass=123456 HTTP/1.1\r\n";

🧪 分解匹配过程:

匹配项

内容

matches[0]

整体匹配:GET /Island/login?user=xiao&pass=123456 HTTP/1.1

matches[1]

方法:GET

matches[2]

路径:/Island/login

matches[3]

查询参数:user=xiao&pass=123456

matches[4]

协议版本:HTTP/1.1

完善

代码语言:javascript
代码运行次数:0
运行
复制
 std::regex method_regex(R"((GET|HEAD|POST|PUT|DELETE) ([^? ]+)(?:\?([^ ]+))? (HTTP/1\.[01])\r?\n)");
📌 区别分析:

特性

第一个

第二个

说明

原始字符串

❌ 普通字符串

✅ 使用R"()"原始字符串

R"()"避免双重转义,提高可读性

路径匹配

([^?]*)

([^? ]+)

第二个明确禁止空格,更符合 HTTP 请求行规范

查询参数匹配

(?:\\?(.*))?

(?:\?([^ ]+))?

第二个限制查询参数不包含空格,更安全

协议版本匹配

(HTTP/1\.[01])

(HTTP/1\.[01])

相同

换行符匹配

(?:\n

\r\n)?

\r?\n


🚨 关键问题:为何第一个正则表达式可能失败?

1)未正确处理换行符

  • std::regex_match 要求完全匹配整个字符串
  • 第一个正则表达式末尾的 (?:\n|\r\n)? 是可选的,但实际输入包含 \r\n,可能导致匹配失败。

2)路径匹配不严谨

  • ([^?]*) 允许路径中包含空格(如 /path with space),但 HTTP 请求行中路径不允许空格 (空格是方法、路径、协议的分隔符)。

3)查询参数匹配贪婪

  • (?:\\?(.*))?.* 是贪婪匹配,可能导致匹配到协议版本前的内容。

四、通用类型 any 类型的实现

1. 前言

🏭 每一个 Connection 对连接进行管理,最终都不可避免需要涉及到应用层协议的处理,因此在 Connection 中需要设置协议处理的上下文来控制处理节奏。但是应用层协议千千万,为了降低合度,这个协议接收解析上下文就不能有明显的协议倾向,它可以是任意协议的上下文信息,因此就需要一个通用的类型来保存各种不同的数据结构

在C语言中,通用类型可以使用 void* 来管理,但是在C++中,boost库 和 C++17 给我们提供了一个通用类型any来灵活使用,如果考虑增加代码的移植性,尽量减少第三方库的依赖,则可以使用C++17特性中的any,或者自己来实现。而这个any通用类型类的实现其实并不复杂,以下是简单的部分实现。

  1. 一个连接必须拥有一个请求接收和解析的上下文
  2. 上下文的类型或结构不能固定,因为服务器支持的协议可能会不断增多,不同的协议可能都会有不同的上下文结构

结论:必须拥有一个容器能够保存各种不同的类型结构数据

通用类型:any

设计实现一个 any 类

① 一个容器,容器中可以保存各种不同类型的数据

解决方案一:模板

代码语言:javascript
代码运行次数:0
运行
复制
template<class T>
class Any {
private: 
    T _content;
};
  • 但是当我们如果用模板类的话,实例化对象的时候, 必须指定容器保存的数据类型: Any<int> a;,此时就需要传类型作为参数模板,也就是说使用的时候要确定其类型
  • 这是行不通的,因为保存在Content中的协议上下文,我们在定义any对象的时候是不知道他们的协 议类型的,因此无法传递类型作为模板参数。而我们需要的是: Any a; a = 10; a = "abc"...

解决方案二:嵌套一个类,专门用于保存其他类型的数据,而 Any 类保存的是固定类的对象

代码语言:javascript
代码运行次数:0
运行
复制
class Any {
private:
    class holder {
        // ...
    };
    template<class T>
    class placeholder: holder {
        T _val;
    };
    holder* _content;
};
  • 定义⼀个基类 placehoder,让 holder 继承于 placeholder,而Any类保存父类指针即可
  • 当 Any 容器需要保存一个数据的时候,只需要通过 place_holder 子类实例化一个特定类型的子类对象出来,让其保存数据,然后让Any类中的父类指针,指向这个子类对象就搞定了
2. any 类概述

std::any 是 C++17 引入的标准库类型,用于存储任意类型的单个值(类型安全的“通用容器”)。它支持动态类型存储和访问,允许在运行时更改所存储的值类型,同时保证类型安全性

2.1 核心特性

(1)动态类型存储 可以存储任何类型的值,但同一时间只能保存一种类型。

代码语言:javascript
代码运行次数:0
运行
复制
std::any a = 42;            // 存储 int
a = std::string("Hello");   // 替换为 string

(2)类型安全访问 通过 std::any_cast<T> 安全地访问存储的值:

  • 指针形式 :返回 T*,若类型不匹配返回 nullptr
  • 引用形式 :抛出 std::bad_any_cast 异常(需捕获)
代码语言:javascript
代码运行次数:0
运行
复制
if (auto* p = std::any_cast<int>(&a)) {
    std::cout << *p << "\n";
} else {
    std::cout << "类型不匹配\n";
}

(3)拷贝与移动语义 支持拷贝构造、赋值和移动操作,但要求存储的类型满足相应操作(如可拷贝/移动)。

代码语言:javascript
代码运行次数:0
运行
复制
std::any a = 10;
std::any b = a;              // 拷贝构造
std::any c = std::move(a);  // 移动构造(a 变为空)

(4)空状态(Empty State) 默认构造的 std::any 为空,调用 has_value() 返回 false

代码语言:javascript
代码运行次数:0
运行
复制
std::any a;
if (!a.has_value()) {
    std::cout << "a 为空\n";
}
2.2 使用示例
代码语言:javascript
代码运行次数:0
运行
复制
<font color = #f9906f>#include <iostream>
#include <any>

int main() {
    std::any a = 3.14;  // 存储 double
    std::cout << *std::any_cast<double>(&a) << "\n";  // 输出 3.14

    a = true;  // 替换为 bool
    std::cout << std::boolalpha << *std::any_cast<bool>(&a) << "\n";  // 输出 true

    a.reset();  // 清空, 会触发析构
    if (!a.has_value()) {
        std::cout << "a 已清空\n";
    }

    try {
        auto val = std::any_cast<int>(a);  // 空 any 转换抛出异常
    } catch (const std::bad_any_cast& e) {
        std::cout << "错误: " << e.what() << "\n";  // 输出错误信息
    }

    return 0;
}
2.3 注意事项
  1. 性能开销 std::any 内部可能使用堆分配存储(小对象优化可能实现),频繁使用可能导致内存碎片或性能瓶颈。
  2. 类型擦除的代价 需要手动管理类型信息,过度依赖 std::any 可能导致代码难以维护。
  3. 线程安全 非线程安全,多线程访问需自行加锁。
  4. std::variant 的区别
    • std::variant:类型受限(编译时指定可选类型),无动态分配,适合有限类型集合。
    • std::any:类型无限制,运行时动态确定,适合通用性场景。
2.4 适用场景
  • 异构数据容器 :需要存储多种类型值的集合(如配置项、事件参数)。
  • 回调/插件系统 :传递任意类型参数给回调函数。
  • 临时类型包装 :简化接口设计,避免模板泛化。

小结std::any 提供了灵活的类型擦除能力,但应权衡其性能与安全性。在需要动态类型处理时(如脚本绑定、配置管理),它是理想选择;但在高性能或类型明确的场景中,优先使用 std::variant 或模板泛型。

3. 代码实现测试
代码语言:javascript
代码运行次数:0
运行
复制
class Any{
private:
    class holder{
    public:
        virtual ~holder() = default;
        virtual const std::type_info& type() const = 0;
        virtual holder* clone() const = 0;
    };
    template<class T>
    class placeholder: public holder{
    public:
        placeholder(const T& val): _val(val) {} 
        virtual const std::type_info& type() const {return typeid(T);} // 获取子类对象保存的数据类型
        virtual holder* clone() const {return new placeholder(_val);} // 针对当前的对象克隆出一个新的子类对象
    public:
        T _val;    
    };
    holder* _content; // 指向holder的指针

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() const { // 返回子类对象保存数据的指针
        // 保证获取的数据类型和保存的数据类型一致
        // if(typeid(T) != _content->type()) // 如果类型不匹配
        //     return nullptr; 
        assert(typeid(T) == _content->type()); // 断言类型匹配 --> 不匹配直接退出
        return &((placeholder<T>*)_content)->_val; // 类型转化->成员->取地址
    }  

    template<class T>
    Any& operator=(const T&val){  // 模板赋值运算符的重载
        // 先构造一个临时对象,然后交换
        Any(val).swap(*this); 
        return *this;
    }
    Any& operator=(const Any& other){
        Any(other).swap(*this);
        return *this;
    }
};

测试如下

代码语言:javascript
代码运行次数:0
运行
复制
// 测试有无内存泄漏
struct Test{
    Test(){std::cout << "构造" << std::endl;}
    Test(const Test&other){std::cout << "拷贝构造" << std::endl;}
    ~Test(){std::cout << "析构" << std::endl;}
};

void func(){
    // 系统自带
    std::any a;
    a = 10;
    int *pi = std::any_cast<int>(&a);
    std::cout << "int a = " << *pi << std::endl; 

    a = std::string("hello");
    std::string *ps = std::any_cast<std::string>(&a);
    std::cout << "string a = " << *ps << std::endl;
}

int main()
{
    Any a;
    a = 10;
    int *pa = a.get<int>(); // 获取int类型的指针
    std::cout << "int a = " << *pa << std::endl; 

    a = std::string("hello");
    std::string *ps = a.get<std::string>();
    std::cout << "string a = " << *ps << std::endl; 

    // 两次析构对象: 相当于在 placeholder 中保存了一个对象的引用
    // 先析构原来的对象,再析构临时对象
    {
        Test t;
        a = t;
    }

    std::cout << "------------" << std::endl;
    func(); // 测试系统自带的any类

    return 0;
}

// 结果如下:
int a = 10
string a = hello
构造
拷贝构造
析构
------------
int a = 10
string a = hello
析构
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-06-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、C++11 中的 bind
  • 二、简单的秒级定时任务实现
    • 1. timerfd
    • 2. 时间轮
  • 三、正则表达式字符串匹配
    • 1. smatch
      • 1.1 std::smatch 是什么?🧠
      • 1.2 常用方法和属性🧩
      • 1.3 示例:提取 URL 中的协议和域名🧪
    • 2. 代码实现
    • 3. 分析
      • 📌 区别分析:
  • 四、通用类型 any 类型的实现
    • 1. 前言
    • 2. any 类概述
      • 2.1 核心特性
      • 2.2 使用示例
      • 2.3 注意事项
      • 2.4 适用场景
    • 3. 代码实现测试
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档