C++ Webserver从零开始:代码书写(十二)——双向链表处理非活动连接

前言

大家好,如题,今天我们来写定时器的代码。更正一下上一章的结束语哈哈哈,因为我发现相比于线程池,定时器类是相对底层的东西。不知道大家有没有玩过有建筑系统的游戏,比如mc,幻兽帕鲁这些,在我看来写一个项目和搭一个房子有一些共通之处,而根据我的经验,就是如果你建房子不把一层搭完,就直接往上盖二层,一旦你突然想在一层内上再划一片区域用来修新的的建筑,那么你新区域与原本的区域之间的一些联动就会比较难设计。而做项目也是如此,等把线程做完再回来写定时器,虽然也不是不行(我相信会有一些人是这也做,我并没有否定这种方式),但我还是更习惯把所有的底层东西写好,再一点一点往上搭建。言归正传,我们今天把定时器这一章搞定


概述

在具体开始写代码之前,让我们来了解一下有关定时器的基础性知识。首先我们知道,我们设计定时器的目的是为了让他去处理非活动连接。因为我们的系统资源是有限的,当一个客户连接长时间不响应时,我们就要考虑关闭这个连接,把资源分给正在使用的客户,以此来提高服务器的运行效率。这一部分知识我们在基础知识里有讲过,忘了的同学可以在回顾一遍:

C++ Webserver从零开始:基础知识(六)——定时器-CSDN博客

本章内容不多,但是各个函数之间的调用有一些复杂,为此我画了下面这副拓扑图帮助大家理解处理非活动连接的逻辑。我建议大家可以把图保存下来,写代码的时候写到某一个部分不了解它的作用时,就用手机打开来看看,这也会对整个代码的框架有一个良好的认知。

图中绿色的图标①②③④⑤是我推荐的理解顺序,可以方便大家一个模块一个模块地理解它地运行逻辑。其中我们实现的是图中右半边的部分,左边地eventLoop的函数我们要到更高层去完成,如果如果大家想看,可以看看上面的链接,我写了一个eventLoop的伪代码,其中低优先级的实现思想在我的伪代码有所体现。

因为定时器和处理非活动连接的相关知识在上面的连接里已经写了,为了避免文章臃肿,我们就不赘述了,直接来看代码。


定时器类

首先,我们先定义一个定时器类,定时器类里我们封装连接资源,定时事件指针,以及超时时间。

连接资源包括:套接字地址,文件描述符和定时器

超时时间:我们设定为绝对时间,即 :浏览器和服务器连接的时刻 + 固定时间TIMESLOT

然后再定义两个指针分别指向上升链表的前后定时器

/*util_timer前置声明,因为client_data使用了util_timer类*/
class util_timer;/*用户数据结构体(连接资源)*/
struct client_data{sockaddr_in address;//客户端的socket地址int sockfd;         //socket文件描述符util_timer *timer;  //定时器
};/*定时器类*/
class util_timer{
public:util_timer():prev(nullptr), next(nullptr){}public:time_t expire;                  //超时时间/*回调函数声明:声明一个返回值为空的函数指针cb_func,传入clent_data指针作为函数参数*/void (*cb_func)(client_data *); //回调函数指针client_data *user_data;         //连接资源util_timer *prev;               //前向定时器util_timer *next;               //后继定时器
};


上升链表类

然后我们再设计定时器容器,这里我们选择的是升序的双向链表,当然,我们也有更高级的定时器容器比如时间轮或者时间堆,如果学有余力,我们后面等项目完成了可以来试试用时间堆来设计定时器容器。

具体到双向链表的代码,除了tick()函数以外就是一个非常简单的数据结构的双向链表的设计了,就不赘述,大家直接看代码理解。

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;
};
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;}add_timer(timer, head);
}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;
}/*按升序插入已lst_head为头节点的lst_timer_lst链表中*/
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) {if (timer->expire < tmp->expire) {prev->next = timer;timer->next = tmp;tmp->prev = timer;timer->prev = prev;break;}prev = tmp;tmp = tmp->next;}/*timer需要放到尾节点*/if (!tmp) {prev->next = timer;timer->prev = prev;timer->next = NULL;tail = timer;}
}


现在我们来实现定时器容器内比较关键的一个函数tick函数,也称为心搏函数。所谓tick()函数即在主循环中每经过一段时间,就调用一次tick,而tick就会完成一定的功能。在我们这里很显然它的功能就是将过期的定时器从链表中删除。

而因为我们设计的是升序链表,所以只需从头节点一路遍历并删除定时器,直到遇到一个未过期的定时器时退出循环即可。

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


抽象工具类——Utils(实用程序)

到了这里为止,我们定时器的设计基本完成了。接下来我们要来设计一些方法来合理的使用它。

首先我们需要思考一个问题是:我们以前写的传统的代码运行逻辑,是从上到下一行一行按顺序运行的,我们不可能提前预判一些不稳定出现的事件然后用代码处理它,我们只能是等不稳定的事件出现后,让他通过某种方式通知我,然后我在通过一些预案(即处理这些事件的代码)来处理该事件。

那么这里的某种方式是什么呢?

我们通常使用信号,这里的逻辑是,我们每过一段时间就给主循环一个信号,主循环收到信号就记录下来,等其他IO事件完成之后,就调用tick()处理非活动连接。所以接下来我们的需求是如何设置信号,如何发送信号,以及了解一下主循环如何接收信号。


我们在lit_timer文件中再创建一个实用程序类Utils来使用定时器,并完成我刚刚说的那些需求

class Utils{
public:Utils(){}~Utils(){}void init(int timeslot);/*对文件描述符设置非阻塞*/int setnonblocking(int fd);/*将内核事件表注册读事件,ET模式,选择开启EPOLLONESHOT*/void addfd(int epollfd, int fd, bool one_shot, int TRIGMode);/*信号处理函数*/static void sig_handler(int sig);/*设置信号处理函数   这里第二个参数void(handler)(int)等价于void(*handler)(int),再作函数参数时,后者的*可以省略*/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;static int u_epollfd;int m_TIMESLOT;
};

可以看到我们在类中定义了两个静态成员变量,分别是:

static int *u_pipefd;

static int u_epollfd;

它们的作用说起来比较不好理解,大家可以看看我的那张拓扑图就好


这里我再用线性的方式给大家梳理一遍处理非活动连接的流程,帮助大家理解Utils类各函数的作用,大家也可以结合拓扑图来看:

  1. 主线程初始化Utils(包括使用pipe接收pipefd,用epollfd接收u_epollfd
  2. 主线程调用addfd将pipe管道与epollfd相关联
  3. 主线程调用addsig将目标信号(SIGALRM SIGTERM)加入监听的信号集
  4. 主线程循环eventLoop()开始,服务器开始运行
  5. 多个客户连接长久未响应
  6. 经过TIMESLOT, 触发信号,主循环收到信号
  7. 主循环调用tick()处理非活动连接

到此为止,我们就经历了处理非活动连接的全过程,接下来我们来实现上面使用的这些函数

void Utils::init(int timeslot) {m_TIMESLOT = timeslot;
}int Utils::setnonblocking(int fd) {/*FILE Control函数用于对文件描述符执行各种操作*/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;/*开启边缘触发模式*/if (TRIGMode == 1) {event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;} else {event.events = EPOLLIN | EPOLLRDHUP;}if (one_shot) {event.events |= EPOLLONESHOT;}epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);setnonblocking(fd);
}/*信号处理函数*/
void Utils::sig_handler(int sig) {/*异步信号处理环境中,信号处理函数可能发生中断并在其他上下文执行,而errno是个全局遍量,若在信号处理函数中发生错误可能会影响其他调用信号处理函数的线程,所以调用完后恢复原值可以避免这种错误*/int save_errno = errno;int msg = sig;send(u_pipefd[1], (char *)&msg, 1, 0);errno = save_errno;
}/*设置信号函数*/
void Utils::addsig(int sig, void(handler)(int), bool restart) {/*创建sigaction结构体*/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();/*设置定时器,经过m_TIMESLOT秒后发送信号*/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;


定时器回调函数

上面的内容已经基本上阐述了处理非活动连接的全部流程,而回调函数在tick()函数内调用。负责把定时器对应的文件描述符关闭。

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


结束语

好了,到此为止我们完成了定时器与定时器链表的设计,实现了处理非活动连接的功能,下一章我们真的做线程池了

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

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

相关文章

芯片开发erp软件有哪些优势?

随着科技的飞速发展&#xff0c;芯片开发行业正逐渐成为推动科技进步的关键力量。在这一领域中&#xff0c;企业资源规划(ERP)软件的应用正逐渐普及&#xff0c;为芯片开发企业带来了许多显著的优势。下面&#xff0c;我们将详细介绍芯片开发ERP软件的优势。 一、提升管理效率 …

蓝桥杯-答疑

原题链接&#xff1a;用户登录 答疑 题目描述 有 n 位同学同时找老师答疑。每位同学都预先估计了自己答疑的时间。 老师可以安排答疑的顺序&#xff0c;同学们要依次进入老师办公室答疑。一位同学答疑的过程如下 1.首先进入办公室&#xff0c;编号为 的同学需要 s&#xff0c;…

如何在本地部署密码管理软件bitwarden并结合cpolar实现远程同步

文章目录 1. 拉取Bitwarden镜像2. 运行Bitwarden镜像3. 本地访问4. 群晖安装Cpolar5. 配置公网地址6. 公网访问Bitwarden7. 固定公网地址8. 浏览器密码托管设置 Bitwarden是一个密码管理器应用程序&#xff0c;适用于在多个设备和浏览器之间同步密码。自建密码管理软件bitwarde…

数据安全之路:深入了解MySQL的行锁与表锁机制

欢迎来到我的博客&#xff0c;代码的世界里&#xff0c;每一行都是一个故事 数据安全之路&#xff1a;深入了解MySQL的行锁与表锁机制 前言基础innodb中锁与索引的关系如何避免表锁 前言 在当今数据密集的应用中&#xff0c;数据库锁成为了确保数据一致性和并发操作的关键工具…

【Spring MVC】处理器映射器:AbstractHandlerMethodMapping源码分析

目录 一、继承体系 二、HandlerMapping 三、AbstractHandlerMapping 四、AbstractHandlerMethodMapping 4.1 成员属性 4.1.1 MappingRegistry内部类 4.2 AbstractHandlerMethodMapping的初始化 4.3 getHandlerInternal()方法&#xff1a;根据当前的请求url&#xff0c;…

前端学习——JS学习

文章目录 1. 定义变量&#xff0c;关键字 var、let、const2. 定义变量&#xff0c;数据类型3. 数组变量的操作4. 对象的操作5. JSON 字符串 1. 定义变量&#xff0c;关键字 var、let、const 这里主要是对var、let做比较 /** 1. var存在变量提升、let不存在变量提升 **/ cons…

WordPress使用

WordPress功能菜单 仪表盘 可以查看网站基本信息和内容。 文章 用来管理文章内容&#xff0c;分类以及标签。编辑文章以及设置分类标签&#xff0c;分类和标签可以被添加到 外观-菜单 中。 分类名称自定义&#xff1b;别名为网页url链接中的一部分&#xff0c;最好别设置为中文…

自然语言处理(NLP)—— 神经网络自然语言处理(2)实际应用

本篇文章的第一部分是关于探索词嵌入&#xff08;word embedding&#xff09;向量空间。词嵌入是一种语言模型和文本表示技术&#xff0c;其中单词或短语从词汇表被映射到向量的高维空间中。通过这种方式&#xff0c;可以通过计算向量之间的距离来捕捉单词之间的语义关系。 1.…

8.9 矢量图层点要素热度图(Heatmap)使用

文章目录 前言热度图&#xff08;Heatmap&#xff09;QGis代码实现 总结 前言 本章介绍如何使用热度图&#xff08;Heatmap&#xff09;说明&#xff1a;文章中的示例代码均来自开源项目qgis_cpp_api_apps 热度图&#xff08;Heatmap&#xff09; 热度图以颜色代表点密度&…

python自带轻量级键值数据库shelve

使用python自带的shelve模块&#xff0c;可以作为轻量级的键值数据库&#xff0c;在使用时可以像字典一样使用&#xff1a; 使用shelve模块的流程如下&#xff1a; 示例程序 import pandas as pd import shelve import numpy as npdef main():_shelve_file "shelve_fi…

常见的音频与视频格式

本专栏是汇集了一些HTML常常被遗忘的知识&#xff0c;这里算是温故而知新&#xff0c;往往这些零碎的知识点&#xff0c;在你开发中能起到炸惊效果。我们每个人都没有过目不忘&#xff0c;过久不忘的本事&#xff0c;就让这一点点知识慢慢渗透你的脑海。 本专栏的风格是力求简洁…

office word保存pdf高质量设置

1 采用第三方pdf功能生成 分辨率越大质量越好

Nginx网络服务三-----(三方模块和内置变量)

1.验证模块 需要输入用户名和密码 我们要用htpasswd这个命令&#xff0c;先安装一下httpd 生成文件和用户 修改文件 访问页面 为什么找不到页面&#xff1f; 对应的路径下&#xff0c;没有这个文件 去创建文件 去虚拟机浏览器查看 有的页面不想被别人看到&#xff0c;可以做…

亚马逊测评 能让买家更快速的喜欢上你的产品,提高转化率

在当今的电子商务时代&#xff0c;亚马逊作为全球最大的在线零售商之一&#xff0c;已经成为了消费者购买各种商品的首选平台。然而&#xff0c;对于消费者来说&#xff0c;如何选择适合自己的产品成为了他们面临的一大难题。因此&#xff0c;本文将介绍亚马逊上如何让买家通过…

【python】0、超详细介绍:json、http

文章目录 一、json二、http2.1 json 读取 request 序列化 三、基本类型3.1 decimal 四、图像4.1 颜色格式转换 一、json import json f open(data.json) # open json file data json.load(f) # 读出 json object for i in data[emp_details]: # 取出一级属性 emp_details, …

代码随想录算法刷题训练营day23

代码随想录算法刷题训练营day23&#xff1a;LeetCode(669)修剪二叉搜索树、LeetCode(108)将有序数组转换为二叉搜索树、LeetCode(538)把二叉树转化为累加树 LeetCode(669)修剪二叉搜索树 题目 代码 /*** Definition for a binary tree node.* public class TreeNode {* …

月薪2W的软件测试工程师,到底是做什么的?

在生活中&#xff0c;我们常常会遇到以下几种窘迫时刻&#xff1a; 准备骑共享单车出行&#xff0c;却发现扫码开锁半天&#xff0c;车子都没有反应&#xff1b;手机导航打车&#xff0c;却发现地图定位偏差很大&#xff0c;司机总是跑错地方&#xff1b;买个水&#xff0c;却…

【牛客】2024牛客寒假算法基础集训营6ABCDEGHIJ

文章目录 A 宇宙的终结题目大意主要思路代码 B 爱恨的纠葛题目大意主要思路代码 C 心绪的解剖题目大意主要思路代码 D 友谊的套路题目大意主要思路代码 E 未来的预言题目大意主要思路代码 G 人生的起落题目大意主要思路代码 I 时空的交织题目大意主要思路代码 J 绝妙的平衡题目…

如何用GPT进行成像光谱遥感数据处理?

第一&#xff1a;遥感科学 从摄影侦察到卫星图像 遥感的基本原理 遥感的典型应用 第二&#xff1a;ChatGPT ChatGPT可以做什么&#xff1f; ChatGPT演示使用 ChatGPT的未来 第三&#xff1a;prompt 提示词 Prompt技巧&#xff08;大几岁&#xff09; 最好的原则和策…

音视频数字化(数字与模拟-电视)

上一篇文章【音视频数字化(数字与模拟-音频广播)】谈了音频的广播,这次我们聊电视系统,这是音频+视频的采集、传输、接收系统,相对比较复杂。 音频系统的广播是将声音转为电信号,再调制后发射出去,利用“共振”原理,收音机接收后解调,将音频信号还原再推动扬声器,我…