C++服务开发入门指南
  • 序言
  • 前言
  • 一、一个简单的服务
    • 1 什么是服务
    • 2 服务可以用来做什么
    • 3 简单服务框架
  • 二、网络通信服务框架
    • 1 网络服务的基本概念
    • 2 增加监听端口
    • 3 处理客户端会话
    • 小结
  • 三、添加基础模块
    • 1 日志模块
    • 2 定时器
    • 3 事件机制
    • 4 线程池
    • 5 线程安全
    • 小结
  • 四、一个聊天服务
    • 1 需求描述及分析
    • 2 概要设计
    • 3 创建服务项目
    • 4 ClientUser实现
    • 5 RoomMgr实现
    • 6 ChatRoom实现
    • 7 RoomIDMgr实现
    • 小结
  • 五、测试、迭代及重构
    • 1 测试
    • 2 迭代
    • 3 重构
    • 4 版本号
  • 六、架构设计
    • 1 单点服务
    • 2 分布式服务
  • 七、部署及发布
    • 1 部署环境
    • 2 编译环境
    • 3 部署服务
    • 4 发布服务
  • 八、线上问题处理
    • 1 线上问题
    • 2 问题处理
  • 九、程序员的职业规划
    • 职业规划
Powered by GitBook
On this page
  • 1、需求分析
  • 2、定时器实现
  1. 三、添加基础模块

2 定时器

定时器是服务端开发比较常用的一个功能,但C++的标准库中,并没有提供一个定时器,本节我们创建一个简易定时器。

1、需求分析

定时器应该可以实现一次性定时器,在指定时间之后,直接回调函数的功能,其调用形式如下:

Timer.NewTimer(300, [](){
   LogInfo("call this func after 300ms"); 
});

我们还希望定时器可以在A函数中添加,在B函数中删除

bool SetTimer(Timer& timer); // 添加定时器
bool KillTimer(Timer& timer); // 删除定时器
bool OnTimer(Timer& timer);  // 执行定时器

为了识别是否是同一个定时器,我们需要定义一些Timer的属性用于比较,可以设计结构如下

struct Timer{
	int    nType;    //定时器类型
	void * nOwner;   //定时器所有者
	time_t tExpTime; //到期时间
	int    nDataLen; //后面跟的数据长度
}

判断一个定时器是否到期,需要有一个线程,不断轮询。我们可以定义轮询周期为50ms,这样一个定时器到期后,最多再过50ms就可以检测到。通过优化,精度可以进一步提高。

定期器到期后,执行其回调函数,最好是新起一个线程执行,或者将其交给一个执行任务的线程池。本节为实现简便,直接在定时器线程内执行回调函数。

2、定时器实现

先实现Timer类

typedef std::function<void()> TimerCBFunc;

class CHYTimer
{
public:
    bool Copy(CHYTimer& timer){
        nType = timer.nType;
        nOwener = timer.nOwener;
        tExpTime = timer.tExpTime;
        nDataLen = timer.nDataLen;
        if (nDataLen != 0)
        {
            memcpy(this+1, &timer+1, nDataLen);
        }
        return true;
    }

    void SetTimerMilli(int milli);
    void SetTimer(int sec);

    static void NewTimer(unsigned int millisecond, TimerCBFunc func);

public:
    int    nType   = 0;
    void*  nOwener = nullptr;
    time_t tExpTime = 0;
    int    nDataLen = 0;
    int    nReserve = 0;
};

Timer类添加了上文提到的四个成员变量,同时预留一个Reserve变量,用于以后改进扩充。

nDataLen表示Timer后面接的数据长度,这样以后方便实现timer的派生类,根据数据长度确定一个Timer的边界。这样基类处理timer的时候,可以做好timer的拷贝,而不用关心派生timer的类型。

Timer实现了Copy函数,在后面有数据的情况下,要一起复制拷贝。

SetTimerMilli和SetTimer是两个便利函数,用于设置定时器时间,函数实现是直接计算出过期时间,赋值到tExpTime变量。

NewTimer做了一个静态函数,方便快速实现一个一次性定时器,到期后直接回调参数传入的回调函数。

Timer的管理类实现如下

typedef std::function<void(CHYTimer*)> DefaultTimerCBFunc;

