【基于one-loop-per-thread的高并发服务器】--- 前置技术

Welcome to 9ilk's Code World

(๑•́ ₃ •̀๑) 个人主页: 9ilk

(๑•́ ₃ •̀๑) 文章专栏: 项目


本篇博客主要是对高并发服务器项目涉及到的知识进行一个补充介绍。

C++11特性bind

函数原型:

cpp 复制代码
template <class Fn, class... Args>
  /* unspecified */ bind (Fn&& fn, Args&&... args);

官方文档对该接口的描述是:Bind function arguments。我们可以将bind接口看作是一个通用的函数适配器 ,它接受一个函数对象,以及函数的各项参数 ,然后返回一个新的函数对象, 但是这个函数对象的参数已经被绑定为设置的参数运行的时候相当于总是调用传入固定参数的原函数。

但是如果进行绑定的时候,传递的参数为std::placeholders::_1,_2....则相当于为新适配生成的函数对象的调用预留一个参数进行传递。

cpp 复制代码
#include <iostream>
#include <functional>

void print_sum(int a, int b) {
    std::cout << "Sum: " << a + b << std::endl;
}

int main() {
    // 使用 std::bind 绑定 print_sum 函数
    auto bound_func = std::bind(print_sum, 10, 20);
    
    // 调用绑定后的函数
    bound_func();  // 输出: Sum: 30

    // 你也可以通过绑定一部分参数来创建新的函数
    auto bound_func_with_one_arg = std::bind(print_sum, 10, std::placeholders::_1);

    // 传递剩余的参数
    bound_func_with_one_arg(30);  // 输出: Sum: 40

    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> arr;                // 任务队列

    arr.push_back(std::bind(print, "hello",10));
    arr.push_back(std::bind(print, "leihou",20));
    arr.push_back(std::bind(print, "nihao",30));
    arr.push_back(std::bind(print, "hh",40));

    for (auto &f : arr)
        f();          // 依次执行

    return 0;
}

timerfd

在当前的高并发服务器中,我们不得不考虑一个问题,那就是连接的超时关闭问题 。我们需要避免一个连接长时间不通信,但是也不关闭,空耗资源 的情况。这时候我们就需要一个定时任务,定时的将超时过期的连接接进行释放

Linux提供的一种定时器操作就是timerfd:

cpp 复制代码
#include <sys/timerfd.h>

//创建一个定时器,当作文件操作
int timerfd_create(int clockid, int flags);
clockid: CLOCK_REALTIME - 系统实时时间,如果修改了系统时间就会出问题;
         CLOCK_MONOTONIC- 从开机到现在的时间是一种相对时间;
flags:   0-默认阻塞属性

timerfd_create()
timerfd_create()  creates  a  new timer object, and returns a file descriptor that refers to that timer.  The clockid
argument specifies the clock that is used to mark the progress of the timer, and must be one of the following:

CLOCK_REALTIME
    A settable system-wide real-time clock.

CLOCK_MONOTONIC
     A nonsettable monotonically increasing clock that measures time from some unspecified point in the  past  that
     does not change after system startup.
  • CLOCK_REALTIME以系统时间作为计时基准值(如果系统时间发生改变就会出问题)。
  • CLOCK_MONOTONIC系统启动时间 进行递增的一个基准值。(定时不会随着系统时间改变而改变)

Linux下一切皆文件,意味着对定时器的操作也是文件操作,这个接口返回值就是fd描述符。定时器的定时原理是每隔一段时间(定时器超时时间),系统就会给这个描述符对应的定时器文件写入一个 8****字节数据( uint64_t**),表示该数据上次读取数据到当前读取数据经过几次超时。**假设你是每隔3s算超时,从启动开始,每隔3s就会往fd文件写入一个1,表示从上次读取数据到现在超时了1次,如果你是30s之后才读取数据,此时会读取到10,表示从上次读取数据到现在超时了10次。

这里设置为阻塞属性是比较合适的,阻塞到有数据就表示超时了。阻塞表示定时器在触发时会阻塞调用的进程,直到定时器超时。这意味着,如果定时器被触发,调用 **read()**系统调用的进程会被阻塞,直到定时器超时并且数据可供读取。

启动定时器:

cpp 复制代码
//启动定时器
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;  /* Interval for periodic timer */第一次之后的超时间隔时间
     struct timespec it_value;     /* Initial expiration */第一次超时时间
};
  • fd:``timerfd_create()创建的定时器的返回值。
  • flags: 0-相对时间,1-绝对时间;默认设置为0即可
  • new:用于设置定时器的新超时时间。
  • old:用于接收原来的超时时间设置,没有还原需求设置为nullpter即可。
  • 返回值:成功返回0,失败返回-1。

演示demo:

cpp 复制代码
  #include<cstdio>
  #include<unistd.h>
  #include<fcntl.h>
  #include<iostream>
  #include<sys/timerfd.h>
  #include<cstdint>
   using namespace std;


  int main()
  {
     //1.创建定时器
    int timerfd = ::timerfd_create(CLOCK_MONOTONIC,0);
     if(timerfd < 0)
     {
         cerr << "timerfd create error!" << endl;
         exit(1);
     }
     //2.启动定时器
    struct itimerspec itime;
    itime.it_value.tv_sec = 1;//第一次超时
    itime.it_value.tv_nsec = 0;
    itime.it_interval.tv_sec = 3;//第一次超市之后的超时间隔
    itime.it_interval.tv_nsec = 0;
    int n = ::timerfd_settime(timerfd,0,&itime,NULL);
    if(n < 0)
    {
      cerr << "start timer error!" << endl;
      exit(2);
    }
    //3.测试
    while(true)
    {
      uint64_t times;
      int ret = ::read(timerfd,&times,sizeof(times));//写入数据之前会阻塞没有数据
      if(ret < 0)
      {
         cerr << "read error" << endl;
         exit(3);
     }
     printf("比上一次读取数据超时了%ld\n",times);
    }
    ::close(timerfd);
     return 0;
  }

定时器

网络程序是需要处理定时事件的,比如定期检测一个连接的活动状态。服务器程序通常管理着众多定时事件,因此有效地组织这些定时事件,使之能在预期的时间点被触发且不影响服务器的主要逻辑,对于服务器的性能有着至关重要的影响。为此比,我们要将每个定时事件分别封装成定时器,并使用某种容器类数据结构,比如链表、排序链表和时间轮,将所有定时器串联起来,以实现对定时事件的统一管理。这里我们讲解的是基于升序链表的定时器、时间堆、时间轮。

基于升序链表的定时器

定时器通常至少要包含两个成员:一个超时时间 (相对时间或者 绝对时间)和一个任务回调函数。有的时候还可能包含回调函数被执行时需要传入的参数,以及是否重启定时器等信息。如果使用链表作为容器来串联所有的定时器,则每个定时器还要包含指向下一个定时器的指针成员。进一步,如果链表是双向的,则每个定时器还需要包 含指向前一个定时器的指针成员。

也就是说我们这里是个升序链表,每个节点是一个定时任务,按照超时时间排序,因此在链表中添加定时任务,无非是比较各个节点的超时时间,将定时器节点插入到合适位置,调整定时器/删除定时器也类似,需要比较超时时间,保证链表的升序性。

cpp 复制代码
#ifndef LST_TIMER
#define LST_TIMER
#include<time.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<stdlib.h>
#include<assert.h>
#include<stdio.h>
#include<errno.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
#define BUFFER_SIZE 64
class util_timer;/*前向声明*/
/*用户数据结构:客户端socket地址、socket文件描述符、读缓存和定时器*/
struct client_data
{
    sockaddr_in address;
    int sockfd;
    char buf[BUFFER_SIZE];
    util_timer*timer;
};
/*定时器类*/
class util_timer
{
public:
    util_timer():
    prev(NULL),next(NULL)
    {}
public:
    time_t expire;/*任务的超时时间,这里使用绝对时间*/
    void(*cb_func)(client_data*);/*任务回调函数*/
    /*回调函数处理的客户数据,由定时器的执行者传递给回调函数*/
    client_data*user_data;
    util_timer*prev;/*指向前一个定时器*/
    util_timer*next;/*指向下一个定时器*/
};

