【C++实现HTTP服务器项目记录】定时器处理非活动连接

一、非活跃连接

- 非活跃连接:客户端(浏览器)与服务器端建立连接后,长时间不交换数据,一直占用服务器端资源。
- 针对非活跃连接,需要在内核事件表删除对应事件、关闭文件描述符、释放连接资源等。

二、信号

- Linux中的信号是一种消息处理机制,它本质上是一个整数,不同的信号对应不同的值,信号在系统中的优先级是非常高的。
- 项目中使用的信号
 	1. SIGALRM:定时器超时信号,超时的时间由系统调用alarm设置,默认终止进程。
 	2. SIGTERM:程序结束信号,kill或Ctrl+C触发,默认终止进程。
- 两个特殊信号
	1. SIGKILL:9号信号,无条件终止进程,不能被捕捉、阻塞和忽略。
	2. SIGSTOP:19号信号,无条件暂停进程,不能被捕捉、阻塞和忽略。

三、信号捕捉

- Linux中的每个信号产生之后都会有对应的默认处理行为,如果想要忽略某些信号或者修改某些信号的默认行为就需要在程序中捕捉该信号。
- 程序中的信号捕捉是一个注册的动作,提前告诉应用程序信号产生之后的处理动作,当进程中对应的信号产生了,这个处理动作也就被调用了。
struct sigaction
{
    void (*sa_handler)(int);                        // 函数指针,指向信号处理函数。
    void (*sa_sigaction)(int, siginfo_t *, void *); // 函数指针,指向信号处理函数。
    sigset_t sa_mask;                               // 在信号处理函数执行期间,临时屏蔽的信号。
    int sa_flags;                                   // SA_RESTART:使被信号打断的系统调用自动重新发起。
                                                    // SA_SIGINFO:使用第二个函数指针。
    void (*sa_restorer)(void);                      // 被废弃的成员。
};

int sigaction(
    int signum,                  // 要捕捉的信号。
    const struct sigaction *act, // 信号的处理动作。
    struct sigaction *oldact     // 上一次信号处理动作,一般指定为NULL。
);

四、定时器

// 倒计时seconds秒,倒计时完成发送SIGALRM信号,当前进程会收到这个信号。
unsigned int alarm(unsigned int seconds);

五、统一事件源

- 1. 一般的信号处理函数需要处理该信号对应的逻辑,当该逻辑比较复杂时,信号处理函数执行时间过长,会导致信号屏蔽太久。
- 解决方法:
	信号处理函数仅发送信号的值,通知程序主循环,程序主循环中执行信号对应的逻辑代码。
	
- 2. 统一事件源:将信号事件与其他事件一样被处理。
- 解决方法:
	信号处理函数往管道的写端写入信号值,主循环则从管道的读端读出信号值。
	使用I/O复用系统调用监听管道读端的可读事件与其他文件描述符,从而实现统一处理。

六、实现代码

1. lst_timer.h

/*
 * 带头尾结点的升序双向链表管理定时器。
 * 为每个连接创建一个定时器,将其添加到链表中,并按照超时时间升序排列。
 * 执行定时任务时,将到期的定时器从链表中删除。
 * 添加定时器的事件复杂度是O(n), 删除定时器的事件复杂度是O(1)。
 */
#ifndef LST_TIMER
#define LST_TIMER

#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"

// 连接资源结构体成员需要用到定时器类
// 需要前向声明
class util_timer;

// 连接资源结构体
struct client_data
{
    // 客户端Socket地址
    sockaddr_in address;
    // Socket文件描述符
    int sockfd;
    // 定时器
    util_timer *timer;
};

// 定时器类
class util_timer
{
public:
    // 构造函数
    util_timer() : prev(NULL), next(NULL) {}

public:
    // 定时器超时时间 = 浏览器和服务器连接时刻 + 固定时间(TIMESLOT)
    time_t expire;
    // 回调函数
    void (*cb_func)(client_data *);
    // 连接资源
    client_data *user_data;
    // 前驱计时器
    util_timer *prev;
    // 后继计时器
    util_timer *next;
};

