- 网络编程需要处理的三类事件:IO、信号、定时器
- 例如:定期检查一个客户连接的活动状态
- 把定时事件封装成定时器。
- 本章学习两种高效管理定时器的容器:时间轮、时间堆
- 定时:指一段时间后触发某段代码的机制。
- Linux提供三种定时方法:
- socket选项的SO_RCVTIMEO和SO_SNDTIMEO
- SIGALRM信号
- IO复用系统的超时参数。
11.1 socket选项的SO_RCVTIMEO和SO_SNDTIMEO
- SO_RCVTIMEO:设置socket接收数据的超时时间;
- SO_SNDTIMEO:设置socket发送数据的超时时间;
这两个选项堆数据接收和发送的socket相关系统调用有关:send、sendmsg、recv’、recvmsg、accept、connect
11.2 SIGALRM信号
- 由alarm和setitimer函数设置的实时闹钟一旦超时,就会触发SIGALRM信号,所有我们可以使用该信号的信号处理函数来处理定时认为。
- 如果需要处理多个定时任务,就需要不停的触发SIGALRM信号,
一般,SIGALRM函数按照固定的频率生成,也就是alarm和setitimer函数设置的定时周期T不变。
11.2.1 基于升序链表的定时器
定时器成员通常至少要包含两个成员:
- 超时时间
- 任务回调函数,回调函数执行时传入参数(可能),是否重启定时器(可能)
如果链表是双向的,则每个定时器还需要包含指向前一个定时器的指针成员。
11.2.2 处理非活动连接
- 服务器处理非活动连接方法:给客户发送重连请求,或者关闭连接
- Linux提供了对连接是否处于活动状态的定期检查机制:socket选项的KEEPALIVE
- 上述方法会使得程序对连接的管理变复杂,所有可以考虑在应用层实现类似KEEPALIVE的机制,管理所有长时间处于非活动状态的连接。
- 例如:使用alarm函数周期性触发SIGALRM信号,信号处理函数利用管道通知主循环执行定时器链表上的定时任务——关闭非活动连接。
11.3 I/O服用系统调用超时参数
- Linux下三组I/O复用系统调用都带有超时参数,所以不仅能处理信号和I/O时间,也能统一处理定时事件
- 不过IO复用可能在超时时间到来之前就返回了(IO事件发生),所以如果需要定时,就要不断更新定时参数以反映剩余时间。
11.4 高性能定时器
11.4.1 时间轮
- 基于排序链表的定时器添加定时器效率偏低。
- 时间轮有N个槽,每一个槽的时间间隔是si,转一周的时间是Nsi。每一个槽有一个定时链表,定时链表之间的定时时间相差Nsi的整数倍。
- 例如:当前指向槽cs,添加一个定时时间为ti的定时器,定时器插入槽ts=(cs + (ti/si))%N
- 多时间轮:不同的时间轮颗粒度不同。
#include <time.h>
#include <netinet/in.h>
#include <stdio.h>
#include <iostream>
#define BUFFER_SIZE 64
class tw_timer;
//绑定socket与定时器
struct client_data {
sockaddr_in address;
int sockfd;
char buf[BUFFER_SIZE];
tw_timer timer;
};
/*定时器类 */
class tw_timer {
public:
tw_timer(int rot, int ts): next(nullptr), prev(nullptr), rotation(rot), time_slot(ts){}
public:
tw_timer* next; //指向下一个定时器
tw_timer* prev; //指向上一个定时器
int rotation; //记录定时器在时间轮转多少圈后生效
int time_slot; //记录定时器属于时间轮上的哪个槽
void (*cb_func)(client_data*); //回调函数
client_data* user_data; //客户数据
};
/*时间轮 */
class time_wheel {
public:
//构造函数:
time_wheel():cur_slot(0)
{
for(int i=0; i<N; ++i){
slot[i] = nullptr;
}
}
//析构函数: 遍历每个槽,销毁其中的定时器
~time_wheel()
{
for(int i=0; i<N; ++i){
tw_timer* tmp = slots[i];
while(tmp){
slots[i] = tmp->next;
delete tmp; //个人觉得应该为: tmp = nullptr;
tmp = slots[i];
}
}
}
//增: 根据定时值timeout创建一个定时器,并插入到合适的槽中
tw_timer* add_timer(int timeout)
{
if(time_out < 0){
return nullptr;
}
int ticks = 0;
/* 下面根据待插入定时器的超时值计算它将在时间轮转动多少个滴答后被触发,并将该滴答数存在变量ticks中
如果待插入定时器的超时值小于时间轮的槽间隔SI,则将ticks向上折合为1,否则就将ticks向下这和为 timeout/SI */
if(timeout < SI)
{
ticks = 1;
}else{
ticks = timeout/SI;
}
//计算待插入的定时器在时间轮转动多少圈后被触发
int roration = ticks/N;
//计算待插入的定时器应该被插入哪个槽中
int ts = (cur_slot + (ticks%N))%N;
/* 创建新的定时器,他在时间轮转动rotation圈后被触发,且位于第ts个槽上*/
tw_timer* timer = new time_timer(rotation, ts);
/* 如果第ts个槽中尚无任何定时器,则把新建的定时器插入其中, 并将该定时器设置为该槽的头节点 */
if(!slots[ts])
{
std::cout<< "add timer, rotation is "<<rotation<<" ts is "<<ts<<"cur_slot is "<<cur_slot<<std::endl;
slots[ts] = timer;
}
/* 否则, 将定时器插入第ts个槽中 */
else{
timer->next = slots[ts];
slots[ts]->prev = nullptr;
slot[ts] = timer;
}
return timer;
}
/* 删除目标定时器timer*/
void del_timer(tw_timer* timer)
{
if(!timer){
return;
}
int ts = timer->time_slot;
/* slots[ts]是目标定时器所在槽的头节点。如果目标定时器就是该头结点,则需要重置第ts个槽的头结点 */
if(timer = slots[ts]){
slots[ts] = slots[ts]->next;
if(slots[ts]){
slots[ts]->prev = nullptr;
}
delete timer;
}
else{
timer->prev->next = timer->next;
if(timer->next)
{
timer->next->prev = timer->prev;
}
delete timer;
}
}
/* SI时间到后,调用该函数,时间轮向前滚动一个槽的间隔 */
void tick()
{
tw_timer* tmp = slots[cur_slot]; //取得时间轮上当前槽点的头结点
std::cout<<"current slot is "<<cur_slot<<std::endl;
while(tmp){
std::cout<<"tick the time once"<<std::endl;
if(tmp->rotation > 0){
tmp->rotation--;
tmp = tmp->next;
}
/* 否则说明定时器已过期,于是执行定时任务,然后删除该定时器 */
else{
tmp->cb_func(tmp->user_data);
if(tmp == slots[cur_slot])
{
std::cout<<"delete header in cur_slots"<<std::endl;
slots[cur_slot] = tmp->next;
delete tmp;
if(slots[cur_slot])
{
slots[cur_slot]->prev = nullptr;
}
tmp = slots[cur_slot];
}else{
tmp->prev->next = tmp->next;
if(tmp->next){
tmp->next->prev = tmp->prev;
}
tw_timer* tmp2 = tmp->next;
delete tmp;
tmp = tmp2;
}
}
}
//更新时间轮的当前槽,以反映时间轮的转动
cur_slot = ++cur_slot % N;
}
private:
static const int N = 60; //时间轮上槽的数量
static const int SI = 1; //每一秒钟转动一次
tw_timer* slots[N]; //时间轮的槽,每一个元素指向一个定时器链表
int cur_slot; //时间轮当前槽
};
11.4.2 时间堆
- 用堆这种数据结构来存储定时器。