【TinyWebServer源码解析】(四)定时器处理非活动连接

本文是对github上万star项目源码解析,供大家学习交流
项目地址:
https://github.com/qinguoyi/TinyWebServer

四、定时器处理非活动连接

定时器处理非活动连接

===============

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

​ =>统一事件源

​ =>基于升序链表的定时器

​ =>处理非活动连接

===============

共有四个类:util_timer、client_data、sort_timer_lst、Utils

===============

头文件:

#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"
#include "../http/http_conn.h"
1、util_timer类
1.1 类的定义

定时器类 util_timer,用于在事件循环中管理定时事件

class util_timer{
public:
    util_timer() : prev(NULL), next(NULL) {} // 构造函数,将prev和next成员变量初始化为NULL

public:
    time_t expire; // 用于记录该定时器的超时时间,以time_t类型表示
    
    void (* cb_func)(client_data *); // 定时器超时时要调用的回调函数指针,该回调函数需要接受一个client_data*类型的参数
    client_data *user_data; // 回调函数使用的数据,通常是一个结构体指针
    util_timer *prev; // 双向链表中的前驱节点指针
    util_timer *next; // 双向链表中的后继节点指针
};
1.2 回调函数cb_func()【独立】

这个回调函数通常被用作定时器到期后执行的操作,用于关闭超时的客户端连接。当定时器到期时,会调用该回调函数来关闭对应的客户端连接,并将其从 epoll 监听集合中删除。同时,由于关闭连接,因此需要将连接计数器减1,以便程序正确统计当前活跃的连接数

void cb_func(client_data *user_data)
{
    // 调用epoll_ctl函数,将user_data->sockfd从epoll监听集合中删除
    epoll_ctl(Utils::u_epollfd, EPOLL_CTL_DEL, user_data->sockfd, 0);
    // 使用assert确保user_data不为NULL(如果user_data为 NULL,就会触发断言失败,程序将会终止运行,并打印出相应的错误信息)
    assert(user_data);
    close(user_data->sockfd); // 关闭user_data->sockfd对应的套接字
    http_conn::m_user_count--; // 将http连接的计数器http_conn::m_user_count减1
}
2、client_data类
2.1 类的定义

用于维护多个客户端连接的信息。address成员变量可以保存客户端的网络地址信息,sockfd则用于标识与该客户端通信的socket文件描述符,而timer则用于管理与该客户端连接的超时时间,防止长时间未响应的连接占用服务器资源

struct client_data{
    sockaddr_in address; // 用于存储客户端的IP地址和端口号信息
    int sockfd; // 用于存储与该客户端通信所使用的socket文件描述符
    util_timer *timer; // 指向util_timer类型的指针变量,用于定时器管理
};
3、sort_timer_lst类
3.1 类的定义

基于链表实现的定时器类sort_timer_lst。它通过双向链表来维护一组定时器,并且保证了定时器按照超时时间升序排序

class sort_timer_lst{
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();// 处理定时事件

private:
    void add_timer(util_timer *timer, util_timer *lst_head); // 将指定定时器插入合适位置

    util_timer *head;
    util_timer *tail;
};
3.2 构造函数与析构函数

构造函数,初始化head和tail为NULL

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;
    }
}
3.3 add_timer()方法

添加一个定时器,如果定时器已经存在,则更新其超时时间,并重新插入到合适的位置

void sort_timer_lst::add_timer(util_timer *timer){
    // 如果timer指针为空,则直接返回
    if (!timer){
        return;
    }
    // 判断当前链表是否为空,若为空,则将head和tail指向timer,表示当前链表只包含这一个定时器
    if (!head){
        head = tail = timer;
        return;
    }
    // 如果新的定时器的超时时间小于head指向的定时器的超时时间,则将其插入到链表的头部位置
    if (timer->expire < head->expire){
        timer->next = head;
        head->prev = timer;
        head = timer;
        return;
    }
    // 如果新的定时器的超时时间不小于head指向的定时器的超时时间,则需要调用私有成员函数add_timer(),将新的定时器插入到lst_head指向的链表中正确的位置上
    add_timer(timer, head);
}
3.4 adjust_timer()方法

调整指定定时器的定时时间,并重新插入到合适的位置

void sort_timer_lst::adjust_timer(util_timer *timer){
    // 如果timer指针为空,则直接返回
    if (!timer){
        return;
    }
    util_timer *tmp = timer->next;
    // 检查当前定时器timer的下一个节点tmp是否存在,且是否需要将timer节点调整到tmp前面
    if (!tmp || (timer->expire < tmp->expire)){
        // 如果不需要调整,则直接返回
        return;
    }
    // 否则,需要从当前位置删除timer节点,并将其插入到正确的位置上
    // 如果timer是链表的头节点head,则需要将head指针指向timer的下一个节点,并将timer从链表中移除
    if (timer == head){
        head = head->next;
        head->prev = NULL;
        timer->next = NULL;
        add_timer(timer, head);
    }
    // 如果timer不是头节点,则需要将timer从链表中移除,并将其插入到正确的位置上
    else{
        timer->prev->next = timer->next;
        timer->next->prev = timer->prev;
        // 使用add_timer()函数实现。
        add_timer(timer, timer->next);
    }
}
3.5 del_timer()方法

