游戏服务器引擎的设计(二)开发游戏服务器底层 及libuv使用

服务器底层,个人任务认为稳定、高效、易用最重要。如果非要排个序的话 稳定 > 易用 > 高效。

我是用的libuv这个库作为基础支持库的。为啥用它,主要是网络库不想自己写了,有现成的最好。这个库是轻量级的库而且跨平台,windows下分装了IOCP,linux下分装了EPOOL。 然后这个库带了一些其他接口,比如说基础的定时器 都是挺好用的,所以就用了。

我写的底层库打算实现一个空的框架,所有业务必须以模块的方式注册进去,给出必要的接口,业务逻辑上层去实现。如下所示:

// 核心服务
#ifndef ServerCore_h__
#define ServerCore_h__

#include "Common.h"
#include 
#include 

namespace SCore
{
#define LOGICSERVICE_PRIVATE_DATA_SIZE 16

	// 逻辑服务
	class ILogicService
	{
		// 事件处理函数类型
		typedef void (*EventSyncFunc)(ILogicService* self, void* param);

		// 定时器回调函数类型
		typedef void (*TimerProcFunc)(ILogicService* self, void* param);

		friend class ServerCore;
	public:
		ILogicService();
		virtual ~ILogicService();

		// 加载配置
		virtual void LoadConfig() {};

		// 初始化
		virtual void OnInit() { }

		// 心跳
		virtual void OnUpdate() {};

		// 销毁时被调用
		virtual void OnDestory() { SetDestoryFinish(); };

		// 设置销毁完成, 当设置销毁完成后,才能正式从核心服务中移除,所有其他销毁动作必须在此之前执行
		void SetDestoryFinish();

		// 获取状态
		virtual int GetStatus() { return m_nStatus; }

		// 设置心跳时间间隔
		void SetHeartInterval(int intervalTime = ServiceHeartInterval_None);

		// 获取服务类型
		int GetServiceType();

		// 获取服务器ID
		long long GetServiceID();

		// 注册事件同步处理接口
		void RegisterSyncEvent(int eventID, EventSyncFunc func);

		// 移除事件同步处理接口
		void UnRegisterSyncEvent(int eventID, EventSyncFunc func);

		// 触发同步事件
		// @eventID		事件ID
		// @param		参数
		void TriggerSyncEvent(int eventID, void* param);

		// 触发同步事件
		// @eventID		事件ID
		// @param		参数
		// @serviceID	指定serviceID 逻辑服务处理
		void TriggerSyncEvent(int eventID, void* param, long long serviceID);

		// 触发同步事件
		// @eventID		事件ID
		// @param		参数
		// @serviceType	指定逻辑服务类型处理
		void TriggerSyncEventByType(int eventID, void* param, int serviceType);

		// 添加定时器
		// @startTime	定时器开始执行时间(多少毫秒后开始执行)
		// @repeat		非单次执行的定时器,每次执行的时间间隔ms 如果为0 则只执行一次
		// @maxRunCount 执行总次数,如果为 0 则不判断
		// @cb			回调函数
		// @param		回调函数参数(指针,具体参数内容要业务逻辑自己保存)
		// @return		返回定时器ID
		long long AddTimer(int startTime, int repeat, int maxRunCount, TimerProcFunc cb, void* param);

		// 移除定时器
		void RemoveTimer(long long id);

	private:
		long long m_lID = 0;		// 唯一ID
		int m_nType = 0;			// 逻辑服务类型 ServiceType
		int m_nHeartType = 0;		// 心跳类型	ServiceHeartInterval
		int m_nStatus = 0;			// 服务状态 ServiceStatus

		// 私有数据
		char m_csCoreData[LOGICSERVICE_PRIVATE_DATA_SIZE];		
	};


	typedef void (*RemoveFinishFunc)(ILogicService*);

	// 服务器核心
	class ICore
	{
	public:

		// === 逻辑服务 begin======================================================
		// 注册逻辑服务
		virtual void RegisterLogicService(ILogicService* pService, int serverType) = 0;
		
		// 查找逻辑服务
		virtual ILogicService* FindLogicService(long long id) = 0;

		// 查找逻辑服务
		virtual ILogicService* FindLogicServiceType(int serviceType) = 0;

		// 移除逻辑服务
		virtual void RemoveLogicService(long long id) = 0;

		// 移除逻辑服务
		virtual void RemoveLogicService(ILogicService* pService) = 0;

		// 添加逻辑服务移除成功回调
		virtual void RegisgerLogicServiceRemoveCB(RemoveFinishFunc cb) = 0;

		// ===== 逻辑服务  end ======================================================
	
		// ========== 核心 方法 ==========================================

		// 启动核心服务
		// @beforeFunc 启动前调用
		// @finishFunc 启动成功后回调
		virtual bool StartCore(std::function beforeFunc) = 0;

		// 关闭核心服务
		// @beforeFunc 关闭前调用
		// @finishFunc 关闭成功后回调
		virtual bool StopCore(std::function afterFunc) = 0;

		// 同组服务器唯一ID
		virtual unsigned short GetID() = 0;

		// 服务器类型
		virtual unsigned short GetType() = 0;

		// 服务器名称
		virtual const char* GetName() const = 0;

		// 获取当前时间
		virtual time_t GetNow() = 0;
	};

	extern ICore* GetCore();
}

class ICore: 这个是全局唯一的一个核心接口, 调用GetCore()将获取唯一指针,而内部只有几个接口:

【1】关于注册、销毁和查找逻辑服务ILogicService的接口;

【2】启动和关闭服务的接口;

【3】几个关于服务器信息的接口包括一个当前时间的接口。

class ILogicService 这个是逻辑服务(所有业务层服务的基类)介绍一下: 

// 加载配置
virtual void LoadConfig() {};

这个接口将在注册服务的时候,逻辑服务器启动前由核心调用,用于加载配置,如果有热更新的话将会重新调用该接口;

// 初始化
virtual void OnInit() { }

这个接口将在逻辑服务器启动前加载配置后面由核心调用,用于初始化业务逻辑

// 心跳
virtual void OnUpdate() {};

这个是心跳接口,如果初始化的时候没有调用SetHeartInterval 设置心跳帧的等级,将不会启动心跳。当然在后期的业务逻辑中也可以调用SetHeartInterval添加心跳循环,这里只设置了已经定义好的几个等级:10ms, 30ms,50ms, 100ms, 500ms,1000ms 还有一个就是和ICore的心跳帧同步,而ICore的心跳帧将在配置文件中读取。

// 销毁时被调用
virtual void OnDestory() { SetDestoryFinish(); };

这个接口将在逻辑模块被销毁前调用,用于业务逻辑自定义销毁逻辑。自定义销毁完成需要调用

SetDestoryFinish(); 告诉核心销毁完成了。核心层才会继续销毁业务逻辑。加了这个接口是因为有些业务逻辑还没有处理完(比如libuv的回调机制),核心层强制销毁导致未知的错误。

下面是事件模块接口:

// 注册事件同步处理接口
void RegisterSyncEvent(int eventID, EventSyncFunc func);

// 移除事件同步处理接口
void UnRegisterSyncEvent(int eventID, EventSyncFunc func);

// 触发同步事件
// @eventID		事件ID
// @param		参数
void TriggerSyncEvent(int eventID, void* param);

事件模块接口时为了业务逻辑模块之间解耦用的,所有逻辑模块在业务关键点发布事件,比如玩家进入游戏事件、离开游戏事件、进入游戏事件等等,用TriggerSyncEvent接口抛出。

而所有需要关心该事件的模块都可以RegisterSyncEvent接口在初始化中注册订阅,或则在任意地方注册进来。当事件产生的会调用回调函数。整个业务系统用事件模式驱动。

而UnRegisterSyncEvent 用于事件的取消订阅。

定时器接口:

// 添加定时器
// @startTime	定时器开始执行时间(多少毫秒后开始执行)
// @repeat		非单次执行的定时器,每次执行的时间间隔ms 如果为0 则只执行一次
// @maxRunCount 执行总次数,如果为 0 则不判断
// @cb			回调函数
// @param		回调函数参数(指针,具体参数内容要业务逻辑自己保存)
// @return		返回定时器ID
long long AddTimer(int startTime, int repeat, int maxRunCount,
 TimerProcFunc cb, void* param);

// 移除定时器
void RemoveTimer(long long id);

AddTimer 用于注册定时器,RemoveTimer用于移除定时器。

ILogicService提供了这些基础接口,所有业务逻辑都必须继承该基类,然后注册到ICore中。这样基础框架就出来了。

下面============================================================

这里简单介绍一下libuv.(相关下载和编译网上很多,都可以搜索到的)

我认为libuv 本质上是一个任务队列分发器,相当于一个任务路由器,自己本身是一个死循环,在循环里不停的查看任务队列中是否有任务,有任务的话把任务取出来,投递到对应的任务处理逻辑中处理。 用户不停的投递任务,然后到处理任务的逻辑中写相应的逻辑。而像网络IO,文件IO,包括定时器这些libuv本身提供的功能,libuv库都把业务逻辑做了相应的处理,处理完了再向任务队列中投递一个处理完成的任务回调, 我们只要调用接口投递任务就行了,然后在处理完成的回调中做自己的业务处理。

libuv 自己提供的功能里大多数模块都会提供一个句柄handle,一般来讲想要用这个模块的话要先定义这个模块的handle, 以 uv_xxx_t 这种命名方式定义。

比如 uv_loop_t 这个是整个libuv 的循环句柄,也是总的句柄(可以定义多个但是没有意义),所有其他的功能句柄都要注册到这个里面,下面是uv_loop_t 的定义:

int main()
{

    // 可以这样声明定义
    uv_loop_t* m_pLoop = uv_default_loop();

    // 也可以这样
    uv_loop_t loop;
    uv_loop_init(&loop));

    // 下面开始运行
    uv_run(&loop, UV_RUN_DEFAULT);
}
uv_run(&loop, UV_RUN_DEFAULT);

这句代码就是开启死循环了,如果是要初始化的其他东西的话,只能在这句上面进行,或者在具体 的业务逻辑任务里处理,这句代码下面的内容永远执行不到,除非服务关闭了。

其他的句柄:uv_tcp_t  网络句柄(简单的理解成对SOCKET的封装)

                uv_shutdown_t 请求关闭模块句柄的句柄

                uv_write_t 发送消息时候的消息句柄

                uv_buf_t 发送与接收消息的时候的缓冲区句柄

                uv_connect_t 客户端连接服务器的连接句柄

                uv_timer_t 定时器句柄

等等。。。

模块句柄一般需要初始化的:以 int uv_xxx_init(uv_loop_t*, uv_xxx_t* handle);方式命名。

有些不需要初始化,不需要初始化的我称为假句柄(只是为了命名方式同一) 比如uv_buf_t,uv_write_t等等,

而需要初始化的那个,销毁的时候需要关闭的调用uv_close 关闭。并且要调用注册到uv_loop_t的特定接口。

        比如:

int r = uv_is_active((uv_handle_t*)(&m_uvTcpHandle));
	if (r == 0)
	{
        // 这里初始化tcp 句柄
		if (0 != uv_tcp_init(g_ServerCore.UV_Core(), &(m_uvTcpHandle)))
		{
			// ERROR
			return false;
		}
	}

uv_is_active这个是检查句柄是否处理活动状态,如果是非活动状态,就用uv_tcp_init初始化一下tcp句柄。需要注意的一点是所有libuv接口除了uv_close,基本上都是由返回值的,判断类型的接口不为0的是正确的(比如uv_is_active, uv_is_closing),而其他类型接口返回0 是执行正确的,非零值是内部错误码,调动uv_err_name(int ),和uv_strerror(int err)可以获取错误信息。最好是在调用接口的时候处理返回值。

还有需要注意的是新手在调用接口的时候很顺心,一旦走销毁处理逻辑的时候就一堆错误。无法正常关闭,主要是两个原因:【1】libuv内部还有handle没有被正确释放,【2】libuv 是任务回调机制,有可能只是执行了调用了销毁接口,但是内部还没有处理完,用户层的内存块都已经被销毁掉,出现野指针了。

关于具体的libuv层的业务,github上都有用例,这个就不说了,大家可以看下

你可能感兴趣的:(游戏开发,c++,后端,架构,游戏引擎,游戏程序)