/*定时器链表。它是一个升序、双向链表,且带有头结点和尾节点*/
class sort_timer_lst
{
public:
    sort_timer_lst()
        :head(NULL),tail(NULL)
        {}
    /*链表被销毁时,删除其中所有的定时器*/
    ~sort_timer_lst()
    {
        util_timer*tmp = head;
        while(tmp)
        {
            head = tmp->next;
            delete tmp;
            tmp = head;
        }
    }
/*将目标定时器timer添加到链表中*/
void add_timer(util_timer*timer)
{
    if(!timer)
    {
        return;
    }
    //链表为空
    if(!head)
    {
        head=tail=timer;
        return;
    }
    /*如果目标定时器的超时时间小于当前链表中所有定时器的超时时间,则把该定时器插
    入链表头部,作为链表新的头节点。否则就需要调用重载函数
    add_timer(util_timer*timer,util_timer*lst_head),把它插入链表中合适的位
    置,以保证链表的升序特性*/
    if(timer->expire < head->expire)
    {
        timer->next=head;
        head->prev=timer;
        head=timer;
        return;
    }
    add_timer(timer,head);
}

/*当某个定时任务发生变化时,调整对应的定时器在链表中的位置。这个函数只考虑被
调整的定时器的超时时间延长的情况,即该定时器需要往链表的尾部移动*/
void adjust_timer(util_timer*timer)
{
    if(!timer)
    {
        return;
    }
    util_timer*tmp=timer->next;
    /*如果被调整的目标定时器处在链表尾部,或者该定时器新的超时值仍然小于其下一个
    定时器的超时值,则不用调整*/
    if(!tmp||(timer->expire < tmp->expire))
    {
        return;
    }
    /*如果目标定时器是链表的头节点,则将该定时器从链表中取出并重新插入链表*/
    if(timer==head)
    {
        head = head->next;
        head->prev=NULL;
        timer->next=NULL;
        add_timer(timer,head);
    }
        /*如果目标定时器不是链表的头节点,则将该定时器从链表中取出,然后插入其原来所
        在位置之后的部分链表中*/
    else
    {
        timer->prev->next = timer->next;
        timer->next->prev = timer->prev;
        add_timer(timer,timer->next);
    }
}
/*将目标定时器timer从链表中删除*/
void del_timer(util_timer*timer)
{
    if(!timer)
    {
        return;
    }
    /*下面这个条件成立表示链表中只有一个定时器,即目标定时器*/
    if((timer==head)&&(timer==tail))
    {
        delete timer;
        head=NULL;
        tail=NULL;
        return;
    }
    /*如果链表中至少有两个定时器,且目标定时器是链表的头结点,则将链表的头结点重
    置为原头节点的下一个节点,然后删除目标定时器*/
    if(timer==head)
    {
        head=head->next;
        head->prev=NULL;
        delete timer;
        return;
    }
    /*如果链表中至少有两个定时器,且目标定时器是链表的尾结点,则将链表的尾结点重
    置为原尾节点的前一个节点,然后删除目标定时器*/
    if(timer==tail)
    {
        tail=tail->prev;
        tail->next=NULL;
        delete timer;
        return;
    }
    /*如果目标定时器位于链表的中间,则把它前后的定时器串联起来,然后删除目标定时
    器*/
    timer->prev->next = timer->next;
    timer->next->prev = timer->prev;
    delete timer;
}

/*SIGALRM信号每次被触发就在其信号处理函数(如果使用统一事件源,则是主函数)
中执行一次tick函数,以处理链表上到期的任务*/
void tick()
{
    if(!head)
    {
        return;
    }
    printf("timer tick\n");
    time_t cur=time(NULL);/*获得系统当前的时间*/
    util_timer*tmp=head;
    /*从头结点开始依次处理每个定时器,直到遇到一个尚未到期的定时器,这就是定时器
    的核心逻辑*/
    while(tmp)
    {
        /*因为每个定时器都使用绝对时间作为超时值,所以我们可以把定时器的超时值和系统
        当前时间,比较以判断定时器是否到期*/
        if(cur < tmp->expire)
        {
        break;
        }
        /*调用定时器的回调函数,以执行定时任务*/
        tmp->cb_func(tmp->user_data);
        /*执行完定时器中的定时任务之后,就将它从链表中删除,并重置链表头结点*/
        head=tmp->next;
        if(head)
        {
            head->prev=NULL;
        }
        delete tmp;
        tmp=head;
    }
}
private:

    /*一个重载的辅助函数,它被公有的add_timer函数和adjust_timer函数调用。该
    函数表示将目标定时器timer添加到节点lst_head之后的部分链表中*/
    void add_timer(util_timer*timer,util_timer*lst_head)
    {
        util_timer*prev=lst_head;
        util_timer*tmp=prev->next;
        /*遍历lst_head节点之后的部分链表,直到找到一个超时时间大于目标定时器的超时
        时间的节点,并将目标定时器插入该节点之前*/
        while(tmp)
        {
            if(timer->expire < tmp->expire)
            {
                prev->next=timer;
                timer->next=tmp;
                tmp->prev=timer;
                timer->prev=prev;
                break;
            }
            prev=tmp;
            tmp=tmp->next;
        }
        /*如果遍历完lst_head节点之后的部分链表,仍未找到超时时间大于目标定时器的超
        时时间的节点,则将目标定时器插入链表尾部,并把它设置为链表新的尾节点*/
        if(!tmp)
        {
            prev->next=timer;
            timer->prev=prev;
            timer->next=NULL;
            tail=timer;
        }
    }
private:
    util_timer*head;
    util_timer*tail;
};

#endif

sort_timer_lst是一 个升序链表。其核心函数tick相当于一个心搏函数,它每隔一段固定的 时间就执行一次,以检测并处理到期的任务。判断定时任务到期的依据是定时器的expire值小于当前的系统时间。从执行效率来看,添加定时器的时间复杂度是O(n),删除定时器的时间复杂度是O(1),执行定时任务的时间复杂度是O(1)。

时间堆

设计定时器的另外一种思路是:将所有定时器中超时时间最小的一个定时器的超时值作为心搏间隔。这样,一旦心搏函数tick被调用,超时时间最小的定时器必然到期,我们就可以在tick函数中处理该定时器。然后,再次从剩余的定时器中找出超时时间最小的一个,并将这段最小时间设置为下一次心搏间隔。如此反复,就实现了较为精确的定时。

最小堆就适合这种设计思想,主要思路是创建一个最小堆,以超时时间排序,每次心博时检查堆顶是不是超时,超时就执行任务,不断pop,直到遇到没有超时的。

cpp 复制代码
using TaskFunc = std::function<void()>;
using ReleaseFunc = std::function<void()>;
//定时器任务对象
class TimeTask
{
private:
   uint64_t _id;//任务对象的id
   uint32_t _timeout;//定时任务的超时时间
   TaskFunc _task_cb; //定时器对象要执行的定制任务
   ReleaseFunc _release;//用于删除TimeWheel中保存的定时器对象信息
   bool _canceled;
   uint64_t _abs_time;
 public:
  
    TimeTask(uint64_t id,uint32_t delay,const TaskFunc& cb)
    :_id(id),_timeout(delay),_task_cb(cb),_canceled(false),_abs_time(time(nullptr)+_timeout)
    {}  
    //析构的时候执行定制任务并且销毁在定时轮中的该任务信息
    ~TimeTask()
    {
        if(!_canceled)
        _task_cb();
        _release();
    }
    //设置销毁定时器任务对象函数
    void SetRelease(const ReleaseFunc& cb)
    {
        _release = cb;
    }
    uint32_t DelayTime()
    {return _timeout;}

    uint64_t AbsTime()
    {
        return _abs_time;
    }

    void setAbsTime(uint64_t abs_time)
    {
        _abs_time = abs_time;
    }

    void Cancel()
    {_canceled = true;}
};

struct LessPtr
{
     using  PtrTask = std::shared_ptr<TimeTask>;
    bool operator()(const PtrTask& x,const PtrTask&y)
    {
        return x->DelayTime() > y->DelayTime();
    }
};

//时间对对象
class TimeHeap
{
private:
     using  WeakTask = std::weak_ptr<TimeTask>;
     using  PtrTask = std::shared_ptr<TimeTask>;
    
     priority_queue<PtrTask,vector<PtrTask>,LessPtr> _pq;
     unordered_map<uint64_t,WeakTask> _timers;
     //从时间轮中删除定时任务
     void RemoveTimeTask(uint64_t id)
     {
         auto it = _timers.find(id);
         if(it != _timers.end())
            _timers.erase(it);
     }

public:
    TimeHeap()
    {

    }
    //在时间轮添加定时任务
    void TimerAdd(uint64_t id,uint32_t delay,const TaskFunc& cb)
    {
      //1.创建定时任务设置好删除函数
        PtrTask pt(new TimeTask(id,delay,cb));
        //注意:因为编译器不会将对象的成员函数隐式转换成函数指针,所以必须在TimeWheel::RemoveTimeTask前添加&;
        pt->SetRelease(std::bind(&TimeHeap::RemoveTimeTask,this,id));//注意这里需要使用bind这是因为RemoveTimeTask第一个参数是this,而且我们统一了ReleaseFunc
        cout << "Create Task.." << _pq.size() << endl ;
        //2.存放到时间轮
        _pq.push(pt);
        _timers[id] = WeakTask(pt);
    }