删除一个指定的定时器

void sort_timer_lst::del_timer(util_timer *timer){
    // 如果timer指针为空,则直接返回
    if (!timer){
        return;
    }
    // 判断链表中只有一个节点,即头尾指针都指向timer,直接将head和tail指针置为NULL并删除timer即可
    if ((timer == head) && (timer == tail)){
        delete timer;
        head = NULL;
        tail = NULL;
        return;
    }
    // 判断timer为头节点的情况,将head指向下一个节点,再将新的头节点的prev指针置为NULL,最后删除timer即可
    if (timer == head){
        head = head->next;
        head->prev = NULL;
        delete timer;
        return;
    }
    // 判断timer为尾节点的情况,将tail指向前一个节点,再将新的尾节点的next指针置为NULL,最后删除timer即可
    if (timer == tail){
        tail = tail->prev;
        tail->next = NULL;
        delete timer;
        return;
    }
    // 最后处理timer在链表中间的情况,将timer的前一节点的next指针指向timer的下一个节点,将timer的下一个节点的prev指针指向timer的前一节点,最后删除timer即可完成该操作
    timer->prev->next = timer->next;
    timer->next->prev = timer->prev;
    delete timer;
}
3.6 tick()方法

处理定时事件,遍历整个定时器链表,查找有没有超时的定时器,并执行相应的回调函数

void sort_timer_lst::tick(){
    // 如果timer指针为空,则直接返回
    if (!head){
        return;
    }
    
    time_t cur = time(NULL); // 获取当前时间并赋值给变量cur
    util_timer *tmp = head; // 初始化一个辅助指针tmp并将其指向head指针指向的节点
    // 在while循环中,判断当前时间是否已经超过了节点的过期时间
    while (tmp){
        // 如果没有超过,则结束循环,等待下一次循环
        if (cur < tmp->expire){
            break;
        }
        // 如果当前时间超过了节点的过期时间,则执行该节点的回调函数cb_func(),并将head指向下一个节点
        tmp->cb_func(tmp->user_data);
        head = tmp->next;
        // 判断新的head指针是否为空
        if (head){
            // 如果不为空,则将其prev指针置为NULL
            head->prev = NULL;
        }
        // 最后删除原来的指针tmp
        delete tmp;
        tmp = head;
    }
}

循环处理完所有已经到期的定时器之后,本次tick操作就结束了。在下一次循环中,如果还有定时器到期,则会再次执行tick函数。

3.7 add_timer()方法

是一个私有函数,用于将指定定时器插入到链表中的合适位置,以保持链表有序。类的成员变量包括头结点指针 head 和尾节点指针 tail,分别指向链表的头部和尾部

void sort_timer_lst::add_timer(util_timer *timer, util_timer *lst_head){
    // 定义了两个指针prev和tmp,分别指向链表头结点和下一个节点
    util_timer *prev = lst_head;
    util_timer *tmp = prev->next;
    // 通过while循环遍历整个链表
    while (tmp){
        // 判断新加入的定时器的超时时间expire是否小于当前节点tmp的超时时间
        if (timer->expire < tmp->expire){
            // 如果是,则说明新定时器应该插入到当前节点之前
            prev->next = timer;
            timer->next = tmp;
            tmp->prev = timer;
            timer->prev = prev;
            //插入操作完成,退出while循环
            break;
        }
        prev = tmp;
        tmp = tmp->next;
    }
    // 如果while循环执行完毕后仍然没有找到合适的位置插入新定时器,则新定时器应该插入到链表尾部
    if (!tmp){
        prev->next = timer;
        timer->prev = prev;
        timer->next = NULL;
        tail = timer;
    }
}
4、Utils类
4.1 类的定义
class Utils{
public:
    Utils() {}
    ~Utils() {}

    void init(int timeslot);
    //对文件描述符设置非阻塞
    int setnonblocking(int fd);
    //将内核事件表注册读事件,ET模式,选择开启EPOLLONESHOT
    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);

public:
    static int *u_pipefd; // 父子进程之间传递信号的管道,由pipe函数创建,是一个长度为2的整型数组,u_pipefd[0]表示读取管道,u_pipefd[1]表示写入管道
    sort_timer_lst m_timer_lst;
    static int u_epollfd; // epoll内核事件表的文件描述符
    int m_TIMESLOT;
};

