Day04 - 定时器【WebServer项目:https://github.com/Hansimov/linux-server】

文章描述了一个用于处理非活跃连接的服务器定时器的实现,利用alarm函数周期性触发SIGALRM信号,通过信号处理函数和管道通信通知主循环执行定时任务。定时器链表采用双向链表结构,支持增删改查操作,超时后关闭客户端连接并释放资源。此外,还涉及到了epoll机制、非阻塞I/O以及信号处理函数的可重入性等技术。
摘要由CSDN通过智能技术生成

day04:timer – 定时器

代码

lst_timer.h

#ifndef LST_TIMER
#define LST_TIMER

#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/epoll.h>
#include <fcntl.h> 
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <sys/stat.h>
#include <string.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <stdarg.h>
#include <errno.h>
#include <sys/wait.h>
#include <sys/uio.h>

#include <time.h>
#include "../log/log.h"

class util_timer; //类声明,这是一个定时器类

//表示一个连接需要的信息:客户端网络地址、客户端文件描述符、用那个定时器表示它的超时时间
class client_data{
public:
    sockaddr_in address; //里面有 ip地址、端口号、协议族
    int sockfd; //客户端套接字的文件描述符
    util_timer* timer;
};

//一个定时器类,需要信息:前后定时器指针(这是为了后面把定时器串成一条链而准备的)、超时时间、回调函数(如果发生超时,应该怎么办?)、属于谁(也就是客户端信息)
class util_timer{
public:
    time_t expire; //超时时间
    void (* cb_func) (client_data*); //参数:需要直到客户端信息,因为超时需要关闭这个连接
    client_data *user_data; //该定时器属于谁?
    util_timer* prev;
    util_timer* next;
public:
    util_timer():prev(NULL),next(NULL){} //未加入链表,前后指空
    ~util_timer();
};

//一条把所有定时器,按照一定规则排序后,串起来的定时器链,实现功能:对这个链上的定时器进行,增删改查
class sort_timer_lst{
private:
    //链表的头、尾
    util_timer* head;
    util_timer* tail;
    void add_timer(util_timer* timer, util_timer* lst_head); //重载add函数:调整链表的内部节点

public:
    sort_timer_lst();
    ~sort_timer_lst(); 

    void add_timer(util_timer* timer); //增
    void adjust_timer(util_timer* timer); //改:调整位置
    void del_timer(util_timer* timer); //删
    void tick(); //定时任务处理函数?
};

//发生超时事件是,具体的处理方法,封装成一个处理类,里面是一些处理的方法
/*
逻辑顺序,设置信号后,触发时调用信号处理函数,信号处理函数通过管道将sig发送到主循环
主循环通过管道接收sig,得知有定时器超时,再调用定时器处理任务函数timer_handler()处理,并且再此设定ALARM信号触发,形成循环
*/
class Utils{
public:
    static int *u_pipefd; //管道
    sort_timer_lst m_timer_lst; //定时器链表
    static int u_epollfd; //
    int m_TIMESLOT; //时间片

public:
    Utils(){}
    ~Utils(){}

    void init(int timerslot); //初始化时间片的大小

    int setnonblocking(int fd); //将文件描述符设置为非阻塞

    //将内核事件表注册读事件、ET模式、选择开启EPOLLONeSHOT
    //采用EPOLLONETSHOT事件的文件描述符上的注册事件只触发一次,
    //要想重新注册事件则需要调用epoll_ctl重置文件描述符上的事件,这样前面的socket就不会出现竞态。
    void addfd(int epollfd, int fd, bool one_shot, int TRIGMode); 

    //信号处理函数
    static void sig_handler(int sig);

    //设置信号函数
    void addsig(int sig, void(handler)(int), bool restart = true);

    //定时处理任务,重新定时,以不触发SIGALRM信号
    void timer_handler();

    void show_error(int connfd, const char*info);
};

void cb_func(client_data* user_data);

#endif

lst_timer.cpp

#include"list_timer.h"
#include"../http/http_conn.h"

