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);
}
  1. sa_handler是一个函数指针,指向信号处理函数
  2. sa_sigaction同样是信号处理函数,有三个参数,可以获得关于信号更详细的信息
  3. sa_mask用来指定在信号处理函数执行期间需要被屏蔽的信号
  4. sa_flags用于指定信号处理的行为
  • SA_RESTART,使被信号打断的系统调用自动重新发起
  • SA_NOCLDSTOP,使父进程在它的子进程暂停或继续运行时不会收到 SIGCHLD 信号
  • SA_NOCLDWAIT,使父进程在它的子进程退出时不会收到 SIGCHLD 信号,这时子进程如果退出也不会成为僵尸进程
  • SA_NODEFER,使对信号的屏蔽无效,即在信号处理函数执行期间仍能发出这个信号
  • SA_RESETHAND,信号处理之后重新设置为默认的处理方式
  • SA_SIGINFO,使用 sa_sigaction 成员而不是 sa_handler 作为信号处理函数
  1. sa_restorer一般不使用

sigaction函数

#include <signal.h>

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
  1. signum表示操作的信号。
  2. act表示对信号设置新的处理方式。
  3. 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]);
  1. domain表示协议族,PF_UNIX或者AF_UNIX
  2. type表示协议,可以是SOCK_STREAM或者SOCK_DGRAM,SOCK_STREAM基于TCP,SOCK_DGRAM基于UDP
  3. protocol表示类型,只能为0
  4. 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来监测,从而实现统一处理。

信号处理机制

每个进程之中,都有存着一个表,里面存着每种信号所代表的含义,内核通过设置表项中每一个位来标识对应的信号类型。

信号处理过程如下图所示:


围绕图示,将信号分成接收、检测、处理三个部分。

  1. 信号的接收

接收信号的任务是由内核代理的,当内核接收到信号后,会将其放到对应进程的信号队列中,同时向进程发送一个中断,使其陷入内核态。

注意:此时信号还只是在队列中,对进程来说暂时是不知道有信号到来的。


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);
}


信号通知逻辑

  1. 创建管道,用于信号处理函数将信号值传递给主循环
  2. 设置SIGALRM(时间到了触发)和SIGTERM(kill会触发,Ctrl+C)信号的处理方式
  • 将handler参数设置信号处理函数(管道写端写入信号的名字)
  • 通过sigaction函数注册信号捕捉函数
  1. 利用I/O复用系统监听管道读端文件描述符的可读事件
  2. 当信号值传递给主循环,主循环再根据接收到的信号值执行目标信号对应的逻辑代码


主循环对应的信号逻辑代码

// 处理信号
            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--;
}


定时器容器设计

项目原本是基于升序双向链表来实现的定时器容器。

这种定时器容器存在着以下缺点:

  1. 每次遍历添加和修改定时器的效率偏低,为(O(n))
  2. 每次以固定的时间间隔触发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;
}
posted @   夜听风雨声`  阅读(366)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
点击右上角即可分享
微信分享提示