typedef std::mutex Mutex;
typedef std::lock_guard<Mutex> AutoLock;

class TimerMgr
{
private:
    TimerMgr();
    bool Init();

public:
    static TimerMgr* Instance();

public:
    void SetDefaultCallBackFunc(DefaultTimerCBFunc func){m_funcDefaultCB = func;}

    CHYTimer* SetTimer(CHYTimer& timer, TimerCBFunc func = nullptr);

    bool KillTimer(CHYTimer& timer);

private:
    unsigned long	WorkThreadFunc();

    bool DelTimer(CHYTimer* timer); // 从列表中删除Timer

    bool DoFunc(CHYTimer* timer);

private:
    std::list<CHYTimer*>         m_listTimer;
	Mutex                        m_lockListTimer;

    DefaultTimerCBFunc           m_funcDefaultCB = nullptr;

    std::unordered_map<CHYTimer*, TimerCBFunc> m_mapFunc;
};

管理类有四个成员变量。

m_listTimer用于保管所以的Timer对象,使用list是因为保存的timer需要按照到期时间内排序,所以先排除了map和unordered_map;定时器到期后经常会从头部删除定时器,因此不选择vector。模版类型使用CHYTimer的指针,而不是CHYTimer类型,是因为stl容器在push的时候会拷贝一份元素,使用指针会减少拷贝开销。

m_mapFunc 用于保存timer对应的回调函数,一般用于保存一次性回调函数。没有设置一次性回调函数的,会使用默认回调函数m_funcDefaultCB。

m_lockListTimer用于保护成员数据m_listTimer和m_mapFunc。因为定时器可能在任意线程调用,多线程安全需要加锁保证。

TimerMgr产生一个全局对象,初始化时启动一个线程检测是否有到期定时器。

bool TimerMgr::Init()
{
    std::thread th(&TimerMgr::WorkThreadFunc, this);
	th.detach();
    return true;
}

线程主要逻辑是:

定时器在插入的时候进行排序,因此整个定时器list是有序的。所以每次检测时,只检测第一个定时器是否到期即可。定时器到期后执行回调,删除定时器后不进行休眠,立即进行下一次检测,这样,如果有几个定时器同时到期,按列表顺序逐个处理即可,直到定时器列表为空,活着下一个定时器没有到期。

unsigned long TimerMgr::WorkThreadFunc()
{
    while (true)
    {
        int nListSize = 0;
        {
            AutoLock lock(m_lockListTimer);
            nListSize = m_listTimer.size();
        }
        
        if (nListSize == 0)
        {
            // 检测没有定时器,直接sleep 50ms再检测
            std::this_thread::sleep_for(std::chrono::milliseconds(TIMER_INTERVAL_CHECK));
            continue;
        }
        time_t tNow = GetTimeStamp();

        // 取第一个定时器查看,是否到期
        CHYTimer* pTimer = nullptr;
        {
            AutoLock lock(m_lockListTimer);
            pTimer = m_listTimer.front();
        }
        if (pTimer)
        {
           if (tNow >= pTimer->tExpTime) //定时器过期,执行定时器回调,删除定时器
            {
               
                DoFunc(pTimer);
                DelTimer(pTimer); // 删除这个timer
            }
            else // 定时器还没到,看到期时间和50ms哪个更小,sleep较小的时间
            {
                int nSpleep = std::min((int)(pTimer->tExpTime - tNow) / 1000, TIMER_INTERVAL_CHECK) ;
                std::this_thread::sleep_for(std::chrono::milliseconds(nSpleep));
            }
        }        
    }
    return 0;
}

添加删除也很简单。

添加时注意拷贝一份新的Timer存储到list中,避免调用方执行完毕数据回收;添加时回调函数func可以是nullptr,这里不用管,直接赋值到map中。添加完成后,需要进行排序。这里的排序规则是,到期时间靠前的,排在前面,检测时只检测第一个。

删除时根据owner和type识别是否是同一个timer,目前实现仅删除距离到期最近的一个定时器,在添加定时器的时候,没有做查重,所以,相同的owner和type可以有多个定时器,管理类不做限制。

删除timer时需要从timer list和func map中同时删除,由于map存储的key是timer的指针,因此,owner和type相同的两个timer,其指针也不相同,不会造成误删回调函数。

最后,删除时要将timer分配的空间删除,调用free,对应添加timer时的new。

bool compareTimer(CHYTimer* timer1, CHYTimer* timer2)
{
    return timer1->tExpTime < timer2->tExpTime;
}

CHYTimer* TimerMgr::SetTimer(CHYTimer& timer, TimerCBFunc func)
{
    int nSize = sizeof(CHYTimer) + timer.nDataLen;
    char* t = new char[nSize];
    CHYTimer* pTimer = (CHYTimer*)t;
    pTimer->Copy(timer);
    {
        AutoLock lock(m_lockListTimer);
        m_listTimer.push_front(pTimer);
        m_mapFunc[pTimer] = func;
        m_listTimer.sort(compareTimer);
    }
    
    return pTimer;
}

bool TimerMgr::KillTimer(CHYTimer& timer)
{
    return DelTimer(&timer);
}

bool TimerMgr::DelTimer(CHYTimer* timer)
{
    AutoLock lock(m_lockListTimer);
    auto itr = std::find_if(m_listTimer.begin(), m_listTimer.end(), [&](CHYTimer* pTimer){
        return (timer->nOwener == pTimer->nOwener && timer->nType == pTimer->nType);
    });

    if (itr != m_listTimer.end())
    {
        CHYTimer* pTimer = (CHYTimer*)*itr;
        m_listTimer.erase(itr);
        m_mapFunc.erase(pTimer);
        char*t = (char*)pTimer;
        delete []t;
    }
    return true;
}

最后时执行回调部分。

优先从函数map中取出定时器对应的回调函数,若添加时未定义回调,则判断是否存在默认回调函数,并使用默认回调函数。

此处处理是在定时器的检测线程执行的,如果回调函数中的逻辑比较耗时,会影响定时器的精度,导致后续定时器延迟。

改进方式是使用任务线程池,将到期的定时器交给线程池处理,可以提高定时器的执行效率。目前简易版本,先不增加线程,后续有时间再优化。

bool TimerMgr::DoFunc(CHYTimer* timer)
{
    TimerCBFunc func = nullptr;
    {
        AutoLock lock(m_lockListTimer);
        func = m_mapFunc[timer];
    }

    if (func)
    {
        func();
    }
    else if(m_funcDefaultCB)
    {
        m_funcDefaultCB(timer);
    }
    
    return true;
}

3、定时器使用

定时器基本功能已经实现,可以有两种调用方式

	LogInfo( "this is a message before timer" );
	CHYTimer::NewTimer(13000,[](){
		LogInfo( "this is a message after 13s by timer" );
	});
	CHYTimer::NewTimer(3000,[](){
		LogInfo( "this is a message after 3s by timer" );
	});
	CHYTimer::NewTimer(8000,[](){
		LogInfo( "this is a message after 8s by timer" );
	});

第一种一次性调用,传入定时器回调函数,定时器到期后执行回调函数,结合日志输出的时间可以看到,定时器已经生效

12/01/22 13:37:11 00000000 [Info]this is a message before timer
12/01/22 13:37:14 00000000 [Info]this is a message after 3s by timer
12/01/22 13:37:19 00000000 [Info]this is a message after 8s by timer
12/01/22 13:37:24 00000000 [Info]this is a message after 13s by timer

第二种将定时器设置为CServiceBase基类函数,以后的派生服务可以重写函数

void CServiceBase::SetTimer(CHYTimer * pTimer)
{
	TimerMgr::Instance()->SetTimer(*pTimer);
}

void CServiceBase::KillTimer(CHYTimer * pTimer)
{
	TimerMgr::Instance()->KillTimer(*pTimer);
}

void CServiceBase::OnTimer(CHYTimer * pTimer)
{
	switch (pTimer->nType)
	{
	case 1:
		LogInfo("this is Service Timer " );
		break;
	
	default:
		break;
	}
}

定义一个20秒的定时器,调用代码如下

	CHYTimer timer;
	timer.SetTimer(20);
	timer.nType = 1;
	SetTimer(&timer);

输出日志为

12/01/22 13:43:28 00000000 [Info]ServiceBase Run ...
12/01/22 13:43:48 00000000 [Info]this is Service Timer
Previous1 日志模块Next3 事件机制

Last updated 2 years ago