// 定时器容器类
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;
};

class Utils
{
public:
    // 构造函数
    Utils() {}
    // 析构函数
    ~Utils() {}

    void init(int timeslot);

    // 对文件描述符设置非阻塞
    int setnonblocking(int fd);

    // 将内核事件表注册读事件
    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;
    // 定时器链表
    sort_timer_lst m_timer_lst;
    // 内核中的EPOLL事件表文件描述符
    static int u_epollfd;
    // 超时时间
    int m_TIMESLOT;
};

void cb_func(client_data *user_data);

#endif

2.lst_timer.cpp

#include "lst_timer.h"
#include "../http/http_conn.h"

// 构造函数
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;
    }
}

// 将目标定时器添加到链表中,将定时器按超时时间升序插入
void sort_timer_lst::add_timer(util_timer *timer)
{
    if (!timer)
    {
        return;
    }
    // 如果当前链表中只有头尾节点
    if (!head)
    {
        // 直接插入
        head = tail = timer;
        return;
    }
    // 如果新的定时器超时时间小于当前头部结点
    if (timer->expire < head->expire)
    {
        // 直接将当前定时器结点作为头结点
        timer->next = head;
        head->prev = timer;
        head = timer;
        return;
    }
    // 其他情况,调用私有成员,重新插入
    add_timer(timer, head);
}

// 调整链表内部结点
void sort_timer_lst::add_timer(util_timer *timer, util_timer *lst_head)
{
    util_timer *prev = lst_head;
    util_timer *tmp = prev->next;
    // 遍历当前结点之后的链表,按照超时时间找到目标定时器对应的位置
    while (tmp)
    {
        // prev <---> timer <---> tmp
        if (timer->expire < tmp->expire)
        {
            prev->next = timer;
            timer->next = tmp;
            tmp->prev = timer;
            timer->prev = prev;
            break;
        }
        prev = tmp;
        tmp = tmp->next;
    }
    // 遍历完还未插入,则目标定时器放到尾结点处
    // prev <---> timer <---> NULL
    if (!tmp)
    {
        prev->next = timer;
        timer->prev = prev;
        timer->next = NULL;
        tail = timer;
    }
}

// 任务发生变化时,调整该定时器在链表中的位置
void sort_timer_lst::adjust_timer(util_timer *timer)
{
    if (!timer)
    {
        return;
    }
    util_timer *tmp = timer->next;

    // 被调整的定时器是链表尾结点 或 定时器超时值仍然小于下一个定时器超时值
    if (!tmp || (timer->expire < tmp->expire))
    {
        // 不调整
        return;
    }
    // 被调整定时器是链表头结点
    if (timer == head)
    {
        // 将定时器取出
        head = head->next;
        head->prev = NULL;
        timer->next = NULL;
        // 调用私有成员,重新插入
        add_timer(timer, head);
    }
    // 被调整定时器在链表内部
    else
    {
        // 将定时器取出
        timer->prev->next = timer->next;
        timer->next->prev = timer->prev;
        // 调用私有成员,重新插入
        add_timer(timer, timer->next);
    }
}

// 删除定时器
void sort_timer_lst::del_timer(util_timer *timer)
{
    if (!timer)
    {
        return;
    }
    // 链表中只有一个定时器
    if ((timer == head) && (timer == tail))
    {
        delete timer;
        head = NULL;
        tail = NULL;
        return;
    }
    // 被删除的定时器为头结点
    if (timer == head)
    {
        head = head->next;
        head->prev = NULL;
        delete timer;
        return;
    }
    // 被删除的定时器为尾结点
    if (timer == tail)
    {
        tail = tail->prev;
        tail->next = NULL;
        delete timer;
        return;
    }

    // 被删除的定时器在链表内部
    timer->prev->next = timer->next;
    timer->next->prev = timer->prev;
    delete timer;
}

// 到期的定时器,调用其回调函数并将其从链表移除
void sort_timer_lst::tick()
{
    if (!head)
    {
        return;
    }

    // 获取此刻时间戳
    time_t cur = time(NULL);

    util_timer *tmp = head;
    // 遍历定时器链表
    while (tmp)
    {
        // 链表容器为升序排列,当前时间小于该定时器的超时时间,则后面的定时器也没有到期
        if (cur < tmp->expire)
        {
            break;
        }
        // 当前定时器到期,则调用回调函数,执行定时事件
        tmp->cb_func(tmp->user_data);
        // 将处理后的定时器从链表容器中删除
        head = tmp->next;
        if (head)
        {
            head->prev = NULL;
        }
        delete tmp;
        // 重置头结点
        tmp = head;
    }
}

// 设置超时时间
void Utils::init(int timeslot)
{
    m_TIMESLOT = timeslot;
}

// 对文件描述符设置为非阻塞
int Utils::setnonblocking(int fd)
{
    int old_option = fcntl(fd, F_GETFL);
    int new_option = old_option | O_NONBLOCK;
    fcntl(fd, F_SETFL, new_option);
    return old_option;
}

// 向内核事件表注册读事件
void Utils::addfd(int epollfd, int fd, bool one_shot, int TRIGMode)
{
    epoll_event event;
    event.data.fd = fd;
    // EPOLLRDHUP:当Socket接收到对方关闭连接时的请求之后触发。
    if (1 == TRIGMode)
    {
        // 边沿触发模式
        event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;
    }
    else
    {
        // 水平触发模式
        event.events = EPOLLIN | EPOLLRDHUP;
    }
    // EPOLLONESHOT:对于connfd,一个线程处理Socket时,其他线程将无法处理。
    // 当该线程处理完后,需要通过epoll_ctl重置EPOLLONESHOT事件
    if (one_shot)
        event.events |= EPOLLONESHOT;
    // 将内核事件表添加该事件
    epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
    // 对文件描述符设置为非阻塞
    setnonblocking(fd);
}

// 信号处理函数
// 可重入性:中断后再次进入该函数,环境变量与之前相同,不会丢失数据。
void Utils::sig_handler(int sig)
{
    // Linux中系统调用的错误都存储于errno中,errno由操作系统维护,存储就近发生的错误。
    // 下一次的错误码会覆盖掉上一次的错误,为保证函数的可重入性,保留原来的errno
    int save_errno = errno;

    int msg = sig;

    // 将信号值从管道写端pipefd[1]写入,传输字符类型,而非整型
    send(u_pipefd[1], (char *)&msg, 1, 0);

    // 将原来的errno赋值为当前的errno
    errno = save_errno;
}

// 设置需要捕捉的信号
void Utils::addsig(int sig, void(handler)(int), bool restart)
{
    struct sigaction sa;
    memset(&sa, '\0', sizeof(sa));
    sa.sa_handler = handler;
    if (restart)
    {
        // 使被信号打断的系统调用自动重新发起
        sa.sa_flags |= SA_RESTART;
    }
    // 在信号处理函数执行期间,屏蔽所有信号
    sigfillset(&sa.sa_mask);
    // 注册给内核
    assert(sigaction(sig, &sa, NULL) != -1);
}

// 定时处理任务
void Utils::timer_handler()
{
    m_timer_lst.tick();
    // 重新定时以不断触发SIGALRM信号
    alarm(m_TIMESLOT);
}

// 向客户端发送错误信息
void Utils::show_error(int connfd, const char *info)
{
    send(connfd, info, strlen(info), 0);
    // 关闭文件描述符
    close(connfd);
}

// 静态数据成员类外初始化
int *Utils::u_pipefd = 0;
int Utils::u_epollfd = 0;

class Utils;
// 定时器回调函数
void cb_func(client_data *user_data)
{
    // 删除非活动连接在Socket上的注册事件
    epoll_ctl(Utils::u_epollfd, EPOLL_CTL_DEL, user_data->sockfd, 0);
    assert(user_data);
    // 关闭文件描述符
    close(user_data->sockfd);
    // 减少连接数
    http_conn::m_user_count--;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值