C++服务器定时器基于LRU算法思想实现定时器遍历插入调整删除时间复杂度都是O(1)

11 篇文章 3 订阅
4 篇文章 0 订阅

感谢我可爱的女朋友每天都听我讲各种服务器端的知识,天天还不带她出去玩。希望未来我可以找到一份满意的工作(最好可以是linux C++),带她去吃好多好吃的来弥补大学生活对她心灵上造成伤害!

写在前面

我一直很想写出一个好用的网络库,可以作为本科毕业前送给自己的礼物,由于我的水平有限,这篇博客的时候写于我大三刚开学2个月,目前只接触过三个月的网络编程和2个月linux下网络编程。很多理解都是按照自己学习的所思所感写下来的,难免会有错误,如果您有发现错误,请您赐教!

我对定时器作用的理解

定时器主要的作用是为了断开那些异常连接(网线被拔了),这种异常是检测不到的,通俗的讲就是关闭不了分配给客户端的套接字,会造成资源浪费,而定时器目的就是检测套接字,如果有超时的套接字就关闭他,从而不会造成资源浪费这是我目前对服务器端定时器作用的理解。

这个小定时器涉及的算法思想

定时器我目前看过两种的设计方案,一种是使用双链表,这对于事件都是相同值的时候效率会很高。另一种是使用二叉堆来造一个时间轮,可以用优先队列来实现这个时间轮,毕竟优先队列队列就是由二叉堆组成的,时间轮适用定时时间不一样的,因为要再此重新排序调整顺序,进堆和出堆的时间复杂度是O(logn),所以时间轮适合不同的定时时间。libevent库也是有这两种定时器的实现方法,默认是二叉堆,如果时间都是一样的话可以用链表来提升速度。

设计这个定时器

用到了双向链表和哈系表,采用绝对的时间来处理定时效果,个人觉得绝对时间较好实现。采用time()来获得时间,muduo库是采用gettimeofday来实现的(因为这个函数精度更高,高到微秒,用法都差不多,我写的是小demo,不太需要那么高的精度,先把整体的了解了在来优化一下东西)来了一个客户套接字,放到双向链表的头部,哈希表存头结点链表的地址,到点了再获取一下时间,比较双向链表尾部和当前的时间,当前时间比尾部的大就删除尾部,直到当前的时间比链表尾部小。插入和删除时间复杂度是O(1)。为什么要用到哈系表?但是如果一个客户端发了一条消息我们就需要重新调整客户端的时间,把他从他的位置再放回链表头部,不用哈系表的话,最坏的情况下是从头遍历到尾,在把尾换到头,插入删除链表的时间的复杂度是O(1),但是遍历的时间复杂度就是O(n),用哈系表记录每个客户的套接字,当要调整客户端的时候只需要查一下哈系表即可。时间复杂度为O(1)。这样插入、删除、调整都是O(1)的时间复杂度,这也是LRU算法思想,我借鉴了过来。

定时器的细节

这个是采用了alarm函数来定时,再捕捉信号,捕捉到了信号再调用定时器开始遍历链表,注意一定要设置一个变量来判断是否可以用alarm函数,如果上一次alarm函数还没结束又再次调用了会造成进程一直阻塞在那里。还要注意的是,alarm到点了,慢启动调用的一些函数会产生中断错误。慢启动调用:术语也适用于那些可能永远阻塞的系统调用。永远阻塞的系统是指调用有可能无法返回,比如下面的epoll_wait函数,没有事件发生的话就一直阻塞在那里。慢系统调用的基本规则是:当阻塞于某个慢系统调用的一个进程捕获某个信号且相应信号处理函数返回时,该系统调用可能会返回一个EINTR错误(摘自UNP第三版107页),所以我们一定要在epoll_wait函数里出错的时候判断一下错误是不是EINTR,如果是的话那就continue,不是的话就break。

单例模式

要把所有的客户端都加在一个定时器中,单例模式呼之欲出。写此定时器的时候只是大概了解过单例模式,写的时候又再次深入了解了一下单例模式。单例模式如名字一样,内部有一个static对象,然后在把构造函数给私有化,通过一个static函数来调用这个对象进行操作。单例模式下面还有两种模式:懒汉模式和饿汉模式,饿汉模式直接在声明的时候就给他初始化了。懒汉模式声明了一个对象但是不定义,用到的时候再去定义,为了防止多次定义对象,用一个if来判断他是否被定义了。伪代码如下。

if(obj == nullptr){
	obj = new Object();
}	

在多线程的环境下,采取了一把锁防止被多次创建,但是如果已经定义了,还要加锁解锁等待那太浪费CPU的时间了,大牛们又加了一个if(也称为双重锁定),伪代码如下

if(obj == nullptr){//如果被创建了就直接返回加快速度
	lock_guard<mutex>();
	if(obj == nullptr()){//如果等待的时候对象定义好了,那就不再需要定义了
		obj = new Object();
	}
}	

定时器的完整代码如下:

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/epoll.h>
#include <iostream>
#include <unordered_map>
#include <list>
#include <memory>
using namespace std;

constexpr int listen_num = 5;
constexpr int MAX_EVENTS_NUM = 1024;

constexpr int TIME = 5;//测试的时候改小一点即可
/*
struct timeval {
        long    tv_sec;         //seconds
        long    tv_usec;        // and microseconds 
};
	1000000us = 1s
*/
struct initial_timer{
    initial_timer() = default;
	//两个必备的材料
	int connfd = 0;
	time_t expire = 0;//到期时间
};
//设置成单例模式里的饿汉模式
class timeout{
private:
    static timeout* t_out;
private:
	using sp_init_tm = shared_ptr<initial_timer>;//智能指针重命名
private:
	list<sp_init_tm> l;//封装成智能指针
	unordered_map<int,list<sp_init_tm>::iterator> um;//通过sock来删除他所在的位置
private:
	void settime(sp_init_tm ptr){
		time_t cur = time(nullptr);
		ptr->expire = cur + TIME;//TIMEs后超时
	}
private:
    timeout() = default;
    //移动和拷贝构造函数都定义成
  //  timeout(const timeout&) = delete;
  //  timeout(timeout&& ) = delete;
public:
	
    static timeout* get_Timeout();
    void del_Timeout(){
        delete t_out;
    }
	void add_time_obj(int epoll_base,int connfd,int listenfd){
        
       connfd = accept(listenfd,nullptr,nullptr);
		//设置连接的对象
        sp_init_tm ptr = make_shared<initial_timer>();
		ptr->connfd = connfd;
        t_out->settime(ptr);

        //把新的连接添加进epoll里面
        epoll_event event;
        event.data.fd = connfd;
        event.events = EPOLLIN;
        int ctl = epoll_ctl(epoll_base,EPOLL_CTL_ADD,connfd,&event);
        if(ctl < 0){
            cout<<"add_time_obj ctl error"<<endl;
            return ;
        }

        //把新的连接添加到链表的头部和哈希表中
        l.emplace_front(ptr);
        um.emplace(connfd,l.begin());
        
	}

    void adjust_obj(int connfd){
        //肯定是有这个套接字才能触发这个函数
        auto it = um.find(connfd);
        sp_init_tm tmp = *it->second;
        l.erase(it->second);
        //重新设置一下时间
        t_out->settime(tmp);
        //统一插到头部上面去
        l.emplace_front(tmp);
        um[connfd] = l.begin();
    }
    //超时、正常关闭都需要调用这个函数
    void del_time_obj(int event_base,int connfd){
        auto it = um.find(connfd);
        int ctl = epoll_ctl(event_base,EPOLL_CTL_DEL,it->first,nullptr);
        if(ctl < 0){
            cout<<"del_time_obj ctl error"<<endl;
            return ;
        }
        //清理信息
        l.erase(it->second);
        um.erase(connfd);
        close(connfd);
    }

