【项目】仿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.1 目标定位:One Thread One Loop主从Reactor模型高并发服务器)

[1.2 模块关系图:](#1.2 模块关系图:)

二、前置知识技术点功能用例:

[2.1 C++11中的bind:这篇文章有详细的讲解](#2.1 C++11中的bind:这篇文章有详细的讲解)

[2.2 高效实现秒级定时任务:](#2.2 高效实现秒级定时任务:)

[2.2.1 Linux系统提供了以下定时器解决方案:](#2.2.1 Linux系统提供了以下定时器解决方案:)

示例:

[2.2.2 时间轮定时器的基本思想理解以及设计完善](#2.2.2 时间轮定时器的基本思想理解以及设计完善)

时间轮

shared_ptr来管理定时器任务对象

[2.2.3 时间轮定时器的代码设计](#2.2.3 时间轮定时器的代码设计)

***智能指针的使用***

[2.2.4 时间轮定时器的代码实现](#2.2.4 时间轮定时器的代码实现)

[2.2.5 时间轮定时器的代码测试](#2.2.5 时间轮定时器的代码测试)

[2.3 正则库的简单使用](#2.3 正则库的简单使用)

[2.3.1 正则表达式基本认识](#2.3.1 正则表达式基本认识)

正则表达式匹配函数

[2.3.2 正则表达式提取 HTTP 请求方法](#2.3.2 正则表达式提取 HTTP 请求方法)

[2.3.3 正则表达式提取 HTTP 请求路径](#2.3.3 正则表达式提取 HTTP 请求路径)

[2.3.4 正则表达式提取 HTTP 查询字符串](#2.3.4 正则表达式提取 HTTP 查询字符串)

[2.3.5 正则表达式提取 HTTP 协议版本](#2.3.5 正则表达式提取 HTTP 协议版本)

[2.3.6 正则表达式提取 HTTP 元素细节完善](#2.3.6 正则表达式提取 HTTP 元素细节完善)

情况1:所给字符串后边跟了\r\n,用以上的e,无法提取出字符串

情况2:所给字符串根本没有查询字符串?user=pupu&passwd=12312

[2.4 实现通用的any类型:](#2.4 实现通用的any类型:)

2.4.1通用类型容器any类设计思想

2.4.2通用类型容器any类结构设计

2.4.3通用类型容器any类功能实现

[2.4.4 通用类型容器any类功能测试](#2.4.4 通用类型容器any类功能测试)

[2.4.5 通用类型容器C++17中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:这篇文章有详细的讲解

cpp 复制代码
bind (Fn&& fn, Args&&... args);

见见使用:

cpp 复制代码
#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直接绑定任务函数的参数,任务池只需取出并执行这些预绑定好的函数即可。

这种设计的优势在于:任务池不需要关心具体任务的处理方式、函数设计或参数数量,有效降低了代码之间的耦合度。

cpp 复制代码
#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-创建定时器

功能:创建一个定时器

cpp 复制代码
#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-启动定时器

功能:启动定时器

cpp 复制代码
int timerfd_settime(int fd, int flags, struct itimerspec *new, struct
itimerspec *old);

fd: timerfd_create返回的文件描述符
flags: 0-相对时间, 1-绝对时间;默认设置为0即可.
new: 用于设置定时器的新超时时间
old: 用于接收当前定时器原有的超时时间

cpp 复制代码
struct timespec
{
    time_t tv_sec; /* Seconds */
    long tv_nsec;  /* Nanoseconds */
};
struct itimerspec
{
    struct timespec it_interval; /* 第⼀次之后的超时间隔时间 */
    struct timespec it_value;    /* 第⼀次超时时间 */
};

定时器每次超时时,会自动向文件描述符(fd)写入8字节数据,该数据表示从上次读取操作到当前读取操作之间发生的超时次数。

示例:
cpp 复制代码
#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, &times, 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 时间轮定时器的代码设计

cpp 复制代码
#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;         // 定时器任务对象ID
    uint32_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 中的定时任务记录。

  1. TimerWheel
  • 功能 :一个定时轮盘,用于管理多个定时任务,支持添加新的定时任务和刷新已存在的定时任务的延迟时间。

  • 主要成员变量

    • _wheel :一个二维向量,模拟定时轮盘的结构。每个元素是一个包含 PtrTaskstd::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 时间轮定时器的代码实现

cpp 复制代码
#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;         // 定时器任务对象ID
    uint32_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_ptr
        int 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_ptr
        if (pt)
            pt->Cancel();
    }
    // 这个函数每秒钟执行一次,相当于秒针向后走了一步
    void RunTimerTask()
    {
        _tick = (_tick + 1) % _capacity;
        _wheel[_tick].clear(); // 清空指定位置的数组,就能将数组中保存的所有管理对象的shared_ptr释放掉
    }
};

1. 动态创建定时任务对象

TimerAdd 函数中,使用 std::shared_ptr 动态创建 TimerTask 对象:

cpp 复制代码
PtrTask pt(new TimerTask(id, delay, cb));

这种方式允许在代码运行时根据需要创建定时任务对象,并通过智能指针管理其生命周期。

2. 设置释放回调函数

在创建定时任务对象后,调用 SetRelease 方法将 RemoveTimerInfo 函数绑定到定时任务对象的释放回调函数上:

cpp 复制代码
pt->SetRelease(std::bind(RemoveTimerInfo, this, id));

当定时任务对象被销毁时,会自动调用 RemoveTimerInfo 函数,从 _timers 映射中移除该定时任务的记录。

3. 计算定时任务在轮盘上的位置
TimerAddTimerRefresh 函数中,根据当前的 _tick 值(轮盘指针位置)和任务的延迟时间 delay,计算定时任务在轮盘上的位置:

cpp 复制代码
int pos = (_tick + delay) % _capacity;

这种方法确保定时任务能够根据其延迟时间被正确地放置在轮盘的相应槽中。

4. 将定时任务添加到轮盘

TimerAddTimerRefresh 函数中,将定时任务的共享指针添加到 _wheel 的对应槽中:

cpp 复制代码
_wheel[pos].push_back(pt);

这使得定时任务能够在轮盘到达该槽时被触发。

5. 更新定时任务映射

TimerAdd 函数中,将定时任务的弱引用添加到 _timers 映射中:

cpp 复制代码
_timers[id] = WeakTask(pt);

通过弱引用,可以在不增加引用计数的情况下跟踪定时任务对象,便于后续的刷新操作。

6. 刷新定时任务

TimerRefresh 函数中,使用弱引用来获取定时任务的共享指针,并重新计算其在轮盘上的位置:

cpp 复制代码
PtrTask pt = it->second.lock();
...
int pos = (_tick + delay) % _capacity;
_wheel[pos].push_back(pt);

这允许在定时任务已经存在的情况下,更新其延迟时间并重新将其添加到轮盘中。

7. 执行定时任务

RunTimerTask 函数中,将轮盘指针向前移动,并清空当前槽中的所有定时任务:

cpp 复制代码
_tick = (_tick + 1) % _capacity;
_wheel[_tick].clear();

清空槽中的任务会减少这些任务的引用计数,如果引用计数变为零,任务对象将被销毁,从而触发其析构函数,执行任务逻辑和释放操作。

8. 在 TimerTask 类中新增任务取消功能

  • 新增 _canceled 成员变量 :布尔类型,默认值为 false,表示定时任务未被取消。当 _canceledtrue 时,表示任务已被取消。

  • 新增 Cancel 方法 :用于将 _canceled 标志设置为 true,表示取消该定时任务。

  • 在析构函数中增加条件判断 :如果任务未被取消(_canceled == false),则执行任务回调函数 _task_cb 。否则,跳过任务的执行,直接调用释放回调函数 _release 。这实现了任务取消功能,即当任务被取消时,其对应的回调函数不会被执行。

9. 在 TimerWheel 类中新增 TimerCancel 方法

  • 调用定时任务的 Cancel 方法 :如果成功获取到共享指针(即定时任务对象存在),则调用该定时任务对象的 Cancel 方法,将其 _canceled 标志设置为 true,从而取消任务。

2.2.5 时间轮定时器的代码测试

通过以下测试代码,测试代码的正确性

cpp 复制代码
// 测试类
//  通过释放过程来看任务执行情况
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
功能 :精确匹配整个字符串
参数

cpp 复制代码
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

示例:

cpp 复制代码
#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;
}

输出:

bash 复制代码
/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),表示匹配并提取其中任意一个字符串

cpp 复制代码
#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 DELETE
    std::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;
}

输出:

cpp 复制代码
GET /pupu's_blog/login?user=pupu&passwd=12312 HTTP/1.1
GET

2.3.3 正则表达式提取 HTTP 请求路径

示例:****([^?]*)

[^?]表示匹配一个非?字符,后边*代表匹配0次或多次(+表示匹配1次或多次)

cpp 复制代码
  std::regex e("(GET|HEAD|POST|PUT|DELETE) ([^?]*) .*");

输出:

cpp 复制代码
GET /pupu's_blog/login?user=pupu&passwd=12312 HTTP/1.1
GET
/pupu's_blog/login

2.3.4 正则表达式提取 HTTP 查询字符串

示例:****\\?(.*)

\\?表示原始的?字符,后边的(.*)表示提取?之后的任意字符0次或多次,直到遇到空格

cpp 复制代码
   std::regex e("(GET|HEAD|POST|PUT|DELETE) ([^?]*)\\?(.*) .*");

输出:

cpp 复制代码
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任意一个字符

cpp 复制代码
std::regex e("(GET|HEAD|POST|PUT|DELETE) ([^?]*)\\?(.*) (HTTP/1\\.[01]).*");

输出:

cpp 复制代码
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,无法提取出字符串
cpp 复制代码
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)

cpp 复制代码
GET /pupu's_blog/login?user=pupu&passwd=12312 HTTP/1.1

GET
/pupu's_blog/login
user=pupu&passwd=12312
HTTP/1.1

但如果,分析的串后边没有**\r\n或者\n****:就输出失败**

解决办法:****(?:\n|\r\n)?

(?:\n|\r\n)? : (?: ...) 表示匹配某个格式字符串,但是不提取他,最后的?表示的是匹配前边的表达式0次或1次

cpp 复制代码
    std::regex e("(GET|HEAD|POST|PUT|DELETE) ([^?]*)\\?(.*) (HTTP/1\\.[01])(?:\n|\r\n)?");

输出结果:

cpp 复制代码
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,无法输出

解决办法:****(?:\\?(.*))?

匹配一个可选的问号(?),如果问号存在,则捕获问号后的所有内容(包括空内容)作为一个分组。

cpp 复制代码
 std::regex e("(GET|HEAD|POST|PUT|DELETE) ([^?]*)(?:\\?(.*))? (HTTP/1\\.[01])(?:\n|\r\n)?");

输出:

cpp 复制代码
GET /pupu's_blog/login HTTP/1.1
GET
/pupu's_blog/login

HTTP/1.1

2.4 实现通用的any类型:

  • 每个Connection都需要管理网络连接,这最终都会涉及应用层协议的处理。因此,Connection中需要配置一个协议处理上下文来控制处理流程。考虑到应用层协议种类繁多,为避免耦合,这个协议解析上下文不应偏向特定协议,而应该能够容纳任意协议的上下文信息,这就需要一个通用的数据类型来存储不同的数据结构。
  • 在C语言中,通用类型可通过void*实现。而在C++中,boost库和C++17都提供了any这一灵活的类型。如需提高代码可移植性并减少第三方库依赖,可以选择使用C++17的any特性或自行实现。

2.4.1通用类型容器any类设计思想

设计实现一个any类

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

解决方案:

1.模版--不可用

cpp 复制代码
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类结构设计

核心设计思想

类型擦除:通过基类指针指向模板派生类,擦除具体类型信息

多态克隆:通过虚函数实现深拷贝

类型安全访问:运行时检查类型匹配

cpp 复制代码
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类功能实现

cpp 复制代码
#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:

cpp 复制代码
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;
}

运行结果:

bash 复制代码
构造
拷贝构造
析构
析构
10
nihao

示例2:

cpp 复制代码
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++参考手册

使用:

结语:

随着这篇博客接近尾声,我衷心希望我所分享的内容能为你带来一些启发和帮助。学习和理解的过程往往充满挑战,但正是这些挑战让我们不断成长和进步。我在准备这篇文章时,也深刻体会到了学习与分享的乐趣。

在此,我要特别感谢每一位阅读到这里的你。是你的关注和支持,给予了我持续写作和分享的动力。我深知,无论我在某个领域有多少见解,都离不开大家的鼓励与指正。因此,如果你在阅读过程中有任何疑问、建议或是发现了文章中的不足之处,都欢迎你慷慨赐教。

你的每一条反馈都是我前进路上的宝贵财富。同时,我也非常期待能够得到你的点赞、收藏,关注这将是对我莫大的支持和鼓励。当然,我更期待的是能够持续为你带来有价值的内容。

相关推荐
linux行者36 分钟前
linux基础重定向及组合重定向
linux·运维
m0_653031361 小时前
腾讯云TCCA认证考试报名 - TDSQL数据库交付运维工程师(PostgreSQL版)
运维·数据库·腾讯云
朱小勇本勇2 小时前
Clang Code Model: Error: The clangbackend executable “D:\Soft\Qt5.12.12\Tool
运维·服务器·数据库·qt·nginx
网硕互联的小客服2 小时前
502 Bad Gateway:服务器作为网关或代理时收到无效响应处理方式
运维·服务器·gateway
安顾里3 小时前
aws各类服务器编号
服务器·云计算·aws
再见晴天*_*3 小时前
超图superMap iObjects for Java的Jar使用中遇到的问题
运维·服务器
Azure DevOps3 小时前
Azure DevOps Server:使用FTP工具上传文件
运维·microsoft·azure·devops
qq_5024289904 小时前
如何搭建战神冰雪传奇手游 从零开始学习游戏架设,玩转云服务器,如何利用云服务器搭建一款属于自己的传奇手游
运维·服务器
我言秋日胜春朝★4 小时前
【Linux网络编程】守护进程
linux·运维·服务器
Mikhail_G5 小时前
Python初学者入门指南
大数据·运维·开发语言·python·数据分析