遊戲服務端架構實現-設計一個高性能HandleMap

設計原因

在使用C++ 開發遊戲服務端中。 大多數我們的玩家對象 會使用一個 類 來表示。 比如 class Human 、class Player 、class Actor 等。在與客戶端通信的時候 我們往往會生成一個唯一的 ID 與一個 實例化的 玩家對象 相綁定。 方便客戶端發出如下的指令 (或者也叫協議) 如何高效的查找插入ID 對實時戰鬥類的遊戲的性能影響是非常大的。而且 我吃飽了撐着想造輪子。

一個典型的協議

攻擊 {
目標ID:12355,
使用技能ID:5
}

常用的實現

我見過大多數的服務端使用的是 std:map <unsigned int ,Player*> 類似的方式 來產生id 對 *Player 的映射。這樣的好處就是簡單 對於新生的ID 只需要 +1就可以了。 當數值達到 2^32 - 1時 表示 handle 用盡。
光查找來說這個性能在I5 4核心 處理器 100W次 大約300 毫秒左右。

《熱血傳奇》的實現

而我見過最牛逼的還是 熱血傳奇的服務端代碼( delphi ) 直接將 Object 的地址轉化爲 int 傳遞給客戶端 優點是兩者的互相轉化 0 消耗。而缺點也很顯而易見,客戶端可以僞造一個錯誤的ID 導致服務器崩潰。
所以爲了避免 這種情況 使用了windows 平臺 特有的 SEH 也就是
Try
轉換 以及運行邏輯
Except
輸出錯誤信息。
end;

調侃一下

而這種做法 一是典型的依賴 windows 而是 因爲SEH 並不是 0 消耗的 實際上 這樣降低了 程序的性能。 而且對於linux 的設計哲學來說, 應當暴露問題 而不是 隱藏起來 比如 也就是程序一旦遇到預期外的運行結果 應當,立馬宕機 保存事故現場。所以這要求 程序的設計者對自己的程序流程 需要有一個嚴格的把控。而windows 提供的SEH 讓很多 數據訪問 或者寫入 異常。不至於導致程序的崩潰。 讓程序看起來 “穩定” 了很多。這兩種設計哲學 好處壞處都很明顯。門檻高低 和 頭髮的多少。所以業界調侃。服務端開發 分爲 “服務端開發” 和 “windows 服務端開發” 。

性能對比

那麼我考慮從新設計一種比這個快速的 映射列表。最快的查找當然是下標直接訪問。先上一個結果對比圖 再上具體實現。debug 情況下

在這裏插入圖片描述
自己實現的handle_map 插入100W 次 和 查詢100W次的耗時分別爲 1796 毫秒 和 532 毫秒。
使用std::unorder_map 實現的 插入100W次 和 查詢100W次 耗時分別爲 9031 毫秒 和 3281 毫秒。

實現原理

簡單來說就是利用 取模 以及 整除 劃分一個整數爲 模數 和 整除 連個部分。
整除數部分 用於對 std::vector 的直接尋址。
vector 裏頭 保存兩個數據 重用次數 和 用戶數據。
那麼便可以做到直接尋址。
使用模數部分 和 vector 裏的 重用次數 進行相比較 即可 確定 這個分配出去的Handle 是否過期。
每次釋放Handle 會加入 待重用列表 一個Handle 最大重用次數 爲模數部分的最大值。
超過這個最大值這個下標將被標記爲不可使用。

優缺點總結

優點: O(1) 的訪問查詢速度快。
缺點: 最大能同時容納的Handle數量爲 整除數部分的數據有效位。 Handle 會被耗盡 取決於使用的 Handle 對應的類型。耗盡以後 的選擇 有 1.程序GG 2.重用過期的句柄(有可能造成衝突)

總結:

如果我使用 unsigned int 20個bit 來保存 整除數部分 那麼就會如圖上所訴 可以同時容納100W個Handle (1 << 20 - 1) 。
對於遊戲服務器來 說 如果用於玩家 或者 怪物 單個服務器承載100W 來說 絕對是夠用了。(當然可以修改bit數量進行增減)
而 2^32 -2 個handle 用於生成 也是足夠了 畢竟 一秒生成一個 也要好幾百年。 當然 如果對於非常頻繁的 那隻能使用 unsigned int64來做爲handle 了

github

已經將實現代碼 和測試代碼 放到github 上了 需要的請自取
https://github.com/SunYoung91/HandleMap

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章