网站 keywords seo,注册建筑工程公司需要什么条件,梧州网站优化,酒业网站建设目录
#x1f382;前言
#x1f33c;定时器设计
#x1f600;容器设计
#x1f33c;任务处理函数
#x1f6a9;源码分析#xff08;定时器的使用#xff09; #x1f382;前言
PS#xff1a;写博客 -- 巩固项目基础过程中#xff0c;可以通过 GPT Google cpp…
目录
前言
定时器设计
容器设计
任务处理函数
源码分析定时器的使用 前言
PS写博客 -- 巩固项目基础过程中可以通过 GPT Google cppreference 的方式加深对某些 专业名词 的理解 定时器处理 非活动连接 模块分 2 部分 1定时方法 与 信号通知流程 2定时器 及其 容器设计定时任务的处理 本博客介绍第 2 部分具体涉及定时器设计 / 容器设计 / 定时任务处理函数 / 使用定时器 定时器设计 将 连接资源 和 定时事件 封装起来具体包括连接资源超时时间回调函数。 回调函数 指向 定时事件 定时器容器设计 将多个 定时器 串联起来统一处理具体涉及 升序链表 的设计 定时任务处理函数 该函数封装在 容器类 中具体的函数遍历 升序链表容器根据超时时间处理对应的定时器 源码分析定时器的使用 定时器设计 TinyWebServer 将 连接资源 / 定时事件 / 超时时间封装为 定时器类具体 连接资源 包括 -- 客户端套接字地址 文件描述符 定时器定时事件 为 回调函数将其封装起来由用户自定义这里删除 非活动 socket 上的注册事件并关闭定时超时时间 浏览器和服务器连接时刻 固定时间(TIMESLOT) 由此可见定时器使用 绝对时间 作为 超时值这里 alarm 设置为 5 秒连接超时 15秒 // 连接资源 结构体成员 要用到 定时器类
// 前向声明
class util_timer;// 连接资源
struct client_data
{// 客户端 socket 地址sockaddr_in address;// socket 文件描述符int sockfd;// 定时器util_timer* timer; // 指向 util_timer
};// 定时器类
class util_timer
{
public:// 构造函数 成员初始化列表util_timer() : prev(NULL), next(NULL) {}public:// 超时时间time_t expire;// 回调函数void (*cb_func)(client_data*); // 函数指针// 连接资源client_data* user_data; // 指向 client_data// 前向定时器util_timer* prev;// 后继定时器util_timer* next;
};
关于上面代码中的 回调函数 1回调函数 cb_func() 的作用是用来处理定时器超时事件时的特定操作 2具体地当一个定时器到达设定的超时时间时会调用与该定时器相关联的回调函数cb_func()并将相应的 client_data 结构体指针 作为参数传递给回调函数 3在这个场景下每个定时器实例都会有一个超时时间 expire 和一个 回调函数指针cb_func()用于指定在定时器超时时需要执行的特定操作 4通过回调函数的机制可以实现在超时时执行不同的处理逻辑例如关闭连接、释放资源、发送心跳包等操作 5结合 client_data 结构体和 util_timer 类我们可以看到在client_data结构体中包含了一个指向 util_timer类实例 的指针timer而 util_timer 类中则包含了一个指向client_data 结构体的指针 user_data 6这种设计是为了在定时器超时时可以同时对连接资源和定时器进行操作实现更灵活和完善的定时器管理功能 7通过回调函数机制在定时器超时时可以执行特定的操作同时利用连接资源结构体和定时器类之间的关联可以更方便地管理和操作定时器及其关联的连接资源 定时事件具体的从 内核事件表 删除时间关闭 文件描述符释放连接资源 // 定时器 回调函数
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--;
} 解释 调用 epoll_ctl() 函数时可以向 epoll 实例由 epollfd 指定添加、修改或删除一个指定文件描述符的事件 assert() 是一个宏用于在代码中插入一条条件判断语句 如果条件为假则终止程序运行并打印错误信息 在这段代码中assert(user_data) 用于确保 user_data 指针不为空即确保传入的 client_data 结构体指针有效 如果 user_data 为空即断言失败程序将终止执行同时会输出错误信息总结回调函数 cb_func 主要实现了对非活动连接的事件删除、文件描述符关闭、连接数减少的操作 容器设计 webserver 中定时器容器 -- 带头尾节点的升序双向链表具体为每个连接创建一个定时器将其添加到链表中并按照 超时时间 升序排列执行定时任务时将到期的定时器从链表删除 实现上主要涉及双向链表的插入删除 其中 添加定时器的时间复杂度 O(n)删除定时器 O(1) 升序双向链表逻辑 创建头尾节点其中头尾节点没有意义仅方便调整add_timer() 函数将目标定时器添加到链表升序添加 链表只有头尾节点直接插入否则升序插入adjust_timer() 函数当定时任务发生变化调整定时器在链表的位置 客户端在设定时间内有数据收发仅当前时刻对该定时器重新设定时间这里只是往后延长超时时间被调整的目标定时器在尾部或定时器新的超时值仍小于下一个定时器的超时不用调整否则将定时器从链表取出重新插入链表del_timer() 函数将超时的定时器从链表删除 常规双向链表删除节点 // 定时器容器类
class sort_timer_lst
{
public:sort_timer_lst() : head(NULL), tail(NULL) {}// 常规销毁链表~sort_timer_lst(){util_timer* tmp head;while (tmp){head tmp-next;delete tmp;tmp head;}}// 添加定时器内部调用私有成员 add_timer// 公有 add_timer() 是外部调用接口只有一个参数// 即要插入的 定时器对象void 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);// ↑ 公有 add_timer() 调用了 私有 add_timer() }// 调整定时器任务发生变化时调整定时器在链表的位置void 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 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-next-prev NULL;delete timer;return;}// 为尾节点if (timer tail) {tail tail-prev;tail-next NULL;delete timer;return;}// 被删除定时器在链表内部常规删除// 符合前面情况的都 return 了这里就不用 iftimer-next-prev timer-prev;timer-prev-next timer-next;delete timer;}private:// 私有成员被公有成员 add_timer 和 adjust_timer 调用// 用于调整链表内部节点// 即 私有 add_timer() 是类内部调用的函数接受 2 个参数// 将 timer 插入到 lst_head 之后合适位置// timer -- 要插入的定时器void add_timer(util_timer* timer, util_timer* lst_head){util_timer* prev lst_head; // 插入的起点util_timer* tmp prev-next; // 起点下一位置// 遍历当前节点之后的链表按照超时时间升序插入while (tmp) {// 由于公有 add_timer() // 此时timer的超时时间一定 lst_headif (timer-expire tmp-expire) {// 插入 prev 和 prev-next 之间prev-next timer;timer-next tmp;tmp-prev timer;timer-prev;break; // 插入完毕}// prev 和 prev_next 一直往后移动prev tmp; tmp tmp-next;}// 如果此时 prev 为尾节点tmp 为 空// timer 超时时间 尾节点 超时时间if (!tmp) { // timer需要作为新的尾节点prev-next timer;timer-prev prev;timer-next NULL;tail timer;}}private:util_timer* head; // 头util_timer* tail; // 尾
};
任务处理函数 使用统一事件源SIGALRM 信号每次被触发主循环中调用一次 定时任务处理函数处理链表容器到期的定时器 具体逻辑 遍历定时器升序链表容器从头节点开始依次处理每个定时器直到遇到尚未到期的定时器若当前时间小于定时器超时时间跳出循环即未找到到期的定时器若当前时间大于定时器超时时间即找到了到期的定时器执行回调函数然后将它从链表删除并继续遍历 // 定时任务处理函数
void 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;}
}
源码分析定时器的使用 服务器首先创建 定时器容器链表然后用统一事件源将 异常事件 / 读写事件 / 信号事件 统一处理根据不同事件的对应逻辑使用定时器 具体的 浏览器与服务器连接时创建该连接 对应的定时器并将该定时器添加到链表上处理 异常事件 时执行定时事件服务器关闭连接从链表移除对应定时器处理 定时信号 时将定时标志设置为 true处理 读事件 时若某连接上发生 读事件将对应定时器 向后移动否则执行定时事件处理 写事件 时若 服务器 通过某连接给 浏览器 发送数据将对应定时器向后移动否则执行定时事件 // 定时处理任务重新定时以不断触发 SIGALRM 信号
void timer_handler()
{timer_lst.tick(); // 处理链表上到期的定时器alarm(TIMESLOT); // 重新定时以不断触发 SIGALRM 信号
}// 创建定时器容器链表
static sort_timer_lst timer_lst;// 创建连接资源数组
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);// 该连接分配的文件描述符int connfd accept(listenfd, (struct sockaddr *)client_address, client_addrlength);// 初始化该连接对应的连接资源users_timer[connfd].address client_address;users_timer[connfd].sockfd connfd;// 创建定时器临时变量util_timer *timer new util_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;// 将该定时器添加到链表timer_lst.add_timer(timer);}// 处理异常事件else if (events[i].events (EPOLLRDHUP | EPOLLHUP | EPOLLERR)){// 服务器关闭连接移除对应的定时器cb_func(users_timer[sockfd]);util_timer *timer users_timer[sockfd].timer;if (timer)timer_lst.del_timer(timer);}// 处理定时器信号else if ((sockfd pipefd[0]) (events[i].events EPOLLIN)){// 接收到 SIGALRM 信号timeout 设置为 truetimeout true;}// 处理客户连接上接收到的数据else if (events[i].events EPOLLIN) {// 创建定时器临时变量将该连接对应的定时器取出来util_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;timer_lst.adjust_timer(timer);}}else {// 服务器关闭连接移除对应的定时器cb_func(users_timer[sockfd]);if (timer)timer_lst.del_timer(timer);}}else if (events[i].events EPOLLOUT){util_timer *timer users_timer[sockfd].timer;if (users[sockfd].write()) {// 若有数据传输则将定时器往后延迟 3 个单位// 并调整新定时器在链表的位置if (timer) {time_t cur time(NULL);timer-expire cur 3*TIMESLOT;timer_lst.adjust_timer(timer);}}else {// 服务器关闭连接移除对应定时器cb_func(users_timer[sockfd]);if (timer)timer_lst.del_timer(timer);}}}// 处理定时器为 非必须 事件收到信号不是马上处理// 完成读写事件后再进行处理if (timeout) {timer_handler(); // 处理超时定时器事件timeout false;}
}有人提出连接资源中的 address 是不是没用 虽然 webserver 中没用到但是实际应用程序中可能需要使用客户端的地址信息 比如记录客户端的 IP 地址用于 日志 记录用户追踪 此外 还可根据 IP 地址进行一些业务逻辑比如限制异地登陆区分不同地区用户。 以上address 提供了一个扩展点所以还是有潜在价值的