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

利用升序定时器链表处理非活动连接

参考自游双《Linux高性能服务器编程》

背景

服务器同常需要定期处理非活动连接:给客户发一个重连请求,或关闭该连接,或者其他。我们可以通过使用升序定时器链表处理非活动连接,下面的代码利用alarm函数周期性的触发SIGALRM信号,该信号的处理函数利用管道通知主循环执行定时器链表上的定时任务—关闭非活动连接。

实现代码

升序定时器链表

定时器通常包含两个成员:超时时间和任务回调函数。

有时还会包含回调函数被执行时需要传入的参数。

下方代码实现了一个简单的升序定时器链表,按照超时时间做升序排列。

// lst_timer.h
// 升序定时器链表
#ifndef LST_TIMER
#define LST_TIMER#include <time.h>
#define BUFFER_SIZE 64
class util_timer;// 用户数据结构
struct client_data
{sockaddr_in address;   // 客户端socket地址int sockfd;            // socket 文件描述符char buf[BUFFER_SIZE]; // 读缓冲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() : head(NULL), tail(NULL){};// 删除所有定时器~sort_timer_lst(){util_timer *tmp = head;while (tmp){head = tmp->next;delete tmp;tmp = head;}}// 将定时器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);}// 当某个定时任务发生变化时,调整对应的定时器的超时时间// 这个函数只考虑被调整的定时器的【超时时间的延长情况】,即该定时器要往链表尾部移动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->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;}// SIGALARM信号每次触发就在其信号处理函数中执行一次tick函数// 来处理链表上到期的任务。void 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;}}
private:// 重载的辅助函数// 被add_timer和adjust_timer调用// 功能:将目标定时器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;}if(!tmp){prev->next = timer;timer->prev = prev;timer->next = NULL;tail = timer;}}
private:util_timer *head;util_timer *tail;
};
#endif

处理非活动连接

// 11_3_closeUnactiveConnections.cpp
// 利用alarm函数周期性触发 SIGALRM信号
// 该信号的信号处理函数利用管道通知主循环执行定时器链表上的定时任务即关闭非活动链接
// 一个用户对应一个连接fd、一个定时器检测是否活跃
#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <sys/epoll.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <fcntl.h>
#include <signal.h>
#include <unistd.h>
#include <netinet/in.h>
#include <errno.h>
#include <stdlib.h>
#include "lst_timer.h"#define FD_LIMIT 65535  
#define MAX_EVENT_NUMBER 1024
#define TIMESLOT 5static int pipefd[2]; // 管道传输信号
// 利用升序链表管理定时器
static sort_timer_lst timer_lst;
static int epollfd = 0;int setnonblocking(int fd)
{int old_option = fcntl(fd, F_GETFL);int new_option = old_option | O_NONBLOCK;fcntl(fd, F_SETFL, new_option);return old_option;
}void addfd(int epollfd, int fd)
{epoll_event event;event.data.fd = fd;event.events = EPOLLIN | EPOLLET; // 注册可读事件epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);setnonblocking(fd);
}void sig_handler(int sig)
{int save_errno = errno;int msg = sig;send(pipefd[1], (char *)&msg, 1, 0);errno = save_errno;
}void addsig(int sig)
{struct sigaction sa;memset(&sa, '\0', sizeof(sa));sa.sa_handler = sig_handler;sa.sa_flags |= SA_RESTART;sigfillset(&sa.sa_mask); // 设置所有信号// 为信号注册处理函数assert(sigaction(sig, &sa, NULL) != -1);
}void timer_handler()
{// 定时处理任务,检查有没有到时的定时器,执行其对应任务timer_lst.tick();// 重新定时alarm(TIMESLOT); // 到时会发出SIGALARM信号
}// 定时器回调函数,删除非活动连接socket上的注册事件,并关闭之
void cb_func(client_data *user_data)
{epoll_ctl(epollfd, EPOLL_CTL_DEL, user_data->sockfd, 0);assert(user_data);close(user_data->sockfd);printf("close fd %d\n", user_data->sockfd);
}int main(int argc, char *argv[])
{if (argc <= 2){printf("usage: %s ip_address port_num\n", basename(argv[0]));return 1;}const char *ip = argv[1];int port = atoi(argv[2]);int ret = 0;struct sockaddr_in addr;bzero(&addr, sizeof(addr));addr.sin_family = AF_INET;inet_pton(AF_INET, ip, &addr.sin_addr);addr.sin_port = htons(port);// 创建TCP socket,并将其绑定到端口port上int listenfd = socket(PF_INET, SOCK_STREAM, 0);assert(listenfd >= 0);// 设置端口复用int opt = 1;setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));ret = bind(listenfd, (struct sockaddr *)&addr, sizeof(addr));assert(ret != -1);ret = listen(listenfd, 5);assert(ret != -1);epoll_event events[MAX_EVENT_NUMBER];int epollfd = epoll_create(5);assert(epollfd != -1);addfd(epollfd, listenfd);// 管道ret = socketpair(PF_UNIX, SOCK_STREAM, 0, pipefd);assert(ret != -1);setnonblocking(pipefd[1]); // 设置写端非阻塞addfd(epollfd, pipefd[0]); // 将读端加入epoll树中进行监视// 设置信号处理函数addsig(SIGALRM); // SIGALRM 到来往管道写端发送信号的数值addsig(SIGTERM);bool stop_server = false;client_data *users = new client_data[FD_LIMIT]; // 客户端数组bool timeout = false;alarm(TIMESLOT);while(!stop_server) {int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);if((number < 0) && (errno != EINTR)){printf("epoll failure\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_addrlength = sizeof(client_address);int connfd = accept(listenfd, (sockaddr*)&client_address, &client_addrlength);addfd(epollfd, connfd); // users[connfd].address = client_address;users[connfd].sockfd = connfd;// 创建定时器,设置其回调函数与超时时间,然后绑定定时器与用户数据// 最后将定时器添加到链表 timer_lst中util_timer *timer = new util_timer;timer->user_data = &users[connfd];timer->cb_func = cb_func;time_t cur = time(NULL);// 设置过期时间,当前时间超过该时间就要回收该定时器绑定的connfdtimer->expire = cur + 3 * TIMESLOT;users[connfd].timer = timer;timer_lst.add_timer(timer);}// 处理信号else if((sockfd == pipefd[0]) && (events[i].events & EPOLLIN)){int sig;char signals[1024];// 管道读端接受数据// send是在SIGARLRM和SIGTERM信号被触发时,通过sig_handler函数来调用的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标志有定时任务要处理// 但不立即处理,因为通常定时任务优先级不高timeout = true;break;}case SIGTERM:{stop_server = true;}}}}}// 处理客户连接上收到的数据else if(events[i].events & EPOLLIN){memset(users[sockfd].buf, BUFFER_SIZE - 1, 0);ret = recv(sockfd, users[sockfd].buf, BUFFER_SIZE - 1, 0);printf("get %d bytes of client data %s from %d \n", ret, users[sockfd].buf, sockfd);util_timer *timer = users[sockfd].timer;if(ret < 0){if(errno != EAGAIN){cb_func(&users[sockfd]); // 回收connfdif(timer){timer_lst.del_timer(timer);}}}else if(ret == 0){// 若对方关闭连接,则我们也关闭连接并删除定时器cb_func(&users[sockfd]);if(timer){timer_lst.del_timer(timer);}}else{// 若某个客户的连接上有数据可读// 则要调整对应的定时器的过期时间(通过users数组找到定时器)if(timer){time_t cur = time(NULL);timer->expire = cur + 3 * TIMESLOT;printf("adjust timer once\n");timer_lst.adjust_timer(timer);}else{// other}}}}// 最后处理定时事件,因为通常IO事件有更高的优先级// 但这样导致定时任务不能精确的执行if(timeout){timer_handler(); // 检查是否有到时(太久没有使用)的定时器(对应一个用户的connfd),有就回收fd删除定时器timeout = false;}}close(listenfd);close(pipefd[1]);close(pipefd[2]);delete []users;return 0;
}

测试

目录结构

.
├── 11_3_closeUnactiveConnections.cpp
├── build
├── CMakeLists.txt
└── lst_timer.h

输入编译指令

g++ -o closeConnection 11_3_closeUnactiveConnections.cpp -I ./

也可以使用CMake

cmake_minimum_required (VERSION 2.8)
PROJECT(closeConnection)
# 手动加入文件
SET(SRC_LIST 11_3_closeUnactiveConnections.cpp)#INCLUDE_DIRECTORIES("${CMAKE_CURRENT_SOURCE_DIR}/dir1")
# 相对路径的方式
INCLUDE_DIRECTORIES(.)# 用SRC_LIST所存的名字的源文件来生成可执行文件 darren
ADD_EXECUTABLE(closeConnection ${SRC_LIST} )

执行程序

在本机任意地址的6666端口监听,同一个机器上不同会话使用客户端程序连接服务器

情况1

当客户端连接上服务器后,若socket在三次tick时间里没有IO操作,第四次tick时就回收socket。

服务器

在这里插入图片描述

客户端

在这里插入图片描述

情况2

当客户端连接上服务器后,若socket在三次tick时间里有IO操作,就会续上3次tick的时间( 3 * TIMESLOT)。

如下在第二次tick后,客户端向服务器发送了一条数据 hello

服务器

在这里插入图片描述

客户端

在这里插入图片描述

相关文章:

利用升序定时器链表处理非活动连接

参考自游双《Linux高性能服务器编程》 背景 服务器同常需要定期处理非活动连接&#xff1a;给客户发一个重连请求&#xff0c;或关闭该连接&#xff0c;或者其他。我们可以通过使用升序定时器链表处理非活动连接&#xff0c;下面的代码利用alarm函数周期性的触发SIGALRM信号&a…...

MySQL 开发规范

一、数据库命名规范所有数据对象名称必须小写 :​​db_user​​禁止使用MySQL 保留关键字&#xff0c;若是则引用 临时表以​​tmp_​​​ 开头&#xff0c;备份表以​​bak_​​ 开头并以时间戳结尾所有存储相同数据的列名和列类型必须一致二、数据库基本设计规范​​1、MySQL…...

【C语言进阶】预处理与程序环境

目录一.详解编译与链接1.前言2.翻译环境3.剖析编译过程4.运行环境二.预处理详解1.预定义符号2.剖析#define(1).定义标识符(2).定义宏(3).替换规则(4).#和##(5).宏与函数的对比(6).#undef3.条件编译4.文件包含(1).头文件包含的方式(2).嵌套文件包含一.详解编译与链接 1.前言 在…...

【Docker知识】将环境变量传递到容器

一、说明 程序通常通过与软件捆绑在一起的配置来控制操作&#xff0c;环境变量允许用户在运行时设置它们。但是&#xff0c;在 Docker 容器中运行进程会使事情变得复杂&#xff0c;那么如何将环境变量传递给容器呢&#xff1f;下面介绍若干个传递办法。 二、环境变量有何用途 环…...

Allegro如何更改铜皮显示密度操作指导

Allegro如何更改铜皮显示密度操作指导 用Allegro做PCB设计的时候,铜皮正常显示模式如下图 铜皮的密度是基本填充满的,Allegro支持更改铜皮的显示密度 如下图 如何更改密度,具体操作如下 点击setup...

ThinkPHP5酒店预订管理系统

有需要请私信或看评论链接哦 可远程调试 ThinkPHP5酒店预订管理系统一 介绍 此酒店预订管理系统基于ThinkPHP5框架开发&#xff0c;数据库mysql&#xff0c;采用了ueditor富文本编辑器。系统角色分为用户&#xff0c;员工和管理员。用户可注册登录并预订酒店和评论等&#xff…...

【MySQL】MyCat分库分表分片规则配置详解与实战(MySQL专栏启动)

&#x1f4eb;作者简介&#xff1a;小明java问道之路&#xff0c;2022年度博客之星全国TOP3&#xff0c;专注于后端、中间件、计算机底层、架构设计演进与稳定性建工设优化。文章内容兼具广度深度、大厂技术方案&#xff0c;对待技术喜欢推理加验证&#xff0c;就职于知名金融公…...

OpenWrt路由器设置域名动态解析手把手教程

文章目录0、前言1、准备工作2、详细步骤2.1、OpenWrt路由器软件包安装2.2、防火墙放行入站数据&#xff08;修改为“接受”并保存应用&#xff09;2.3、域名解析服务商对域名的解析设置2.4、路由器中动态域名插件的设置0、前言 因为一直用着内网穿透&#xff08;zerotier或者是…...

java流浪动物救助系统(毕业设计)

项目类型&#xff1a;Java web项目/Java EE项目&#xff08;非开源&#xff09; 项目名称&#xff1a;基于JSPServlet的流浪动物救助网站[dwjz_web] 进行源码获取 用户类型&#xff1a;双角色&#xff08;爱心人士、管理员&#xff09; 项目架构&#xff1a;B/S架构 设计思…...

阿里代码规范插件中,Apache Beanutils为什么被禁止使用?

在实际的项目开发中&#xff0c;对象间赋值普遍存在&#xff0c;随着双十一、秒杀等电商过程愈加复杂&#xff0c;数据量也在不断攀升&#xff0c;效率问题&#xff0c;浮出水面。 问&#xff1a;如果是你来写对象间赋值的代码&#xff0c;你会怎么做&#xff1f; 答&#xf…...

NFC enable NFC使能流程

同学,别退出呀,我可是全网最牛逼的 WIFI/BT/GPS/NFC分析博主,我写了上百篇文章,请点击下面了解本专栏,进入本博主主页看看再走呗,一定不会让你后悔的,记得一定要去看主页置顶文章哦。 NFC enable NFC使能流程 认识nfc系统如何工作,最好的方法就是了解nfc的各个流程,…...

Redis实例绑定CPU物理核优化Redis性能

进入本次Redis性能调优之前&#xff0c;首先要知道CPU结构也会影响Redis的性能。接下来&#xff0c;具体了解一下&#xff01;为什么CPU结构也会影响Redis的性能&#xff1f;主流的 CPU 架构一个 CPU 处理器中一般有多个物理核&#xff0c;每个物理核都可以运行应用程序。每个物…...

STC15中断系统介绍

STC15中断系统介绍✨本篇参考来源于STC官方stc15系列手册:538页- 589页。&#xff08;文末提供该摘取部分的文档资料&#xff09; &#x1f389;在官方提供的手册资料中&#xff0c;一个系列一份手册&#xff0c;手册内容涵盖了数据手册和参考手册以及例程案例。对于学习着来说…...

力扣HOT100 11-15

11.盛水最多的容器 思路&#xff1a;最大水量 底边 * 高度。较短的一边控制最大水量&#xff0c;因此&#xff0c;采用双指针的方式&#xff0c;左、右指针指向开始和末尾&#xff0c;逐个向中间移动&#xff0c;判断左右指针所指向的高度哪个更低&#xff0c;它就向中间移动一…...

深入浅出单调栈与单调队列

目录一、单调栈情形一&#xff1a;寻找一个数左边第一个小于它的数情形二&#xff1a;寻找一个数左边第一个小于它的数的下标情形三&#xff1a;寻找一个数右边第一个大于它的数情形四&#xff1a;寻找一个数右边第一个大于它的数的下标二、单调栈的应用2.1 单调栈模板题I2.2 单…...

深入C语言——实现可变参数函数

文章目录初步示例函数解析最大值函数初步示例 stdarg.h提供了C语言对可变参数的支持&#xff0c;先举一个简短的例子 //testStdArg.c #include <stdarg.h> #include <stdio.h>void printIntList(int N, ...){va_list args; //存放...所代表的参数va_start(…...

41-Dockerfile-Dockerfile简介

Dockerfile简介前言Dockerfile 简介基础知识使用Dockerfile 构建镜像步骤Dockerfile 构建过程Dockerfile基本结构Dockerfile示例总结前言 本篇开始来学习下Dockerfile相关的用法 Dockerfile 简介 Dockerfile : 是用来构建 Docker 镜像的文本文件&#xff0c;是有一条条构建镜…...

【408】操作系统 - 刻骨铭心自测题1(上)

文章目录OS练习题第一部分&#xff1a;1&#xff1a;2&#xff1a;3&#xff1a;4&#xff1a;5&#xff1a;6&#xff1a;7&#xff1a;8&#xff1a;9&#xff1a;10&#xff1a;11&#xff1a;12&#xff1a;13&#xff1a;14&#xff1a;15&#xff1a;16&#xff1a;17&am…...

【老卫拆书】009期:Vue+Node肩挑全栈!《Node.js+Express+MongoDB+Vue.js全栈开发实战》开箱

今天刚拿到一本新书&#xff0c;叫做《Node.jsExpressMongoDBVue.js全栈开发实战》&#xff0c;做个开箱。 外观 先从外观上讲&#xff0c;这本是全新的未开封的&#xff0c;膜还在。 这本书介绍从技术原理到整合开发实战&#xff0c;以丰富的项目展现全栈开发的一个技巧。 …...

【LeetCode】动态规划总结

动态规划解决的问题 动态规划和贪心的区别&#xff1a; 动态规划是由前一个状态推导出来的&#xff1b; 贪心是局部直接选最优的。 动态规划解题步骤 状态定义&#xff1a;确定dp数组以及下标的含义状态转移方程&#xff1a;确定递推公式初始条件&#xff1a;dp如何初始化遍历…...

Python爬虫实战:研究MechanicalSoup库相关技术

一、MechanicalSoup 库概述 1.1 库简介 MechanicalSoup 是一个 Python 库,专为自动化交互网站而设计。它结合了 requests 的 HTTP 请求能力和 BeautifulSoup 的 HTML 解析能力,提供了直观的 API,让我们可以像人类用户一样浏览网页、填写表单和提交请求。 1.2 主要功能特点…...

地震勘探——干扰波识别、井中地震时距曲线特点

目录 干扰波识别反射波地震勘探的干扰波 井中地震时距曲线特点 干扰波识别 有效波&#xff1a;可以用来解决所提出的地质任务的波&#xff1b;干扰波&#xff1a;所有妨碍辨认、追踪有效波的其他波。 地震勘探中&#xff0c;有效波和干扰波是相对的。例如&#xff0c;在反射波…...

突破不可导策略的训练难题:零阶优化与强化学习的深度嵌合

强化学习&#xff08;Reinforcement Learning, RL&#xff09;是工业领域智能控制的重要方法。它的基本原理是将最优控制问题建模为马尔可夫决策过程&#xff0c;然后使用强化学习的Actor-Critic机制&#xff08;中文译作“知行互动”机制&#xff09;&#xff0c;逐步迭代求解…...

黑马Mybatis

Mybatis 表现层&#xff1a;页面展示 业务层&#xff1a;逻辑处理 持久层&#xff1a;持久数据化保存 在这里插入图片描述 Mybatis快速入门 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/6501c2109c4442118ceb6014725e48e4.png //logback.xml <?xml ver…...

mongodb源码分析session执行handleRequest命令find过程

mongo/transport/service_state_machine.cpp已经分析startSession创建ASIOSession过程&#xff0c;并且验证connection是否超过限制ASIOSession和connection是循环接受客户端命令&#xff0c;把数据流转换成Message&#xff0c;状态转变流程是&#xff1a;State::Created 》 St…...

YSYX学习记录(八)

C语言&#xff0c;练习0&#xff1a; 先创建一个文件夹&#xff0c;我用的是物理机&#xff1a; 安装build-essential 练习1&#xff1a; 我注释掉了 #include <stdio.h> 出现下面错误 在你的文本编辑器中打开ex1文件&#xff0c;随机修改或删除一部分&#xff0c;之后…...

从深圳崛起的“机器之眼”:赴港乐动机器人的万亿赛道赶考路

进入2025年以来&#xff0c;尽管围绕人形机器人、具身智能等机器人赛道的质疑声不断&#xff0c;但全球市场热度依然高涨&#xff0c;入局者持续增加。 以国内市场为例&#xff0c;天眼查专业版数据显示&#xff0c;截至5月底&#xff0c;我国现存在业、存续状态的机器人相关企…...

CocosCreator 之 JavaScript/TypeScript和Java的相互交互

引擎版本&#xff1a; 3.8.1 语言&#xff1a; JavaScript/TypeScript、C、Java 环境&#xff1a;Window 参考&#xff1a;Java原生反射机制 您好&#xff0c;我是鹤九日&#xff01; 回顾 在上篇文章中&#xff1a;CocosCreator Android项目接入UnityAds 广告SDK。 我们简单讲…...

NXP S32K146 T-Box 携手 SD NAND(贴片式TF卡):驱动汽车智能革新的黄金组合

在汽车智能化的汹涌浪潮中&#xff0c;车辆不再仅仅是传统的交通工具&#xff0c;而是逐步演变为高度智能的移动终端。这一转变的核心支撑&#xff0c;来自于车内关键技术的深度融合与协同创新。车载远程信息处理盒&#xff08;T-Box&#xff09;方案&#xff1a;NXP S32K146 与…...

使用LangGraph和LangSmith构建多智能体人工智能系统

现在&#xff0c;通过组合几个较小的子智能体来创建一个强大的人工智能智能体正成为一种趋势。但这也带来了一些挑战&#xff0c;比如减少幻觉、管理对话流程、在测试期间留意智能体的工作方式、允许人工介入以及评估其性能。你需要进行大量的反复试验。 在这篇博客〔原作者&a…...