    //对指定定时任务进行刷新/延迟

    void RefreshTask(uint64_t id)
    {
         auto it = _timers.find(id);
         if(it == _timers.end())
            return; //没找到指定定时任务 返回
        PtrTask pt = it->second.lock();//相当于新建了一个shared_ptr共享计数
        uint32_t delay = pt->DelayTime();
        pt->setAbsTime(time(nullptr)+delay);
        _pq.push(pt);
    }


    //指针移动执行任务
    void RunTimeTask()
    {
        auto now_time = time(nullptr);
        while(!_pq.empty() && _pq.top()->AbsTime() <= now_time)
           _pq.pop();
    }
    
    //取消定时任务
    void CancelTask(uint64_t id)
    {
        auto it = _timers.find(id);
         if(it == _timers.end())
            return; //没找到指定定时任务 返回
        PtrTask pt = it->second.lock();//相当于新建了一个shared_ptr共享计数
        if(pt) pt->Cancel();
    }

};

注意:这里的取消定时任务其实还是一种"伪删除", 任务实体仍滞留在槽heap里 ,直到那一刻才销毁,适用于对内存/及时释放不敏感的场景。

时间轮

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

下拉数组

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

多层级时间轮

上述操作也有一些缺陷 ,比如我们如果要定义一个60s后的的任务,则需要将数组的元素个数 设置为60才可以,如果设置一小时后的定时任务,则需要定义3600个元素的数组,这样无疑是比较麻烦的 。因此,可以采用多层级的时间轮 ,有秒针轮,分针轮,时针轮,60<time<3600time/60就是分针轮 对应存储的位置,当tick/3600等于对应位置的时候,将其位置的任务向分针,秒针轮进行移动 。因为当前我们的应用中,倒是不用设计的这么麻烦,因为我们的定时任务通常设置的30s以内,所以简单的单层时间轮就够用了。

类的析构函数

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

shared_ptr

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

shared_ptr有个计数器,当计数为0的时候,才会真正释放一个对象,那么如果连接在第10s进行了一次通信,则我们继续向定时任务中,添加一个30s后(也就是第40s)的任务类对象的shared_ptr,则这时候两个任务shared_ptr计数为2,则第30s的定时任务被释放的时候,计数-1,变为1,并不为0,则并不会执行实际的析构函数,那么就相当于这个第30s的任务失效了,只有在第40s的时候,这个任务才会被真正释放。

weak_ptr

在时间轮内我们需要统一管理定时器,为了不增加计数,我们可以使用weak_ptr,共享同一个计数。

cpp 复制代码
using TaskFunc = std::function<void()>;
using ReleaseFunc = std::function<void()>;

//定时器任务对象
class TimeTask
{
private:
   uint64_t _id;//任务对象的id
   uint32_t _timeout;//定时任务的超时时间
   TaskFunc _task_cb; //定时器对象要执行的定制任务
   ReleaseFunc _release;//用于删除TimeWheel中保存的定时器对象信息
   bool _canceled;
 public:
    TimeTask(uint64_t id,uint32_t delay,const TaskFunc& cb)
    :_id(id),_timeout(delay),_task_cb(cb),_canceled(false)
    {}  
    //析构的时候执行定制任务并且销毁在定时轮中的该任务信息
    ~TimeTask()
    {
        _task_cb();
        _release();
    }
    //设置销毁定时器任务对象函数
    void SetRelease(const ReleaseFunc& cb)
    {
        _release = cb;
    }
    uint32_t DelayTime()
    {return _timeout;}

    void Cancel()
    {_canceled = true;}
};

//时间轮对象
class TimeWheel
{
private:
     using  WeakTask = std::weak_ptr<TimeTask>;
     using  PtrTask = std::shared_ptr<TimeTask>;
    
     int _tick;//表示执行定时任务的指针,走到哪里释放哪里,释放哪里相当于执行哪里的任务
     int _capacity;//相当于时间轮的一圈多少 --- 最大延迟时间
      vector<vector<PtrTask>> _wheel;//二维数组作为时间轮注意要比capacity后声明
     unordered_map<uint64_t,WeakTask> _timers; //注意这里需要使用WeakPtr是因为添加新的定时任务(shared_ptr<TimeTask>)再使用shared_Ptr会增加不必要的计数
     
     //从时间轮中删除定时任务
     void RemoveTimeTask(uint64_t id)
     {
         auto it = _timers.find(id);
         if(it != _timers.end())
            _timers.erase(it);
     }

public:
    TimeWheel()
    :_tick(0),_capacity(60),_wheel(_capacity)
    {}
    //在时间轮添加定时任务
    void TimerAdd(uint64_t id,uint32_t delay,const TaskFunc& cb)
    {
        //1.创建定时任务设置好删除函数
        PtrTask pt(new TimeTask(id,delay,cb));
        //注意:因为编译器不会将对象的成员函数隐式转换成函数指针,所以必须在TimeWheel::RemoveTimeTask前添加&;
        pt->SetRelease(std::bind(&TimeWheel::RemoveTimeTask,this,id));//注意这里需要使用bind这是因为RemoveTimeTask第一个参数是this,而且我们统一了ReleaseFunc
        cout << "Create Task.." << _wheel.size() << endl ;
        //2.存放到时间轮
        int pos = (_tick+delay)%_capacity;
        _wheel[pos].push_back(pt);
        _timers[id] = WeakTask(pt);
    }
    //对指定定时任务进行刷新/延迟
    void RefreshTask(uint64_t id)
    {
        auto it = _timers.find(id);
         if(it == _timers.end())
            return; //没找到指定定时任务 返回
        PtrTask pt = it->second.lock();//相当于新建了一个shared_ptr共享计数
        int delay = pt->DelayTime();
        int pos = (_tick+delay)%_capacity;
        _wheel[pos].push_back(pt);
    }
    //指针移动执行任务
    void RunTimeTask()
    {
        _tick = (_tick+1)%_capacity;
        //走到哪释放哪
        _wheel[_tick].clear(); //清空指定位置的数组,就会把数组中保存的所有管理定时器对象的shared_ptr軽放掉
    }

    void CancelTask(uint64_t id)
    {
        auto it = _timers.find(id);
         if(it == _timers.end())
            return; //没找到指定定时任务 返回
        PtrTask pt = it->second.lock();//相当于新建了一个shared_ptr共享计数
        if(pt) pt->Cancel();
    }
};

正则库

正则表达式是字符串的匹配规则,而正则库就是给我们提供这一套接口实现正则匹配功能。

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

正则表达式的使用,可以使得HTTP请求的解析更加简单 (这里指的是程序员的工作变得的简单,这并不代表处理效率会变高,实际上效率上是低于直接的字符串处理的),使我们实现的HTTP组件库使用起来更加灵活。

主要接口:

cpp 复制代码
std::regex_match(const std::string& src,std::smatch& matched,std::regex& e)
//src:原始字符串
//matches:正则表达式可以从原始字符串中匹配并提取符合某种规则的数据,提取的数据就放在matches中,是
//一个类似于数组的容器。
//e:正则表达式的匹配规则

提取HTTP请求要素demo:

cpp 复制代码
string exp = "GET http://42.194.139.95:8082/login HTTP/1.1";
   //.匹配/n/r之外的任何单个字符
   //*匹配前面的子表达式零次或多次
   std::regex e("(GET|HEAD|POST|DELETE) ([^?]*)(?:\\?(.*))? (HTTP/1\\.[01])(?:\n|\r\n)?");
   //(GET|HEAD|POST|DELETE) 表示匹配并提取其中任意一个字符串
   //[^?*] [^?]匹配非问号
   // \\?(.*)表示匹配原始的?字符之后的任意字符0次或多次,直到遇到空格
   // HTTP/1\\.[01] 表示匹配以HTTP/1.开始

   std::smatch matches;
   //
  bool ret = std::regex_match(exp,matches,e);
  if(ret == false) //匹配失败
  {
   cerr << "match error" << endl;
    exit(1);
  }
  for(auto& s : matches)
      cout << s << endl;

正则表达式学习:正则表达式

通用类型Any

每一个Connection对连接进行管理,最终都不可避免需要步及到应用层协议 的处理,因此在Connection中需要设置协议处理的上下文来控制处理节奏 (有可能发送一条请求很大,缓冲区不足以存储这一条完整请求,而且我们还得解决粘包问题,它还要包含解析请求的要素,解析到哪里等)。但是应用层协议千千万,为了降低耦合度 ,这个协议接收解析上下文就不能有明显的协议倾向 ,它可以是任意协议的上下文信息 ,因此就需要一个通用的类型来保存各种不同的数据结构

