游戏服务器引擎的设计(六)定时器设计

对于游戏服务器来说 定时器必不可少,我们设计定时器的方法一般有两种:

一种是设计一个定时器队列。对定时器的超时时间进行排序,每次在服务器的帧循环中从头开始检查定时器是否超时,如果超时了,把定时器从队列中移除,然后执行定时器回调函数,如果没有超时直接跳出循环。最简单的是我们利用STL库的map容器,map容器的内部实现是红黑树,遍历的时候是从小到大遍历, 我们把定时器的下次执行时间作为键, 每次在帧循环里遍历map容器,如果键小于当前时间则超时了,我们就执行定时器内容,然后删除定时器继续往后遍历。 如果键大于当前时间则没有超时直接跳出遍历。优化的版本是我们设计一个普通的队列,每次检查最小的超时时间,如果超时了就执行定时器,继续检查直到没有超时。而每次我们检查时会找出最小的那个时间节点,所以我们用最小堆算法,每次只查找最小的。这样不需要在每次的插入删除时进行排序,但是每次检查时需要求出最小的那个时间节点。

另一种是在帧上面做的定时器(原理上是个时间轮),这里我根据游戏服务器的特点,自己做了特殊的定时器。超时时间跟帧绑定,比如:我们的逻辑帧时间间隔时30ms, 而我们将在2000ms后执行定时器操作, 则我们把超时时间转换成帧数 (2000 + (30 - 1)) / 30 = 67,  意思我们将在67帧以后超时,如果我们的当前帧时第200帧,则我们将在第267帧执行定时器操作。 由此我们定义一个键值映射的容器(可以用std::map,也可以用std::unordered_map), 键是帧数,而值定义为该帧的超时队列。服务器每帧的循环中对这个容器查找,是否有该帧的超时队列。如果有则执行对列中所有定时器,然后删除掉。而我们在定时器插入的时候只需计算出所在帧数数,进行插入操作就好了。与正式的时间轮相比,精确度没有那么高,比如说某一帧执行时间超时了,那么后面每帧的时间都会超时,但是这个基本不会影响游戏的逻辑执行,而如果每帧都超时则服务器的压力到达极限了,不应该是定时器的问题,而要从其他的方面查找问题了,这种方式大大的减少了定时器的运算,时间基本上的O(1), 理论上定时器不限个数可以无限加。

下面贴一下我的部分代码片段:

// 定义定时器,和定时器容器,定时器帧超时容器

// 定时器相关定义:
struct TimerInfo
{
	TimerInfo() {}

	long long id = 0;	    // id
	int interval = 0;		// 时间间隔
	int maxCount = 0;		// 最大运行次数
	int curCount = 0;		// 当前已经运行次数
	ILogicService::TimerProcFunc func = NULL;	// 回调函数
	ILogicService* self = NULL;	// 逻辑服指针
	void* param = NULL;		// 回调函数参数
	LinkNode timeoutNode;	// 超时节点
};

// 定时器存储容器
StoreHash m_hashTimer;

// 定时器超时容器
StoreHash m_hashTimerOut;

// 添加操作:

// 添加定时器
long long ServerCore::LogicServiceAddTimer(ILogicService* pSelf, int startTime,
    int repeat, int maxRunCount, ILogicService::TimerProcFunc cb, void* param)
{
	if (m_bClosing || pSelf->m_nStatus >= ServiceStatus_DoDestory)
	{
		return 0;
	}

	// 创建定时器信息
	TimerInfo* pTimer = (TimerInfo*)je_malloc(sizeof(TimerInfo));
	if (!pTimer)
	{
		return 0;
	}

	long long id = m_TimerID++;
	new (pTimer) TimerInfo(id, repeat, maxRunCount, 0, cb, pSelf, param);
	pTimer->timeoutNode.front = pTimer->timeoutNode.next = NULL;
	m_hashTimer.Insert(id, pSelf->GetServiceID(), pTimer);

	// 添加到超时列表
	AddToTimeroutList(startTime, pTimer);

	return id;
}

// 添加到超时队列:// m_lCoreHeartCount 为当前帧序号

// 添加到超时列表
void ServerCore::AddToTimeroutList(int startTime, TimerInfo* pTimer)
{
	if (!pTimer)
	{
		return;
	}

	if (pTimer->timeoutNode.front)
	{
		LinkRemoveSelf(&pTimer->timeoutNode);
	}

	// 多少帧之后超时
	int heartCount = (startTime + (m_nHeartInterval - 1)) / m_nHeartInterval;
	if (heartCount == 0)
	{
		heartCount = 1;
	}

	// 超时心跳帧
	long long TimeoutHeart = m_lCoreHeartCount + heartCount;

	// 加入到超时心跳帧
	auto iter = m_hashTimerOut.find(TimeoutHeart);
	if (iter == m_hashTimerOut.end())
	{
		iter = m_hashTimerOut.Insert(TimeoutHeart, 0, LinkNode());
		if (iter == m_hashTimerOut.end())
		{
			// WARNING....
			return;
		}

		InitLink(&(iter->second));
	}

	LinkAddToTail(&(iter->second), &(pTimer->timeoutNode));
}

// 每帧遍历超时:// m_lCoreHeartCount 为当前帧序号

// 每帧超时定时器检查
void ServerCore::UpdateTimer()
{
	// 当前帧是否有超时定时器
	auto iter = m_hashTimerOut.find(m_lCoreHeartCount);
	if (iter == m_hashTimerOut.end())
	{
		return;
	}

	// 遍历所有超时定时器
	while (auto pNode = LinkFirst(&(iter->second)))
	{
		if (!LinkNodeValid(&(iter->second), pNode))
		{
			break;
		}

		TimerInfo* pTimer = LinkNodeData(&(iter->second), pNode, TimerInfo, timeoutNode);

		// 从超时定时器中移除
		LinkRemoveSelf(&(pTimer->timeoutNode));

		// 执行回调
		if (pTimer->func)
		{
			pTimer->func(pTimer->self, pTimer->param);
		}

		// 执行次数加1
		++pTimer->curCount;

		// 是否结束了
		if ((pTimer->maxCount > 0 && pTimer->maxCount <= pTimer->curCount) ||
            pTimer->interval <= 0)
		{
			// 结束,删除定时器
			m_hashTimer.Remove(pTimer->id);
			continue;
		}

		// 继续添加到下次执行的超时队列帧中
		AddToTimeroutList(pTimer->interval, pTimer);
	}

	// 移除当前帧节点
	m_hashTimerOut.erase(iter);
}

你可能感兴趣的:(游戏开发,服务器,架构,游戏,游戏程序,c++)