那麼今天,我們就來點實際的代碼,完成以上的所有功能吧。
按照昨天的思路,我需要兩個程序,一個是和客戶端通訊的程序,這個程序我們姑且認爲它就是遊戲服務器,那麼,與之對應的,還有一個專門負責和後來存儲介質通訊的服務進程。
既然要做這道菜,先看看我們需要點什麼佐料。
(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。
裏面包含了這樣一個數據頭結構:
- //記錄每個隊列的數據容器
- 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;
- }
- };
這個結構就是一個完整的數據"頭",這裏我要解釋一下,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的消息給它。就這麼簡單,簡單吧。
洋洋灑灑寫了這麼多,就是爲了舉個例子,當然,我希望你能夠在我的例子上,加上你的想法,並把它改的更加高效,這纔是進步。如果願意,你也可以在這裏分享給大家你的改進結果。
我一直認爲,技術這種東西,是可以後天學習的。但是信仰,纔會使我們走的更遠。
學習就是這樣,先走別人走過的路,然後根據自己的感悟和習慣,融合成屬於自己的實現,這樣的程序,纔是優秀的程序,把你的思維和理解留在代碼的字裏行間,並讓那些追逐夢想的後者,從中獲益。只有敢於讓別人踩在你的肩膀上,信念纔會傳承,而路也會越走越遠,不是嗎?
代碼如下: