当前位置: 首页 > news >正文

TinyWebserver的复现与改进(6):定时器处理非活动连接

如果客户端长时间没有动作,会占用了许多连接资源,严重影响服务器的性能。因此需要通过实现一个服务器定时器,处理这种非活跃连接,释放连接资源。

定时器处理流程

image-20240816210048538

  1. SIGALARM触发:整个流程开始于一个 SIGALARM 信号,该信号每5秒会触发一次。
  2. 发送消息:当SIGALARM触发时,会进入 sig_handler 中断服务函数中,在该函数中,会向管道写端发送数据
  3. Epoll轮询:管道读端接收到消息后,使用 epoll 模型对所有的 socket(特别是管道读端 pipefd[0] )连接进行轮询。由于管道读端有数据,会将 timeout ➡ true
  4. Tick计时器:当 timeout 为 true 时,会进入tick 函数中检查连接是否超时,由于定时器的数据结构是一个链表,所以这个过程实际上是链表的查询操作
  5. 超时处理:当链表查询到客户端超时连接时,服务器会调用 cb_func 回调函数来断开这些连接。在这个函数中,服务器可能会执行一些必要的清理工作,比如从 epoll 监视的 socket 列表中删除超时的 sockfd,以及删除定时器。
  6. 重定时:当查询到各个客户端都不超时或者执行完 cb_func 时,会重新给 SIGALARM 信号 定时5s,然后等待下一次触发

定时器的实现

struct client_data
{sockaddr_in address;int sockfd;util_timer *timer;
};// 定时器类
class util_timer{
public:// 构造函数util_timer(): prev(NULL), next(NULL) {}public:time_t expire; // 任务超时时间,这里是使用绝对时间void (*cb_func)(client_data*);  // 任务回调函数, 回调函数处理的客户数据,由定时器的执行者传递给回调函数client_data* user_data;util_timer* prev;util_timer* next;
};

定时器实际上是链表的一个节点,里面存着任务超时时间,这里是使用绝对时间,以及任务的回调函数(cb_func),客户端数据使用的是一个额外的结构体,里面有客户端的地址族、sockfd、以及下一个定时器的指针。(当然也可以用http_conn这个数据类,但比较麻烦)

每个客户端都会产生一个定时器结点,它会存储在一个定时器链表中。

// 定时器链表, 是升序的双向链表, 带有头节点和尾节点
class sort_timer_lst{
public:sort_timer_lst();// 析构函数~sort_timer_lst();// 将目标定时器放在链表中void add_timer(util_timer* timer);// 将定时器 timer 从链表中删除void del_timer(util_timer* timer);// 当某个定时任务发生变化, 调整该定时器在链表中的位置,此函数只考虑定时时间延长的情况,即定时器往链表的后面移动void adjust_timer(util_timer* timer);/* SIGALARM每触发一次就在信号处理函数执行一次tick函数, 以处理链表上的到期任务 */void tick();util_timer* head;util_timer* tail;
private:// 将目标定时器 timer 添加到节点 lst_head 之后的部分链表中void 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->expire 是最大的,则插入到末尾if(!tmp){prev->next = timer;timer->prev = prev;timer->next = NULL;tail = timer;}}
};

这个链表类函数的增删改查都与链表的差不多,对此不再赘述

具体函数说明

sig_handler

// 信号的中断处理函数
void timer_sig_handler(int sig)
{int save_errno = errno;int msg = sig;send(pipefd[1], (char*)&msg, 1, 0);errno = save_errno;
}
  • pipefd[0] 对应的是管道的读端,pipefd[1] 对应的是管道的写端

这个函数是向管道写端写入信号(alarm)的值

cb_func

// 定时器回调函数, 从epoll上删除sockfd
void cb_func(client_data *user_data)
{printf("close fd : %d\n", user_data->sockfd);epoll_ctl(epollfd, EPOLL_CTL_DEL, user_data->sockfd, 0);assert(&user_data);close(user_data->sockfd);
}

初始化

// 信号的初始化
void timer_sig_init()
{// 创建int ret = socketpair(AF_UNIX, SOCK_STREAM, 0, pipefd);assert(ret != -1);setnonblocking(pipefd[1]);addfd(epollfd, pipefd[0], false);addsig(SIGALRM, timer_sig_handler);addsig(SIGTERM, timer_sig_handler);
}
  • socketpair 函数用于创建一个全双工的、相互连接的、无名的套接字对。这两个套接字就像是同一个管道的两端,但它们是网络通信的范畴,而不是进程间管道通信的范畴。(简单来说就是模拟成管道通信)
  • 在默认情况下,当使用 writesend 函数向管道写入数据时,如果管道的读端缓冲区已满,写操作将被阻塞,直到有空间可用。因此我们需要将 pipefd[1] 设置为非堵塞,防止服务器卡在某一个地方

将新客户端添加到链表中

//初始化client_data数据
//创建定时器,设置回调函数和超时时间,绑定用户数据,将定时器添加到链表中
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((sockfd == pipefd[0])&&(events[i].events & EPOLLIN))
{int sig;char signals[1024];ret = recv(pipefd[0], signals, sizeof(signals), 0);if (ret == -1){continue;}else if (ret == 0){continue;}else{for (int i = 0; i < ret; ++i){switch (signals[i]){case SIGALRM:{timeout = true;break;}case SIGTERM:{stop_server = true;}}}}
}

当 pipe 读端有数据时,会进入这个if循环中,然后使用 recv 读取”管道“的数据,再依次判断

系列文章

GitHub - yzfzzz/MyWebServer: Linux高并发服务器项目,参考了TinyWebServer,将在此基础上进行性能改进与功能增加。为方便读者学习,附带详细注释和博客!

webserver

TinyWebserver的复现与改进(1):服务器环境的搭建与测试-CSDN博客

TinyWebserver的复现与改进(2):项目的整体框架-CSDN博客

TinyWebserver的复现与改进(3):线程同步机制类封装及线程池实现-CSDN博客

TinyWebserver的复现与改进(4):主线程的具体实现-CSDN博客

TinyWebserver的复现与改进(5):HTTP报文的解析与响应-CSDN博客

完整代码

main.cpp

#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include "threadpool.hpp"
#include "locker.h"
#include "http_conn.h"
#include <signal.h>
#include <assert.h>  
#include "lst_timer.h"
#define MAX_FD 65536  // 最大的文件描述符
#define MAX_EVENT_NUMBER 10000 // 监听的最大事件数
#define TIMESLOT 5
//设置定时器相关参数
static int pipefd[2];
static sort_timer_lst timer_lst;
static int epollfd = 0;
// 信号的中断处理函数
void timer_sig_handler(int sig)
{int save_errno = errno;int msg = sig;send(pipefd[1], (char*)&msg, 1, 0);errno = save_errno;
}
// 定时器回调函数, 从epoll上删除sockfd
void cb_func(client_data *user_data)
{printf("close fd : %d\n", user_data->sockfd);epoll_ctl(epollfd, EPOLL_CTL_DEL, user_data->sockfd, 0);assert(&user_data);close(user_data->sockfd);
}
void timer_handler()
{timer_lst.tick();alarm(TIMESLOT);
}
/* 函数指针的声明: 类型说明符 (*函数名) (参数)void(handler)(int) 声明了一个名为 handler 的函数指针,它指向一个接受一个 int 参数并返回 void 的函数
*/
void addsig(int sig, void(handler)(int), bool restart = false)
{// sigaction的输入参数struct sigaction sa;// 指定sa内存区域的前n个字节都设置为某个特定的值('\0'),用于对新分配的内存进行初始化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 timer_sig_init()
{int ret = socketpair(AF_UNIX, SOCK_STREAM, 0, pipefd);assert(ret != -1);setnonblocking(pipefd[1]);addfd(epollfd, pipefd[0], false);addsig(SIGALRM, timer_sig_handler);addsig(SIGTERM, timer_sig_handler);
}int main(int argc, char* argv[])
{if(argc <= 1){// 要求输入格式为 ./a.out 10000  其中10000是端口号 printf("usage: %s port_number\n", basename(argv[0]));return 1;}// 端口号 string -> intint port = atoi(argv[1]);// 如果向一个没有读端的管道写数据,不用终止进程addsig(SIGPIPE, SIG_IGN);   // SIG_IGN: 忽略信号,这里指的是忽略信号 ·  SIGPIPE// 定义一个线程池指针threadpool<http_conn>* pool = NULL;try {// 开辟一个线程池pool = new threadpool<http_conn>;}catch(...){// 若异常则退出return 1;}// 开辟一块连续的http_conn数组,保存所有正在连接的客户端信息http_conn* users = new http_conn[MAX_FD];client_data *users_timer = new client_data[MAX_FD];// 设置监听int listenfd = socket(AF_INET, SOCK_STREAM, 0);int ret = 0;struct sockaddr_in address;address.sin_addr.s_addr = INADDR_ANY;address.sin_family = AF_INET;address.sin_port = htons(port);// 设置端口复用int reuse = 1;setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));// 绑定ret = bind(listenfd, (struct sockaddr*)&address, sizeof(address));if(ret == -1){perror("bind");exit(-1);}// 开始监听ret = listen(listenfd, 5);if(ret == -1){perror("listen");exit(-1);}// 将listend添加到epoll模型中epoll_event events[MAX_EVENT_NUMBER];epollfd = epoll_create(5);addfd(epollfd, listenfd, false);http_conn::m_epollfd = epollfd;timer_sig_init();bool timeout = false;bool stop_server = false;alarm(TIMESLOT);while(!stop_server){// epoll轮询,等待有数据发送int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER,-1);if((number < 0) && (errno != EINTR)){printf("epoll failture\n");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_addresslen = sizeof(client_address);int connfd = accept(listenfd, (struct sockaddr*)&client_address, &client_addresslen);if(connfd < 0){printf("errno is %d\n", errno);continue;}if(http_conn::m_user_count >= MAX_FD){close(connfd);continue;}users[connfd].init(connfd, client_address);//初始化client_data数据//创建定时器,设置回调函数和超时时间,绑定用户数据,将定时器添加到链表中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((sockfd == pipefd[0])&&(events[i].events & EPOLLIN)){int sig;char signals[1024];ret = recv(pipefd[0], signals, sizeof(signals), 0);if (ret == -1){continue;}else if (ret == 0){continue;}else{for (int i = 0; i < ret; ++i){switch (signals[i]){case SIGALRM:{timeout = true;break;}case SIGTERM:{stop_server = true;}}}}}// 若对方异常端开或错误else if(events[i].events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR)){users[sockfd].close_conn();}// 有读事件发生(可读)else if(events[i].events & EPOLLIN){util_timer *timer = users_timer[sockfd].timer;// 有读事件发生if(users[sockfd].read()){// 读的到数据pool->append(users+sockfd);//若有数据传输,则将定时器往后延迟3个单位//并对新的定时器在链表上的位置进行调整if (timer){time_t cur = time(NULL);timer->expire = cur + 3 * TIMESLOT;timer_lst.adjust_timer(timer);}}else{printf("Read Fail!\n");// 读不到数据timer->cb_func(&users_timer[sockfd]);if (timer){timer_lst.del_timer(timer);}// users[sockfd].close_conn();}}// 有写事件发生(可写)else if(events[i].events & EPOLLOUT){util_timer *timer = users_timer[sockfd].timer;if(users[sockfd].write()){if (timer){time_t cur = time(NULL);timer->expire = cur + 3 * TIMESLOT;timer_lst.adjust_timer(timer);}}else{printf("Write Fail!\n");timer->cb_func(&users_timer[sockfd]);if (timer){timer_lst.del_timer(timer);}// users[sockfd].close_conn();}}}if (timeout){timer_handler();timeout = false;}}close(epollfd);close(listenfd);close(pipefd[1]);close(pipefd[0]);delete [] users;delete[] users_timer;delete pool;return 0;
}

lst_timer.h

#pragma once
#include <stdio.h>
#include <time.h>
#include <arpa/inet.h>
#define BUFFER_SIZE 64class util_timer;
struct client_data
{sockaddr_in address;int sockfd;util_timer *timer;
};// 定时器类
class util_timer{
public:// 构造函数util_timer(): prev(NULL), next(NULL) {}public: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);// 将定时器 timer 从链表中删除void del_timer(util_timer* timer);// 当某个定时任务发生变化, 调整该定时器在链表中的位置,此函数只考虑定时时间延长的情况,即定时器往链表的后面移动void adjust_timer(util_timer* timer);/* SIGALARM每触发一次就在信号处理函数执行一次tick函数, 以处理链表上的到期任务 */void tick();util_timer* head;util_timer* tail;
private:// 将目标定时器 timer 添加到节点 lst_head 之后的部分链表中void 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->expire 是最大的,则插入到末尾if(!tmp){prev->next = timer;timer->prev = prev;timer->next = NULL;tail = timer;}}
};

lst_timer.cpp

#include "lst_timer.h"
#include <signal.h>
#include <errno.h>
#include <cassert>
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;}/* 如果目标定时器的超时时间小于当前链表中所有定时器的超时时间,则把该定时器插入链表头部,作为链表新的头节点,否则就需要调用重载函数 add_timer(),把它插入链表中合适的位置,以保证链表的升序特性 */if(timer->expire < head->expire){timer->next = head;head->prev = timer;head = timer;return;}add_timer(timer, head);
}// 将定时器 timer 从链表中删除
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::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);}
}/* SIGALARM每触发一次就在信号处理函数执行一次tick函数, 以处理链表上的到期任务 */
void sort_timer_lst::tick()
{if(!head){return;}printf( "Timer Tick\n" );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;printf("close client request\n");}
}

相关文章:

TinyWebserver的复现与改进(6):定时器处理非活动连接

如果客户端长时间没有动作&#xff0c;会占用了许多连接资源&#xff0c;严重影响服务器的性能。因此需要通过实现一个服务器定时器&#xff0c;处理这种非活跃连接&#xff0c;释放连接资源。 定时器处理流程 SIGALARM触发&#xff1a;整个流程开始于一个 SIGALARM 信号&…...

ThinkPHP5 5.0.23 远程代码执行漏洞

目录 1、启动环境 2、漏洞利用 3、更改传参方式 4、修改参数 5、发送数据 1、启动环境 docker-compose up -d 2、访问靶机ip端口号8080 2、漏洞利用 使用burpsuite抓包软件抓包 3、更改传参方式 将 GET传参改为POST传参 4、修改参数 url参数 /index.php?scaptcha post参…...

C++鼠标键盘操作自动化

C鼠标键盘操作自动化 #pragma once #include <Windows.h> enum KEYS{A 65,W87,S83,D68,SHIFTVK_LSHIFT,ALT18,Tilde 126,//~TABVK_TAB,B66,SPACEVK_SPACE,ESCVK_ESCAPE,Q81 }; enum MOUSE {ML,MW,MR//左&#xff0c;中&#xff0c;右 }; class simulator//模拟器 { pu…...

多个主流Python GUI库全面解析,助你用Python轻松构建精美界面

Python 作为一门易学易用的编程语言&#xff0c;在各个领域都拥有广泛的应用。而 GUI (Graphical User Interface) 编程更是让 Python 变得更加灵活&#xff0c;可以帮助我们创建各种各样的桌面应用&#xff0c;为用户提供直观的交互体验。本文将介绍几个Python GUI 编程中常用…...

Kotlin学习-01创建kotlin学习

安装idea https://www.jetbrains.com/zh-cn/ 创建项目 选择kotlin 修改Main.kt fun main() {print("Hello World!") }运行...

Java、python、php版的企业单位考勤打卡管理系统的设计与实现(源码、调试、LW、开题、PPT)

&#x1f495;&#x1f495;作者&#xff1a;计算机源码社 &#x1f495;&#x1f495;个人简介&#xff1a;本人 八年开发经验&#xff0c;擅长Java、Python、PHP、.NET、Node.js、Android、微信小程序、爬虫、大数据、机器学习等&#xff0c;大家有这一块的问题可以一起交流&…...

在IntelliJ IDEA中使用Git推送项目

去gitee网站注册用户 gitee网站地址:https://gitee.com/ github网站地址:https://github.com/ 一、创建仓库 以下以gitee为例进行介绍&#xff0c;github操作雷同。 1、创建仓库 点击页面右上方的"“并选择"创建仓库” 2、设置仓库相关信息 首先输入仓库名&…...

CNN代码实战

CNN的原理 从 DNN 到 CNN &#xff08;1&#xff09;卷积层与汇聚 ⚫ 深度神经网络 DNN 中&#xff0c;相邻层的所有神经元之间都有连接&#xff0c;这叫全连接&#xff1b;卷积神经网络 CNN 中&#xff0c;新增了卷积层&#xff08;Convolution&#xff09;与汇聚&#xff08…...

迁移学习代码复现

一、前言 说来可能令人难以置信,迁移学习技术在实践中是非常简单的,我们仅需要保留训练好的神经网络整体或者部分网络,再在使用迁移学习的情况下把保留的模型重新加载到内存中,就完成了迁移的过程。之后,我们就可以像训练普通神经网络那样训练迁移过来的神经网络了。 我们…...

Elasticsearch(ES)常用命令

常用运维命令 一、基本命令1.1、查看集群的健康状态1.2、查看节点信息1.3、查看索引列表1.4、创建索引1.5、删除索引1.6、关闭索引1.7、打开索引1.8、查看集群资源使用情况&#xff08;各个节点的状态&#xff0c;包括磁盘&#xff0c;heap&#xff0c;ram的使用情况&#xff0…...

C/C++ 不定参函数

C语言不定参函数 函数用法总结 Va_list 作用&#xff1a;类型定义&#xff0c;生命一个变量&#xff0c;该变量被用来访问传递给不定参函数的可变参数列表用法&#xff1a;供后续函数进调用&#xff0c;通过该变量访问参数列表 typedefchar* va_list; va_start 作用&#xff…...

C语言——函数专题

1.概念 在C语言中引入函数的概念&#xff0c;有些翻译为子程序。C语言中的函数就是一个完成某项特定任务的一小段代码&#xff0c;这个代码是有特殊的写法和调用方法的。一般我们可以分为两种函数&#xff1a;库函数和自定义函数。 2.库函数 C语言国际标准ANSIC规定了一些常…...

springboot打可执行jar包

1. pom文件如下 <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><m…...

【SQL】科目种类

目录 题目 分析 代码 题目 表: Teacher ------------------- | Column Name | Type | ------------------- | teacher_id | int | | subject_id | int | | dept_id | int | ------------------- 在 SQL 中&#xff0c;(subject_id, dept_id) 是该表的主键。 该表…...

【深度学习】【语音】TTS,最新TTS模型概览,扩散模型TTS,MeloTTS、StyleTTS2、Matcha-TTS

文章目录 基础介绍对比基础介绍 MeloTTS: MeloTTS 是 MyShell.ai 开发的一个多语言语音合成模型,支持包括英语、西班牙语、法语、中文、日语和韩语等多种语言。它以高质量的语音合成为特色,尤其擅长处理中英混合内容。该模型优化了在 CPU 上的实时推理能力,使其在多种应用场…...

【论文笔记】LION: Linear Group RNN for 3D Object Detection in Point Clouds

原文链接&#xff1a;https://arxiv.org/abs/2407.18232 简介&#xff1a;Transformer在3D点云感知任务中有二次复杂度&#xff0c;难以进行长距离关系建模。线性RNN则计算复杂度较低&#xff0c;适合进行长距离关系建模。本文提出基于窗口的网络线性组RNN&#xff08;即对分组…...

打造高可用集群的基石:深度解析Keepalived实践与优化

高可用集群 集群类型 集群类型主要分为负载均衡集群&#xff08;LB&#xff09;、高可用集群&#xff08;HA&#xff09;和高性能计算集群&#xff08;HPC&#xff09;三大类。每种集群类型都有其特定的应用场景和优势。 1. 负载均衡集群&#xff08;LB&#xff09; 负载均衡集…...

Web大学生网页作业成品——环保主题介绍网页网站设计与实现(HTML+CSS)(5个页面)

&#x1f389;&#x1f389;&#x1f389; 常见网页设计作业题材有**汽车、环保、明星、文化、国家、抗疫、景点、人物、体育、植物、公益、图书、节日、游戏、商城、旅游、家乡、学校、电影、动漫、非遗、动物、个人、企业、美食、婚纱、其他**等网页设计题目, 可满足大学生网…...

Qt登录窗口设计

widget.h #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QIcon> //图标类 #include <QPushButton> #include <QLineEdit> //行编辑 #include <QLabel> #include <QTextEdit> #include <QMovie>class Widge…...

探索数据矿藏:我的AI大模型与数据挖掘实战经验分享

&#x1f680; 探索数据矿藏&#xff1a;我的AI大模型与数据挖掘实战经验分享 &#x1f496; 前言&#xff1a;数据的金矿&#xff0c;AI的翅膀 在人工智能的浪潮中&#xff0c;我有幸作为项目负责人&#xff0c;带领团队深入挖掘数据的潜力&#xff0c;利用AI大模型的力量&am…...

OpenLayers 可视化之热力图

注&#xff1a;当前使用的是 ol 5.3.0 版本&#xff0c;天地图使用的key请到天地图官网申请&#xff0c;并替换为自己的key 热力图&#xff08;Heatmap&#xff09;又叫热点图&#xff0c;是一种通过特殊高亮显示事物密度分布、变化趋势的数据可视化技术。采用颜色的深浅来显示…...

MFC内存泄露

1、泄露代码示例 void X::SetApplicationBtn() {CMFCRibbonApplicationButton* pBtn GetApplicationButton();// 获取 Ribbon Bar 指针// 创建自定义按钮CCustomRibbonAppButton* pCustomButton new CCustomRibbonAppButton();pCustomButton->SetImage(IDB_BITMAP_Jdp26)…...

STM32+rt-thread判断是否联网

一、根据NETDEV_FLAG_INTERNET_UP位判断 static bool is_conncected(void) {struct netdev *dev RT_NULL;dev netdev_get_first_by_flags(NETDEV_FLAG_INTERNET_UP);if (dev RT_NULL){printf("wait netdev internet up...");return false;}else{printf("loc…...

UE5 学习系列(三)创建和移动物体

这篇博客是该系列的第三篇&#xff0c;是在之前两篇博客的基础上展开&#xff0c;主要介绍如何在操作界面中创建和拖动物体&#xff0c;这篇博客跟随的视频链接如下&#xff1a; B 站视频&#xff1a;s03-创建和移动物体 如果你不打算开之前的博客并且对UE5 比较熟的话按照以…...

Python爬虫(二):爬虫完整流程

爬虫完整流程详解&#xff08;7大核心步骤实战技巧&#xff09; 一、爬虫完整工作流程 以下是爬虫开发的完整流程&#xff0c;我将结合具体技术点和实战经验展开说明&#xff1a; 1. 目标分析与前期准备 网站技术分析&#xff1a; 使用浏览器开发者工具&#xff08;F12&…...

JVM 内存结构 详解

内存结构 运行时数据区&#xff1a; Java虚拟机在运行Java程序过程中管理的内存区域。 程序计数器&#xff1a; ​ 线程私有&#xff0c;程序控制流的指示器&#xff0c;分支、循环、跳转、异常处理、线程恢复等基础功能都依赖这个计数器完成。 ​ 每个线程都有一个程序计数…...

【Android】Android 开发 ADB 常用指令

查看当前连接的设备 adb devices 连接设备 adb connect 设备IP 断开已连接的设备 adb disconnect 设备IP 安装应用 adb install 安装包的路径 卸载应用 adb uninstall 应用包名 查看已安装的应用包名 adb shell pm list packages 查看已安装的第三方应用包名 adb shell pm list…...

第7篇:中间件全链路监控与 SQL 性能分析实践

7.1 章节导读 在构建数据库中间件的过程中&#xff0c;可观测性 和 性能分析 是保障系统稳定性与可维护性的核心能力。 特别是在复杂分布式场景中&#xff0c;必须做到&#xff1a; &#x1f50d; 追踪每一条 SQL 的生命周期&#xff08;从入口到数据库执行&#xff09;&#…...

SQL Server 触发器调用存储过程实现发送 HTTP 请求

文章目录 需求分析解决第 1 步:前置条件,启用 OLE 自动化方式 1:使用 SQL 实现启用 OLE 自动化方式 2:Sql Server 2005启动OLE自动化方式 3:Sql Server 2008启动OLE自动化第 2 步:创建存储过程第 3 步:创建触发器扩展 - 如何调试?第 1 步:登录 SQL Server 2008第 2 步…...

k8s从入门到放弃之HPA控制器

k8s从入门到放弃之HPA控制器 Kubernetes中的Horizontal Pod Autoscaler (HPA)控制器是一种用于自动扩展部署、副本集或复制控制器中Pod数量的机制。它可以根据观察到的CPU利用率&#xff08;或其他自定义指标&#xff09;来调整这些对象的规模&#xff0c;从而帮助应用程序在负…...