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

文章详细描述了一个使用C++实现的定时器数据结构,包括插入、删除和调整定时器操作,以及与非活动连接关联的定时任务处理。通过SIGALRM信号和epoll机制,确保了定时任务的执行和连接管理的高效性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

#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信号每一次被触发,主循环中调用一次定时任务处理函数,用来处理到期的定时器。

#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;
}

【资源说明】 基于C++实现的HTTP服务器改进版源码+项目使用说明+详细注释.zip 1、技术架构 **本项目实现了基于Epoll管理连接、基于定时器处理活动连接、基于线程池实现Reactor模式、基于cgi脚本处理http请求结果的HTTP服务器。主要框架如下:**\ ![](./image/newhttpd.jpg) 2、模块介绍 **1)主线程实现eventLoop**:主线程基于Reactor并通过Epoll管理,采用ET工作模式进行事件触发,事件注册包括监听、管道监控、读信息监控;\ **2)定时器处理活动连接**:\ **①基于升序链表定时器**:将每个需要监控的连接注册为一个时间结点,每个结点包括双向指针以及期待的时间和回调函数指针;包含添加、删除以及调整结点;回调函数主要实现对当前连接的close;\ **②基于信号和管道的定时事件处理**:建立监听数据集(新连接会加入一个数据集和时间结点,新信息读入会读取数据集并修改时间结点),基于sigaction形式实现对信号和信号处理函数的绑定,信号处理函数向管道发送信号消息,主线程监听到管道消息读入后判断信号类别,并进行关闭连接操作。\ **3)Http响应处理**:基于tinyhttpd进行修改,捕获GET、POST方法,基于cgi脚本(python撰写)实现post请求响应,基于多进程机制并通过双通道实现进程间通信,并用waitpid进行子进程管控。具体结构如下图所示:\ ![](./image/httpd.jpg) \ **4)线程池**:基于C++的生产者消费者模式的并发开发,具体技术运用如下:\ **①线程池底层结构**:线程池创建相当于消费者,队列添加相当于生产者,通过vector维护线程池,通过queue<function<>>维护任务队列;构造函数实现线程池创建并开始运行,enqueue函数实现消息队列,通过future实现异步工作的lambda函数的传递;\ **②同步机制实现**:基于unique_lock以及condition_variable实现同步和互斥,符合RAII原则;\ **5)简单客户端**:(可以通过浏览器进行服务端访问,也可以通过该客户端实现交互以及活动连接处理的测试)\ **①基于POLL的IO复用**:对管道和连接进行事件监听和处理;\ **②基于双管道的简易CGI实现**:修改stdin的定向为管道写端,实现终端对客户端的直接输入和对服务端的发送;\ **6)改进方向**:待进行压力测试并提高抗压性能、可处理的HTTP请求较为简单(数据体的处理还待增加以及CGI功能的完善)、内存池。 3、编译使用 **服务端**:进入linux系统后,进入当前文件夹,首先修改可执行权限,然后通过CMake编译执行即可: ~~~c cd minghttp chmod 600 test.html chmod 600 post.html chmod +X post.cgi cd .. cmake . make ./httpserver ~~~ **客户端**:一方面可以通过浏览器直接进行服务器访问,一方面可以使用自己创建的客户端进行连接和消息互传(使用方案如下): ~~~c g++ simclient.cpp ./a.out ip port ~~~ ![](./image/out.jpg) 4、呈现效果 上一部分的图片已经展现定时器处理活动连接的效果;\ 1)项目默认端口号为8000,ip地址需要通过ifconfig进行查看;\ 2)将ip和端口号进行替换输入,如下输入后可以得到如下界面:\ ![](./image/test.jpg)\ 3)POST的界面信息:\ ![](./image/jie.jpg)\ 4)POST的CGI脚本回显,基于python进行撰写,内容传输为html语言:\ ![](./image/cgi.jpg)\ 5)定时器的相关讯息也可以得到:可以看到5秒信号的定时器信息输出:\ ![](./image/jie1.jpg) 【备注】 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 2、本项目适合计算机相关专业(如计科、人工智能、通信工程、自动化、电子信息等)的在校学生、老师或者企业员工下载使用,也适合小白学习进阶,当然也可作为毕设项目、课程设计、作业、项目初期立项演示等。 3、如果基础还行,也可在此代码基础上进行修改,以实现其他功能,也可直接用于毕设、课设、作业等。 欢迎下载,沟通交流,互相学习,共同进步!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Rockict_z

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值