多级缓冲的服务器数据服务机制实现(二)

来源:互联网 发布:安达信软件怎么装 编辑:程序博客网 时间:2024/04/30 05:27

昨天,写了一篇关于多级缓冲服务的文章。
那么今天,我们就来点实际的代码,完成以上的所有功能吧。
按照昨天的思路,我需要两个程序,一个是和客户端通讯的程序,这个程序我们姑且认为它就是游戏服务器,那么,与之对应的,还有一个专门负责和后来存储介质通讯的服务进程。
既然要做这道菜,先看看我们需要点什么佐料。
(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。
里面包含了这样一个数据头结构:
  1. //记录每个队列的数据容器
  2. struct _SMBlock
  3. {
  4.   T*     m_pT;           //数据对象
  5.   int    m_nID;          //数据当前编号
  6.   bool   m_blUse;        //是否在使用true是正在使用,false是没有使用
  7.   time_t m_ttUpdateTime; //DS服务器更新完成后回写的信息时间。
  8.   _SMBlock()
  9.   {
  10.    m_pT    = NULL;
  11.    m_nID   = 0;
  12.    m_blUse = false;
  13.   }
  14. };
复制代码

这个结构就是一个完整的数据"头",这里我要解释一下,DS是啥,这个是我私自起的名字(DataServer服务进程的简称,就是我说的共享内存和IO同步所执行的进程)。在这里m_ttUpdateTime是由DS负责修改,当IO写入成功之后,需要更新这个变量,这样,只要比对t*中的时间戳和这个时间戳,我就知道哪些数据需要我更新到IO里面,因为很有可能,大部分数据在某时刻是不需要更新的。m_pT就是你的数据类,这个类实现是在PlayerObject.h里面,这里面有一个基类CObject是需要被填充的。而实际体class CPlayerData : public CObject
我先说说,CObject有什么关键性数据:
  1. //数据结构体的基类
  2. class CObject
  3. {
  4. public:
  5. CObject() { m_blWrite = false; m_ttUpdateTime = time(NULL); };
  6. virtual ~CObject() {};
  7. void EnterWrite() { m_blWrite = true;}
  8. void LeavelWrite() { m_ttUpdateTime = time(NULL); m_blWrite = false;}
  9. bool GetWriteSate() { return m_blWrite; }
  10. #define ENTERWRITE() EnterWrite(); //定义写入的宏
  11. #define LEAVELWEITE() LeavelWrite(); //定义写完的宏
  12. private:
  13. bool m_blWrite; //写标记
  14. public:
  15. time_t m_ttUpdateTime; //数据更新时间,DS服务器会更具这个时间来决定是否更新。
  16. };
复制代码

这个类有一个更新写标记,这个写标记给DS使用的,当DS判断写标记正在写入的时候,就不会存储这些数据,等到下一次执行的时候在存储,同时当写标记完成的时候,基类自动更新m_ttUpdateTime这个时间戳,DS会比对_SMBlock.m_ttUpdateTime和T->m_ttUpdateTime的数值,看看需要不需要进行存储。继承这个类,当数据发生修改的时候,一定要套用写入宏,比如这样:
  1. void Create(const char* pPlayerName)
  2. {
  3.   ENTERWRITE();  //标记写标记
  4.   //你要做的事情在这里做
  5.   sprintf_safe(m_szPlayerName, 50, "%s", pPlayerName);
  6.   m_nPlayerID = 0;
  7.   m_nLevel    = 1;
  8.   LEAVELWEITE();  //释放写标记
  9. };
复制代码

这样就能最大程度的保证数据的完整性,尽量减少存入半截数据的风险。
我的CPlayerData类只是举一个例子,当然,你可以为这个类提供更多的变量和方法。根据你的需求而定。
好了,话说回来。我的CSMPool是个什么样子呢?
  1. class CSMPool
  2. {
  3. private:
  4. //记录每个队列的数据容器
  5. struct _SMBlock
  6. {
  7. };
  8. public:
  9. CSMPool()
  10. {
  11. };
  12. ~CSMPool()
  13. {
  14. };
  15. bool Init(SMKey key, int nMaxCount)   //根据一个key打开或者新建指定的共享内存单元,并指定块数。
  16. {
  17. };
  18. void Close()     //当共享内存需要关闭的时候需要做的一些事情。
  19. {
  20. }
  21. T* NewObject()   //获得一个新的T*(CPlayerData指针)
  22. {
  23. };
  24. bool DeleteObject(T* pData)  //删除一个没用的T*,这不是真的删除了共享内存,只是将此块内存指针归还给free指针列表。
  25. {
  26. };
  27. int GetFreeObjectCount()  //得到共享内存池中可用的空闲内存块的个数
  28. {
  29. }
  30. int GetUsedObjectCount()  //得到共享内存池中已有的内存块得个数
  31. {
  32. };
  33. T* GetUsedObject(int nIndex)  //根据ID得到相应的内存块指针
  34. {
  35. };
  36. const time_t GetObjectHeadTimeStamp(T* pData)   //得到_SMBlock时间戳
  37. {
  38. };
  39. bool SetObjectHeadTimeStamp(T* pData)   //修改指定的_SMBlock时间戳,只有DS会干
  40. {
  41. }
复制代码

(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   //用户离开

客户端会给我以上的调用,那么,我们来根据以上的方法去实现代码吧。
对应以上需求,我定义了:
  1. CPlayerData* Do_PlayerInsert(const char* pPlayerNick);
  2. bool         Do_PlayerUpdate(CPlayerData* pPlayerData);
  3. bool         Do_PlayerDelete(const char* pPlayerNick);
  4. CPlayerData* Do_PlayerSearch(const char* pPlayerNick);
  5. CPlayerData* Do_PlayerLogin(const char* pPlayerNick);
  6. bool         Do_PlayerLogOff(const char* pPlayerNick);
复制代码

以上的方法来实现对这些命令的处理。具体方法可以参考PlayerPoolCommand.cpp的实现。这里就不多说了。
(4)对于共享内存和介质之间的操作。
为了举例,我不用数据库,使用文件来说明,假设我的文件就是我的数据源。当然,你可以用你的数据库引擎替代这里的实现。
  1. bool DeletePlayer(const char* pPlayerNick); //删除一个用户数据文件
  2. bool SavePlayer(CPlayerData* pPlayerData); //保存创建用户的数据
  3. CPlayerData* GetPlayer(const char* pPlayerNick); //这里在IO里面查找,找到了就new一个CPlayerData对象出来,返回给上层,由上层用完负责删除
复制代码

这里你可以填充你的代码。
(5)这里因为我用的是ACE的框架,所以自然也就用ACE的定时器,比较手熟,当然,也可以用你自己喜欢的定时器替换。
  1. typedef ACE_Thread_Timer_Queue_Adapter<ACE_Timer_Heap> ActiveTimer;
  2. //定时器处理类(处理定时数据更新)
  3. class CTimeHeart : public ACE_Event_Handler
  4. {
  5. public:
  6. CTimeHeart();
  7. ~CTimeHeart();
  8. void Init();
  9. virtual int handle_timeout(const ACE_Time_Value &tv, const void *arg);
  10. private:
  11. CSMPool<CPlayerData> m_UserPool;     //共享内存池
  12. CIOData m_IOData;                    //IO数据接口
  13. bool    m_blRunState;                //处理是否正在运行
  14. SMKey   m_key;                       //共享内存Key
  15. }; 
  16. class CTimeManager
  17. {
  18. public:
  19. CTimeManager(void);
  20. ~CTimeManager(void);
  21. void Init();
  22. bool Start(int nTimeIntervel);
  23. void KillTimer();
  24. private:
  25. ActiveTimer m_ActiveTimer;
  26. CTimeHeart  m_TimeHeart;
  27. int         m_nTimerID;
  28. };
复制代码

代码很简单,其实是我最喜欢的,因为越简单的代码,出错机会越低。
以上完整代码如下:我在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)

原创粉丝点击