前面,写了一篇关于多级缓冲服务的文章。
//记录每个队列的数据容器
struct _SMBlock
{
T* m_pT; //数据对象
int m_nID; //数据当前编号
bool m_blUse; //是否在使用true是正在使用,false是没有使用
time_t m_ttUpdateTime; //DS服务器更新完成后回写的信息时间。
_SMBlock()
{
m_pT = NULL;
m_nID = 0;
m_blUse = false;
}
};
那么今天,我们就来点实际的代码,完成以上的所有功能吧。
按照昨天的思路,我需要两个程序,一个是和客户端通讯的程序,这个程序我们姑且认为它就是游戏服务器,那么,与之对应的,还有一个专门负责和后来存储介质通讯的服务进程。
既然要做这道菜,先看看我们需要点什么佐料。
(1)一个共享内存的类,这个类提供给我们与共享内存交互的功能,对外的接口需要,获得一个内存指针地址,获得当前已有的数据个数,删除其中一个数据,得到自由的内存块个数等等。
(2)我需要一个提供MRU算法的类,用于管理我的有效的共享内存指针,并提供相应的替换算法。
(3)对应我的开源服务器,我需要实现一个dll(或者so),来处理玩家创建,登陆,离开,更新,查询操作。
(4)我需要一个IO的类,用于如果我在内存中命中不到数据的时候,可以从IO里进行查找获得数据。
(5)一个定时执行的类,用于数据进程定时刷入IO接口。
好的,让我们开始把。
对于(1),我需要组织一个支持windows和linux共享内存的接口。要能自动根据操作系统的不同选用不同的方法,并实现一个模板类,完成对共享内存的管理,对于我而言,每个数据都是一个T*,当共享内存创建的时候,我需要指定一个T的个数,我会根据这个sizeof(T)*nCount来创建一整块巨大的内存,从里面切分出不同的T*,这样没有内存碎片。同时,还需要共享内存提供一个"头"的数据空间,用来标明每个T*的使用状态,当然,可能聪明的你已经发现,我使用的是sizrof(T),这样是不是有问题呢?因为对于玩家而言,我可能有玩家的数据,还可能有各种的数组,比如装备格子,技能格子等。这里我要强调一下,为了保证我对数据的统一管理和检查,我要求玩家数据必须是定长的,也就是说对于玩家数据vector,dueue,map等STL容器以及带缓冲的容器是不被允许的,因为变长会导致出错后内存查找的困难。当然,你在逻辑计算过程中,可以使用这个。但是元数据一定是需要定长的。如果你有兴趣,可以尝试在这里改造成变长的。
我的共享内存实现,你可以查看CSMAccessObject类和ShareMemoryAPI类,基础知识。
关键借助这两个类,我实现了一个CSMPool。
里面包含了这样一个数据头结构:
这个结构就是一个完整的数据"头",这里我要解释一下,DS是啥,这个是我私自起的名字(DataServer服务进程的简称,就是我说的共享内存和IO同步所执行的进程)。在这里m_ttUpdateTime是由DS负责修改,当IO写入成功之后,需要更新这个变量,这样,只要比对t*中的时间戳和这个时间戳,我就知道哪些数据需要我更新到IO里面,因为很有可能,大部分数据在某时刻是不需要更新的。m_pT就是你的数据类,这个类实现是在PlayerObject.h里面,这里面有一个基类CObject是需要被填充的。而实际体class CPlayerData : public CObject
我先说说,CObject有什么关键性数据:
//数据结构体的基类 class CObject { public: CObject() { m_blWrite = false; m_ttUpdateTime = time(NULL); }; virtual ~CObject() {}; void EnterWrite() { m_blWrite = true;} void LeavelWrite() { m_ttUpdateTime = time(NULL); m_blWrite = false;} bool GetWriteSate() { return m_blWrite; } #define ENTERWRITE() EnterWrite(); //定义写入的宏 #define LEAVELWEITE() LeavelWrite(); //定义写完的宏 private: bool m_blWrite; //写标记 public: time_t m_ttUpdateTime; //数据更新时间,DS服务器会更具这个时间来决定是否更新。 };
复制代码这个类有一个更新写标记,这个写标记给DS使用的,当DS判断写标记正在写入的时候,就不会存储这些数据,等到下一次执行的时候在存储,同时当写标记完成的时候,基类自动更新m_ttUpdateTime这个时间戳,DS会比对_SMBlock.m_ttUpdateTime和T->m_ttUpdateTime的数值,看看需要不需要进行存储。继承这个类,当数据发生修改的时候,一定要套用写入宏,比如这样:
void Create(const char* pPlayerName) { ENTERWRITE(); //标记写标记 //你要做的事情在这里做 sprintf_safe(m_szPlayerName, 50, "%s", pPlayerName); m_nPlayerID = 0; m_nLevel = 1; LEAVELWEITE(); //释放写标记 };
复制代码这样就能最大程度的保证数据的完整性,尽量减少存入半截数据的风险。
我的CPlayerData类只是举一个例子,当然,你可以为这个类提供更多的变量和方法。根据你的需求而定。
好了,话说回来。我的CSMPool是个什么样子呢?
class CSMPool { private: //记录每个队列的数据容器 struct _SMBlock { }; public: CSMPool() { }; ~CSMPool() { }; bool Init(SMKey key, int nMaxCount) //根据一个key打开或者新建指定的共享内存单元,并指定块数。 { }; void Close() //当共享内存需要关闭的时候需要做的一些事情。 { } T* NewObject() //获得一个新的T*(CPlayerData指针) { }; bool DeleteObject(T* pData) //删除一个没用的T*,这不是真的删除了共享内存,只是将此块内存指针归还给free指针列表。 { }; int GetFreeObjectCount() //得到共享内存池中可用的空闲内存块的个数 { } int GetUsedObjectCount() //得到共享内存池中已有的内存块得个数 { }; T* GetUsedObject(int nIndex) //根据ID得到相应的内存块指针 { }; const time_t GetObjectHeadTimeStamp(T* pData) //得到_SMBlock时间戳 { }; bool SetObjectHeadTimeStamp(T* pData) //修改指定的_SMBlock时间戳,只有DS会干 { }
复制代码(2)MRU算法
CMapTemplate类的实现,这部分代码我改进了当初我写的MRU代码文章,添加了几个函数和扩展了一些函数参数来满足我的需求。具体可以参考MapTemplate.h
(3)对应我的PruenessScopeServer框架,我只需要创建一个dll工程,引用一些头文件就可以完全不用框架代码了。具体引用的头文件在IObject目录里面,这样,我可以无视PruenessScopeServer是否存在,只要专心开发我的业务逻辑即可。
当然,规范还是要有的,具体看看我的PlayerPool.cpp,里面90%的代码写法都是固定的。可以和开源框架中的Base的dll工程比较一下,呵呵,唯一不同的就是,我这个类需要支持以下处理方法。
#define COMMAND_PLAYINSERT 0x1010 //用户数据创建
#define COMMAND_PLAYUPDATE 0x1011 //用户数据更新
#define COMMAND_PLAYDELETE 0x1012 //用户数据删除
#define COMMAND_PLAYSEACH 0x1013 //用户查询
#define COMMAND_PLAYLOGIN 0x1014 //用户登陆
#define COMMAND_PLAYLOGOFF 0x1015 //用户离开
客户端会给我以上的调用,那么,我们来根据以上的方法去实现代码吧。
对应以上需求,我定义了:
CPlayerData* Do_PlayerInsert(const char* pPlayerNick); bool Do_PlayerUpdate(CPlayerData* pPlayerData); bool Do_PlayerDelete(const char* pPlayerNick); CPlayerData* Do_PlayerSearch(const char* pPlayerNick); CPlayerData* Do_PlayerLogin(const char* pPlayerNick); bool Do_PlayerLogOff(const char* pPlayerNick);
复制代码以上的方法来实现对这些命令的处理。具体方法可以参考PlayerPoolCommand.cpp的实现。这里就不多说了。
(4)对于共享内存和介质之间的操作。
为了举例,我不用数据库,使用文件来说明,假设我的文件就是我的数据源。当然,你可以用你的数据库引擎替代这里的实现。
bool DeletePlayer(const char* pPlayerNick); //删除一个用户数据文件 bool SavePlayer(CPlayerData* pPlayerData); //保存创建用户的数据 CPlayerData* GetPlayer(const char* pPlayerNick); //这里在IO里面查找,找到了就new一个CPlayerData对象出来,返回给上层,由上层用完负责删除
复制代码这里你可以填充你的代码。
(5)这里因为我用的是ACE的框架,所以自然也就用ACE的定时器,比较手熟,当然,也可以用你自己喜欢的定时器替换。
typedef ACE_Thread_Timer_Queue_Adapter<ACE_Timer_Heap> ActiveTimer; //定时器处理类(处理定时数据更新) class CTimeHeart : public ACE_Event_Handler { public: CTimeHeart(); ~CTimeHeart(); void Init(); virtual int handle_timeout(const ACE_Time_Value &tv, const void *arg); private: CSMPool<CPlayerData> m_UserPool; //共享内存池 CIOData m_IOData; //IO数据接口 bool m_blRunState; //处理是否正在运行 SMKey m_key; //共享内存Key }; class CTimeManager { public: CTimeManager(void); ~CTimeManager(void); void Init(); bool Start(int nTimeIntervel); void KillTimer(); private: ActiveTimer m_ActiveTimer; CTimeHeart m_TimeHeart; int m_nTimerID; };
复制代码
代码很简单,其实是我最喜欢的,因为越简单的代码,出错机会越低。
以上完整代码如下:我在window7+VS2005下测试通过,linux版本还没测试,等有时间我在上面编译一下,我相信会很顺利的。
好了,把PlayerPool编译一下,生成dll,然后再框架的配置文件main.conf里面添加
ModuleString=PlayerPool.dll
行了,PurenessScopeServer框架启动就会加载PlayerPool模块,并把相关PlayerPool的消息给它。就这么简单,简单吧。
洋洋洒洒写了这么多,就是为了举个例子,当然,我希望你能够在我的例子上,加上你的想法,并把它改的更加高效,这才是进步。如果愿意,你也可以在这里分享给大家你的改进结果。
我一直认为,技术这种东西,是可以后天学习的。但是信仰,才会使我们走的更远。
学习就是这样,先走别人走过的路,然后根据自己的感悟和习惯,融合成属于自己的实现,这样的程序,才是优秀的程序,把你的思维和理解留在代码的字里行间,并让那些追逐梦想的后者,从中获益。只有敢于让别人踩在你的肩膀上,信念才会传承,而路也会越走越远,不是吗?
代码如下:
PlayerPool.rar (32.69 KB)
DataServer.rar (19.45 KB)
测试用例:
TestPrue.rar (9.03 KB)
附件下载在Linux公社的1号FTP服务器里,下载地址:
FTP地址:ftp://www.linuxidc.com
在 2011年LinuxIDC.com\7月\多级缓冲的服务器数据服务机制实现