关于定时器的初步认识

定时是指在一段时间之后触发某端代码的机制,我们可以在这段代码中依次处理所有到期的定时器。


即定时机制是定时器得以被处理的原动力,linux提供三种定时方法:
    1、socket选项, SO_RCVTIMEO / SO_SNDTIMEO
    2、SIGALRM信号
    3、I/O复用系统调用的超时参数
接下来就先围绕这三个方法进行讨论


socket选项SO_RCVTIMEO和SO_SNDTIMEO
    他们分别用来设置socket接收数据超时时间和发送数据超时时间。因此,这两个选项仅对与数据接收和发送相关的socket专用系统调用有效
关于定时器的初步认识_第1张图片


    由图可见,我们可以通过系统调用的返回值以及errno来判断超时时间是否已到,进而决定是否开始处理定时任务。
    如下,以connect为例:

struct timeval timeout;
timeout.tv_sec = time;
timeout.tv_usec = 0;
socklen_t len = sizeof(timeout);
if(setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &timeout, len) < 0)
{}
if(connect(...) < 0)
{
    //connect 超时返回的错误是EINPROGRESS
    if(errno == EINPROGRESS)
    {
        //超时发生,这里处理超时任务
    }
    printf("error occur");
    return -1;
}
// ...



SIGALRM信号
    由alarm和setitimer函数设置的实时闹钟一旦超时,将触发此信号。因此,定时任务可以由信号处理函数处理。
    但是,如果要处理多个定时任务,我们就需要不断的触发SIGALRM信号,并在其信号处理函数中执行到期的任务。
    这里我们通过处理非活动连接,来介绍如何使用SIGALRM信号定时。

    下面,我们先给出一种简单的定时器实现---基于升序链表的定时器,并将其应用到处理非活动连接中去。

//定时器链表实现
#ifndef LST_TIMER
#define LST_TIMER

#include 
#define BUFE_SIZE 64

class util_timer;

//用户的数据结构
struct client_data
{
    sockaddr_in     cd_address;
    int         cd_sockfd;
    char         cd_buf[BUFE_SIZE];        //读缓存
    util_timer*    timer;                //定时器
};


//定时器
//定时器通常至少包含两个成员,一个超时时间,一个任务回调函数。
//有时候还包括回调函数被执行时需要传入的参数,以及是否重启定时器等信息
//如果用链表作为容器串联所有定时器,每个定时器还要(可能)包含前一个和下一个定时器的指针
//这里是一个简单的升序定时器链表
class util_timer
{
public:
    //可以看出,我们用的是双向链表
    util_timer():prev(NULL),next(NULL) {}
public:
    time_t expire;                    //任务超时时间,这使用绝对时间
    void (*cb_fun)(client_data*);            //任务回调函数
    
    /*回调函数处理的客户数据,由定时器的执行者传递给回调函数*/
    client_data*    user_data;
    util_timer*    prev;
    util_timer*    next;
};

//定时器链表
//属性: 升序, 双向, 带头节点, 带尾节点
class sort_timer_list
{
public:
    //构造和析构
    sort_timer_list():head(NULL),tail(NULL) {}
    ~sort_timer_list()
    {
        util_timer* tmp = head;
        while(tmp)
        {
            head = tmp->next;
            delete tmp;
            tmp = head;
        }
    }

    //将目标定时器添加仅链表
    void add_timer(util_timer *timer)
    {
        if(!timer)
            return;
        else if(!head)
        {
            head = tail = timer;
            return;
        }
        //节点插入的顺序由expire时间决定
        else if(timer->expire < head->expire)
        {
            timer->next = head;
            head->prev = timer;
            head = timer;
            return;
        }
        
        //调用重载函数,将对象插入适当位置
        add_timer(timer, head);
    }

    void add_timer(util_timer* timer, util_timer *head)
    {
        util_timer* tmp = head;
        while(tmp)
        {
            if(timer->expire < tmp->expire)
                break;
            tmp = tmp->next;
        }

        if(NULL == tmp)
        {
            timer->prev = tail;
            tail->next = timer;
            tail = timer;
        }
        else
        {
            timer->prev = tmp->prev;
            timer->next = tmp;
            tmp->prev->next = timer;    //注意,这样使用要提前判断是否为头节点,因为调用此函数之前将此可能剔除了,所以这里不判断
            tmp->prev = timer;
        }
    }

    //当某个定时任务发生变化,调整对应的定时器在链表中的位置。这里只考虑被调整的定时器超时时间被延长的情况
    void adjust_timer(util_timer * timer)
    {
        //不存在
        if(NULL == timer)
            return;

        util_timer *tmp = timer->next;
        //本身是尾部,或调整后仍小于后面节点
        if(!tmp || timer->expireexpire)
            return;

        //为头节点,取下来重插
        if(timer == head)
        {
            head = timer->next;
            head->prev = NULL;
            timer->next = NULL;
            add_timer(timer,head);
        }
        //是里面的某节点,取下来从后面节点开始,重插
        else
        {
            tmp->prev = timer->prev;
            timer->prev->next = tmp;    //要注意的是,因为前面已经判断过是否为尾部,否则这里要使用timer->next->prev要提前判断是否存在这个next对象
            timer->prev = timer->next = NULL;
            add_timer(timer,tmp);
        }
    }

    //删除目标定时器
    void del_timer(util_timer* timer)
    {
        if(NULL == timer)
            return;

        if(timer == head && timer == tail)
            head = tail = NULL;
        else if(timer == head)
        {
            head = timer->next;
            head->prev = NULL;
        }
        else if(timer == tail)
        {
            timer->prev->next = NULL;
            tail = timer->prev;
        }
        else{
            timer->prev->next = timer->next;
            timer->next->prev = timer->prev;
        }

        delete timer;
    }

