WebServer -- 定时器处理非活动连接(下)

目录

🎂前言

🌼定时器设计

😀容器设计

🌼任务处理函数

🚩源码分析(定时器的使用)


🎂前言

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 提供了一个扩展点,所以还是有潜在价值的

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/699649.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于事件触发机制的孤岛微电网二次电压与频率协同控制MATLAB仿真模型

微❤关注“电气仔推送”获得资料&#xff08;专享优惠&#xff09; 本模型质量非常高&#xff0c;运行效果完美。本模型为4机并联孤岛系统&#xff0c;在下垂控制的基础上加入二次控制&#xff0c;二次电压与频率协同控制策略利用事件触发的方法来减少控制器的更新次数。该方法…

easyui 手风琴Accordion 面板的高度设置

今天接到一个新的小需求&#xff0c;如下图&#xff0c;当预算表单只有一个时&#xff0c;要求不显示预算表单这块的内容。 考虑到页面创建时用到了表单的回调和点击方法&#xff0c;所以不能单纯的移除&#xff0c;移除右侧表格的创建会报错&#xff0c;所以只能隐藏。 隐藏…

Docusaurus框架——快速搭建markdown文档站点介绍sora

文章目录 ⭐前言⭐初始化项目&#x1f496; 创建项目&#xff08;react-js&#xff09;&#x1f496; 运行项目&#x1f496; 目录文件&#x1f496; 创建一个jsx页面&#x1f496; 创建一个md文档&#x1f496; 创建一个介绍sora的文档 ⭐总结⭐结束 ⭐前言 大家好&#xff0…

劫持已经存在的DLL

这里找到一个成功加载的 这里先把原来程序正常的dll改名为libEGL1.dll&#xff0c;然后将我们自己的dll改名为libEGL.dll 然后再重新执行程序&#xff0c;这里同样是弹出了窗口

QEMU源码全解析 —— virtio(20)

接前一篇文章&#xff1a; 上回书重点解析了virtio_pci_modern_probe函数。再来回顾一下其中相关的数据结构&#xff1a; struct virtio_pci_device struct virtio_pci_device的定义在Linux内核源码/drivers/virtio/virtio_pci_common.h中&#xff0c;如下&#xff1a; /* O…

centos7 docker 安装

1.卸载之前docker环境 sudo yum remove docker \ docker-client \ docker-client-latest \ docker-common \ docker-latest \ docker-latest-logrotate \ docke…

Sora-OpenAI 的 Text-to-Video 模型:制作逼真的 60s 视频片段

OpenAI 推出的人工智能功能曾经只存在于科幻小说中。 2022年&#xff0c;Openai 发布了 ChatGPT&#xff0c;展示了先进的语言模型如何实现自然对话。 随后&#xff0c;DALL-E 问世&#xff0c;它利用文字提示生成令人惊叹的合成图像。 现在&#xff0c;他们又推出了 Text-t…

编程笔记 Golang基础 023 切片

编程笔记 Golang基础 023 切片 一、切片二、定义与初始化三、基本操作四、示例 Go语言中的切片&#xff08;slices&#xff09;是基于数组的抽象数据类型&#xff0c;它提供了一种灵活的方式来处理可变长度的数据序列。切片本身不存储任何数据&#xff0c;而是指向底层数组的一…

bat脚本进程停止与启动

在Windows操作系统中&#xff0c;批处理&#xff08;Batch&#xff09;脚本是一种常见的自动化脚本工具&#xff0c;通过BAT文件来执行一系列DOS命令。通过BAT脚本&#xff0c;我们可以轻松地控制进程的启动和停止。 一、进程停止 在BAT脚本中&#xff0c;要停止一个进程&…

如何设计一个数模混合芯片?

一、如何设计一个数模混合芯片&#xff1f; 根据需求功能设计数模混合芯片的架构是一个多步骤的过程。以下是一些关键步骤和考虑因素&#xff1a; 需求分析&#xff1a;首先&#xff0c;需要明确数模混合芯片的功能需求。这包括确定模拟电路和数字电路的具体需求&#xff0c;以…

推荐几个不错的技术管理的视频讲座

推荐几个不错的技术管理的视频讲座&#xff0c;均来自DEEPLUS公众号组织的线上视频讲座&#xff0c;地址如下&#xff1a; 1&#xff09; 技术管理者如何搭班子、带队伍&#xff1f;技术管理者如何搭班子、带队伍&#xff1f;_哔哩哔哩_bilibili 2&#xff09; 从技术转管理后&…

Stable Diffusion 3 发布,AI生图效果,再次到达全新里程碑!

AI生图效果&#xff0c;再次到达全新里程碑&#xff01; Prompt&#xff1a;Epic anime artwork of a wizard atop a mountain at night casting a cosmic spell into the dark sky that says "Stable Diffusion 3" made out of colorful energy 提示&#xff08;意译…

靡语IT:JavaScript函数

目录 一、基本概念 二、函数的声明和调用&#xff1a; 1、创建函数&#xff1a; ​编辑 2 、函数调用&#xff1a; 3、函数参数&#xff1a; 三、全局变量和局部变量 1、局部JavaScript 变量 2 、全局 JavaScript 变量 四、arguments 对象: 五、return 作用 六、嵌…

172基于matlab的MPPT智能算法

基于matlab的MPPT智能算法&#xff0c;通过细菌觅食进行优化。算法引入了趋向性操作&#xff0c;用以进行局部范围内的最优寻找&#xff1b;引入了复制操作&#xff0c;用以避免种群更新盲目随机性&#xff0c;加快了算法的收敛速度&#xff1b;引入了迁徙操作用以避免算法陷入…

【深度学习笔记】卷积神经网络——填充和步幅

填充和步幅 在前面的例子 fig_correlation中&#xff0c;输入的高度和宽度都为 3 3 3&#xff0c;卷积核的高度和宽度都为 2 2 2&#xff0c;生成的输出表征的维数为 2 2 2\times2 22。 正如我们在 sec_conv_layer中所概括的那样&#xff0c;假设输入形状为 n h n w n_h\tim…

【Flink精讲】Flink组件通信

主要指三个进程中的通讯 CliFrontendYarnJobClusterEntrypointTaskExecutorRunner Flink内部节点之间的通讯使用Akka&#xff0c;比如JobManager和TaskManager之间。而operator之间的数据传输是利用Netty。 RPC是统称&#xff0c;Akka&#xff0c;Netty是实现 Akka与Ac…

Window系统上通过SSH搭建git服务器

Window系统上通过SSH搭建git服务器 1. OpenSSH安装&#xff08;如果已安装&#xff0c;请忽略&#xff09; 打开“设置”&#xff0c;选择“系统”&#xff0c;然后选择“可选功能”。 扫描列表&#xff0c;查看是否已安装 OpenSSH。 如果未安装&#xff0c;请在页面顶部选择…

Java实现毕业生追踪系统 JAVA+Vue+SpringBoot+MySQL

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 登陆注册模块2.2 学生基本配置模块2.3 就业状况模块2.4 学历深造模块2.5 信息汇总分析模块2.6 校友论坛模块 三、系统设计3.1 用例设计3.2 实体设计 四、系统展示五、核心代码5.1 查询我的就业状况5.2 初始化就业状况5.…

[算法沉淀记录] 排序算法 —— 希尔排序

排序算法 —— 希尔排序 算法介绍 希尔排序&#xff08;Shell Sort&#xff09;是一种基于插入排序的算法&#xff0c;由Donald Shell于1959年提出。希尔排序的基本思想是将待排序的序列划分成若干个子序列&#xff0c;分别进行插入排序&#xff0c;待整个序列中的记录基本有…

php对接抖音小程序授权登录

后端代码 /** * 抖音授权 获取openid * param Request $request * return Json*/ public function code2Session(Request $request): Json {$param $request->param();$config config(douyinconfig.douyin);$arr [appid > $config[appid], //抖音appidsecret > $co…