    //主要用来检测网线被拔这种异常的情况,因为这种异常没有关闭套接字
	void tick(int event_base){
        //cout<<"in tick "<<l.max_size()<<endl;
        time_t cur = time(nullptr);
        auto it = l.rbegin();
        while(it != l.rend()){
            sp_init_tm tmp = *it;
            if(cur > tmp->expire){//超时了,删掉他
                //让del函数去重新给it赋值
                t_out->del_time_obj(event_base,tmp->connfd);
                it = l.rbegin();
            }
            else{
                break;
            }
        }
	}
};
timeout* timeout::t_out = new timeout();
 timeout* timeout::get_Timeout(){
     return t_out;
 }
static void sig_alrm(int);
int epoll_base;
bool timeout_flag = false;
int main(int argc,char** argv){
    int port = 9996;
 	if( argc > 1 )
    {
        port = atoi( argv[1] );
    }
   // const char* ip = argv[1];
  	const char* ip = "127.0.0.1"; 
   // const char* ip = "49.234.62.42";
    int ret = 0;
    struct sockaddr_in address;
    bzero( &address, sizeof( address ) );
    address.sin_family = AF_INET;
    inet_pton( AF_INET, ip, &address.sin_addr );
    address.sin_port = htons( port );

    int listenfd = socket( PF_INET, SOCK_STREAM, 0 );
    assert( listenfd >= 0 );

    //level是事务的等级
    int reuse = 1;
    setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&reuse,sizeof(reuse) );
    ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) );
    assert( ret != -1 );

    ret = listen( listenfd, listen_num );
    assert( ret != -1 );
    if(signal(SIGALRM,sig_alrm) < 0){
        cout<<"sig_alrm error"<<endl;
        return -1;
    }

    //创建epoll时间
    epoll_base = epoll_create(listen_num);
    //创建一次最大能处理多少个套接字,设置大小也是为了存放事件的下标
    epoll_event events[MAX_EVENTS_NUM];

    //不需要0阿1啊这样的下标
    epoll_event listenfd_event;
    listenfd_event.data.fd = listenfd;
    listenfd_event.events = EPOLLIN;
    epoll_ctl(epoll_base,EPOLL_CTL_ADD,listenfd,&listenfd_event);
    
    cout<<"服务器启动成功"<<endl;
    char buff[1024] = { 0 }; 
    while(1){
        if(!timeout_flag)//只有这样才可以触发定时信号
        {
            alarm(TIME + 3);//给个3秒缓冲
            timeout_flag = true;
        }
    	int event_num = epoll_wait(epoll_base,events,MAX_EVENTS_NUM,-1);
    	if(event_num < 0){
    		//cout<<"epoll_wait 出错了:"<<strerror(errno)<<endl;
            //产生中断的话不退出
            if(errno == EINTR)continue;
            else{
    		    break;
            }
    	}
    	for(int i = 0;i < event_num;++i){
            int connfd = events[i].data.fd;
    		if(connfd == listenfd){
    			//int connfd
                timeout::get_Timeout()->add_time_obj(epoll_base,connfd,listenfd);
    		}
            else if(events[i].events & EPOLLIN){
                timeout::get_Timeout()->adjust_obj(connfd);
                int r = recv(connfd,buff,sizeof(buff),0);
                if(r == 0){
                    timeout::get_Timeout()->del_time_obj(epoll_base,events[i].data.fd);
                    continue;
                }
                buff[r] = '\0';
                cout<<"收到一条消息:"<<buff<<endl;
            }
    	}
    }
    timeout::get_Timeout()->del_Timeout();
    close(listenfd);
    return 0;
}

static void sig_alrm(int){
    timeout::get_Timeout()->tick(epoll_base);
    timeout_flag = false;
}

/*遇到的难点
*1、智能指针
*2、rbegin和begin的迭代器类型是不一样的
*3、超时会产生中断信号,导致epoll错误
*
*
*/

谈一谈此定时器缺点

1、这只是一个小demo,并没有考虑线程安全,比如多个线程共同添加客户端的套接字,可能会造成数据混乱,所以如果在多线程的模式下,一定要在插入的时候加上一把锁。
2、不适用超时时间不相等的服务器的上。

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值