文章目录

  • Linux定时方法
  • 定时器链表
  • 空闲断开

Linux定时方法

Linux中为我们提供了三种定时方法,分别是Socket超时选项,SIGALRM信号,I/O复用超时参数。下面一一对其进行介绍。

Socket超时选项
socket中的SO_RCVTIMEOSO_SNDTIMEO选项分别用来设置接收数据超时时间发送数据超时时间 。所以这两个选项仅仅适用于那些用来收发数据的socket系统调用,如send,recv,recvmsg,accept,connect。

下面是这两个选项对这些系统调用的影响

I/O复用超时参数
在Linux下的三组I/O复用系统调用都带有超时参数,所以他们不仅可以统一处理信号和I/O时间,也能统一处理定时事件。
但是I/O复用系统调用可能会在超时时间到期之前提前返回(有I/O事件就绪),所以如果要使用该参数进行定时,就需要不断更新定时参数来反应剩余的时间。

SIGALRM信号
Linux下的alarm函数和setitimer函数也可以用于设定闹钟,一旦闹钟到时,就会触发SIGALRM信号 ,所以我们可以利用该信号的处理函数来处理定时任务。

#include <unistd.h>
unsigned int alarm(unsigned int seconds);#include <sys/time.h>
int setitimer(int which, const struct itimerval *new_value,struct itimerval *old_value);struct itimerval {struct timeval it_interval; /* next value */struct timeval it_value;    /* current value */
};struct timeval {time_t      tv_sec;         /* seconds */suseconds_t tv_usec;        /* microseconds */
};

定时器链表

由于服务器程序通常管理着众多定时事件,因此有效地组织这些定时事件,使他们能够在预期的时间点被触发并且不影响服务器的主要逻辑,对于服务器的性能有着至关重要的影响。因此我们通常会将每个定时事件封装成定时器,并利用某种容器类数据结构对定时事件进行统一管理。

下面就使用一个以到期时间进行升序排序的双向带头尾节点的链表来作为容器,实现定时器链表。
具体的细节以及实现思路都写在了注释里。

#ifndef __TIMER_LIST_H__
#define __TIMER_LIST_H__#include<time.h>
#include<stdio.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>const int MAX_BUFFER_SIZE = 1024;class util_timer;//用户数据
struct client_data
{sockaddr_in addr;int sock_fd;char buff[MAX_BUFFER_SIZE];util_timer* timer;
};//定时器类
struct util_timer
{public:util_timer(): _next(nullptr), _prev(nullptr){}time_t _expire;             //到期时间void (*fun)(client_data*);  //处理函数client_data* _user_data;    //用户参数util_timer* _next;util_timer* _prev;
};//定时器链表,带头尾双向链表,定时器以升序排序
class timer_list
{typedef util_timer node;public:timer_list(): _head(nullptr), _tail(nullptr){}~timer_list(){node* cur = _head;while(cur){node* next = cur->_next;delete cur;cur = next;}}//插入定时器void push(node* timer){if(timer == nullptr){return;}//如果头节点为空,则让新节点成为头节点if(_head == nullptr){_head = _tail = timer;return;}//如果节点比头节点小,则让他成为新的节点if(timer->_expire < _head->_expire){timer->_next = _head;_head->_prev = timer;   _head = timer;return;}node* prev = _head;node* cur = _head->_next;//找到插入的位置while(cur){if(timer->_expire < cur->_expire){timer->_next = cur;cur->_prev = timer;prev->_next = timer;timer->_prev = prev;return;}prev = cur;cur = cur->_next;}//如果走到这里还没有返回,则说明当前定时器大于链表中所有节点,所以让他成为新的尾节点if(cur == nullptr){prev->_next = timer;timer->_prev = prev;timer->_next = nullptr;_tail = timer;}}//如果节点的时间发生修改,则将他调整到合适的位置上void adjust_node(node* timer){if(timer == nullptr){return;}//先将节点从链表中取出,再插回去。if(timer == _head && timer == _tail){_head = _tail = nullptr;}//如果该节点是头节点if(timer == _head){_head = timer->_next;if(_head){_head->_prev = nullptr;}}//如果该节点是尾节点if(timer == _tail){_tail = _tail->_prev;if(_tail){_tail->_next = nullptr;}}//该节点在中间else{timer->_prev->_next = timer->_next;timer->_next->_prev = timer->_prev;}//将节点重新插入回链表中push(timer);}//删除指定定时器void pop(node* timer){if(timer == nullptr){return;}//如果链表中只有一个节点if(timer == _head && timer == _tail){delete timer;_head = _tail = nullptr;}//如果删除的是头节点else if(timer == _head){_head = _head->_next;_head->_prev = nullptr;delete timer;timer = nullptr;}//如果删除的是尾节点else if(timer == _tail){_tail = _tail->_prev;_tail->_next = nullptr;delete timer;timer = nullptr;}else{//此时删除节点就是中间的节点timer->_prev->_next = timer->_next;timer->_next->_prev = timer->_prev;delete timer;timer = nullptr;}}//处理链表上的到期任务void tick(){//此时链表中没有节点if(_head == nullptr){return;}printf("time tick\n");time_t cur_time = time(nullptr);    //获取当前时间node* cur = _head;while(cur){//由于链表是按照到期时间进行排序的,所以如果当前节点没到期,后面的也不可能到期if(cur->_expire > cur_time){break;}//如果当前节点到期,则调用回调函数执行定时任务。cur->fun(cur->_user_data);//执行完定时任务后,将节点从链表中删除node* next = cur->_next;//前指针置空if(next != nullptr){next->_prev = nullptr;}delete cur;cur = next;}}private:node* _head;node* _tail;
};
#endif // !__TIMER_LIST_H__

时间复杂度
添加节点:O(n)
删除节点:O(1)
执行定时任务:O(1)


空闲断开

对于服务器来说,定期处理非活动连接是保证其高可用的一项不必可少的功能,下面就以上一篇博客中实现的统一事件源服务器举例,演示一下如何使用定时器链表来实现空闲断开的功能。
Linux网络编程 | 信号 :信号函数、信号集、统一事件源 、网络编程相关信号

实现的思路很简单,我们为每一个连接设定一个定时器,将定时器放入定时器链表中,并通过alarm函数来周期性触发SIGALRM信号。
如果某一个连接当前有新的活动,则说明该连接为活跃连接,重置其定时器并且调整定时器在链表中的位置。
我们设定一个监控周期,每当周期到则会触发SIGALRM信号,信号处理函数则利用管道将信号发送给主循环。如果主循环监控的管道读端有数据,并且待处理信号为SIGALRM,则说明此时需要执行定时器链表上的定时任务(关闭当前不活跃的连接)。

#include<fcntl.h>
#include<signal.h>
#include<stdlib.h>
#include<sys/socket.h>
#include<sys/epoll.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include<stdio.h>
#include<errno.h>
#include<netinet/in.h>
#include<unistd.h>#include"timer_list.h"
const int MAX_LISTEN = 5;
const int MAX_EVENT = 1024;
const int MAX_BUFFER = 1024;
const int TIMESLOT = 5;
const int FD_LIMIT = 65535;static int pipefd[2];        //管道描述符
static int epoll_fd = 0;     //epoll操作句柄
static timer_list timer_lst; //定时器链表    //设置非阻塞
int setnonblocking(int fd)
{int flag = fcntl(fd, F_GETFL, 0);fcntl(fd, F_SETFL, flag |= O_NONBLOCK);return flag;
}//将描述符加入epoll监听集合中
void epoll_add_fd(int epoll_fd, int fd)
{struct epoll_event event;event.data.fd = fd;event.events = EPOLLIN | EPOLLET;epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &event);
}//信号处理函数
void sig_handler(int sig)
{//保留原本的errno, 再函数末尾恢复, 确保可重入性int save_errno = errno;send(pipefd[1], (char*)&sig, 1, 0); //将信号值通过管道发送给主循环errno = save_errno;
}//设置信号处理函数
void set_sig_handler(int sig)
{struct sigaction sa;sa.sa_handler = sig_handler;sa.sa_flags |= SA_RESTART;  //重新调用被信号中断的系统函数sigfillset(&sa.sa_mask);    //将所有信号加入信号掩码中if(sigaction(sig, &sa, NULL) < 0){exit(EXIT_FAILURE);}
}//alarm信号处理函数
void timer_handler()
{timer_lst.tick();   //执行到期任务alarm(TIMESLOT);    //开始下一轮计时
}//到时处理任务
void handler(client_data* user_data)
{if(user_data == nullptr){return;}//将过期连接的从epoll中移除,并关闭描述符epoll_ctl(epoll_fd, EPOLL_CTL_DEL, user_data->sock_fd, NULL);close(user_data->sock_fd);printf("close fd : %d\n", user_data->sock_fd);
}int main(int argc, char*argv[])
{if(argc <= 2){printf("输入参数:IP地址 端口号\n");return 1;}const char* ip = argv[1];   int port = atoi(argv[2]);//创建监听套接字int listen_fd = socket(PF_INET, SOCK_STREAM, 0);    if(listen_fd == -1){printf("listen_fd socket.\n");return -1;}//绑定地址信息struct sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(port);addr.sin_addr.s_addr = inet_addr(ip);if(bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0){printf("listen_fd bind.\n");return -1;}//开始监听if(listen(listen_fd, MAX_LISTEN) < 0){printf("listen_fd listen.\n");return -1;}//创建epoll,现版本已忽略大小,给多少都无所谓int epoll_fd = epoll_create(MAX_LISTEN);if(epoll_fd == -1){printf("epoll create.\n");return -1;}epoll_add_fd(epoll_fd, listen_fd);  //将监听套接字加入epoll中//使用sockpair创建全双工管道,对读端进行监控,统一事件源if(socketpair(PF_UNIX, SOCK_STREAM, 0, pipefd) < 0){printf("socketpair.\n");return -1;}setnonblocking(pipefd[1]);  //将写端设为非阻塞epoll_add_fd(epoll_fd, pipefd[0]);    //将读端加入epoll监控集合set_sig_handler(SIGALRM);   //设置定时信号set_sig_handler(SIGTERM);   //用户按下中断键(DELETE或者Ctrl+C)struct epoll_event events[MAX_LISTEN];client_data* users = new client_data[FD_LIMIT];bool stop_server = false;bool time_out;alarm(TIMESLOT);    //开始计时while(!stop_server){int number = epoll_wait(epoll_fd, events, MAX_LISTEN, -1);if(number < 0 && errno != EINTR){printf("epoll_wait.\n");break;}for(int i = 0; i < number; i++){int sock_fd = events[i].data.fd;//如果监听套接字就绪则处理连接if(sock_fd == listen_fd){struct sockaddr_in clinet_addr;socklen_t len = sizeof(clinet_addr);int conn_fd = accept(listen_fd, (struct    sockaddr*)&clinet_addr, &len);if(conn_fd < 0){printf("accept.\n");continue;}epoll_add_fd(epoll_fd, sock_fd);//存储用户信息users[conn_fd].addr = clinet_addr;users[conn_fd].sock_fd = conn_fd;//创建定时器util_timer* timer = new util_timer;users->timer = timer;timer->_user_data = &users[conn_fd];timer->fun = handler;time_t cur_time = time(nullptr);timer->_expire = cur_time + 3 * TIMESLOT;    //设置超时时间timer_lst.push(timer);  //将定时器放入定时器链表中}//如果就绪的是管道的读端,则说明有信号到来,要处理信号else if(sock_fd == pipefd[0] && events[i].events & EPOLLIN){int sig;char signals[MAX_BUFFER];int ret = recv(pipefd[0], signals, MAX_BUFFER, 0);if(ret == -1){continue;}else if(ret == 0){continue;}else{//由于一个信号占一个字节,所以按字节逐个处理信号for(int j = 0; j < ret; j++){switch (signals[i]){case SIGALRM:{time_out = true;break;}case SIGINT:{stop_server = true;}}}}}//如果就绪的是可读事件else if(events[i].events & EPOLLIN){int ret = recv(sock_fd, users[sock_fd].buff, MAX_BUFFER - 1, 0);    util_timer* timer = users[sock_fd].timer;//连接出现问题,断开连接并且删除对应定时器if(ret < 0){if(errno != EAGAIN){handler(&users[sock_fd]);if(timer){timer_lst.pop(timer);}}}//如果读写出现问题,也断开连接else if(ret == 0){handler(&users[sock_fd]);if(timer){timer_lst.pop(timer);}}else{//如果事件成功执行,则重新设置定时器的超时时间,并调整其在定时器链表中的位置if(timer){time_t cur_time = time(nullptr);timer->_expire = cur_time + 3 * TIMESLOT;timer_lst.adjust_node(timer);}}}else{/* 业务逻辑和写事件暂不实现,本程序主要用于定时器链表如何实现空闲断开功能 */}}//如果超时,则调用超时处理函数,并且重置标记    if(time_out == true){timer_handler();time_out = false;}}//关闭文件描述符close(listen_fd);close(pipefd[1]);close(pipefd[0]);delete[] users;return 0;
}

