【遊戲後端】遊戲伺服器端開發的一些建議(轉載)
一 專業基礎
1.1 網路
- 1.1.1 理解TCP/IP協議
- 網路傳輸模型
- 滑動視窗技術
- 建立連線的三次握手與斷開連線的四次握手
- 連線建立與斷開過程中的各種狀態
- TCP/IP協議的傳輸效率
- 思考
- 1)請解釋DOS攻擊與DRDOS攻擊的基本原理
- 2)一個100Byte資料包,精簡到50Byte, 其傳輸效率提高了50%
- 3)TIMEWAIT狀態怎麼解釋?
- 1.1.2 掌握常用的網路通訊模型
- Select
- Epoll,邊緣觸發與平臺出發點區別與應用
- Select與Epoll的區別及應用
- 1.2 儲存
- 計算機系統儲存體系
- 程式執行時的記憶體結構
- 計算機檔案系統,頁表結構
- 記憶體池與物件池的實現原理,應用場景與區別
- 關係資料庫MySQL的使用
- 共享記憶體
- 1.3 程式
- 對C/C++語言有較深的理解
- 深刻理解介面,封裝與多型,並且有實踐經驗
- 深刻理解常用的資料結構:陣列,連結串列,二叉樹,雜湊表
- 熟悉常用的演算法及相關複雜度:氣泡排序,快速排序
二 遊戲開發入門
- 2.1防禦式程式設計
- 不要相信客戶端資料,一定要檢驗。作為伺服器端你無法確定你的客戶端是誰,你也不能假定它是善意的,請做好自我保護。(這是判斷一個伺服器端程式設計師是否入門的基本標準)
- 務必對於函式的傳人蔘數和返回值進行合法性判斷,內部子系統,功能模組之間不要太過信任,要求低耦合,高內聚
- 外掛式的模組設計,模組功能的健壯性應該是內建的,儘量減少模組間耦合
- 2.2 設計模式
- 道法自然。不要迷信,迷戀設計模式,更不要生搬硬套
- 簡化,簡化,再簡化,用最簡單的辦法解決問題
- 借大寶一句話:設計本天成,妙手偶得之
- 2.3 網路模型
- 自造輪子: Select, Epoll, Epoll一定比Select高效嗎?
- 開源框架: Libevent, libev, ACE
- 2.4 資料持久化
- 自定義檔案儲存,如《夢幻西遊》
- 關係資料庫: MySQL
- NO-SQL資料庫: MongoDB
- 2.5 記憶體管理
- 使用記憶體池和物件池,禁止執行期間動態分配記憶體
- 對於輸入輸出的指標引數,嚴格檢查,寧濫勿缺
- 寫記憶體保護。使用帶記憶體保護的函式(strncpy, memcpy, snprintf, vsnprintf等),嚴防陣列下標越界
- 防止讀記憶體溢位,確保字串以’\0’結束
- 2.6 日誌系統
- 簡單高效,大量日誌操作不應該影響程式效能
- 穩定,做到伺服器崩潰是日誌不丟失
- 完備,玩家關鍵操作一定要記日誌,理想的情況是通過日誌能重建任何時刻的玩家資料
- 開關,開發日誌的要加級別開關控制
- 2.7 通訊協議
- 採用PDL(Protocol Design Language), 如Protobuf,可以同時生成前後端程式碼,減少前後端協議聯調成本, 擴充套件性好
- JSON,文字協議,簡單,自解釋,無聯調成本,擴充套件性好,也很方便進行包過濾以及寫日誌
- 自定義二進位制協議,精簡,有高效的傳輸效能,完全可控,幾乎無擴充套件性
- 2.8 全域性唯一Key(GUID)
- 為合服做準備
- 方便追蹤道具,裝備流向
- 每個角色,裝備,道具都應對應有全域性唯一Key
- 2.9 多執行緒與同步
- 訊息佇列進行同步化處理
- 2.10 狀態機
- 強化角色的狀態
- 前置狀態的檢查校驗
- 2.11 資料包操作
- 合併, 同一幀內的資料包進行合併,減少IO操作次數
- 單副本, 用一個包儘量只儲存一份,減少記憶體複製次數
- AOI同步中減少中間過程無用資料包
- 2.12 狀態監控
- 隨時監控伺服器內部狀態
- 記憶體池,物件池使用情況
- 幀處理時間
- 網路IO
- 包處理效能
- 各種業務邏輯的處理次數
- 2.13 包頻率控制
- 基於每個玩家每條協議的包頻率控制,癱瘓變速齒輪
- 2.14 開關控制
- 每個模組都有開關,可以緊急關閉任何出問題的功能模組
- 包頻率控制可以消滅變速齒輪
- 包id自增校驗,可以消滅WPE
- 包校驗碼可以消滅包攔截篡改
- 圖形識別嗎,可以踢掉99%非人的操作
- 魔高一尺,道高一丈
- 2.16 熱更新
- 核心配置邏輯的熱更新,如防沉迷系統,包頻率控制,開關控制等
- 程式碼基本熱更新,如Erlang,Lua等
- 2.17 防刷
- 關鍵系統資源(如元寶,精力值,道具,裝備等)的產出記日誌
- 資源的產出和消耗盡量依賴兩個或以上的獨立條件的檢測
- 嚴格檢查各項操作的前置條件
- 校驗引數合法性
- 2.18 防崩潰
- 系統底層與具體業務邏輯無關,可以用大量的機器人壓力測試暴露各種bug,確保穩定
- 業務邏輯建議使用指令碼
- 系統性的保證遊戲不會崩潰
- 2.19 效能優化
- IO操作非同步化
- IO操作合併緩寫 (事務性的提交db操作,包合併,檔案日誌緩寫)
- Cache機制
- 減少競態條件 (避免頻繁進出切換,儘量減少鎖定使用,多執行緒不一定由於單執行緒) 多執行緒不一定比單執行緒快
- 減少記憶體複製
- 自己測試,用資料說話,別猜
- 2.20 運營支援
- 介面支援:實時查詢,控制指令,資料監控,客服處理等
- 實現考慮提供Http介面
- 2.21 容災與故障預案
- 略
三 伺服器端架構
- 3.1 什麼是好的架構?
- 滿足業務要求
- 能迅速的實現策劃需求,響應需求變更
- 系統級的穩定性保障
- 簡化開發。將複雜性控制在架構底層,降低對開發人員的技術要求,邏輯開發不依賴於開發人員本身強大的技術實力,提高開發效率
- 完善的運營支撐體系
- 3.2 架構實踐的思考
- 簡單,滿足需求的架構就是好架構
- 設計效能,抓住重要的20%, 沒必要從程式程式碼裡面去摳效能
- 熱更新是必須的
- 人難免會犯錯,儘可能的用一套機制去保障邏輯的健壯性
遊戲伺服器的設計是一項頗有挑戰性的工作,遊戲伺服器的發展也由以前的單服結構轉變為多服機構,甚至出現了bigworld引擎的分散式解決方案,最近了解到Unreal的伺服器解決方案atlas也是基於叢集的方式。
負載均衡是一個很複雜的課題,這裡暫不談bigworld和atlas的這類伺服器的設計,更多的是基於功能和場景劃分伺服器結構。
首先說一下思路,伺服器劃分基於以下原則:
- 分離遊戲中佔用系統資源(cpu,記憶體,IO等)較多的功能,獨立成伺服器。
- 在同一伺服器架構下的不同遊戲,應儘可能的複用某些伺服器(程序級別的複用)。
- 以多執行緒併發的程式設計方式適應多核處理器。
- 寧可在伺服器之間多複製資料,也要保持清晰的資料流向。
- 主要按照場景劃分程序,若需按功能劃分,必須保持整個邏輯足夠的簡單,並滿足以上1,2點。
伺服器結構圖:
各個伺服器的簡要說明:
Gateway 是應用閘道器,主要用於保持和client的連線,該伺服器需要2種IO,對client採用高併發連線,低吞吐量的網路模型,如IOCP等,對伺服器採用高吞吐量連線,如阻塞或非同步IO。
閘道器主要有以下用途:
- 分擔了網路IO資源
- 同時,也分擔了網路訊息包的加解密,壓縮解壓等cpu密集的操作。
- 隔離了client和內部伺服器組,對client來說,它只需要知道閘道器的相關資訊即可(ip和port)。
- client由於一直和閘道器保持常連線,所以切換場景伺服器等操作對client來說是透明的。
- 維護玩家登入狀態。
World Server 是一個控制中心,它負責把各種計算資源分佈到各個伺服器,它具有以下職責:
- 管理和維護多個Scene Server。
- 管理和維護多個功能伺服器,主要是同步資料到功能伺服器。
- 複雜轉發其他伺服器和Gateway之間的資料。
- 實現其他需要跨場景的功能,如組隊,聊天,幫派等。
Phys Server 主要用於玩家移動,碰撞等檢測。
所有玩家的移動類操作都在該伺服器上做檢查,所以該伺服器本身具備所有地圖的地形等相關資訊。具體檢查過程是這樣的:首先,Worldserver收到一個移動資訊,WorldServer收到後向Phys Server請求檢查,Phys Server檢查成功後再返回給world Server,然後world server傳遞給相應的Scene Server。
Scene Server 場景伺服器,按場景劃分,每個伺服器負責的場景應該是可以配置的。理想情況下是可以動態調節的。
ItemMgr Server 物品管理伺服器,負責所有物品的生產過程。在該伺服器上儲存一個物品掉落資料庫,伺服器初始化的時候載入到記憶體。任何需要產生物品的伺服器均與該伺服器直接通訊。
AIServer 又一個功能伺服器,負責管理所有NPC的AI。AI伺服器通常有2個輸入,一個是Scene Server傳送過來的玩家相關操作資訊,另一個時鐘Timer驅動,在這個設計中,對其他伺服器來說,AIServer就是一個擁有很多個NPC的客戶端。AIserver需要同步所有與AI相關的資料,包括很多玩家資料。由於AIServer的Timer驅動特性,可在很大程度上使用TBB程式庫來發揮多核的效能。
把網路遊戲伺服器分拆成多個程序,分開部署。這種設計的好處是模組自然分離,可以單獨設計。分擔負荷,可以提高整個系統的承載能力。
缺點在於,網路環境並不那麼可靠。跨程序通訊有一定的不可預知性。伺服器間通訊往往難以架設除錯環境,並很容易把事情攪成一團糨糊。而且正確高效的管理多連線,對程式設計師來說也是一項挑戰。
前些年,我也曾寫過好幾篇與之相關的設計。這幾天在思考一個問題:如果我們要做一個底層通用模組,讓後續開發更為方便。到底要解決怎樣的需求。這個需求應該是單一且基礎的,每個應用都需要的。
正如 TCP 協議解決了網際網路上穩定可靠的點對點資料流通訊一樣。遊戲世界實際需要的是一個穩定可靠的在遊戲系統內的點對點通訊需要。
我們可以在一條 TCP 連線之上做到這一點。一旦實現,可以給遊戲服務的開發帶來極大的方便。
可以把遊戲系統內的各項服務,包括並不限於登陸,拍賣,戰鬥場景,資料服務,等等獨立服務看成網路上的若干終端。每個玩家也可以是一個獨立終端。它們一起構成一個網路。在這個網路之上,終端之間可以進行可靠的連線和通訊。
實現可以是這樣的:每個虛擬終端都在遊戲虛擬網路(Game Network)上有一個唯一地址 (Game Network Address , GNA) 。這個地址可以預先設定,也可以動態分配。每個終端都可以通過遊戲網路的若干接入點 ( GNAP ) 通過唯一一條 TCP 連線接入網路。接入過程需要通過鑑權。
鑑權過程依賴內部的安全機制,可以包括密碼證書,或是特別的接入點區分。(例如,玩家接入網路就需要特定的接入點,這個接入點接入的終端都一定是玩家)
鑑權通過後,網路為終端分配一個固定的遊戲域名。例如,玩家進入會分配到 player.12345 這樣的域名,資料庫接入可能分配到 database 。
遊戲網路預設提供一個域名查詢服務(這個服務可以通過鑑權的過程註冊到網路中),讓每個終端都能通過域名查詢到對應的地址。
然後,遊戲網路裡所有合法接入的終端都可以通過其地址相互發起連線並通訊了。整個協議建立在 TCP 協議之上,工作於唯一的這個 TCP 連線上。和直接使用 TCP 連線不同。遊戲網路中每個終端之間相互發起連線都是可靠的。不僅玩家可以向某個服務發起連線,反過來也是可以的。玩家之間的直接連線也是可行的(是否允許這樣,取決於具體設計)。
由於每個虛擬連線都是建立在單一的 TCP 連線之上。所以減少了互連網上發起 TCP 連線的各種不可靠性。鑑權過程也是一次性唯一的。並且我們提供域名反查服務,我們的遊戲服務可以清楚且安全的知道連線過來的是誰。
系統可以設計為,遊戲網路上每個終端離網,域名服務將廣播這條訊息,通知所有人。這種廣播服務在網際網路上難以做到,但無論是廣播還是組播,在這個虛擬遊戲網路中都是可行的。
在這種設計上。在邏輯層面,我們可以讓玩家直接把聊天資訊從玩家客互端傳送到聊天伺服器,而不需要建立多餘的 TCP 連線,也不需要對轉發處理聊天訊息做多餘的處理。聊天伺服器可以獨立的存在於遊戲網路。也可以讓廣播服務主動向玩家推送訊息,由伺服器向玩家發起連線,而不是所有連線請求都是由玩家客互端發起。
虛擬遊戲網路的構成是一個獨立的層次,完全可以撇開具體遊戲邏輯來實現,並能夠單獨去按承載量考慮具體設計方案。非常利於剝離出具體遊戲專案來開發並優化。
最終,我們或許需要的一套 C 庫,用於遊戲網路內的通訊。api 可以和 socket api 類似。額外多兩條接入與離開遊戲網路即可。