Linux下的高性能轻量级Web服务器(四)
4.使用定时器
服务器程序通常管理着众多定时事件,因此有效地组织这些定时事件,使之能在预期的时间点被触发且不影响服务器的主要逻辑,对于服务器的性能至关重要。为此,要将每个定时事件封装成定时器,并使用某种容器类数据结构,如链表、排序链表和时间轮等,将所有定时器串联起来,以实现对事件的统一管理。
代码块
time_heap.h
#ifndef intIME_HEAP
#define intIME_HEAP
#include <iostream>
#include <netinet/in.h>
#include <time.h>
using std::exception;
#define BUFFER_SIZE 64
// 向前声明
class heap_timer;
// 连接资源
struct client_data
{
sockaddr_in address;
int sockfd;
heap_timer* timer;
};
// 定时器类
class heap_timer
{
public:
heap_timer(){}
~heap_timer(){}
public:
time_t expire; // 定时器生效的绝对时间
void (*cb_func)( client_data* ); // 定时器的回调函数
client_data* user_data; // 用户数据
};
// 时间堆类
class time_heap
{
public:
// 构造函数 1,初始化一个大小为 cap 的空堆
time_heap( int cap ) throw ( std::exception );
// 构造函数 2,用已有数组来初始化堆
time_heap( heap_timer** init_array, int size, int capacity ) throw ( std::exception );
// 销毁时间堆
~time_heap();
public:
// 添加目标定时器 timer
void add_timer( heap_timer* timer ) throw ( std::exception );
// 删除目标定时器 timer
void del_timer( heap_timer* timer );
// 获得堆顶部的定时器
heap_timer* top() const;
// 删除堆顶部的定时器
void pop_timer();
// 调整目标定时器位置
void adjust_timer( heap_timer* timer );
// 心搏函数
void tick();
/*
这里说明一下 const 在函数返回值前和函数名后的区别
const 在返回值前表示该函数的返回值只能被读取,不能被修改
const 在函数名后表示该函数内部只能访问类成员,而不能修改类成员
*/
bool empty() const { return cur_size == 0; }
private:
// 下滤操作,保证数组中的每个结点都满足最小堆性质
void percolate_down( int hole );
// 将堆数组扩大一倍
void resize() throw ( std::exception );
private:
heap_timer** array; // 堆数组
int capacity; // 堆数组容量
int cur_size; // 堆数组当前包含元素的元素个数
};
#endif
time_heap.cpp
#include "time_heap.h"
// 构造函数 1,初始化一个大小为 cap 的空堆
time_heap::time_heap( int cap ) throw ( std::exception ) : capacity( cap ), cur_size( 0 )
{
array = new heap_timer* [capacity]; // 创建堆数组
if ( ! array )
{
throw std::exception();
}
for( int i = 0; i < capacity; ++i )
{
array[i] = NULL;
}
}
// 构造函数 2,用已有数组来初始化堆
time_heap::time_heap( heap_timer** init_array, int size, int capacity ) throw ( std::exception ) : cur_size( size ), capacity( capacity )
{
if ( capacity < size )
{
throw std::exception();
}
array = new heap_timer* [capacity]; // 创建堆数组
if ( ! array )
{
throw std::exception();
}
for( int i = 0; i < capacity; ++i )
{
array[i] = NULL;
}
if ( size != 0 )
{
// 用已有数组初始化堆数组
for ( int i = 0; i < size; ++i )
{
array[ i ] = init_array[ i ];
}
// 对数组中的第 (cur_size-1)/2 ~ 0 个元素执行下滤操作
for ( int i = (cur_size-1)/2; i >= 0; --i )
{
percolate_down( i );
}
}
}
// 销毁时间堆
time_heap::~time_heap()
{
for ( int i = 0; i < cur_size; ++i )
{
delete array[i];
}
delete [] array;
}
// 添加目标定时器 timer
void time_heap::add_timer( heap_timer* timer ) throw ( std::exception )
{
if( !timer )
{
return;
}
// 如果当前堆数组容量不够,则将其扩大 1 倍
if( cur_size >= capacity )
{
resize();
}
// 新插入了一个元素,当前堆大小加1,hole 是新建空穴位置
int hole = cur_size++;
int parent = 0;
// 对从空穴到根节点的路径上的所有结点执行上滤操作
for( ; hole > 0; hole=parent )
{
parent = (hole-1)/2; //数组下标从 0 开始,所以要减 1
if ( array[parent]->expire <= timer->expire )
{
break;
}
array[hole] = array[parent];
}
array[hole] = timer;
}
// 删除目标定时器 timer
void time_heap::del_timer( heap_timer* timer )
{
if( !timer )
{
return;
}
/*
延迟销毁,仅仅将目标定时器的回调函数设置为空。
这将节省真正删除该定时器造成的开销,但使得堆数组容易膨胀
*/
timer->cb_func = NULL;
}
// 获得堆顶部的定时器
heap_timer* time_heap::top() const
{
if ( empty() )
{
return NULL;
}
return array[0];
}
// 删除堆顶部的定时器
void time_heap::pop_timer()
{
if( empty() )
{
return;
}
if( array[0] )
{
delete array[0];
// 将原来的堆顶元素替换为堆数组中的最后一个元素,再进行下滤操作
array[0] = array[--cur_size];
percolate_down( 0 );
}
}
// 调整目标定时器位置
void time_heap::adjust_timer( heap_timer* timer )
{
if( !timer )
{
return;
}
// 找到目标定时器的位置
for(int hole = 0; hole < cur_size; hole++)
{
if( array[hole] == timer)
{
break;
}
}
// 进行下滤,因为其到期时间延长了
percolate_down(hole);
}
// 心搏函数
void time_heap::tick()
{
// tmp 暂存堆顶元素
heap_timer* tmp = array[0];
// 当前时间
time_t cur = time( NULL );
// 循环处理堆中到期的定时器
while( !empty() )
{
if( !tmp )
{
break;
}
// 如果堆顶定时器没到期,则退出循环。因为是最小堆,所有只需判断堆顶。
if( tmp->expire > cur )
{
break;
}
/*
判断当前堆顶定时器是否被删除了
因为在 del_timer 中采用了延迟删除的方法,只是将其回调函数置 NULL,其还在堆中
*/
if( array[0]->cb_func )
{
array[0]->cb_func( array[0]->user_data );
}
// 删除当前堆顶,同时生成新的堆顶定时器
pop_timer();
tmp = array[0];
}
}
// 下滤操作,保证数组中的每个结点都满足最小堆性质
void time_heap::percolate_down( int hole )
{
heap_timer* temp = array[hole];
int child = 0;
for ( ; ((hole*2+1) <= (cur_size-1)); hole=child )
{
child = hole*2+1;
if ( (child < (cur_size-1)) && (array[child+1]->expire < array[child]->expire ) )
{
++child;
}
if ( array[child]->expire < temp->expire )
{
array[hole] = array[child];
}
else
{
break;
}
}
array[hole] = temp;
}
// 将堆数组扩大一倍
void time_heap::resize() throw ( std::exception )
{
heap_timer** temp = new heap_timer* [2*capacity];
for( int i = 0; i < 2*capacity; ++i )
{
temp[i] = NULL;
}
if ( ! temp )
{
throw std::exception();
}
capacity = 2*capacity;
for ( int i = 0; i < cur_size; ++i )
{
temp[i] = array[i];
}
delete [] array;
array = temp;
}
基础知识
1)非活跃:是指客户端与服务器端建立连接后,长时间不交换数据,一直占用服务器端的文件描述符,导致连接资源的浪费。
2)定时事件:是指固定一段时间之后触发某段代码,由该段代码处理一个事件,如从内核事件表删除事件,并关闭文件描述符,释放连接资源。
3)定时器:是指利用结构体或其他形式,将多种定时事件进行封装起来。具体的,这里只涉及一种定时事件,即定期检测非活跃连接,这里将该定时事件与连接资源封装为一个结构体定时器。
4)定时器容器:是指使用某种容器类数据结构,将上述多个定时器组合起来,便于对定时事件统一管理。具体的,项目中使用升序链表将所有定时器串联组织起来。
本项目中,服务器主循环为每一个连接创建一个定时器,并对每个连接进行定时。另外,利用升序时间链表容器将所有定时器串联起来,若主循环接收到定时通知,则在链表中依次执行定时任务。
Linux下提供了三种定时的方法:
- socket 选项 SO_RECVTIMEO 和 SO_SNDTIMEO
- SIGALRM 信号
- I/O复用系统调用的超时参数
三种方法没有一劳永逸的应用场景,也没有绝对的优劣。由于项目中使用的是 SIGALRM 信号,这里仅对其进行介绍,另外两种方法自行查阅。
具体的,利用 alarm 函数周期性地触发 SIGALRM 信号,信号处理函数利用管道通知主循环,主循环接收到该信号后对升序链表上所有定时器进行处理,若该段时间内没有交换数据,则将该连接关闭,释放所占用的资源。
从上面的简要描述中,可以看出定时器处理非活动连接模块,主要分为两部分,其一为定时方法与信号通知流程,其二为定时器及其容器设计与定时任务的处理。
基础API
sigaction结构体
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
}
- sa_handler是一个函数指针,指向信号处理函数
- sa_sigaction同样是信号处理函数,有三个参数,可以获得关于信号更详细的信息
- sa_mask用来指定在信号处理函数执行期间需要被屏蔽的信号
- sa_flags用于指定信号处理的行为
- SA_RESTART,使被信号打断的系统调用自动重新发起
- SA_NOCLDSTOP,使父进程在它的子进程暂停或继续运行时不会收到 SIGCHLD 信号
- SA_NOCLDWAIT,使父进程在它的子进程退出时不会收到 SIGCHLD 信号,这时子进程如果退出也不会成为僵尸进程
- SA_NODEFER,使对信号的屏蔽无效,即在信号处理函数执行期间仍能发出这个信号
- SA_RESETHAND,信号处理之后重新设置为默认的处理方式
- SA_SIGINFO,使用 sa_sigaction 成员而不是 sa_handler 作为信号处理函数
- sa_restorer一般不使用
sigaction函数
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
- signum表示操作的信号。
- act表示对信号设置新的处理方式。
- oldact表示信号原来的处理方式,一般为NULL。
sigaction函数的功能是检查或修改与指定信号相关联的处理动作(可同时两种操作)
返回值:0 表示成功,-1 表示有错误发生。
sigfillset函数
#include <signal.h>
int sigfillset(sigset_t *set);
用来将参数set信号集初始化,然后把所有的信号加入到此信号集里。
注:sigset_t 类型的信号集实际是一个长整型数组,数组的每个元素的每个位表示一个信号。
SIGALRM、SIGTERM信号
#define SIGALRM 14 //由alarm系统调用产生timer时钟信号
#define SIGTERM 15 //终端发送的终止信号
alarm函数
#include <unistd.h>;
unsigned int alarm(unsigned int seconds);
设置信号传送闹钟,即用来设置信号 SIGALRM 在经过参数 seconds 秒数后发送给目前的进程。如果未设置信号 SIGALRM 的处理函数,那么alarm()默认处理为终止目前进程。
一次 alarm 调用只会引起一次 SIGALRM 信号
socketpair函数
在linux下,使用socketpair函数能够创建一对套接字进行通信(可以双工通信)。
#include <sys/types.h>
#include <sys/socket.h>
int socketpair(int domain, int type, int protocol, int sv[2]);
- domain表示协议族,PF_UNIX或者AF_UNIX
- type表示协议,可以是SOCK_STREAM或者SOCK_DGRAM,SOCK_STREAM基于TCP,SOCK_DGRAM基于UDP
- protocol表示类型,只能为0
- sv[2]表示套节字柄对,该两个句柄作用相同,均能进行读写双向操作
返回值: 0为创建成功,-1为创建失败
注意:用来创建全双工通道,不过只局限于父子进程之间。
pipe函数
调用pipe函数,会在内核中开辟出一块缓冲区用来进行进程间通信,这块缓冲区称为管道,它有一个读端和一个写端,即管道是单向的,只能往一个方向传输数据,想要双向传输数据,则需要两个管道。
注意:只有拥有血缘关系的进程之间才能使用管道进行通信
#include<unistd.h>
int pipe(int pipefd[2]);
pipefd参数是一个长度为2的int型数组,如果调用成功会通过此数组传出给用户程序两个文件描述符。pipefd [0]指向管道的读端,pipefd [1]指向管道的写端,那么此时这个管道对于用户程序就是一个文件,可以通过 read(pipefd [0]) 或者 write(pipefd [1]) 进行操作。
返回值:成功返回 0,否则返回 -1。
使用管道进行通信的步骤:
1)父进程创建管道,得到两个文件描述符指向管道的两端
2)利用fork函数创建出子进程,则子进程也得到两个文件描述符指向同一管道
3)父进程关闭读端(pipe[0]),子进程关闭写端pipe[1],则此时父进程可以往管道中进行写操作,子进程可以从管道中读,从而实现了通过管道的进程间通信。
send函数
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
当套接字发送缓冲区变满时,send通常会阻塞,除非套接字设置为非阻塞模式。
非阻塞模式下当缓冲区变满时,返回 EAGAIN 或者 EWOULDBLOCK 错误,此时可以调用select函数来监视何时可以发送数据。
信号通知流程
Linux下的信号采用的异步处理机制,信号处理函数和当前进程是两条不同的执行路线。具体的, 当进程收到信号时,操作系统会中断进程当前的正常流程,转而进入信号处理函数执行操作,完成后再返回中断的地方继续执行。
为避免信号竞态现象发生,信号处理期间系统不会再次触发它。所以,为确保该信号不被屏蔽太久,信号处理函数需要尽可能快地执行完毕。
一般的信号处理函数需要处理该信号对应的逻辑,当该逻辑比较复杂时,信号处理函数执行时间过长,会导致信号屏蔽太久。
本项目采用的是一种典型的解决方案,信号处理函数仅仅发送信号通知程序主循环,将信号对应的处理逻辑放在程序主循环中,由主循环执行信号对应的逻辑代码。
统一事件源
统一事件源,是指将信号事件与其他I/O事件一样被处理。
具体的,信号处理函数往管道的写端写入信号值,主循环则从管道的读端读出信号值,使用I/O复用系统调用来监听管道读端的可读事件,这样信号事件与其他文件描述符都可以通过epoll来监测,从而实现统一处理。
信号处理机制
每个进程之中,都有存着一个表,里面存着每种信号所代表的含义,内核通过设置表项中每一个位来标识对应的信号类型。
信号处理过程如下图所示:
围绕图示,将信号分成接收、检测、处理三个部分。
- 信号的接收
接收信号的任务是由内核代理的,当内核接收到信号后,会将其放到对应进程的信号队列中,同时向进程发送一个中断,使其陷入内核态。
注意:此时信号还只是在队列中,对进程来说暂时是不知道有信号到来的。
2. 信号的检测
进程陷入内核态后,有两种场景会对信号进行检测:
1)进程从内核态返回到用户态前进行信号检测
2)进程在内核态中,从睡眠状态被唤醒的时候进行信号检测
当发现有新信号时,便会进入下一步,信号的处理。
3. 信号的处理
信号处理函数是运行在用户态的,调用处理函数前,内核会将当前内核栈的内容备份拷贝到用户栈上,并且修改指令寄存器(eip)将其指向信号处理函数。
接下来进程返回到用户态中,执行相应的信号处理函数。
信号处理函数执行完成后,返回内核态,检查是否还有其它信号未处理,如果还有未处理信号,则进行处理。当所有信号都处理完成后,就会将内核栈恢复(从用户栈的备份拷贝回来),同时恢复指令寄存器(eip)将其指向中断前的运行位置,最后回到用户态继续执行进程。
信号相关代码详解
信号处理函数
自定义信号处理函数,创建sigaction结构体变量,设置信号函数。
//信号处理函数
void sig_handler(int sig)
{
//为保证函数的可重入性,保留原来的errno
int save_errno = errno;
int msg = sig;
//将信号值从管道写端写入,传输字符类型,而非整型
send(pipefd[1], (char *)&msg, 1, 0);
//将原来的errno赋值为当前的errno
errno = save_errno;
}
//设置信号函数
void addsig(int sig, void(handler)(int), bool restart = true)
{
struct sigaction sa;
memset(&sa, '\0', sizeof(sa));
//信号处理函数中仅仅发送信号值,不做对应逻辑处理
sa.sa_handler = handler;
if (restart)
sa.sa_flags |= SA_RESTART;
//将所有信号添加到信号集中,即在信号处理函数运行期间,屏蔽所有信号
sigfillset(&sa.sa_mask);
//执行sigaction函数
assert(sigaction(sig, &sa, NULL) != -1);
}
信号通知逻辑
- 创建管道,用于信号处理函数将信号值传递给主循环
- 设置SIGALRM(时间到了触发)和SIGTERM(kill会触发,Ctrl+C)信号的处理方式
- 将handler参数设置信号处理函数(管道写端写入信号的名字)
- 通过sigaction函数注册信号捕捉函数
- 利用I/O复用系统监听管道读端文件描述符的可读事件
- 当信号值传递给主循环,主循环再根据接收到的信号值执行目标信号对应的逻辑代码
主循环对应的信号逻辑代码
// 处理信号
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:
{
// 先做标记,最后处理定时器事件
// 因为 I/O 事件有更高的优先级
timeout = true;
break;
}
case SIGTERM:
{
stop_server = true;
}
}
}
}
}
代码详解
定时器设计
项目中将连接资源、定时事件和超时时间封装为定时器类。
- 连接资源包括客户端套接字地址、文件描述符和定时器
- 定时事件为回调函数,将其封装起来由用户自定义,这里是删除非活动socket上的注册事件,并关闭文件描述符,释放资源
- 定时器超时时间 = 浏览器和服务器连接时刻 + 3 * 固定时间(TIMESLOT),其中TIMESLOT为5秒,即连接超时时间为15秒
// 向前声明
class heap_timer;
// 连接资源
struct client_data
{
sockaddr_in address;
int sockfd;
heap_timer* timer;
};
// 定时器类
class heap_timer
{
public:
heap_timer(){}
~heap_timer(){}
public:
time_t expire; // 定时器生效的绝对时间
void (*cb_func)( client_data* ); // 定时器的回调函数
client_data* user_data; // 用户数据
};
具体的定时事件(即回调函数)
//定时器回调函数,删除非活动连接在socket上的注册事件,并关闭
void cb_func(client_data *user_data)
{
//删除非活动连接在socket上的注册事件
epoll_ctl(epollfd, EPOLL_CTL_DEL, user_data->sockfd, 0);
assert(user_data);
//关闭文件描述符
close(user_data->sockfd);
//减少连接数
http_conn::m_user_count--;
}
定时器容器设计
项目原本是基于升序双向链表来实现的定时器容器。
这种定时器容器存在着以下缺点:
- 每次遍历添加和修改定时器的效率偏低,为(O(n))
- 每次以固定的时间间隔触发SIGALRM信号,调用tick函数处理超时连接会造成一定的触发浪费,举个例子,若当前的TIMESLOT=5,即每隔5ms触发一次SIGALRM,跳出循环执行tick函数,这时如果当前即将超时的任务距离现在还有20ms,那么在这个期间,SIGALRM信号被触发了4次,tick函数也被执行了4次,可是在这4次中,前三次触发都是无意义的。
为了进行优化,提高服务器性能,在此改为用时间堆来实现定时器。
这种定时器的设计思路如下:
将所有定时器中超时时间最小的一个定时器的超时值作为心搏间隔。这样,一旦心搏函数 tick 被调用,超时时间最小的定时器必然到期,我们就可以在 tick 函数中处理该定时器。然后再从剩余的定时器中找出超时时间最小的一个,并将其超时值设置为下一次心搏间隔。如此反复,就实现了较为精确的定时。
最小堆就很适合这种定时方案,最小堆的说明如下图。
对时间堆来说,添加一个定时器的时间复杂度是 O(lgn),删除和执行一个定时器的时间复杂度为 O(1)。
具体实现
// 时间堆类,声明在 time_heap.h 文件中
class time_heap
{
public:
// 构造函数 1,初始化一个大小为 cap 的空堆
time_heap( int cap ) throw ( std::exception );
// 构造函数 2,用已有数组来初始化堆
time_heap( heap_timer** init_array, int size, int capacity ) throw ( std::exception );
// 销毁时间堆
~time_heap();
public:
// 添加目标定时器 timer
void add_timer( heap_timer* timer ) throw ( std::exception );
// 删除目标定时器 timer
void del_timer( heap_timer* timer );
// 获得堆顶部的定时器
heap_timer* top() const;
// 删除堆顶部的定时器
void pop_timer();
// 调整目标定时器位置
void adjust_timer( heap_timer* timer );
// 心搏函数
void tick();
/*
这里说明一下 const 在函数返回值前和函数名后的区别
const 在返回值前表示该函数的返回值只能被读取,不能被修改
const 在函数名后表示该函数内部只能访问类成员,而不能修改类成员
*/
bool empty() const { return cur_size == 0; }
private:
// 下滤操作,保证数组中的每个结点都满足最小堆性质
void percolate_down( int hole );
// 将堆数组扩大一倍
void resize() throw ( std::exception );
private:
heap_timer** array; // 堆数组
int capacity; // 堆数组容量
int cur_size; // 堆数组当前包含元素的元素个数
};
------------------------------------------------------------------------
//成员函数的具体实现,在 time_heap.cpp 文件中
// 构造函数 1,初始化一个大小为 cap 的空堆
time_heap::time_heap( int cap ) throw ( std::exception ) : capacity( cap ), cur_size( 0 )
{
array = new heap_timer* [capacity]; // 创建堆数组
if ( ! array )
{
throw std::exception();
}
for( int i = 0; i < capacity; ++i )
{
array[i] = NULL;
}
}
// 构造函数 2,用已有数组来初始化堆
time_heap::time_heap( heap_timer** init_array, int size, int capacity ) throw ( std::exception ) : cur_size( size ), capacity( capacity )
{
if ( capacity < size )
{
throw std::exception();
}
array = new heap_timer* [capacity]; // 创建堆数组
if ( ! array )
{
throw std::exception();
}
for( int i = 0; i < capacity; ++i )
{
array[i] = NULL;
}
if ( size != 0 )
{
// 用已有数组初始化堆数组
for ( int i = 0; i < size; ++i )
{
array[ i ] = init_array[ i ];
}
// 对数组中的第 (cur_size-1)/2 ~ 0 个元素执行下滤操作
for ( int i = (cur_size-1)/2; i >= 0; --i )
{
percolate_down( i );
}
}
}
// 销毁时间堆
time_heap::~time_heap()
{
for ( int i = 0; i < cur_size; ++i )
{
delete array[i];
}
delete [] array;
}
// 添加目标定时器 timer
void time_heap::add_timer( heap_timer* timer ) throw ( std::exception )
{
if( !timer )
{
return;
}
// 如果当前堆数组容量不够,则将其扩大 1 倍
if( cur_size >= capacity )
{
resize();
}
// 新插入了一个元素,当前堆大小加1,hole 是新建空穴位置
int hole = cur_size++;
int parent = 0;
// 对从空穴到根节点的路径上的所有结点执行上滤操作
for( ; hole > 0; hole=parent )
{
parent = (hole-1)/2; //数组下标从 0 开始,所以要减 1
if ( array[parent]->expire <= timer->expire )
{
break;
}
array[hole] = array[parent];
}
array[hole] = timer;
}
// 删除目标定时器 timer
void time_heap::del_timer( heap_timer* timer )
{
if( !timer )
{
return;
}
/*
延迟销毁,仅仅将目标定时器的回调函数设置为空。
这将节省真正删除该定时器造成的开销,但使得堆数组容易膨胀
*/
timer->cb_func = NULL;
}
// 获得堆顶部的定时器
heap_timer* time_heap::top() const
{
if ( empty() )
{
return NULL;
}
return array[0];
}
// 删除堆顶部的定时器
void time_heap::pop_timer()
{
if( empty() )
{
return;
}
if( array[0] )
{
delete array[0];
// 将原来的堆顶元素替换为堆数组中的最后一个元素,再进行下滤操作
array[0] = array[--cur_size];
percolate_down( 0 );
}
}
// 调整目标定时器位置
void adjust_timer( heap_timer* timer )
{
if( !timer )
{
return;
}
// 找到目标定时器的位置
for(int hole = 0; hole < cur_size; hole++)
{
if( array[hole] == timer)
{
break;
}
}
// 进行下滤,因为其到期时间延长了
percolate_down(hole);
}
// 心搏函数
void time_heap::tick()
{
// tmp 暂存堆顶元素
heap_timer* tmp = array[0];
// 当前时间
time_t cur = time( NULL );
// 循环处理堆中到期的定时器
while( !empty() )
{
if( !tmp )
{
break;
}
// 如果堆顶定时器没到期,则退出循环。因为是最小堆,所有只需判断堆顶。
if( tmp->expire > cur )
{
break;
}
/*
判断当前堆顶定时器是否被删除了
因为在 del_timer 中采用了延迟删除的方法,只是将其回调函数置 NULL,其还在堆中
*/
if( array[0]->cb_func )
{
array[0]->cb_func( array[0]->user_data );
}
// 删除当前堆顶,同时生成新的堆顶定时器
pop_timer();
tmp = array[0];
}
}
// 下滤操作,保证数组中的每个结点都满足最小堆性质
void time_heap::percolate_down( int hole )
{
heap_timer* temp = array[hole];
int child = 0;
for ( ; ((hole*2+1) <= (cur_size-1)); hole=child )
{
child = hole*2+1;
if ( (child < (cur_size-1)) && (array[child+1]->expire < array[child]->expire ) )
{
++child;
}
if ( array[child]->expire < temp->expire )
{
array[hole] = array[child];
}
else
{
break;
}
}
array[hole] = temp;
}
// 将堆数组扩大一倍
void time_heap::resize() throw ( std::exception )
{
heap_timer** temp = new heap_timer* [2*capacity];
for( int i = 0; i < 2*capacity; ++i )
{
temp[i] = NULL;
}
if ( ! temp )
{
throw std::exception();
}
capacity = 2*capacity;
for ( int i = 0; i < cur_size; ++i )
{
temp[i] = array[i];
}
delete [] array;
array = temp;
}
定时任务处理函数
使用统一事件源,SIGALRM信号每次被触发,主循环中调用一次定时任务处理函数,处理时间堆容器中到期的定时器。
具体逻辑如下:
- 依次处理堆顶定时器,直到遇到尚未到期的堆顶定时器
// 心搏函数
void time_heap::tick()
{
// tmp 暂存堆顶元素
heap_timer* tmp = array[0];
// 当前时间
time_t cur = time( NULL );
// 循环处理堆中到期的定时器
while( !empty() )
{
// 时间堆为空堆
if( !tmp )
{
break;
}
// 如果堆顶定时器没到期,则退出循环。因为是最小堆,所有只需判断堆顶。
if( tmp->expire > cur )
{
break;
}
/*
判断当前堆顶定时器是否被删除了
因为在 del_timer 中采用了延迟删除的方法,只是将其回调函数置 NULL
*/
if( array[0]->cb_func )
{
array[0]->cb_func( array[0]->user_data );
}
// 删除当前堆顶,同时生成新的堆顶定时器
pop_timer();
tmp = array[0];
}
}
如何使用定时器
服务器首先创建时间堆,然后用统一事件源将异常事件,读写事件和信号事件统一处理,根据不同事件的对应逻辑使用定时器。
具体流程如下:
浏览器与服务器连接时,创建该连接对应的定时器,并将该定时器添加到时间堆中
1)处理异常事件时,执行定时事件,服务器关闭连接,从堆中移除对应定时器
2)处理定时信号时,将定时标志设置为true,处理完当前的I/O时间后再处理定时器
3)处理读事件时,若某连接上发生读事件,,否则,执行定时事件
4)处理写事件时,若服务器通过某连接给浏览器发送数据,将对应定时器向后移动,否则,执行定时事件
//定时处理任务,重新定时以不断触发SIGALRM信号
void timer_handler()
{
m_time_heap.tick();
heap_timer* top_timer = m_time_heap.top();
if( top_timer == NULL)
{
//空堆,则间隔时间依旧为固定时间
alarm(TIMESLOT);
}
else
{
time_t cur = time(NULL);
// 间隔时间为堆顶定时器的到期时间
alarm(top_timer->expire - cur);
}
}
//创建时间堆
static time_heap m_time_heap(1024);
//创建连接资源数组
client_data *users_timer = new client_data[MAX_FD];
//超时默认为False
bool timeout = false;
//alarm定时触发SIGALRM信号
alarm(TIMESLOT);
while (!stop_server)
{
int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
if (number < 0 && errno != EINTR)
{
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);
#ifdef listenfdLT
int connfd = accept(listenfd, (struct sockaddr *)&client_address, &client_addrlength);
if (connfd < 0)
{
continue;
}
if (http_conn::m_user_count >= MAX_FD)
{
show_error(connfd, "Internal server busy");
continue;
}
users[connfd].init(connfd, client_address);
//初始化client_data数据
users_timer[connfd].address = client_address;
users_timer[connfd].sockfd = connfd;
// 创建定时器,并初始化
heap_timer *timer = new heap_timer;
timer->user_data = &users_timer[connfd];
timer->cb_func = cb_func;
time_t cur = time(NULL);
timer->expire = cur + 3 * TIMESLOT;
users_timer[connfd].timer = timer;
// 将定时器添加到时间堆
m_time_heap.add_timer(timer);
#endif
#ifdef listenfdET
while (1)
{
int connfd = accept(listenfd, (struct sockaddr *)&client_address, &client_addrlength);
if (connfd < 0)
{
break;
}
if (http_conn::m_user_count >= MAX_FD)
{
show_error(connfd, "Internal server busy");
break;
}
users[connfd].init(connfd, client_address);
//初始化client_data数据
users_timer[connfd].address = client_address;
users_timer[connfd].sockfd = connfd;
// 创建定时器,并初始化
heap_timer *timer = new heap_timer;
timer->user_data = &users_timer[connfd];
timer->cb_func = cb_func;
time_t cur = time(NULL);
timer->expire = cur + 3 * TIMESLOT;
users_timer[connfd].timer = timer;
// 将定时器添加到时间堆
m_time_heap.add_timer(timer);
}
continue;
#endif
}
//服务器端关闭连接,移除对应的定时器
else if (events[i].events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR))
{
heap_timer *timer = users_timer[sockfd].timer;
timer->cb_func(&users_timer[sockfd]);
if (timer)
{
m_time_heap.del_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)
{
heap_timer *timer = users_timer[sockfd].timer;
if (users[sockfd].read_once())
{
//若监测到读事件,将该事件放入请求队列
pool->append(users + sockfd);
//若有数据传输,则将定时器往后延迟3个单位
if (timer)
{
time_t cur = time(NULL);
timer->expire = cur + 3 * TIMESLOT;
//调整目标定时器位置
m_time_heap.adjust_timer(timer);
}
}
//如果发生读错误 或 对方已经关闭连接,则服务端也关闭连接,删除定时器
else
{
timer->cb_func(&users_timer[sockfd]);
if (timer)
{
m_time_heap.del_timer(timer);
}
}
}
// 处理往客户连接上写入数据
else if (events[i].events & EPOLLOUT)
{
heap_timer *timer = users_timer[sockfd].timer;
if (users[sockfd].write())
{
//若有数据传输,则将定时器往后延迟3个单位
if (timer)
{
time_t cur = time(NULL);
timer->expire = cur + 3 * TIMESLOT;
//调整目标定时器位置
m_time_heap.adjust_timer(timer);
}
}
//如果发生写错误 或 对方已经关闭连接,则服务端也关闭连接,删除定时器
else
{
timer->cb_func(&users_timer[sockfd]);
if (timer)
{
timer_lst.del_timer(timer);
}
}
}
}
// 最后处理定时器事件,因为 I/O 事件有更高的优先级
if (timeout)
{
timer_handler();
timeout = false;
}
}
close(epollfd);
close(listenfd);
close(pipefd[1]);
close(pipefd[0]);
delete[] users;
delete[] users_timer;
delete pool;
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix