【网络编程】定时器的应用:基于升序链表的定时器处理非活动连接

首先我们实现一个数据结构用来存储定时器,它是一个升序的双向链表。主要在其中实现了插入定时器,删除定时器,调整定时器位置的操作,其实现如下:

cpp 复制代码
#ifndef LST_TIMER
#define LST_TIMER

#include <netinet/in.h>
#include <stdio.h>
#include <time.h>
#define BUFFER_SIZE 64
class util_timer;


/*用户数据结构:客户端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;
        }
    }

    /*将目标定时器timer添加到链表中*/
    void add_timer(util_timer* timer){
        if( !timer ) return ;  /*timer不存在,插入不可能实现,直接返回*/
        if( !head ){          /*头节点不存在,则当前定时器作为链表的头节点和尾节点*/
            head = tail = timer;
            return ;
        }
        /*如果定时器的超时时间比头节点的超时时间少,则当前定时器作为头节点*/
        if(timer -> expire < head -> expire){
            timer -> next = head;
            head -> prev = timer;
            head = timer;
            return ;
        }
        /*如果定时器的超时时间比头节点的超时时间多,则调用重载函数add_timer(util_timer* timer, util_timer* lst_head)
            来插入到链表的指定位置,保证了链表的升序性质
        */
        add_timer(timer, head);
    }

    /*当定时器任务发生改变时,修改定时器在链表中的位置*/
    void adjust_timer(util_timer* timer){
        if( !time ) return ;
        util_timer* tmp = timer -> next;

        if( !tmp || (timer -> expire < tmp -> expire)) return ; /*如果此时的timer为队尾或者定时器的超时时间仍然比下一个元素小,则不改变位置*/
        /*如果定时器是链表的头节点,则把当前定时器取出来重新插入*/
        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 tail;
            return ;
        }
        timer -> prev -> next = timer -> next;
        timer -> next -> prev = timer -> prev;
        return ;
    }
    

    /*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_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;
        /*遍历head之后的链表,直到找到超时时间大于定时器的节点,并把定时器插入*/
        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

在接下来的对于非活动连接的处理方面,我们将每一对服务端和客户端之间的连接相关的连接数据保存下来,并且加上其超时时间共同打包到上述数据结构的单个节点中。

同时,我们设置了统一事件源,SIGALRM信号每一次被触发,主循环中调用一次定时任务处理函数,用来处理到期的定时器。

cpp 复制代码
#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;   /*设置该事件数据可读,并且采用ET模式来操作该文件描述符*/
    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( TIMESLOT );
}

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("usag: %s ip_address port_number \n", basename(argv[0]));
        return 1;
    }
    const char* ip = argv[1];
    int port = atoi(argv[2]); /*atoi是将字符串转化为整数*/

    int ret = 0;
    struct sockaddr_in address;  /*ipv4类型的socket结构体*/
    bzero(&address, sizeof(address)); /*初始化结构体为\0*/
    address.sin_family = AF_INET;   /*设置地址族*/
    address.sin_port = htons(port); /*htons是将主机字节序转化为网络字节序,该行设置socket的端口*/
    inet_pton(AF_INET, ip, &address.sin_addr); /*网络字节序和点分十进制的转化,设置socket的ip地址*/

    int listenfd = socket(PF_INET, SOCK_STREAM, 0);/*设置TCP传输方式,该socket的文件描述符为listenfd*/
    assert(listenfd >= 0);/*如果socket没有创建成功,则assert会打出断言失败的信息*/
    /*assert函数,其中表达式为真,继续执行,其中表达式为假,输出断言失败信息*/

    ret = bind(listenfd, (struct sockaddr*)&address, sizeof(address));/*将address的地址分配给listenfd文件描述符
                                                                        即代表listenfd和address绑定*/
    assert(ret != -1);

    ret = listen(listenfd, 5);/*监听listenfd上的客户端连接*/
    assert(ret != -1);

    epoll_event events[MAX_EVENT_NUMBER];
    int epollfd = epoll_create(5);/*创建一个文件描述符来标识内核事件表,并设置内核事件表大小为5*/
    assert(epollfd != -1);
    addfd(epollfd, listenfd); /*将listenfd文件描述符的可读事件添加到内核事件表中*/

    /*创建无名套接字存放在pipefd中,这对套接字全双工*/
    ret = socketpair(PF_UNIX, SOCK_STREAM, 0, pipefd); 
    assert(ret != -1);
    setnonblocking(pipefd[1]); /*设置写端非阻塞*/
    addfd(epollfd, pipefd[0]); /*将读端加入到epoll内核事件表中监视*/

    /*设置信号处理函数*/
    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); /*在一段时间内等待文件描述符上的事件,返回的number为就绪文件描述符的个数*/
        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);
                /*同意服务端和客户端连接,connfd为该连接的文件描述符*/
                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)){
                int sig;
                char signals[1024];
                ret = recv(pipefd[0], signals, sizeof( signals ), 0);
                if(ret == -1) continue;
                else if(ret == 0) continue;
                else{
                    for(int i = 0; i < ret ; ++ i){
                        switch( signals[i] ){
                            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 {
                    /*如果有数据传输,则将超时时间延后三个单位,并且调整定时器在队列中的位置*/
                    if(timer){
                        time_t cur = time(NULL);
                        timer -> expire = cur + TIMESLOT * 3;
                        printf("adjust timer once\n");
                        timer_lst.adjust_timer(timer);
                    }
                }
            }
            else{
                //others
            }
        }
        if(timeout){
            timer_handler();
            timeout = false;
        }
    }
    close(listenfd);
    close(pipefd[1]);
    close(pipefd[0]);
    delete [] users;
    return 0;
}
相关推荐
Aiden_SHU31 分钟前
Wireshark中的length栏位
服务器·网络·wireshark
叫我龙翔1 小时前
【计网】实现reactor反应堆模型 --- 多线程方案优化 ,OTOL方案
linux·运维·网络
?crying2 小时前
蓝队基础4 -- 安全运营与监控
网络·安全·web安全
茶颜悦色vv2 小时前
蓝队知识浅谈(中)
网络·web安全·网络安全
Xlbb.2 小时前
安全见闻6-9
网络·安全·web安全·网络安全
win x3 小时前
链表(Linkedlist)
数据结构·链表
写bug的小屁孩3 小时前
websocket初始化
服务器·开发语言·网络·c++·websocket·网络协议·qt creator
域智盾系统3 小时前
挖到宝了!统一dlp数据防泄漏解决方案有哪些?千字长文带你熟知这6款!
网络·数据防泄漏·统一dlp数据防泄漏解决方案·数据安全防护措施
茶颜悦色vv3 小时前
蓝队知识浅谈(上)
网络·web安全·网络安全
石牌桥网管3 小时前
DNS Resolver解析服务器出口IP查询
运维·网络·tcp/ip·dns