定时器概念:
使用定时器的目的是周期性的执行一个任务,或者是到某一时间去执行某一任务。本章用来处理断开连接超时的客户端,为此,将每个定时时间封装成定时器,并使用链表,时间轮(也是链表),堆等容器类数据结构,对定时时间统一管理。
在网络编程中,我们通过socket创建套接字,然后通过setsockopt()函数设置套接口选项。
函数原型
setsockopt( SOCKET s, int level, int optname, const char FAR* optval, int option);
第三个参数optname可用来指定超时接受(SO_RCVTIMEO)或者超时发送(SO_SNDTIMEO),与其关联的第四个参数此时为timeout类型,指定具体的超时时间。然后用connect()函数去连接客户端,超时对应的errno是EINPROGRESS。检测到此errno则关闭连接。此处根据系统调用的返回值来判断超时时间是否已到,据此处理定时任务即关闭连接。
除了通过系统调用判断,更多的使用信号。SIGALRM是在定时器终止时发送给进程的信号。由alarm()和setitimer()函数设置的实时闹钟一旦超时,将触发此信号,然后在其处理函数中处理到期的任务。
为了便于处理多个同类型不同时间的定时事件,我们可以设计基于升序链表的定时器,即保持一个超时时间从小到大的时间有序的定时器链表。
主要部分有三个
1./*用户数据结构*/
struct client_data
{
sockaddr_in address; /*客户端socket地址*/
int sockfd; /*客户端套接字*/
char buffer[ BUFFER_SIZE ]; /*读缓冲区*/
util_timer* timer; /*定时器*/
};
2./*定时器类*/
struct util_timer
{
public:
util_timer() : prev( NULL ), next( NULL ) { }
public:
time_t expire; /*任务的超时时间*/
void ( *cd_func )(client_data*); /*任务回调函数*/
client_data* user_data; /*用户数据结构*/
util_timer* prev; /*双向链表*/
util_timer* next;
};
3./*定时器链表。它是一个升序、双向链表,且带有头结点和尾节点*/
class sort_timer_lst
{
public:
/*构造函数*/
sort_timer_lst() : head( NULL ), tail( NULL ) {}
/*析构函数,清空定时器链表*/
~sort_timer_lst();
/*将目标定时器timer添加到链表中*/
void add_timer( util_timer* timer );
/*当某个定时任务发生变化时,调整对应的定时器在链表的位置,这个函数只考虑被调整的定时器事件延长情况,即只需向尾部移*/
void adjust_timer( util_timer* timer );
/*将目标定时器timer从链表中删除*/
void del_timer( util_timer* timer );
/*SIGALRM信号每次被触发就在其信号处理函数中执行一次tick函数,以处理链表上到期的任务*/
void tick();
private:
/*一个重载的辅助函数,被函数add和adjust调用,该函数表示将time添加到head之后的位置*/
void add_timer( util_timer* timer, util_timer* lst_head );
private:
util_timer* head;
util_timer* tail;
};
信号处理函数
void tick()
{
time_t cur = time(NULL); /*获取当前时间*/
util_timer* temp = head;
while( temp )
{
if( cur < temp-> expire ) /*超过当前时间则为超时*/
{
break;
}
temp -> cb_func( tmp -> user_data );
/*...删除处理完的定时器,处理头结点...*/
}
}
SIGALRM信号每次触发就执行一次tick()函数,通过与当前时间比较来判断是否为超时事件。如果是则处理类中的信号处理函数。
基于升序链表的定时器用起来很方便,但一旦定时事件多起来,效率就会很低。
因此,通过改进,就有了时间轮。
指针每转动一下为一个滴答,图中指向1的指针为当前槽,指向2的指针(本来是虚线来着,技术渣没画出来)指向下一个槽。
一个滴答的时间称为时间轮的槽间隔si(心博时间),时间轮共N个槽,因此运转一周时间为 N*si . 每个槽指向一个定时器链表,每个链表上的定时器具有相同的特征,他们的定时时间相差 N*si的整数倍。
如果N=12,指针指向了tick=2处,要加一个再经过tick为15的定时任务,怎么放置?
计算可得:2 + 15%12 = 5,所以将其放置在tick=5的槽中,待tick从2跳一轮回到2再跳3下到tick=5,这个事件就被执行。
简单的时间轮类(仅有一个轮子):
class tw_timer
{
public:
tw_timer(int rot, int ts);
public:
int rotation; /*记录定时器再时间轮中转多少圈后生效*/
int time_slot; /*记录定时器属于时间轮上哪个槽*/
void (*cb_func)( client_data* ); /*定时器回调函数*/
client_data* user_data; /*客户数据*/
tw_timer* next; /*指向下一个定时器*/
tw_timer* prev; /*指向上一个定时器*/
}
class time_wheel
{
public:
time_wheel();
~time_wheel();
/*根据定时值timeout创建一个定时器并插入合适槽中*/
tw_timerr* add_timer( int timeout );
/*删除目标定时器*/
void del_timer( tw_timer* timer );
/*SI时间到后,调用该哈数,时间轮像前滚动一个槽*/
void tick();
}
添加或者删除一个定时器的复杂度是O(1),执行一个定时器的时间复杂度为O(n)。
时间堆
这章的学习中,感觉效率比较高的是时间堆。
因为处理定时器事件的时候都是超时时间最小的,所以可以采用最小堆的方式来打理这群定时器,即每个节点的值都小于或等于他的子节点的值的二叉树。
插入定时器
在树下创建一个空穴,将新节点插入,如果不影响堆序,则插入完成,否则执行上滤操作。即交换空穴和它的父节点上的元素,不断执行直到插入成功。例如插入值为14的元素。时间复杂度为O(lgn)。
时间堆的删除很简单,即删除堆的根结点即可。删除后调整堆序,先在根结点建立空穴,此时根结点已被删除。此时堆中少了一个元素,我们可以把堆最后一个元素X移动到该堆之外(但不能丢),如果X可以插入根据诶点,则删除成功。但一般不能,此时就对堆中元素执行上滤操作,即交换空穴和他的两个儿子中的较小者,不断进行此过程,直到X可以被成功插入,删除成功。删除一个定时器的时间复杂度为O(1)。
下滤操作代码:(二叉树采用数组存储,对于任意i位置,2i+1为左孩子)
time_heap::percolate_down( int hole ) //hole即空穴下标
{
heap_timer* temp = array[ hole ];
int child = 0;
for( ; (hole*2+1) <= (cur_size-1); hole=child )
{
child = hole*2 + 1;
if( child < (cur_size-1) && (array[child+1] -> expire < array[child] -> expire))
{
++child;
}
if( arrry[child] -> expire < temp -> expire )
{
array[hole] = array[child];
}
else
{
break;
}
}
array[hole] = temp;
}
定时器中的时间轮优化下,可以有多层轮子,提高精确度。比如初始化一个三层时间轮:秒刻盘:0~59个SecList, 分刻盘:0~59个MinList, 时刻盘:0~12个HourList。
SecTick由外界推动,每跳一轮(60格),SecTick复位至0,同时MinTick跳1格;
同理MinTick每跳一轮(60格),MinTick复位至0,同时HourTick跳1格;
最高层:HourTick跳一轮(12格),HourTick复位至0,一个时间轮完整周期完成。恩,有时间了回来实现吧。