class Utils;
//回调函数(如果发生超时,应该怎么办?)
void cb_func(client_data* user_data){
    //删除在epoll例程内部监视对象文件描述符
    epoll_ctl(Utils::u_epollfd, EPOLL_CTL_DEL,user_data->sockfd,0);
   
    assert(user_data);

    //关闭连接
    close(user_data->sockfd);

    //服务器连接减少1
    http_conn::m_user_count--;
}

//实现sort_timer_lst成员函数
//构造函数:空链表
sort_timer_lst::sort_timer_lst(){
    head = NULL;
    tail = NULL;
}

//析构函数:遍历链表,手动释放每个结点的内存空间
sort_timer_lst::~sort_timer_lst(){
    util_timer* tmp = head;

    while(tmp){
        head = tmp->next;
        delete tmp;
        tmp = head;
    }
}

//往链表里面添加一个定时器,按照超时时间,插到合适的位置

//中间插入指定的定时器
void sort_timer_lst::add_timer(util_timer*timer,util_timer*head){
    util_timer *prev = head; //保存头结点
    util_timer *temp = prev->next; //当前处理的结点

    while(temp){
        if(timer->expire < temp->expire){
            prev->next = timer;
            timer->next = temp;
            timer->prev = prev;
            tem->prev = timer;
            break;
        }

        prev = temp;
        temp = prev->next;
    }

}

void sort_timer_lst::add_timer(util_timer*timer){
    if(!timer){ //无效定时器
        return;
    }

    if(!head){ //插入第一个结点
        head = tail = timer;
    }

    //寻找合适的 位置
    //头插
    if(timer->expire <  head->expire){
        timer->next = head;
        head->prev = timer; //别忘了是双向链表
        head = timer; 
    }
    //尾插
    if(timer->expire > tail->expire){
        timer->prev = tail;
        tail->next = timer; //别忘了是双向链表
        timer->next=NULL;
        tail = timer; 
    }

    //中间插入
    add_timer(timer,head);
}

//调整指定的定时器,任务发生变化时(可能超时时间修改了),调整定时器在链表中的位置
void sort_timer_lst::adjust_timer(util_timer *timer) {
    if (!timer)
    {
        return;
    }

    util_timer *tmp = timer->next;
    //被调整的定时器在链表尾部
    //or 定时器超时值仍然小于下一个定时器超时值,不调整
    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 sort_timer_lst::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;
}

//处理掉那些超时的定时器,关闭连接,释放定时器内存空间
void sort_timer_lst::tick()
{
    if(!head) {
        return;
    }

    time_t cur = timer(NULL); //获取当前的系统时间
    util_timer* temp = head;
    while(temp){
        if(cur < temp->expire){
            return ; //当前定时器不超时,后面的定时器都不会超时
        }

        temp->cb_func(temp->user_data); //处理掉对应的客户端连接
        head = temp->next;

        if(head){
            head->prev = NULL;
        }

        delete temp;
        temp = head;
    }
}

int *Utils::u_pipefd = 0;
int Utils::u_epoll = 0;


// 初始化时间片
void Utils::init(int timesolt){
    m_TIMESLOT = timesolt;
}

//对文件描述符设置为非阻塞
int Utils::setnonblocking(int fd){
    int old_option = fcntl(fd,F_GETFL,0); // 获取之前设置的属性信息
    int new_option = old_option | O_NONBLOCK; 
    fcntl(fd,f_SETFL,new_option); //在此基础上修改,添加非阻塞 O_NONBLOCK标志
    return old_option;
}

// 将内核事件表注册读事件,ET模式,选择开启EPOLLONESHOT
void Utils::addfd(int epollfd, int fd, bool one_shot, int TRIGMode){
    epoll_event event; //监视对象的事件类型
    event.data.fd = fd;

    if(1 == TRIGMode){
        event.events = EPOLLIN | EPOLLLET|EPOLLRDHUP; //在什么时候会产生事件?
    }else{
        event.events = EPOLLONESHOT;
    }

    if(one_shot){
        event.events |= EPOLLONESHOT;
    }

    epoll_ctl(epollfd,EPOLL_CT_ADD,fd,&event); //注册事件
    setnonblocking(fd);
}