Linux网络编程 | 定时事件 :Linux常见定时方法、定时器链表、空闲断开相关推荐

  1. linux网络编程 华清,Linux网络编程之套接字

    一 :套接字属性 套接字由域(domain),类型(type)和协议(protocol)三个属性确定其特性. 1)套接字的域 域指定套接字通信中使用的网络 介质,常见的套接字域是AF_INET,它指的 ...

  2. Linux网络编程一步一步学+基础

    转自:http://blogold.chinaunix.net/u1/48325/showart_413841.html ·Linux网络编程基础(一) ·Linux网络编程基础(二) ·Linux网 ...

  3. Linux网络编程基础和一步一步学

    ·Linux网络编程 基础(一) ·Linux网络编程 基础(二) ·Linux网络编程 基础(三) ·Linux网络编程 基础(四) ·Linux网络编程 基础(五) ·Linux网络编程 基础(六 ...

  4. 【Linux】Linux网络编程(含常见服务器模型,上篇)

    基本数据结构介绍 Linux系统是通过提供嵌套字(socket)来进行网络编程的.网络程序通过socket和其他几个函数的调用,会返回一个通用的文件描述符,用户可以将这个描述符看成普通的文件的描述符来 ...

  5. 【Linux】Linux网络编程(含常见服务器模型,下篇)

    上一篇文章:[Linux]Linux网络编程(含常见服务器模型,上篇). 高级嵌套字函数 前面介绍的一些函数(read.write等)都是网络程序里最基本的函数,也是最原始的通信函数.下面介绍一下几个 ...

  6. Linux网络编程——千峰物联网笔记

    B站视频:千峰物联网学科linux网络编程 网址:https://www.bilibili.com/video/BV1RJ411B761?p=1 目录 第一章:计算机网络概述 1.1计算机网络发展简史 ...

  7. linux网络编程(三)select、poll和epoll

    linux网络编程(三)select.poll和epoll 一.为什么会有多路I/O转接服务器? 二.select 三.poll 三.epoll 一.为什么会有多路I/O转接服务器? 为什么会有多路I ...

  8. Linux网络编程基础知识

    Linux网络编程基础知识 1. 协议的概念 1.1 什么是协议 1.2 典型协议 2 网络应用程序设计模式 2.1 C/S模式 2.2 B/S模式 2.3 优缺点 3 分层模型 3.1 OSI七层模 ...

  9. 【Linux网络编程】UDP 套接字编程

    [Linux网络编程]UDP 套接字编程 [1]用户数据报协议(UDP) UDP是一个简单的传输层协议,不保证UDP数据报会到达其最终目的地,不保证各个数据报的先后顺序跨网络后保持不变,也不保证每个数 ...

最新文章

  1. 直流降压的简单方法_空调室内机电源电路检修方法。
  2. iOS 9应用开发教程之使用代码添加按钮美化按钮
  3. fekit前端代码模块化工具
  4. java临时的api,JAVA API-day03
  5. 超级马里奥游戏像素素材_《超级马里奥3D世界+库巴之怒》游戏文件尺寸相当小...
  6. apache cxf_Wildfly,Apache CXF和@SchemaValidation
  7. linux安装源码mysql失败,linux停mysql源码安装
  8. restTemplate重定向问题 cookie问题
  9. PULSE:一种基于隐式空间的图像超分辨率算法
  10. im4java开发向导
  11. STM32的EXTI相关学习笔记
  12. 大学计算机思维导图_【21计算机考研】改考408+1!上海电力大学翻倍扩招,仍考数据结构!...
  13. 安卓gridview条目点击事件_敬业签的安卓手机版设置便签按正序排序教程
  14. pre-commit钩子,代码质量检查
  15. win7 计算机 局域网共享,win7局域网一键共享工具(教程)
  16. 安徽自招大专计算机,2021年安徽自主招生学校名单有哪些,安徽自主招生大专院校名单【详细版】...
  17. 学校计算机机房维护年度总结,学校计算机机房的管理和维护建议原稿(全文完整版)...
  18. python爬百度贴吧_python爬虫-爬取百度贴吧帖子加图片
  19. 嵌入式linux下的触屏模拟
  20. 认识MyBatis、Mybatis笔记.MyBatis的核心配置,动态Mapper,动态SQL,表的关联及分页操作和缓存理解

热门文章

  1. CSDN博主排名更新公告
  2. github上面图片不显示
  3. Cortex‐M3-存储器映射
  4. 电机串电阻会有什么影响?
  5. Altium Designer 18 怎么导出CAD文件
  6. redis设置开机自启动
  7. 固特异发布人工智能球形轮胎:自动变形、自我修复、超级灵活
  8. 常用的 16 个 Sublime Text 快捷键
  9. CS1704问题汇总
  10. STC12单片机串口U盘测试程序源码