C++ Webserver从零开始:基础知识(六)——定时器

定时器容器和定时器

        一个服务器程序不仅要处理读事件和写事件,还要处理的一类事件是定时事件。

什么是定时事件:在服务器程序中,每过一段固定的时间触发某段代码,由该代码处理一个事件,如:从内核事件表中删除事件并关闭文件描述符,释放连接支援

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一次,下同),这种方法显而易见性能不是非常高。

        一种方案是:在所有定时器中找到超时时间最小的一个定时器,用它的超时值作为tick触发时间,这样当tick触发就一定有超时事件处理。然后再从剩余的定时器找最小的继续上述处理,就实现了较为精准的定时。

方案

        分析了上述需求后,我们可以使用小根堆来实现。

        在C++ 中小根堆也就是优先队列,其底层是一个完全二叉树,且该二叉树的子节点的值永远>=父节点的值,因此其根节点一定是最小的。且小根堆各个操作的时间复杂度如下:

                添加 :O(logn)

                删除 :O(1)

                执行定时事件(在时间堆的情景下):O(1)

        可见时间堆的效率是非常高的,小根堆的原理我有时间另开一篇文章,放个链接待定

                                                【数据结构:小根堆文章链接】

你可能感兴趣的:(c++,开发语言,服务器)