第一部分:
首先我们需要了解定时器。
我们将每个定时事件分别封装成定时器,并使用某种容器类数据结构,比如链表,排序链表和时间轮,将所有定时器串联起来,以实现对定时事件的统一管理。
在这主要了解时间轮和时间堆。
定时是指一段时间之后触发某段代码的机制,我们可以在这段代码中依次处理所有到期的定时器。
Linux提供了三种定时方法:
1.socket选项SO_RCVTIMEO和SO_SNDTIMEO.
他们是分别用来接收数据超时时间和发送数据超时时间。
所以这两个选项仅对与数据接收和发送相关的socket专用系统调用有效,这些系统调用包括send,sendmsg,recv,recvmsg,accept和connect。
可以根据系统调用的返回值以及ERRNO来判断时间是否已到,进而决定是否开始处理定时任务。
2.SIGALRM信号。
前面知道由alarm和setitimer函数设置的实时闹钟一旦超时,将触发SIGALRM信号。因此,我们可以利用该信号的信号处理函数来处理定时任务。
基于升序链表的定时器
定时器通常至少要包含两个成员,一个超时时间和一个任务回调函数。如果使用链表作为容器来串联所有的定时器,则每个定时器还要包含指向下一个定时器的指针成员。
#ifndef 它是if not define 的简写,是宏定义的一种,实际上确切的说,这应该是预处理功能三种(宏定义、文件包含、条件编译)中的一种----条件编译。
在c语言中,对同一个变量或者函数进行多次声明是不会报错的。所以如果h文件里只是进行了声明工作,即使不使用#
ifndef宏定义,多个c文件包含同一个h文件也不会报错。但是在c++语言中,#ifdef的作用域只是在单个文件中。所以如果h文件里定义了全局变量,即使采用#ifdef宏定义,多个c文件包含同一个h文件还是会出现全局变量重定义的错误。
使用#ifndef可以避免下面这种错误:如果在h文件中定义了全局变量,一个c文件包含同一个h文件多次,如果不加#ifndef宏定义,会出现变量重复定义的错误;如果加了#ifndef,则不会出现这种错误。
示例:
#ifndef x //先测试x是否被宏定义过
升序定时器链表
#ifndef LST_TIMER
#define LST_TIMER
#include<time.h>
#define BUFFER_SIZE 64
class util_timer;
using namespace std;
//用户数据结构:客户端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;
}
}
void 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;
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 = nullptr;
timer->next = null;
add_timer(timer, head);
}
//中间节点,都是取出来然后在放进去
else {
timer->prev->next = timer->next;
timer->next->prev = timer->prev;
add_timer(timer, timer->next);
}
}
void del_timer(util_timer *timer) {
if (!timer)return;
if ((timer == head) && (timer == tail)) {
delete timer;
head = nullptr;
tail = nullptr;
return;
}
if (timer == head) {
head = head->next;
head->prev = nullptr;
delete timer;
return;
}
if (timer == tail) {
tail = tail->prev;
tail->next = nullptr;
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 = nullptr;
}
delete tmp;
tmp = head;
}
}
private:
//重载添加函数
void 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;
break;
}
prev = tmp;
tmp = tmp->next;
}
//插入到尾节点
if (!tmp) {
prev->next = timer;
timer->prev = prev;
timer->next = nullptr;
tail = timer;
}
}
private:
util_timer *head;
util_timer *tail;
};
升序定时器链表的实际应用:用来处理非活动连接。
服务器程序通常要定期处理非活动连接:给客户端发一个重连请求,或者关闭该连接。
3.I/O复用系统调用的超时参数。
#define TIMEOUT 5000
int timeout = TIMEOUT;
time_t start = time(NULL);
time_t end = time(NULL);
while(1)
{
printf("timeout\n");
start = time(NULL);
int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, timeout);
if((number<0)&&(errno!=EINTR))
{
printf("epoll failure\n");
break;
}
//如果epoll_wait成功返回0,说明超时时间到,此时可以处理定时任务,并且重置定时时间
if(number==0)
{
timeout = TIMEOUT;
continue;
}
end = time(NULL);
/*如果返回值大于0,我们要减去这次调用的时间,然后获得下次调用的超时参数*/
timeout-=(end-start)*1000;
//重新计算之后也有可能等于0,说明调用返回时候,不仅有文件描述符就绪,而且超时时间也刚好到达
if(timeout<=0)
{
timeout = TIMEOUT;
}
}
第二部分:时间轮和时间堆
1.时间轮
基于排序链表的定时器存在一个问题:添加定时器的效率偏低。
时间轮的槽间隔si,时间轮共有N个槽,因此它运转一周的时间是Nsi,每个槽指向一条定时器链表,所以每条链表上的定时器具有相同的特征:他们的定时时间相差Nsi的整数倍。利用这个关系将定时器散列到不同的链表中。加入现在指针指向槽cs,要添加一个定时时间为ti的定时器,则该定时器将被插入槽ts对应的链表中:
ts=(cs+(ti/si))%N;
2.时间堆
这种设计思路是:将所有定时器中超时时间最小的一个定时器的超时值作为心搏间隔,这样,一旦心搏函数tick被调用,超时时间最小的定时器必然到期,我们就可以在tick函数中处理该定时器。然后在接着从剩余的定时器中超出超时时间最小的一个。最小堆可以用来实现这种结构。