服务器底层,个人任务认为稳定、高效、易用最重要。如果非要排个序的话 稳定 > 易用 > 高效。
我是用的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上都有用例,这个就不说了,大家可以看下