第11章 定时器
定时器容器是容器类数据结构,比如时间轮;定时器则是容器内容结容纳的一个个对象,它是对定时事件的封装。
11.1 socket选项SO_RCVTIMEO和SO_SNDTIMEO
SO_RCVTIMEO和SO_SNDTIMEO:分别用来设置socket接收数据超时时间和发送数据超时时间。
#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>
int timeout_connect(const char *ip, int port, int time)
{
int ret = 0;
struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
inet_pton(AF_INET, ip, &address.sin_addr);
address.sin_port = htons(port);
int sockfd = socket(PF_INET, SOCK_STREAM, 0);
assert(sockfd > 0);
struct timeval timeout;
timeout.tv_sec = time;
timeout.tv_usec = 0;
socklen_t len = sizeof(timeout);
ret = setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &timeout, len);
assert(ret != -1);
ret = connect(sockfd, (struct sockaddr *)&address, sizeof(address));
if (ret == -1)
{
if (errno == EINPROGRESS)
{
printf("connecting timeout\n");
return -1;
}
printf("error occur when connecting to server\n");
return -1;
}
return sockfd;
}
int main(int argc, char *argv[])
{
if (argc <= 2)
{
printf("usage: %s ip_address port_number \n", basename(argv[0]));
return 1;
}
const char *ip = argv[1];
int port = atoi(argv[2]);
int sockfd = timeout_connect(ip, port, 10);
if (sockfd < 0)
{
return 1;
}
close(sockfd);
return 0;
}
11.2 SIGALRM信号
由alarm和setitimer函数设置的实时闹钟一旦超时,将触发SIGALRM信号。定时周期T反映了定时的精度。
定时器成员:超时时间、任务回调函数、(回调函数的参数、是否重启定时器、使用链表作为容器时还需要指向下(上)一个定时器的指针)
#ifndef _LST_TIMER_
#define _LST_TIMER_
#include <time.h>
#define BUFFER_SIZE 64
class util_timer;
//用户数据结构
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超时时间最小
{
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);
}
}
//删除目标定时器
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信号每次被触发就在其信号处理函数中执行,处理到期任务
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:
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;
timer->prev = prev;
break;
}
prev = tmp;
tmp = tmp->next;
}
if (!tmp)
{
prev->next = timer;
timer->prev = prev;
timer->next = NULL;
tail = timer;
}
}
private:
util_timer *head;
util_timer *tail;
};
#endif
时间复杂度:添加O(n),删除O(1),执行定时任务O(1)
服务器程序处理非活动连接:给客户端发一个重连请求,或者关闭该连接。在应用层实现类似KEEPALIVE的机制,以管理所有长时间处于非活动状态的连接。 利用alarm函数周期性地触发SIGALRM信号,该信号的信号处理函数利用管道通知主循环执行定时器链表上的定时任务——关闭非活动的连接。
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <pthread.h>
#include "lst_timer.h"
#define FD_LIMIT 65535
#define MAX_EVENT_NUMBER 1024
#define TIMESLOT 5
static int pipefd[2];
static sort_timer_lst timer_lst; //升序链表
static int epollfd = 0;
int setnonblocking(int fd)
{
int old_option = fcntl(fd, F_GETFL);
int new_option = old_option | O_NONBLOCK;
fcntl(fd, F_SETFL, new_option);
return old_option;
}
void addfd(int epollfd, int fd)
{
epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN | EPOLLET;
epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
setnonblocking(fd);
}
void sig_handler(int sig)
{
int save_errno = errno;
int msg = sig;
send(pipefd[1], (char *)&msg, 1, 0);
errno = save_errno;
}
void addsig(int sig)
{
struct sigaction sa;
memset(&sa, '\0', sizeof(sa));
sa.sa_handler = sig_handler;
sa.sa_flags |= SA_RESTART;
sigfillset(&sa.sa_mask);
assert(sigaction(sig, &sa, NULL) != -1);
}
void timer_handler()
{
timer_lst.tick();
//一次alarm只会引起一次SIGALRM信号,所以要重新定时. 以不断触发SIGALRM
alarm(TIMESLOT);
}
//定时器回调函数,剔除非活动连接socket上的注册事件,并关闭
void cb_func(client_data *user_data)
{
epoll_ctl(epollfd, EPOLL_CTL_DEL, user_data->sockfd, 0);
assert(user_data);
close(user_data->sockfd);
printf("close fd %d\n", user_data->sockfd);
}
int main(int argc, char *argv[])
{
if (argc <= 2)
{
printf("usage: %s ip_address port_number\n", basename(argv[0]));
return 1;
}
const char *ip = argv[1];
int port = atoi(argv[2]);
int ret = 0;
struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
inet_pton(AF_INET, ip, &address.sin_addr);
address.sin_port = htons(port);
int listenfd = socket(PF_INET, SOCK_STREAM, 0);
assert(listenfd >= 0);
ret = bind(listenfd, (struct sockaddr *)&address, sizeof(address));
assert(ret != -1);
ret = listen(listenfd, 5);
assert(ret != -1);
epoll_event events[MAX_EVENT_NUMBER];
int epollfd = epoll_create(5);
assert(epollfd != -1);
addfd(epollfd, listenfd);
ret = socketpair(PF_UNIX, SOCK_STREAM, 0, pipefd);
assert(ret != -1);
setnonblocking(pipefd[1]);
addfd(epollfd, pipefd[0]);
//add all the interesting signals here
addsig(SIGALRM);
addsig(SIGTERM);
bool stop_server = false;
client_data *users = new client_data[FD_LIMIT];
bool timeout = false;
alarm(TIMESLOT); //定时
while (!stop_server)
{
int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
if ((number < 0) && (errno != EINTR))
{
printf("epoll failure\n");
break;
}
for (int i = 0; i < number; ++i)
{
int sockfd = events[i].data.fd;
if (sockfd == listenfd) //处理新到的客户连接
{
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof(client_address);
int connfd = accept(listenfd, (struct sockaddr *)&client_address, &client_addrlength);
addfd(epollfd, connfd);
users[connfd].address = client_address;
users[connfd].sockfd = connfd;
util_timer *timer = new util_timer; //创建定时器
timer->user_data = &users[connfd];
timer->cb_func = cb_func;
time_t cur = time(NULL);
timer->expire = cur + 3 * TIMESLOT;
users[connfd].timer = timer;
timer_lst.add_timer(timer);
}
//处理信号
else if ((sockfd == pipefd[0]) && (events[i].events & EPOLLIN))
{
char signals[1024];
ret = recv(pipefd[0], signals, sizeof(signals), 0);
if (ret == -1)
{
//handle the error
continue;
}
else if (ret == 0)
{
continue;
}
else
{
for (int j = 0; j < ret; ++j)
{
switch(signals[j])
{
case SIGALRM:
{
//标记有定时任务需要处理,但不立即处理
timeout = true;
break;
}
case SIGTERM:
{
stop_server = true;
}
}
}
}
}
//处理客户连接上接收到的数据
else if (events[i].events & EPOLLIN)
{
memset(users[sockfd].buf, '\0', BUFFER_SIZE);
ret = recv(sockfd, users[sockfd].buf, BUFFER_SIZE - 1, 0);
printf("get %d bytes of client data %s from %d\n", ret, users[sockfd].buf,
sockfd);
util_timer *timer = users[sockfd].timer;
if (ret < 0)
{
//发送读错误,则关闭连接,并移除对应的定时器
if (errno != EAGAIN)
{
cb_func(&users[sockfd]);
if (timer)
{
timer_lst.del_timer(timer);
}
}
}
else if (ret == 0)
{
//对方已经关闭连接,我们也关闭连接,并移除定时器
cb_func(&users[sockfd]);
if (timer)
{
timer_lst.del_timer(timer);
}
}
else
{
//客户连接上有数据可读,则要调整该连接对应的定时器,延迟连接关闭时间
send(sockfd, users[sockfd].buf, BUFFER_SIZE - 1, 0);
if (timer)
{
time_t cur = time(NULL);
timer->expire = cur + 3 * TIMESLOT;
printf("adjust timer once\n");
timer_lst.adjust_timer(timer);
}
}
}
else
{
//other
}
}
//最后处理定时事件,因为I/O事件优先级更高
if (timeout)
{
timer_handler();
timeout = false;
}
}
close(listenfd);
close(pipefd[0]);
close(pipefd[1]);
delete[] users;
return 0;
}
11.3 I/O复用系统调用的超时参数
3组I/O复用系统调用都带有超时参数,因此它们不仅能统一处理信号和I/O事件,也能统一处理定时事件。但是由于I/O复用系统调用可能在超时时间到期之前就返回(有I/O事件发生),所以如果要利用它们来定时,就需要不断更新定时参数以反映剩余的时间。
11.4 高性能定时器
时间轮:解决基于排序链表的定时器添加效率低的问题。
时间轮内指针指向轮子上的槽,以恒定速度顺时针转动,每次转动称为一个滴答(tick),一个滴答的时间称为时间轮的槽间隔si(slot interval),就是心搏时间。该时间轮共有N个槽,因此它运转一周的时间是Nsi。每个槽指向一条定时器链表,每条链表上的定时器具有相同的特征:它们的定时时间相差Nsi的整数倍。时间轮正是利用这个关系将定时器散列到不同的链表中。假如现在指针指向槽cs,我们要添加一个定时时间为ti的定时器,则该定时器将被插入槽ts(timer slot)对应的链表中:
ts = (cs + (ti / si)) % N
哈希表的思想。si越小,定时精度越高;N越大,效率越高。
时间复杂度:添加O(1),删除O(1),执行定时任务O(n)
时间堆:将所有定时器中超时时间最小的一个定时器的超时值作为心搏间隔。这样,一旦心搏函数tick被调用,超时时间最小的定时器必然到期,就可以在tick函数中处理该定时器。然后,再次从剩余的定时器中找出超时时间最小的一个,并将这段最小时间设置为下一次心搏间隔。最小堆实现。
数组表示:i,左儿子:2i+1,右儿子:2i+2,父节点:[(i-1)/2]
时间复杂度:添加O(lgn),删除O(1),执行定时任务O(1)