在lst_timer.cpp中对静态变量进行初始化

int *Utils::u_pipefd = 0;
int Utils::u_epollfd = 0;
4.2 init()方法

初始化定时器链表和定时器信号,timeslot为每个时间槽的间隔时间

void Utils::init(int timeslot){
    m_TIMESLOT = timeslot;
}
4.3 setnonblocking()方法

将文件描述符设置为非阻塞模式

int Utils::setnonblocking(int fd){
    int old_option = fcntl(fd, F_GETFL); // 通过fcntl函数获取fd的旧的文件状态标志
    int new_option = old_option | O_NONBLOCK; // 将O_NONBLOCK标志位添加到旧标志中得到新的文件状态标志
    fcntl(fd, F_SETFL, new_option); // 使用fcntl函数设置fd的文件状态标志为新的标志值
    return old_option;
}

设置文件描述符为非阻塞模式的目的在于避免在读写操作时被阻塞而导致程序无法响应其他事件。例如,在网络编程中,若一个 socket 连接处于阻塞状态,那么当没有数据可读或者可写时,程序会一直停留在相应的读或写操作上,从而无法处理其他连接请求或者信号等事件。

总之,setnonblocking 函数通过修改文件状态标志来实现对文件描述符非阻塞模式的设置,以提高程序的响应能力。该函数返回旧的文件状态标志以便于后续恢复文件描述符原有的状态。

4.4 addfd()方法

向epoll内核事件表中注册文件描述符fd的读事件,并设置相应的事件类型和触发模式

//将内核事件表注册读事件,ET模式,选择开启EPOLLONESHOT
void Utils::addfd(int epollfd, int fd, bool one_shot, int TRIGMode){
    // 首先创建一个epoll_event结构体,并设置其data.fd成员为传入的fd
    epoll_event event;
    event.data.fd = fd;
	// 根据传入的TRIGMode参数,设置event的events成员
    // 其中包括EPOLLIN(可读事件)、EPOLLET(ET 触发模式)和EPOLLRDHUP(对端关闭连接)
    if (1 == TRIGMode)
        event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;
    else
        event.events = EPOLLIN | EPOLLRDHUP;
	// 如果one_shot参数为true,则在events中添加EPOLLONESHOT标志,以确保每个socket连接只被处理一次
    if (one_shot)
        event.events |= EPOLLONESHOT;
    // 调用epoll_ctl函数将fd添加到epollfd指定的epoll实例中
    epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
    // 调用setnonblocking函数将fd设置为非阻塞模式,以便于后续读写操作
    setnonblocking(fd);
}
4.5 sig_handler()方法

信号处理函数,主要用于处理SIGALRM信号,重新定时并触发下一个SIGALRM信号

//信号处理函数
void Utils::sig_handler(int sig){
    //为保证函数的可重入性,保留原来的errno
    int save_errno = errno;
    int msg = sig; // 将信号值赋值给变量msg
    send(u_pipefd[1], (char *)&msg, 1, 0); // 通过send函数将msg的值写入管道u_pipefd[1]中
    errno = save_errno; // 将原来的errno值恢复,确保不影响其他代码中errno的使用
}
4.6 addsig()方法

向系统注册信号sig,并指定信号处理函数为handler。可以选择是否自动重启被中断的系统调用

//设置信号函数
void Utils::addsig(int sig, void(handler)(int), bool restart){
    // 创建一个sigaction结构体sa,并将其初始化为0
    struct sigaction sa;
    memset(&sa, '\0', sizeof(sa));
    // 设置结构体中的sa_handler参数为传入的处理函数指针handler
    sa.sa_handler = handler;
    if (restart)
        // 如果参数restart为 true,设置sa_flags标志位为SA_RESTART,表示当信号处理函数返回时,系统会自动重启被该信号中断的系统调用
        sa.sa_flags |= SA_RESTART;
    // 调用sigfillset函数将sa_mask中的所有信号都设置为阻塞状态,以确保在执行处理函数期间不会再收到其他信号
    sigfillset(&sa.sa_mask);
    // 调用assert和sigaction函数来尝试为指定信号sig注册新的处理函数sa。如果注册失败,则会触发assert断言错误
    assert(sigaction(sig, &sa, NULL) != -1);
}
4.7 timer_handler()方法

定时器任务处理函数,负责遍历定时器链表,执行超时任务并删除过期节点

//定时处理任务,重新定时以不断触发SIGALRM信号
void Utils::timer_handler(){
    m_timer_lst.tick();
    alarm(m_TIMESLOT);
}
4.8 show_error()方法

向客户端发送错误信息

void Utils::show_error(int connfd, const char *info){
    send(connfd, info, strlen(info), 0);
    close(connfd);
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值