升序链表的定时器

引言

参考书目《Linux高性能服务器编程》,在接下来的几篇文章里面我们都会用代码的方式解释不同的定时器的架构。因为书上的内容是用c++98写的,我这里全部都用c++11重写了一遍。完整的代码在github上面:

fengyue05/Linux-: 本仓库里面会有关于《Linux高性能服务器编程》的一些主要代码实现

代码

我们先模拟一个客户端数据的结构体,其中每一个客户数据都绑定了对应的定时器容器。但是注意,我们这里的绑定是用虚指针,因为如果互相绑定会导致内存无法释放。

cpp 复制代码
struct client_data {
    sockaddr_in address;
    int sockfd;
    std::array<char, BUFFER_SIZE> buffer;
    std::weak_ptr<util_timer> timer;
};

这是一个定时器的容器,其中包含一个定时器的回调函数,这个回调函数我们会在主函数里面设置,主要就是删除对应的定时器,这个我们后面再说。然后对于定时器,需要一个绝对的到期时间,一个回调函数,互相绑定的一个客户端数据,最后是因为所有的定时器都是在一个链表上面,所以需要next指针和prev指针。

cpp 复制代码
class util_timer {
public:
    using Clock = std::chrono::steady_clock;
    using TimeOut = Clock::time_point;
    using Callback = std::function<void(std::shared_ptr<client_data>)>;

    util_timer();
    friend class SortTimerLsit;
public:
    // 定时器的绝对到期时间点
    TimeOut expire;
    
    // 定时器到期后执行的回调函数
    Callback callback;

    // 回调函数所需要客户的数据
    std::shared_ptr<client_data> m_userData;

    // 双向链表的指针
    std::shared_ptr<util_timer> m_next;
    std::weak_ptr<util_timer> m_prev;
};

这个是一个升序的链表,其中包含了对于定时器的调整函数,包括添加、调整、刷新、删除、处理已经到时间的定时器容器,然后因为这是一个链表,而且设置为一个双向的链表,所以我们需要记录其头部和尾部。

我们的链表头部是很快要结束的,尾部是最晚要结束的

cpp 复制代码
class SortTimerLsit {
public:
    SortTimerLsit() = default;
    ~SortTimerLsit();

    void addTimer(std::shared_ptr<util_timer> timer);
    void adjustTimer(std::shared_ptr<util_timer> timer);

    // 延长定时器
    template<typename Rep, typename Period>  // 数据类型 和 时间间隔
    void refreshTimer(std::shared_ptr<util_timer> timer, const std::chrono::duration<Rep, Period>& timeout) {
        if (timer == nullptr) {
            return;
        }

        timer->expire = util_timer::Clock::now() + timeout;
        adjustTimer(timer);
    }

    /**
     * 删除指定定时器
     * 1. 将定时器从链表摘除
     * 2. 释放定时器内存
     */
    void deleteTimer (std::shared_ptr<util_timer> timer);
    /**
     * 处理所有已经到期的定时
     * 因为链表按到期时间升序排列,所以只需要从头部开始检查。
     * 遇到第一个尚未到期的定时器即可停止。
     */
    void tick();

    bool empty() const {return m_head == nullptr;}
    std::shared_ptr<util_timer> getFront() const {return m_head;}

private:
    void clear();
    std::shared_ptr<util_timer> detchTimer(std::shared_ptr<util_timer> timer);

    SortTimerLsit(const SortTimerLsit&) = delete;
    SortTimerLsit& operator=(const SortTimerLsit&) = delete;
    std::shared_ptr<util_timer> m_head;
    std::weak_ptr<util_timer> m_tail;
};

添加定时器

cpp 复制代码
void SortTimerLsit::addTimer(std::shared_ptr<util_timer> timer) {
    if (timer == nullptr) {
        return;
    }

    timer->m_next.reset();
    if (m_head == nullptr) {
        m_head = timer;
        m_tail = timer;
        return;
    }

    // 新定时器比头节点更早到期,插入头部
    if (timer->expire < m_head->expire) {
        timer->m_next = m_head;
        m_head->m_prev = timer;
        m_head = timer;
        return;
    }

    auto current = m_head;
    while(current->m_next != nullptr && timer->expire > current->m_next->expire) {
        current = current->m_next;
    }

    timer->m_next = current->m_next;
    timer->m_prev = current;

    if (current->m_next) {
        current->m_next->m_prev = timer;
    }
    else {
        m_tail = timer;
    }
    current->m_next = timer;
}

调整一个定时器的位置函数,如果时间不符合链表的位置,操作就是摘除对应的定时器,然后重新执行添加定时器的操作。

cpp 复制代码
void SortTimerLsit::adjustTimer(std::shared_ptr<util_timer> timer) {
    if (timer == nullptr) {
        return;
    }
    const bool vaildWithPrevious = timer->m_prev.lock() == nullptr || timer->expire >= timer->m_prev.lock()->expire;
    const bool vaildWithNext = timer->m_next == nullptr || timer->expire <= timer->m_next->expire;

    if (vaildWithNext && vaildWithPrevious) {
        return;
    }

    timer = detchTimer(timer);
    addTimer(timer);
}

删除的内核就是把定时器摘除,但是内核就是detchTimer,只是没有返回值

cpp 复制代码
void SortTimerLsit::deleteTimer(std::shared_ptr<util_timer> timer)
{
    if (timer == nullptr) {
        return;
    }

    std::shared_ptr<client_data> userData = timer->m_userData;

    detchTimer(timer);
}

这个就是摘除的函数,逻辑和删除的一样,但是用temp记录了我们要删除的定时器,然后返回这个定时器。

cpp 复制代码
std::shared_ptr<util_timer> SortTimerLsit::detchTimer(std::shared_ptr<util_timer> timer) {
    if (timer == nullptr) {
        return nullptr;
    }

    std::shared_ptr<util_timer> temp;
    temp = timer;
    // timer前面存在结点
    if (timer->m_prev.lock() != nullptr) {
        timer->m_prev.lock()->m_next = timer->m_next;
    } 
    else {
        m_head = timer->m_next;
    }

    if (timer->m_next != nullptr) {
        timer->m_next->m_prev = timer->m_prev;
    }
    else {
        m_tail = timer->m_prev;
    }

    timer->m_prev.reset();
    timer->m_next.reset();
    return temp;
}

当操作整个链表里面的定时器的时候,我们先要得到目前的时间,然后就拿这个时间跟着定时器一一比较,如果超时的就全部删除,并且同时执行其定时器的回调函数

cpp 复制代码
void SortTimerLsit::tick() {
    const auto currentTime = util_timer::Clock::now();
    while(m_head != nullptr) {
        if (m_head->expire > currentTime) {
            break;
        }

        std::shared_ptr<util_timer> expiredTimer = m_head;
        std::shared_ptr<client_data> userData = expiredTimer->m_userData;

        util_timer::Callback callback = expiredTimer->callback;

        detchTimer(expiredTimer);

        if (callback) {
            callback(userData);
        }
    }
}

接下来就是主函数

我们要在T的时间周期里面触发一次时间信号,然后每一次触发就要执行一些非活动连接,比如说给客户端发送一个重连请求,或者关闭该连接,或者其他。所以每一次触发了alarm之后就需要重新设置,因为这个信号每一次设置只能触发一次。然后每一次触发时间信号,就需要处理链表里面的定时器。

然后这个回调函数就是我们要传递给定时器里面的那个回调函数。其作用就是删除epoll里面对应连接客户端的socketfd,然后关闭文件描述符。

cpp 复制代码
void timeHandler() {
    // 定时处理任务
    sortTimerList.tick();
    // 因为一次alarm调用只会引起一次SIGALRM信号,所以我们要重新定时,不断的触发SIGALARM信号
    alarm(TIMESLOT);
}

// 定时器回调函数,他删除非活动连接socket上的注册事件,并关闭
void callbcak(std::shared_ptr<client_data> user_data) {
    assert (user_data);
    epoll_ctl(epollfd, EPOLL_CTL_DEL, user_data->sockfd, 0);
    close(user_data->sockfd);
    std::cout << user_data->sockfd << std::endl;
}

这个函数我们原来的操作,并且设置了信号。

cpp 复制代码
    epoll_event events[MAX_EVENT_NUMBER];
    epollfd = epoll_create(1);
    assert (epollfd != -1);
    addFd(epollfd, listenfd);

    ret = socketpair(AF_UNIX, SOCK_STREAM, 0, pipefd);
    assert (ret != -1);
    setNoBlocking(pipefd[1]);
    addFd(epollfd, pipefd[0]);

    addsig(SIGALRM);
    addsig(SIGTERM);
    bool server_stop = false;

    alarm(TIMESLOT);
    bool timeout = false;
    std::array<std::shared_ptr<client_data>, FD_LIMIT> users;

在整个循环里面,当我们监听到了事件的到来,就要分类进行处理。

如果是有连接的到来,首先是加入users,并且设置各种用户端的信息,方便我们之后管理,然后设置一对一的定时器,定时器里面就包含了用户的数据(从users里面取得),定时器的回调函数,定时器的终止时间,最后与users进行互相的绑定,然后把定时器添加进入了链表里面。

如果管道里面来了信号,并且信号是时间信号,我们先做一个标记,因为定时器的回调函数执行并不是特别的重要,所以我们可以放在后面执行,而其他的事件先执行,所以这也会导致我们的周期时间出现一点一点的偏差。

如果是有读事件到来,我们不仅仅要读取信息,而且还需要处理定时器,首先在读取数据的时候,我们需要重新调整定时器的终止时间,因为数据没有读完,延迟连接关闭的时间,因为我们这里读数据设置的都是非阻塞,而且没有while()读取,会反复的触发epoll,但是如果没有更新关闭的时间,那么可能数据没有读完,下一次读取数据的时候就关闭了。那我们什么时候关闭呢?就是在读完之后,我们先执行回调函数,关闭socket,然后删除链表里面的定时器。