    /*SIGALRM信号每被触发一次就在其信号处理函数中执行一次tick函数,以处理链表上到期的任务*/
    void tick()
    {
        if(!head)
            return;

        printf("time tick\n");
        time_t cur = time(NULL);    //获取系统当前时间
        util_timer *tmp = head;

        while(tmp)
        {
            if(cur < tmp->expire)
                break;
            
            //执行定时任务
            tmp->cb_fun(tmp->user_data);
            //执行完就删了
            head = tmp->next;
            if(head)
                head->prev = NULL;
            delete tmp;
            tmp = head;
        }
    }

private:
    util_timer*    head;
    util_timer*    tail;
}
//核心函数是tick函数,相当于一个心博函数,每隔一段时间执行一次,检测并处理到期任务


//这个程序大意是,为每个连接设置一个定时器。定时器被挂在定时器链表上。
//定时器一般会在连接没有数据传输的3个TIMEOUT时间后将连接清除;如果有数据来,就重置定时器
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "lst_timer.h"

#define TIMEOUT 5                    //这个就相当于以 TIMEOUT 为时间单位
#define MAX_EVENT_NUMBER 1024
#define FD_LIMIT 65535

static sort_timer_list timer_lst;            //定时器链表
static pipefd[2];                    //统一事件源用的管道

void sig_handler(int s);                //信号处理函数,但是我们这里使用事件源,所以信号处理函数只是单纯的将接收到的信号传给主函数
void setnonblocking(int fd);                //设置非阻塞
void addsig(int sig, void (*sig_handler)(int));        //为某信号添加信号处理函数
void addfd(int epollfd, int fd);            //将对某描述符的监听添加进内核事件表中,等待epoll返回
void timer_handler();                    //定时任务
void cb_fun(client_data* user_data);            //定时任务要调用的回调函数

int main(int ac, char *av[])
{
    if(ac != 3)
    {
        fprintf(stderr, "Usage: %s addr port\n",av[0]);
        exit(1);
    }

    char *ip = av[1];
    int port = atoi(av[2]);
    int ret;

    struct sockaddr_in addr;
    bzero(&addr, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    inet_pton(AF_INET, ip, &addr.sin_addr);

    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if(sock < 0)
    {
        perror("socket error");
        exit(1);
    }

    ret = bind(sock, (struct sockaddr *)&addr, sizeof(addr));
    if(ret < 0)
    {
        perror("bind error");
        exit(1);
    }

    ret = listen(sock, 10);
    if(ret < 0)
    {
        perror("listen error");
        exit(1);
    }
                //以上可忽略
    struct epoll_event events[MAX_EVENT_NUMBER];
    int epollfd = epoll_create(5);
    if(socketpair(AF_UNIX, SOCK_STREAM, 0, pipefd) < 0)    //用于传递信号,实现统一事件源
    {
        perror("socketpair error");
        exit(1);
    }
    setnonblocking(pipefd[1]);

    client_data *users = new client_data[FD_LIMIT];        //这个users的使用和chat_room的users的意义一样

    addfd(epollfd,sock);
    addfd(epollfd,pipefd[0]);

    addsig(SIGALRM);
    addsig(SIGTERM);

    bool stop_server = false;            //如果收到SIGTERM, 那么就为true

    int nready = 0;
    alarm(TIMEOUT);                    //开始定时
    bool time_out = false;                //这个标志下面会讲到

    while(!stop_server)
    {
        nready = epoll(epollfd, events, MAX_EVENT_NUMBER, -1);
        if(nready < 0 && errno!=EINTR)
        {
            fprintf(stderr,"epoll failed\n");
            exit()1;
        }
        
        for(int i=0; iexpire = cur + 3*TIMEOUT;        //在3个TIMEOUT的时间后,若没有数据传输,会执行定时任务,即将此连接清除
                timer->cb_fun = cb_fun;
                timer->prev = timer->next = NULL;
                timer->user_data = &users[connfd];

                users[connfd].timer = timer;
                timer_lst.add_timer(timer);
            }
            //处理信号
            else if((fd==pipefd[0]) && (events[i].events & EPOLLIN))
            {
                int sig;
                char signals[1024];
                ret = recv(pipefd[0], signals, sizeof(signals), 0);
                if(ret <= 0)        //出错了我也不知道怎么去处理
                {
                    continue;
                }
                else
                {
                    for(int j=0; jsockfd, 0);
    close(user_data->sockfd);
    printf("closed fd %d\n",user_data->sockfd);
}



I/O复用系统调用的超时参数
    使用I/O复用系统调用所带有的超时参数是可以的,但可能会因为有事件就绪而导致提前返回,我们需要进一步处理。

#define TIMEOUT 5000
int timeout = TIMEOUT;
time_t start, end;

while(1)
{
    printf("now the timeout is %d\n",timeout);
    start = time(NULL);
    int nready = epoll_wait(..., timeout);        //这里的timeout单位是 millisecond
    if(nready < 0)
    {
        //不是EINTR那就可能出错了
    }
    else if(nready == 0)        //这就是正好超时了,期间没有任何描述符事件就绪
    {
        //这里可以进行定时任务了
        timeout = TIMEOUT;    //记得重置
        continue;
    }
    
    //返回大于0, 表明有事件就绪了, 我们就要进行计算了
    end = time(NULL);
    timeout -= (end-start) * 1000;
    
    if(timeout <= 0)        // 这种情况是既有事件就绪,又正好超时事件到
    {
        //还是可以进行定时任务的处理
        timeout = TIMEOUT;
    }
    //大于0的话就是继续等待那么一段时间
}



你可能感兴趣的:(Unix网络编程)