多級緩衝的服務器數據服務機制實現(二)

昨天,寫了一篇關於多級緩衝服務的文章。
那麼今天,我們就來點實際的代碼,完成以上的所有功能吧。
按照昨天的思路,我需要兩個程序,一個是和客戶端通訊的程序,這個程序我們姑且認爲它就是遊戲服務器,那麼,與之對應的,還有一個專門負責和後來存儲介質通訊的服務進程。
既然要做這道菜,先看看我們需要點什麼佐料。
(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的消息給它。就這麼簡單,簡單吧。

洋洋灑灑寫了這麼多,就是爲了舉個例子,當然,我希望你能夠在我的例子上,加上你的想法,並把它改的更加高效,這纔是進步。如果願意,你也可以在這裏分享給大家你的改進結果。
我一直認爲,技術這種東西,是可以後天學習的。但是信仰,纔會使我們走的更遠。
學習就是這樣,先走別人走過的路,然後根據自己的感悟和習慣,融合成屬於自己的實現,這樣的程序,纔是優秀的程序,把你的思維和理解留在代碼的字裏行間,並讓那些追逐夢想的後者,從中獲益。只有敢於讓別人踩在你的肩膀上,信念纔會傳承,而路也會越走越遠,不是嗎?

代碼如下:
發佈了4 篇原創文章 · 獲贊 13 · 訪問量 17萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章