定时器容器和定时器
一个服务器程序不仅要处理读事件和写事件,还要处理的一类事件是定时事件。
什么是定时事件:在服务器程序中,每过一段固定的时间触发某段代码,由该代码处理一个事件,如:从内核事件表中删除事件并关闭文件描述符,释放连接支援
Linux的定时机制(方法):
socket选项SO_RCVTIMEO和SO_SNDTIMEO
SIGALRM信号
I/O复用系统调用的超时参数
(本文将介绍如何使用SIGALRM信号处理非活动连接)。
定时事件有什么用:服务器通常管理众多定时事件,因此有效地组织这些定时事件,使之能在预期地事件点内被触发且不影响服务器的主要逻辑。
如何做到:将每个定时事件封装成定时器,并用某种容器类数据结构将其统一的管理和保存,这个容器类数据结构称为定时器容器,常见的定时器容器有:
升序链表,时间轮,时间堆。
本文将只讲解时间堆,升序链表和时间轮读者可以自行搜索相关资料了解。
处理非活动连接
Web服务器通常要定期处理非活动连接:给客户端发一个重连请求,或者关闭该连接,或者其他。Linux在内核中提供了堆连接是否处于活动状态的检查机制,我们可以使用socket选项KEEPALIVE来激活它。
但这种方法会让引用程序对连接的管理变得复杂,因此我们可以考虑在应用层实现类似于KEEPALIVE的机制,以管理所有长时间处于非活动状态的连接。
PS:因为不好直接贴大段大段的代码,因为这样读者估计也看得痛苦,文章的可读性也会很低,但代码逻辑又是必不可少的,所以我会以我自己的理解写一段伪代码,写的不好还望海涵
//引用众多库函数,预定义好众多宏和静态变量
#include<...>
int main(){
创建监听文件描述符socket;
绑定(命名)socket;
监听socket;
创建epoll;
将监听的文件描述符加入epoll中,并设置epoll;
创建socketpair并将其也加入epoll中;
设置信号处理函数;
创建用户数据数组users;
设置定时时间周期timeout;
调用alarm定时器;
设置bool timeout = false 变量,用以记录是否有定时事件需要处理;
while(服务器未关闭){
调用epoll_wait对I/O事件进行处理,并放入epoll事件表中;
for(每一个I/O事件){
if(事件是新到的连接请求){
接收连接;
创建定时器;
绑定定时器和用户数据;
设置其回调函数;
设置其超时事件;
将定时器添加到定时器容器中;
}
else if(事件是信号集){
for(每一个信号){
if(是SIGALRM信号){
标记:需要清理非活动连接;
(将timeout变量设为true)
}
else{
处理其他信号;
}
}
}
else if(事件是读/写事件){
进行读写逻辑处理;
if(读写出错){
移除对应的计时器;
}else if(客户端关闭连接){
移除对应的计时器;
}else if(某个客户连接有新数据可读){
增加该连接对应的计时器的时间;
}else{
处理其他事件;
}
}
}
if(timeout为真,即有定时事件需要处理){
处理定时事件,即释放非活动连接;
}
}
关闭服务器创建的各个socket;
main函数返回
}
可以看到这段代码中,我们设置了一个bool变量 timeout = false(14行)
因为I/O事件和信号统一事件源用epoll创建的事件表存储,所以当检测到信号(26行),且是SIGALRM信号(28行),我们就将bool变量标记为true,这样程序知道了有定时事件需要处理(本节定时事件即处理非活动连接)
但程序不会立刻开始释放连接,而是继续进行I/O,直到循环结尾(50行)时才进行处理。这是因为在web服务器设计中I/O事件优先级一般比定时事件要高。
定时器容器设计
常用的定时器容器有升序链表,时间轮和时间堆,本文讲解双向升序链表
问题
我们在为每个定时时间封装了定时器后还需将其放入一个定时器容器升序链表中,以便能处理定时事件。在具体的定时方案中,我们采用每一段固定的时间就检测一次(即tick一次,下同),同时定时器内还需要有两个指针分别指向上下两个定时器的指针(头尾定时器除外)
方案
我们创建一个sort_timer_lst数据结构,其核心函数tick()为心搏函数,它每隔一段固定的时间执行一次,已检查到期的任务;依据是定时器的expiere值小于当前的系统时间;然后调整sort_timer_lst的结构。
执行效率分析:
添加定时器 O(n)
删除定时器的时间复杂度:O(1)
执行定时任务:O(1)