在C语言中,通用类型可以使用void*来管理,但是在C++中,boost库和C++17给我们提供了一个通用类型any来灵活使用,如果考虑增加代码的移植性,尽量减少第三方库的依赖,则可以使用C++17特性Any。

总结一下:

  1. 一个连接必须拥有一个请求接收与解析的上下文。
  2. 上下文的类型/结构不能固定,因为服务器支持的协议有可能会不断增多,不同协议可能都会有不同的上下文结构。
  3. **结论:**必须拥有一个容器保存不同类型结构的数据->通用类型any

Any模拟实现

方案一:模板

cpp 复制代码
template<class T>
class any
{
private:
    T content;
}

此时实例化对象的写法是Any<int> a,我们要的是Any a = 2;

方案二:嵌套设计一个类,专门保存其他类型数据,而Any类保存的是固定类的对象。

cpp 复制代码
class Any 
{
private:
    template<class T>    
    class placeholer 
    {
        T val
    };
  placeholder<T> _content;
}

但是这样还得指定对应类型。真正的解决方法是采用多态 : 设置holder为父类 , placeholder继承这个类, 在Any类定义父类指针,当要保存一个成员,就实例化一个子类对象来保存数据,父类指针指向子类对象,通过它访问子类对象的各个成员合法方法

cpp 复制代码
class Any
{
 private:   
   class holder
   {
     public:
       virtual ~holder(){}
       virtual const std::type_info& type()=0; //获取子类对象保存的数据类型
       //针对当前对象自身克隆出一个新子类对象,方便我们后续针对一个Any对象构造新的Any对象
       virtual holder* clone() = 0; 
   };

   template<class T>
   class placeholder : public holder
   {
     public:
       ~placeholder()
       {}
       placeholder(const T& val):_val(val){}
       virtual const std::type_info& type(){return typeid(_val);}
       virtual holder* clone() {return new placeholder<T>(_val);}
     public:
        T _val;  
   };

   holder* _content;
public:

   //交换子类对象
    Any& swap(Any& otehr)
    {
        std::swap(_content,otehr._content);
        return *this;
    }                                                                                                                                                     

    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) //判断是否为空 不为空则clone
    {}

    ~Any()
    {delete _content;}
    
    template<class T>
    T* get() //返回子类对象保存的数据的指针
    {
        return &((placeholder<T>*)_content)->_val;
    }

    //为val构造一个临时的通用容器,然后与当前容器自身进行指针交换,临时对象释放的时候,原先保存的数据也就被释放
    template<class T>
    Any& operator=(const T& val)//返回值Any&是为了能连续赋值
    {
        Any(val).swap(*this);
        return *this;
    }
    Any& operator=(const Any& other)
    {
        // 通过外部other对象构造一个临时Any对象
         Any(other).swap(*this);
        return *this;
    }
    
};

C++17 Any

使用demo:

cpp 复制代码
int main()
{
  std::any a;//any a(10)
  a = 10;
  cout << "a: " << std::any_cast<int>(a) << endl;
  cout << "*a: " << *std::any_cast<int>(&a) << endl;  

  a  = string("hello C++17");
  cout << "string: " << std::any_cast<string>(a) << endl;
  cout << "*string: " << *std::any_cast<string>(&a) << endl;
  return 0;
}
相关推荐
路過的好心人2 小时前
Nginx 的多个场景配置
运维·网络·nginx
qq_343247032 小时前
docker 下搭建 nacos
运维·docker·容器
人工智能训练2 小时前
Ubuntu系统中Docker的常用命令总结
linux·运维·人工智能·ubuntu·docker·ai
Felix_XXXXL3 小时前
IDEA + Spring Boot 的三种热加载方案
java·后端
程序员爱钓鱼3 小时前
Python编程实战:面向对象与进阶语法——上下文管理器(with语句)
后端·python·ipython
我命由我123453 小时前
IDEA - IDEA 快速回到页面首尾、页面快速滑动、快速定位到指定行
java·运维·ide·后端·java-ee·intellij-idea·intellij idea
程序员爱钓鱼3 小时前
Python编程实战:面向对象与进阶语法——装饰器(Decorator)
后端·python·ipython
苏比的博客4 小时前
Windows MFC添加类,变量,类导向
c++·windows·mfc
yudiandian20144 小时前
MFC - 使用 Base64 对图片进行加密解密
c++·mfc