序言
近年來,我身邊的朋友有很多都從web轉向了遊戲開發。他們以前都沒有做過遊戲伺服器開發,更談不上什麼經驗,而從網上找的例子或遊戲方面的知識,又是那麼的少,那麼的零散。當他們進入遊戲公司時,顯得一臉茫然。如果是大公司還好點,起碼有人帶帶,能學點經驗,但是有些人是直接進入了小公司,甚至這些小公司只有他一個後臺。他們一肩扛起了公司的遊戲後端的研發,也扛起了公司的成敗。他們也非常盡力,他們也想把遊戲的後端做好。可是就是因為沒什麼經驗,剛開始時以為做遊戲伺服器和做web差不多,但是經過一段時間之後,才發現代碼太多,太亂了,一看代碼都想重構,都是踩著坑往前走。這裡我把一些遊戲開發方面的東西整理一下,希望能對那些想做遊戲伺服器開發的朋友有所幫助。
首先,要明確一點,做遊戲伺服器開發和做傳統的web開發有著本質的區別。遊戲伺服器開發,如果沒有經驗,一開始根本沒有一個明確清析的目標,不像web那樣,有些明確的MVC架構,往往就是為了儘快滿足策劃的需求,儘快的實現功能,儘快能讓遊戲跑起來。但是隨著功能越來越多,在老代碼上面修改的越來越頻繁,遊戲測試時暴露出來的一堆bug,更讓人覺得束手無策,這個時候我們想到了重構,想到了架構的設計。
遊戲的構架設計非常重要,好的構架代碼清析,責任明確,擴展性強,易調試。這些會為我們的開發省去不少時間。那要怎麼樣設計遊戲的構架呢?可能每個遊戲都不一樣,但是本質上還是差不多的。
對於遊戲伺服器的構架設計,我們首先要了解遊戲的伺服器構架都有什麼組成的?一款遊戲到上線,需要具備哪些功能?有些人可能會說,只要讓遊戲跑起來,訪問伺服器不出問題不就行了嗎?答案是不行的,遊戲構架本身代表的是一個體系,它包括:
1,系統初始化 2,遊戲邏輯 3,資料庫系統 4,緩存系統。 5,遊戲日誌 6,遊戲管理工具 7,公共服務組件
這一系統的東西都是不可少的,它們共同服務於遊戲的整個運營過程。我們一點點來介紹各個系統的功能。
一,系統初始化
系統初始化是在沒有客戶端連接的時候,伺服器啟動時所需要做的工作。基本上就是配置文件的讀取,初始化系統參數。但是我們必須要考慮的是,系統初始化需要的參數配置在哪兒,是配置在本地伺服器,還是配置在資料庫,伺服器啟的時候去資料庫取。配置的修改需不需要重啟伺服器等。
二,遊戲邏輯
遊戲邏輯是遊戲的核心功能實現,也是整個遊戲的服務中心,它被開發的好壞,直接決定了遊戲伺服器在運行中的性能。那在遊戲邏輯的開發中我們要注意些什麼呢?
(1)網絡通信
遊戲是一種網絡交互比較強的業務,好的底層通信,可以最大化遊戲的性能,增加單臺伺服器處理的同時在線人數,給遊戲帶來更好的體驗,至少不容易出現因為網絡層導致的數據交互卡頓的現象。在這裡我推薦使用Netty,它是目前最流行的NIO框架,它的用法可以在我之前的文章中查看,這裡不再多說了。
有人疑問,代碼也需要分層次?這個是當然了,不同的代碼,代表了不同的功能實現。現在的開發語言都是面向對象的,如果我們不加思考,不加整理的把功能代碼亂堆一起,起始看起來是快速實現了功能,但是到後期,如果要修改需求,或在原來的代碼上增加新的需求,那真是被自己打敗了。所以代碼一定要分層,主要有以下幾層:
a,協議層,也叫前後臺交互層,它主要負責與前臺交互協議的解析和返回數據。在這一層基本上沒有什麼業務邏輯實現。與前臺交互的數據都在這一層開始,也在這一層終止。比如你使用了Netty框架,那麼Netty的ChannelHandlerContext即Ctx只能出現在這一層,他不能出現到遊戲業務邏輯代碼的實現中,接收到客戶端的請求,在這一層把需要的參數解析出來,再把參數傳到業務邏輯方法中,業務邏輯方法處理完後,把要返回給客戶端的數據再返回到這一層,在這一層組織數據,返回給客戶端,這樣就可以把業務邏輯和網絡層分離,業務邏輯只關心業務實現,而且也方便對業務邏輯進行單元測試。
b,業務邏輯層,這裡處理真正的遊戲邏輯,該計算價格計算價格,該通關的通關,該計時的計時。該保存數據的保存數據。但是這一層不直接操作緩存或資料庫,只是處理遊戲邏輯計算。因為業務邏輯層是整個遊戲事件的處理核心,所以他的處理是否正確直接決定遊戲的正確性。所以這一層的代碼要儘量使用面向對角的方法去實現。不要出現重複代碼或相似的功能進行複製粘貼,這樣修改起來非常不方便,可能是修改了某一處,而忘記了修改另外同樣的代碼。還要考慮每個方法都是可測試的,一個方法的行數最好不要超過一百行。另外,可以多看看設計模式的書,它可以幫助我們設計出靈活,整潔的代碼。
三,資料庫系統
資料庫是存儲資料庫的核心,但是遊戲數據在存儲到資料庫的時候會經過網絡和磁碟的IO,它的訪問速度相對於內存來說是很慢的。一般來說,每次訪問資料庫都要和資料庫建立連接,訪問完成之後,為了節省資料庫的連接資源,要再把連接斷開。這樣無形中又為伺服器增加了開銷,在大量的數據訪問時,可能會更慢,而遊戲又是要求低延時的,這時該怎麼辦呢?我們想到了資料庫連接池,即把訪問資料庫的連接放到一個地方管理,用完我不斷開,用的時候去那拿,用完再放回去。這樣不用每次都建立新的連接了。但是如果要我們自己去實現一套連接池管理組件的話,需要時間不說,對技術的把控也是一個考驗,還要再經過測試等等,幸好網際網路開源的今天,有一些現成的可以使用,這裡推薦Mybatis,即實現了代碼與SQL的分離,又有足夠的SQL編寫的靈活性,是一個不錯的選擇。
四,緩存系統
遊戲中,客戶端與伺服器的交互是要求低延遲的,延遲越低,用戶體驗越好。像之前說過的一樣,低延遲就是要求伺服器處理業務儘量的快,客戶端一個請求過來,要在最短的時間內響應結果,最低不得超過500ms,因為加上來回的網絡傳輸耗時,基本上就是600ms-到700ms了,再長玩家就會覺得遊戲卡了。如果直接從資料庫中取數據,處理完之後再存回資料庫的話,這個性能是跟不上的。在伺服器,數據在內存中處理是最快的,所以我們要把一部分常用的數據提前加載到內存中,比如說遊戲數據配置表,經常登陸的玩家數據等。這樣在處理業務時,就不用走資料庫了,直接從內存中取就可以了,速度更快。遊戲中常見的緩存有兩種,1,直接把數據存儲在jvm或伺服器內存中,2,使用第三方的緩存工具,這裡推薦Redis,詳細的用法可以自己去查詢。
五,遊戲日誌
日誌是個好東西呀,一個遊戲中更不能少了日誌,而且日誌一定要記錄的詳細。它是玩家在整個遊戲中的行為記錄,有了這個記錄,我們就可以分析玩家的行為,查找遊戲的不足,在處理玩家在遊戲中的問題時,日誌也是一個良好的憑證和快速處理方式。 在遊戲中,日誌分為:1,系統日誌,主要記錄遊戲伺服器的系統情況。比如:資料庫能否正常連接,伺服器是否正常啟動,數據是否正常加載;2,玩家行為日誌,比如玩家發送了什麼請求,得到了什麼物品,消費了多少貨幣等等;3,統計日誌,這種日誌是對遊戲中所有玩家某種行為的一種統計,根據這個統計來分析大部分玩家的行為,得出一些共性或不同之處,以方法運營做不同的活動吸引用戶消費。 在構架設計中,日誌記錄一定要做為一種強制行為,因為不強制的話,可能由於某種原因某個功能忘記加日誌了,那麼當這個功能出問題了,或者運營跟我們要這個功能的一些資料庫,就傻眼了。又得加需求,改代碼了。日誌一定要設計一種良好的格式,日誌記錄的數據要容易讀取,分解。日誌行為可以用枚舉描述,在功能最後的處理方法裡面加上這個枚舉作為參數,這樣不管誰在調用這個方法時,都要去加參數描述。 俗話說,工欲善其事,必先利其器。遊戲管理工具是對遊戲運行中的一系列問題處理的一種工具。它不僅是給開發人員用,大多數是給運營使用。遊戲上線後,我們需要針對線上的問題進行不同的處理。不可能把所有問題都讓程式設計師去處理吧,於是程式設計師們想到了一個辦法,給你們做一個工具,你們愛誰處理誰處理去吧。
六, 遊戲管理工具
遊戲管理工具是一個不斷增漲的系統,因為它很多時候是伴隨著遊戲中遇到的問題而實現的。但是根據經驗,有一些功能是必須有的,比如:伺服器管理,主要負責伺服器的開啟,關閉,伺服器配置信息,玩家信息查詢,玩家管理,比如踢人,封號;統計查詢,玩家行為日誌查詢,統計查詢,次留率查詢,郵件服務,修改玩家數據等,根據遊戲的不同要求,凡是可以能過工具實現的,都做到遊戲管理工具裡面。它是針對所有伺服器的管理。一個好的,全的遊戲管理工具,可以提高遊戲運營中遇到問題處理的效率,為玩家提供更好的服務。
七,公共組件
公共組件是為遊戲運行中提供公共的服務,比如,充值伺服器,我們沒必須一個服用一個充值,而且你也不能對外提供多個充值伺服器地址,和第三方公司對接,他們絕對不幹,這是要瘋呀;還有運營搞活動時的禮包碼,還有註冊用戶的管理,玩家一個註冊帳號可以進不同的區等。這些都是針對所有區服提供的服務,所以要單獨做,與遊戲邏輯分開,這樣方便管理,部署和負載均衡。還有SDK的登陸驗證,現在手遊比較多,與渠道對接裡要進行驗證,這往往是很多http請求,速度慢,所以這個也要拿出來單獨做,不要在遊戲邏輯中去驗證,因為網絡IO的訪問時間是不可控制的,http是阻塞的請求。 所以,綜上來看,一個遊戲伺服器起碼有幾個大的功能模塊組成:a,遊戲邏輯工程;b,日誌處理工程;c,充值工程;d,遊戲管理工具工程;e,用戶登陸工程;f,公共活動工程等,根據遊戲的不同需要,可能還有其它的。所在在構架的設計中,一定要考慮到系統的分布式部署,儘量把公共的功能拆出來做,這樣可以增強系統的可擴展性。
伺服器端開發的一些建議
本文作為遊戲伺服器端開發的基本大綱,是遊戲實踐開發中的總結。第一部分專業基礎,用於指導招聘和實習考核, 第二部分遊戲入門,講述遊戲伺服器端開發的基本要點,第三部分服務端架構,介紹架構設計中的一些基本原則。希望能幫到大家
一 專業基礎
1.1 網絡
1.1.1 理解TCP/IP協議網絡傳輸模型滑動窗口技術建立連接的三次握手與斷開連接的四次握手連接建立與斷開過程中的各種狀態TCP/IP協議的傳輸效率思考1)請解釋DOS攻擊與DRDOS攻擊的基本原理2)一個100Byte數據包,精簡到50Byte, 其傳輸效率提高了50%3)TIMEWAIT狀態怎麼解釋?
1.1.2 掌握常用的網絡通信模型SelectEpoll,邊緣觸發與平臺出發點區別與應用Select與Epoll的區別及應用
1.2 存儲計算機系統存儲體系程序運行時的內存結構計算機文件系統,頁表結構內存池與對象池的實現原理,應用場景與區別關係資料庫MySQL的使用共享內存
1.3 程序對C/C++語言有較深的理解深刻理解接口,封裝與多態,並且有實踐經驗深刻理解常用的數據結構:數組,鍊表,二叉樹,哈希表熟悉常用的算法及相關複雜度:冒泡排序,快速排序
二 遊戲開發入門
2.1防禦式編程不要相信客戶端數據,一定要檢驗。作為伺服器端你無法確定你的客戶端是誰,你也不能假定它是善意的,請做好自我保護。(這是判斷一個伺服器端程式設計師是否入門的基本標準)務必對於函數的傳人參數和返回值進行合法性判斷,內部子系統,功能模塊之間不要太過信任,要求低耦合,高內聚插件式的模塊設計,模塊功能的健壯性應該是內建的,儘量減少模塊間耦合
2.2 設計模式道法自然。不要迷信,迷戀設計模式,更不要生搬硬套簡化,簡化,再簡化,用最簡單的辦法解決問題借大寶一句話:設計本天成,妙手偶得之
2.3 網絡模型自造輪子: Select, Epoll, Epoll一定比Select高效嗎?開源框架: Libevent, libev, ACE
2.4 數據持久化自定義文件存儲,如《夢幻西遊》關係資料庫: MySQLNO-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 開關控制每個模塊都有開關,可以緊急關閉任何出問題的功能模塊
2.15 反外掛反作弊包頻率控制可以消滅變速齒輪包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 類似。額外多兩條接入與離開遊戲網絡即可。