前言
大家好,如题,今天我们来写定时器的代码。更正一下上一章的结束语哈哈哈,因为我发现相比于线程池,定时器类是相对底层的东西。不知道大家有没有玩过有建筑系统的游戏,比如mc,幻兽帕鲁这些,在我看来写一个项目和搭一个房子有一些共通之处,而根据我的经验,就是如果你建房子不把一层搭完,就直接往上盖二层,一旦你突然想在一层内上再划一片区域用来修新的的建筑,那么你新区域与原本的区域之间的一些联动就会比较难设计。而做项目也是如此,等把线程做完再回来写定时器,虽然也不是不行(我相信会有一些人是这也做,我并没有否定这种方式),但我还是更习惯把所有的底层东西写好,再一点一点往上搭建。言归正传,我们今天把定时器这一章搞定
概述
在具体开始写代码之前,让我们来了解一下有关定时器的基础性知识。首先我们知道,我们设计定时器的目的是为了让他去处理非活动连接。因为我们的系统资源是有限的,当一个客户连接长时间不响应时,我们就要考虑关闭这个连接,把资源分给正在使用的客户,以此来提高服务器的运行效率。这一部分知识我们在基础知识里有讲过,忘了的同学可以在回顾一遍:
C++ Webserver从零开始:基础知识(六)——定时器-CSDN博客
本章内容不多,但是各个函数之间的调用有一些复杂,为此我画了下面这副拓扑图帮助大家理解处理非活动连接的逻辑。我建议大家可以把图保存下来,写代码的时候写到某一个部分不了解它的作用时,就用手机打开来看看,这也会对整个代码的框架有一个良好的认知。
图中绿色的图标①②③④⑤是我推荐的理解顺序,可以方便大家一个模块一个模块地理解它地运行逻辑。其中我们实现的是图中右半边的部分,左边地eventLoop的函数我们要到更高层去完成,如果如果大家想看,可以看看上面的链接,我写了一个eventLoop的伪代码,其中低优先级的实现思想在我的伪代码有所体现。
因为定时器和处理非活动连接的相关知识在上面的连接里已经写了,为了避免文章臃肿,我们就不赘述了,直接来看代码。
定时器类
首先,我们先定义一个定时器类,定时器类里我们封装连接资源,定时事件指针,以及超时时间。
连接资源包括:套接字地址,文件描述符和定时器
超时时间:我们设定为绝对时间,即 :浏览器和服务器连接的时刻 + 固定时间TIMESLOT
然后再定义两个指针分别指向上升链表的前后定时器
/*util_timer前置声明,因为client_data使用了util_timer类*/
class util_timer;/*用户数据结构体(连接资源)*/
struct client_data{sockaddr_in address;//客户端的socket地址int sockfd; //socket文件描述符util_timer *timer; //定时器
};/*定时器类*/
class util_timer{
public:util_timer():prev(nullptr), next(nullptr){}public:time_t expire; //超时时间/*回调函数声明:声明一个返回值为空的函数指针cb_func,传入clent_data指针作为函数参数*/void (*cb_func)(client_data *); //回调函数指针client_data *user_data; //连接资源util_timer *prev; //前向定时器util_timer *next; //后继定时器
};
上升链表类
然后我们再设计定时器容器,这里我们选择的是升序的双向链表,当然,我们也有更高级的定时器容器比如时间轮或者时间堆,如果学有余力,我们后面等项目完成了可以来试试用时间堆来设计定时器容器。
具体到双向链表的代码,除了tick()函数以外就是一个非常简单的数据结构的双向链表的设计了,就不赘述,大家直接看代码理解。
class sort_timer_lst{
public:sort_timer_lst();~sort_timer_lst();void add_timer(util_timer *timer); //添加定时器void adjust_timer(util_timer *timer); //调整定时器void del_timer(util_timer *timer); //删除定时器void tick(); //定时任务处理函数private:void add_timer(util_timer *timer,util_timer *lst_head);util_timer *head;util_timer *tail;
};
sort_timer_lst::sort_timer_lst() {head = NULL;tail = NULL;
}
sort_timer_lst::~sort_timer_lst() {util_timer *tmp = head;while (tmp) {head = tmp -> next;delete tmp;tmp = head;}
}void sort_timer_lst::add_timer(util_timer *timer) {if (!timer) {return;}if (!head) {head = tail = timer;return;}if (timer->expire < head->expire) {timer->next = head;head->prev = timer;head = timer;}add_timer(timer, head);
}void sort_timer_lst::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);}
}/*常规双向节点删除*/
void sort_timer_lst::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;
}/*按升序插入已lst_head为头节点的lst_timer_lst链表中*/
void sort_timer_lst::add_timer(util_timer *timer, util_timer *lst_head) {util_timer *prev = lst_head;util_timer *tmp = prev->next;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;}/*timer需要放到尾节点*/if (!tmp) {prev->next = timer;timer->prev = prev;timer->next = NULL;tail = timer;}
}
现在我们来实现定时器容器内比较关键的一个函数tick函数,也称为心搏函数。所谓tick()函数即在主循环中每经过一段时间,就调用一次tick,而tick就会完成一定的功能。在我们这里很显然它的功能就是将过期的定时器从链表中删除。
而因为我们设计的是升序链表,所以只需从头节点一路遍历并删除定时器,直到遇到一个未过期的定时器时退出循环即可。
void sort_timer_lst::tick() {if (!head) {return;}/*获取时间*/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;}
}
抽象工具类——Utils(实用程序)
到了这里为止,我们定时器的设计基本完成了。接下来我们要来设计一些方法来合理的使用它。
首先我们需要思考一个问题是:我们以前写的传统的代码运行逻辑,是从上到下一行一行按顺序运行的,我们不可能提前预判一些不稳定出现的事件然后用代码处理它,我们只能是等不稳定的事件出现后,让他通过某种方式通知我,然后我在通过一些预案(即处理这些事件的代码)来处理该事件。
那么这里的某种方式是什么呢?
我们通常使用信号,这里的逻辑是,我们每过一段时间就给主循环一个信号,主循环收到信号就记录下来,等其他IO事件完成之后,就调用tick()处理非活动连接。所以接下来我们的需求是如何设置信号,如何发送信号,以及了解一下主循环如何接收信号。
我们在lit_timer文件中再创建一个实用程序类Utils来使用定时器,并完成我刚刚说的那些需求
class Utils{
public:Utils(){}~Utils(){}void init(int timeslot);/*对文件描述符设置非阻塞*/int setnonblocking(int fd);/*将内核事件表注册读事件,ET模式,选择开启EPOLLONESHOT*/void addfd(int epollfd, int fd, bool one_shot, int TRIGMode);/*信号处理函数*/static void sig_handler(int sig);/*设置信号处理函数 这里第二个参数void(handler)(int)等价于void(*handler)(int),再作函数参数时,后者的*可以省略*/void addsig(int sig, void(handler)(int), bool restart = true);/*定时处理任务, 重新定时以不触发SIGALRM信号*/void timer_handler();void show_error(int connfd, const char *info);public:static int *u_pipefd;sort_timer_lst m_timer_lst;static int u_epollfd;int m_TIMESLOT;
};
可以看到我们在类中定义了两个静态成员变量,分别是:
static int *u_pipefd;
static int u_epollfd;
它们的作用说起来比较不好理解,大家可以看看我的那张拓扑图就好
这里我再用线性的方式给大家梳理一遍处理非活动连接的流程,帮助大家理解Utils类各函数的作用,大家也可以结合拓扑图来看:
- 主线程初始化Utils(包括使用pipe接收pipefd,用epollfd接收u_epollfd
- 主线程调用addfd将pipe管道与epollfd相关联
- 主线程调用addsig将目标信号(SIGALRM SIGTERM)加入监听的信号集
- 主线程循环eventLoop()开始,服务器开始运行
- 多个客户连接长久未响应
- 经过TIMESLOT, 触发信号,主循环收到信号
- 主循环调用tick()处理非活动连接
到此为止,我们就经历了处理非活动连接的全过程,接下来我们来实现上面使用的这些函数
void Utils::init(int timeslot) {m_TIMESLOT = timeslot;
}int Utils::setnonblocking(int fd) {/*FILE Control函数用于对文件描述符执行各种操作*/int old_option = fcntl(fd, F_GETFL);int new_option = old_option | O_NONBLOCK;fcntl(fd, F_SETFL, new_option);return old_option;//返回的是旧的设置以用于恢复改动状态
}void Utils::addfd(int epollfd, int fd, bool one_shot, int TRIGMode) {epoll_event event;event.data.fd = fd;/*开启边缘触发模式*/if (TRIGMode == 1) {event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;} else {event.events = EPOLLIN | EPOLLRDHUP;}if (one_shot) {event.events |= EPOLLONESHOT;}epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);setnonblocking(fd);
}/*信号处理函数*/
void Utils::sig_handler(int sig) {/*异步信号处理环境中,信号处理函数可能发生中断并在其他上下文执行,而errno是个全局遍量,若在信号处理函数中发生错误可能会影响其他调用信号处理函数的线程,所以调用完后恢复原值可以避免这种错误*/int save_errno = errno;int msg = sig;send(u_pipefd[1], (char *)&msg, 1, 0);errno = save_errno;
}/*设置信号函数*/
void Utils::addsig(int sig, void(handler)(int), bool restart) {/*创建sigaction结构体*/struct sigaction sa;memset(&sa, '\0', sizeof(sa));/*信号处理函数只发送信号值,不作对应的逻辑处理*/sa.sa_handler = handler;if (restart)sa.sa_flags |= SA_RESTART;/*将所有信号添加到信号集中*/sigfillset(&sa.sa_mask);assert(sigaction(sig, &sa, NULL) != -1);
}void Utils::timer_handler() {m_timer_lst.tick();/*设置定时器,经过m_TIMESLOT秒后发送信号*/alarm(m_TIMESLOT);
}void Utils::show_error(int connfd, const char *info) {send(connfd, info, strlen(info), 0);close(connfd);
}int *Utils::u_pipefd = 0;
int Utils::u_epollfd = 0;
定时器回调函数
上面的内容已经基本上阐述了处理非活动连接的全部流程,而回调函数在tick()函数内调用。负责把定时器对应的文件描述符关闭。
class Utils;
void cb_func(client_data *user_data) {/*删除非活动连接在socket上的注册时间*/epoll_ctl(Utils::u_epollfd, EPOLL_CTL_DEL, user_data->sockfd, 0);assert(user_data);/*关闭文件描述符*/close(user_data->sockfd);http_conn::m_user_count--;
}
结束语
好了,到此为止我们完成了定时器与定时器链表的设计,实现了处理非活动连接的功能,下一章我们真的做线程池了