//信号处理函数
void Utils::sig_handler(int sig){
    int save_errno = errno;
    int msg = sig;
    send(u_pipefd[1], (char*)&msg,1,0);
    //send:是一个系统调用函数,用来发送消息到一个套接字中,和sendto,sendmsg功能相似。
    //send和write的唯一区别就是最后一个参数:flags的存在,当我们设置flags为0时,send和wirte是同等的。
    errno = save_error; //为了保证系统的可重入性,保留原来的error
}

//设置信号函数
void Utils::addsig(int sig, void(handler)(int), bool restart){
    //void(handler)(int):handler 指向一个 接受一个int参数返回值为void的类型 的函数
    struct signaction sa;
    memset(&sa,'\0',sizeof(sa));
    sa.sa_handler = handler;
    if(restart){
        sa.sa_flags |= SA_RESTART;
    }
    sigfillset(&sa.sa_mask);
    assert(signaction(sig,&sa,NULL) = -1);
}

//定时处理任务,重新定时一不断触发SIGALRAM信号
void Utils::timer_handler(){
    m_timer_ls.tick();
    alarm(m_TIMSLOT);
}

//错误函数
void show_error(int connfd,const char *info){
    send(connfd,info,strlen(info),0);
    close(connfd);
}

总 结

初衷:

由于非活跃连接占用了连接资源,严重影响服务器的性能,通过实现一个服务器定时器,处理这种非活跃连接,释放连接资源。利用alarm函数周期性地触发SIGALRM信号,该信号的信号处理函数利用管道通知主循环执行定时器链表上的定时任务.实现:

  • 统一事件源
  • 基于升序链表的定时器
  • 处理非活动连接

设计

类:

  1. 客户端信息类:因为每一个定时器都必须要对应着一个客户端连接,所以必须得知道客户端的信息 – 客户端套接字描述符、客户端的网咯地址信息结构体
  2. 定时器类:对于一个定时器而言,需要知道的信息是,这个定时器的超时时间,属于谁,如果发生超时需要执行的操作
  3. 定时器链类:
    1. 既然是一个定时器链,就需要把所有的定时器连起来,就要求,每一个定时器都是一个结点;
    2. 选择将定时器链设计成双向链表:每个结点都有前后指针、链表的头尾指针、链表的增删改查操作
    3. 定时器链还需要提供一个操作:那就是定期检查链表上的定时器,并做出修改,把过期的定时器处理掉,注意,这个处理可不是直接删掉定时器,而是关闭客户端连接、释放资源、删除定时器。
  4. 与定时器有关的函数,将其封装成一个工具类,包括:信号处理、epoll机制、通信机制

实现:

回调函数:如果发生超时事件,应该如何处理
  1. 回调函数功能(删除注册时间、关闭连接、更新连接数)
定时器类:
  1. 初始化:前后指针指向空
  2. 其他可以是默认值。

定时器链类

  1. 初始化:头尾指针指空
  2. 回到双向链表的层面,实现对链表的增删改查操作
  3. 实现tick函数:如果发现有超时定时器、调用回调函数、删除定时器。

工具类

  1. 初始化超时时间
  2. 将内核事件表注册读事件,ET模式,选择开启EPOLLONESHOT,对事件文件描述符设置为非阻塞
  3. 信号处理函数
  4. 设置信号函数
  5. 定时处理任务,重新定时一不断触发SIGALRAM信号
  6. 错误函数

知识点:

assert 宏

  1. assert宏的原型定义在<assert.h>中,其作用是如果它的条件返回错误,则终止程序执行,
  2. assert的作用是现计算表达式 expression ,如果其值为假(即为0),那么它先向stderr打印一条出错信息,然后通过调用 abort 来终止程序运行。

函数的可重入性

  1. 可重入的函数必须满足以下三个条件:

    1. 可以在执行的过程中可以被打断;

    2. 被打断之后,在该函数一次调用执行完之前,可以再次被调用(或进入,reentered)。

    3. 再次调用执行完之后,被打断的上次调用可以继续恢复执行,并正确执行。

  2. 可重入函数可以在任意时刻被中断,稍后再继续运行,不会丢失数据。

  3. 不可重入(non-reentrant)函数不能由超过一个任务所共享,除非能确保函数的互斥(或者使用信号量,或者在代码的关键部分禁用中断)。

