利用升序定时器链表处理非活动连接
参考自游双《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高性能服务器编程》 背景 服务器同常需要定期处理非活动连接:给客户发一个重连请求,或关闭该连接,或者其他。我们可以通过使用升序定时器链表处理非活动连接,下面的代码利用alarm函数周期性的触发SIGALRM信号&a…...
MySQL 开发规范
一、数据库命名规范所有数据对象名称必须小写 :db_user禁止使用MySQL 保留关键字,若是则引用 临时表以tmp_ 开头,备份表以bak_ 开头并以时间戳结尾所有存储相同数据的列名和列类型必须一致二、数据库基本设计规范1、MySQL…...

【C语言进阶】预处理与程序环境
目录一.详解编译与链接1.前言2.翻译环境3.剖析编译过程4.运行环境二.预处理详解1.预定义符号2.剖析#define(1).定义标识符(2).定义宏(3).替换规则(4).#和##(5).宏与函数的对比(6).#undef3.条件编译4.文件包含(1).头文件包含的方式(2).嵌套文件包含一.详解编译与链接 1.前言 在…...
【Docker知识】将环境变量传递到容器
一、说明 程序通常通过与软件捆绑在一起的配置来控制操作,环境变量允许用户在运行时设置它们。但是,在 Docker 容器中运行进程会使事情变得复杂,那么如何将环境变量传递给容器呢?下面介绍若干个传递办法。 二、环境变量有何用途 环…...

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

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

【MySQL】MyCat分库分表分片规则配置详解与实战(MySQL专栏启动)
📫作者简介:小明java问道之路,2022年度博客之星全国TOP3,专注于后端、中间件、计算机底层、架构设计演进与稳定性建工设优化。文章内容兼具广度深度、大厂技术方案,对待技术喜欢推理加验证,就职于知名金融公…...

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

java流浪动物救助系统(毕业设计)
项目类型:Java web项目/Java EE项目(非开源) 项目名称:基于JSPServlet的流浪动物救助网站[dwjz_web] 进行源码获取 用户类型:双角色(爱心人士、管理员) 项目架构:B/S架构 设计思…...

阿里代码规范插件中,Apache Beanutils为什么被禁止使用?
在实际的项目开发中,对象间赋值普遍存在,随着双十一、秒杀等电商过程愈加复杂,数据量也在不断攀升,效率问题,浮出水面。 问:如果是你来写对象间赋值的代码,你会怎么做? 答…...

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

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

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

力扣HOT100 11-15
11.盛水最多的容器 思路:最大水量 底边 * 高度。较短的一边控制最大水量,因此,采用双指针的方式,左、右指针指向开始和末尾,逐个向中间移动,判断左右指针所指向的高度哪个更低,它就向中间移动一…...
深入浅出单调栈与单调队列
目录一、单调栈情形一:寻找一个数左边第一个小于它的数情形二:寻找一个数左边第一个小于它的数的下标情形三:寻找一个数右边第一个大于它的数情形四:寻找一个数右边第一个大于它的数的下标二、单调栈的应用2.1 单调栈模板题I2.2 单…...
深入C语言——实现可变参数函数
文章目录初步示例函数解析最大值函数初步示例 stdarg.h提供了C语言对可变参数的支持,先举一个简短的例子 //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 镜像的文本文件,是有一条条构建镜…...

【408】操作系统 - 刻骨铭心自测题1(上)
文章目录OS练习题第一部分:1:2:3:4:5:6:7:8:9:10:11:12:13:14:15:16:17&am…...

【老卫拆书】009期:Vue+Node肩挑全栈!《Node.js+Express+MongoDB+Vue.js全栈开发实战》开箱
今天刚拿到一本新书,叫做《Node.jsExpressMongoDBVue.js全栈开发实战》,做个开箱。 外观 先从外观上讲,这本是全新的未开封的,膜还在。 这本书介绍从技术原理到整合开发实战,以丰富的项目展现全栈开发的一个技巧。 …...

【LeetCode】动态规划总结
动态规划解决的问题 动态规划和贪心的区别: 动态规划是由前一个状态推导出来的; 贪心是局部直接选最优的。 动态规划解题步骤 状态定义:确定dp数组以及下标的含义状态转移方程:确定递推公式初始条件:dp如何初始化遍历…...
golang循环变量捕获问题
在 Go 语言中,当在循环中启动协程(goroutine)时,如果在协程闭包中直接引用循环变量,可能会遇到一个常见的陷阱 - 循环变量捕获问题。让我详细解释一下: 问题背景 看这个代码片段: fo…...
C++:std::is_convertible
C++标志库中提供is_convertible,可以测试一种类型是否可以转换为另一只类型: template <class From, class To> struct is_convertible; 使用举例: #include <iostream> #include <string>using namespace std;struct A { }; struct B : A { };int main…...

Swift 协议扩展精进之路:解决 CoreData 托管实体子类的类型不匹配问题(下)
概述 在 Swift 开发语言中,各位秃头小码农们可以充分利用语法本身所带来的便利去劈荆斩棘。我们还可以恣意利用泛型、协议关联类型和协议扩展来进一步简化和优化我们复杂的代码需求。 不过,在涉及到多个子类派生于基类进行多态模拟的场景下,…...
【JavaSE】绘图与事件入门学习笔记
-Java绘图坐标体系 坐标体系-介绍 坐标原点位于左上角,以像素为单位。 在Java坐标系中,第一个是x坐标,表示当前位置为水平方向,距离坐标原点x个像素;第二个是y坐标,表示当前位置为垂直方向,距离坐标原点y个像素。 坐标体系-像素 …...

(转)什么是DockerCompose?它有什么作用?
一、什么是DockerCompose? DockerCompose可以基于Compose文件帮我们快速的部署分布式应用,而无需手动一个个创建和运行容器。 Compose文件是一个文本文件,通过指令定义集群中的每个容器如何运行。 DockerCompose就是把DockerFile转换成指令去运行。 …...
[Java恶补day16] 238.除自身以外数组的乘积
给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。 题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。 请 不要使用除法,且在 O(n) 时间复杂度…...
JAVA后端开发——多租户
数据隔离是多租户系统中的核心概念,确保一个租户(在这个系统中可能是一个公司或一个独立的客户)的数据对其他租户是不可见的。在 RuoYi 框架(您当前项目所使用的基础框架)中,这通常是通过在数据表中增加一个…...
Go 并发编程基础:通道(Channel)的使用
在 Go 中,Channel 是 Goroutine 之间通信的核心机制。它提供了一个线程安全的通信方式,用于在多个 Goroutine 之间传递数据,从而实现高效的并发编程。 本章将介绍 Channel 的基本概念、用法、缓冲、关闭机制以及 select 的使用。 一、Channel…...
第7篇:中间件全链路监控与 SQL 性能分析实践
7.1 章节导读 在构建数据库中间件的过程中,可观测性 和 性能分析 是保障系统稳定性与可维护性的核心能力。 特别是在复杂分布式场景中,必须做到: 🔍 追踪每一条 SQL 的生命周期(从入口到数据库执行)&#…...

Android写一个捕获全局异常的工具类
项目开发和实际运行过程中难免会遇到异常发生,系统提供了一个可以捕获全局异常的工具Uncaughtexceptionhandler,它是Thread的子类(就是package java.lang;里线程的Thread)。本文将利用它将设备信息、报错信息以及错误的发生时间都…...