当所有的判断都结束了之后,再来执行超时的处理函数,这个超时的处理函数我们仅仅只是简单的封装了一下,实际上这个函数里面会解决更多的事情。

cpp 复制代码
    while(!server_stop) {
        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) {
                sockaddr_in client_address;
                socklen_t client_len = sizeof(client_address);
                int connfd = accept(listenfd, (sockaddr*)& client_address, &client_len);
                users[connfd] = std::make_shared<client_data>();
                users[connfd]->address = client_address;
                users[connfd]->sockfd = connfd;
                addFd(epollfd, connfd);
                // 创建定时器,设置其回调函数与超时时间,然后绑定定时器与用户数据,最后将定时器添加到链表上
                std::shared_ptr<util_timer> timer = std::make_shared<util_timer>();
                timer->m_userData = users[connfd];
                timer->callback = callbcak;
                std::chrono::steady_clock::time_point cur = std::chrono::steady_clock::now();
                timer->expire = cur + std::chrono::seconds(3 * TIMESLOT);
                users[connfd]->timer = timer;
                sortTimerList.addTimer(timer);
            }
            else if ((sockfd == pipefd[0]) && (events[i].events & EPOLLIN)) {
                int sig;
                char signals[1024];
                while (1) {
                    ret = recv (pipefd[0], signals, sizeof(signals), 0);
                    if (ret < 0) {
                        break;
                    }
                    else if (ret == 0) {
                        break;
                    }
                    else {
                        for (int i = 0; i < ret; i++) {
                            switch (signals[i])
                            {
                            case SIGALRM:
                                // 用timeout变量标记有定时任务时需要处理,但不立即处理定时任务。
                                // 这是因为定时任务的优先级不是很高,我们优先处理其他更重要的任务
                                timeout = true;
                                break;
                            case SIGTERM:
                                server_stop = true;
                            default:
                                break;
                            }
                        }
                    }
                }
            }
            else if (events[i].events & EPOLLIN) {
                memset(users[sockfd]->buffer.data(), '\0', users[sockfd]->buffer.size());
                ret = recv(sockfd, users[sockfd]->buffer.data(), BUFFER_SIZE - 1, 0);
                std::cout << users[sockfd]->buffer.data() << std::endl;
                std::shared_ptr<util_timer> timer = users[sockfd]->timer.lock();
                if (ret < 0) {
                    // 如果发生读错误,则关闭连接,并移除对应的定时器
                    if (errno != EAGAIN) {
                        callbcak(users[sockfd]);
                        if (timer) {
                            sortTimerList.deleteTimer(timer);
                        }
                    }
                }
                else if (ret == 0) {
                    callbcak(users[sockfd]);
                    if (timer) {
                        sortTimerList.deleteTimer(timer);
                    }
                }
                else {
                    // 如果某个客户连接上有数据可读,则我们要调整该连接对应的定时器,以延迟该连接关闭的时间
                    if (timer) {
                        std::chrono::steady_clock::time_point cur = std::chrono::steady_clock::now();
                        timer->expire = cur + std::chrono::seconds(3 * TIMESLOT);
                        sortTimerList.adjustTimer(timer);
                    }
                    else {
                        // others
                    }
                }
            }
            // 最后处理定时事件,因为I/O事件有更高的优先级。当然,这样做将导致定时任务不能精确按照预期执行任务
            if (timeout) {
                timeHandler();
                timeout = false;
            }   
        }
    }

总结

本篇文章到这里就结束了!!!希望可以帮助大家理解~~~

相关推荐
闻缺陷则喜何志丹1 小时前
【计算几何 第10章】更多几何数据结构:截窗
数据结构·数学·线段树·计算几何·截窗·优先查找树·区间树
WBluuue1 小时前
数据结构与算法:带权并查集
数据结构·c++·算法
智塑未来1 小时前
企业服务器托管怎么选?多维评测标准与主流方案选型指南
服务器·git·github
mwmbfh1 小时前
【CentOS7环境下Redis 6.2.14 源码编译部署说明】
linux·运维·数据库·redis
175063319451 小时前
Ubuntu24 DNS 问题
网络·数据库
智恒百亿1 小时前
5090八卡算力服务器的技术架构与AI应用场景分析
服务器·人工智能·架构
渡我白衣1 小时前
Acceptor模块的设计与实现
java·linux·服务器·开发语言·网络·c++·人工智能
leagsoft_10031 小时前
联软科技推出UniNDR,打通终端、服务器与网络侧的威胁检测链路
服务器·网络·科技
云雾J视界1 小时前
英伟达AI服务器涨价超15%:HBM存储成本飙升,AI算力成本重构开始
服务器·人工智能·芯片·英伟达·hbm