send 函数

  1. send:是一个系统调用函数,用来发送消息到一个套接字中,和sendto,sendmsg功能相似。
  2. send和write的唯一区别就是最后一个参数:flags的存在,当我们设置flags为0时,send和wirte是同等的。

对文件描述符设置为非阻塞

    #include<fcntl.h> //fcntl函数头文件
    //...
    int flags = fcntl(fd,F_GETFL,0); //获取文件描述符fd的标志位
    fcntl(fd,f_SETFL,flags|O_NONBLOCK); //将文件描述符设置为非阻塞模式
    //...

将内核事件表注册读事件,ET模式,选择开启EPOLLONESHOT

   //相关头文件
   #include<sys/epoll.h>
   #include<fcntl.h>
   #include<errno.h>
   #include<stdio.h>
   #include<stdlib.h>

   #define MAX_EVENTS 10

   /*创建一个非阻塞文件描述符,并将其添加到内核事件表中*/
   int add_event_to_epoll(int epoll_fd, int fd){
       struct epoll_event event;
       event.data.fd = epoll_fd; 
       event.events = EPOLLIN|EPOLLET; //ET模式,只有当文件描述符上有新的数据可读时才会触发事件

       int state = epoll_ctl(epoll_fd,EPOLL_CTL_ADD,fd,&event);

       if(state == 0) {
           cout<<"成功"<<endl;
       }else{
           cout<<"失败 "<<endl;
       }
   }

   /*event 结构体是啥?它的作用是什么?*/
   struct epoll_event {
       __unit32_t events; //记录 事件的触发条件 比如:读事件、缓冲区空、发生错误等
       epoll_data_t data; //记录 发生事件的描述符
   }

   typedef union epoll_data {
       void *ptr;
       int fd;
       __unit32_t u32;
       __unit64_t u64;
   }epoll_data_t;

设置信号函数

    //sigaction函数
    int sigaction(int signo, const struct sigaction *act, struct sigaction *oldact);
    //参数:注册的信号值、sigaction结构体对象变量地址、获取之前注册的信号处理函数指针
    
    //sigaction结构体:
    struct sigaction{
        void (*sa_handler)(int); //函数指针
        sigset_t sa_mask; //设置为0
        int sa_flags;  //设置为0
    }

    //信号常数:
    SIGALRM: 已到通过调用alarm函数注册的时间
    SIGINT:输入CTL+C
    SIGCHLD: 子进程终止

    //示例:
    //信号处理函数
    void timeout(int sig){
        if(sig == SIGGALRM){
            puts("Timer Out");
        }
    }

    //信号注册函数
    //...
    //1. 填写sigaction结构体对象
    struct sigaction act;
    act.sa_handler = timeout; //绑定信号处理函数
    sigemotyset(&act.sa_mask);
    act.sa_flgs = 0;
    //2. 注册信号
    sigaction(SIGALRM, &act, 0); //如果发生 超时事件,对发出值为SIGALRM的信号,对应的处理函数会被调用
    //...

sigfillset函数

  1. Sigfillset 函数概述
    初始化一个满的信号集,集合当中有所有的信号,所有的信号都被添加到这个集合中了,用法和空集合一样。
    初始化一个满的信号集

    #include<signal.h>  
    int sigfillset (sigset_t *set)  
    

    功能: 初始化信号集合 set,将信号集合设置为所有信号的集合。
    参数: 信号集标识的地址,以后操作此信号集,对 set 进行操作就可以了。
    返回值: 成功返回 0,失败返回-1。

  2. 操作

    //定义一个set2
    sigset_t set2;         
    int ret;             
    ret = sigfillsel (&sel2);   
    /*
        就给set2这个集合赋值了,set2包含了所有信号。       
        成功返回0,失败返回非0。 
    */           
    

c++类中含有成员指针

  1. 在c++中,如果一个类有指针类型的成员变量,那么在创建对象的时候这些成员变量是不会被初始化的,也就是他们的值是未知的,可能是空指针
  2. 对于双向链表类中的头指针和尾指针,如果不显式地初始化,在后面使用的时候会出现未定义行为,所以需要显式地初始化,因为后面需要根据这两个指针寻找数据。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Geminfit

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

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

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

打赏作者

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

抵扣说明:

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

余额充值