定时器处理非活动连接
由于非活跃连接占用了连接资源,严重影响服务器的性能,通过实现一个服务器定时器,处理这种非活跃连接,释放连接资源。利用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"
二、util_timer类
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;// 双向链表中的后继节点指针
};
2.回调函数cb_func() (独立)
这个回调函数通常被用作定时器到期后执行的操作,用于关闭超时的客户端连接。
并将其从 epoll 监听集合中删除。同时,由于关闭连接,因此需要将连接计数器减1,以便程序正确统计当前活跃的连接数
void cb_func(client_data *user_data)
{
epoll_ctl(Utils::u_epollfd, EPOLL_CTL_DEL, user_data->sockfd, 0);
assert(user_data);
close(user_data->sockfd);
http_conn::m_user_count--;
}
三、client_data类
1.类的定义
用于维护多个客户端连接的信息
struct client_data
{
sockaddr_in address;// 用于存储客户端的IP地址和端口号信息
int sockfd;// 用于存储与该客户端通信所使用的socket文件描述符
util_timer *timer;// 指向util_timer类型的指针变量,用于定时器管理,防止长时间未响应的连接占用服务器资源
};
四、sort_timer_lst类
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;
};
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.add_timer()方法 (公有)
添加一个定时器,如果定时器已经存在,则更新其超时时间,并重新插入到合适的位置
都是依靠私有的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);
}
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(timer, timer->next);
}
}
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;
}
6.tick()方法
处理定时事件,遍历整个定时器链表,查找有没有超时的定时器,并执行相应的回调函数
循环处理完所有已经到期的定时器之后,本次tick操作就结束了。在下一次循环中,如果还有定时器到期,则会再次执行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;
}
}
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;
}
}
五、Utils类
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;
2. init()方法
初始化定时器链表和定时器信号,timeslot为每个时间槽的间隔时间
void Utils::init(int timeslot){
m_TIMESLOT = timeslot;
}
3.setnonblocking()方法
设置文件描述符为非阻塞模式的目的在于避免在读写操作时被阻塞而导致程序无法响应其他事件。例如,在网络编程中,若一个 socket 连接处于阻塞状态,那么当没有数据可读或者可写时,程序会一直停留在相应的读或写操作上,从而无法处理其他连接请求或者信号等事件。
总之,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;
}
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);
}
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的使用
}
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_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);
}
7.timer_handler()方法
定时器任务处理函数,负责遍历定时器链表,执行超时任务并删除过期节点
重新定时以不断触发SIGALRM信号
void Utils::timer_handler()
{
m_timer_lst.tick();
alarm(m_TIMESLOT);
}
8.show_error()方法
向客户端发送错误信息
void Utils::show_error(int connfd, const char *info)
{
send(connfd, info, strlen(info), 0);
close(connfd);
}
476

被折叠的 条评论
为什么被折叠?



