




已閱讀5頁,還剩12頁未讀, 繼續(xù)免費(fèi)閱讀
版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請進(jìn)行舉報或認(rèn)領(lǐng)
文檔簡介
百萬用戶級游戲服務(wù)器架構(gòu)設(shè)計服務(wù)器結(jié)構(gòu)探討 - 最簡單的結(jié)構(gòu) 所謂服務(wù)器結(jié)構(gòu),也就是如何將服務(wù)器各部分合理地安排,以實現(xiàn)最初的功能需求。所以,結(jié)構(gòu)本無所謂正確與錯誤;當(dāng)然,優(yōu)秀的結(jié)構(gòu)更有助于系統(tǒng)的搭建,對系統(tǒng)的可擴(kuò)展性及可維護(hù)性也有更大的幫助。 好的結(jié)構(gòu)不是一蹴而就的,而且每個設(shè)計者心中的那把尺都不相同,所以這個優(yōu)秀結(jié)構(gòu)的定義也就沒有定論。在這里,我們不打算對現(xiàn)有游戲結(jié)構(gòu)做評價,而是試著從頭開始搭建一個我們需要的 MMOG 結(jié)構(gòu)。 對于一個最簡單的游戲服務(wù)器來說,它只需要能夠接受來自客戶端的連接請求,然后處理客戶端在游戲世界中的移動及交互,也即游戲邏輯處理即可。如果我們把這兩項功能集成到一個服務(wù)進(jìn)程中,則最終的結(jié)構(gòu)很簡單: client - server 嗯,太簡單了點(diǎn),這樣也敢叫服務(wù)器結(jié)構(gòu)?好吧,現(xiàn)在我們來往里面稍稍加點(diǎn)東西,讓它看起來更像是服務(wù)器結(jié)構(gòu)一些。 一般來說,我們在接入游戲服務(wù)器的時候都會要提供一個帳號和密碼,驗證通過后才能進(jìn)入。關(guān)于為什么要提供用戶名和密碼才能進(jìn)入的問題我們這里不打算做過多討論,云風(fēng)曾對此也提出過類似的疑問,并給出了只用一個標(biāo)識串就能進(jìn)入的設(shè)想,有興趣的可以去看看他們的討論。但不管是采用何種方式進(jìn)入,照目前看來我們的服務(wù)器起碼得提供一個帳號驗證的功能。 我們把觀察點(diǎn)先集中在一個大區(qū)內(nèi)。在大多數(shù)情況下,一個大區(qū)內(nèi)都會有多組游戲服,也就是多個游戲世界可供選擇。簡單點(diǎn)來實現(xiàn),我們完全可以拋棄這個大區(qū)的概念,認(rèn)為一個大區(qū)也就是放在同一個機(jī)房的多臺服務(wù)器組,各服務(wù)器組間沒有什么關(guān)系。這樣,我們可為每組服務(wù)器單獨(dú)配備一臺登錄服。最后的結(jié)構(gòu)圖應(yīng)該像這樣: loginServer gameServer | / | / client 該結(jié)構(gòu)下的玩家操作流程為,先選擇大區(qū),再選擇大區(qū)下的某臺服務(wù)器,即某個游戲世界,點(diǎn)擊進(jìn)入后開始帳號驗證過程,驗證成功則進(jìn)入了該游戲世界。但是,如果玩家想要切換游戲世界,他只能先退出當(dāng)前游戲世界,然后進(jìn)入新的游戲世界重新進(jìn)行帳號驗證。 早期的游戲大都采用的是這種結(jié)構(gòu),有些游戲在實現(xiàn)時采用了一些技術(shù)手段使得在切換游戲服時不需要再次驗證帳號,但整體結(jié)構(gòu)還是未做改變。 該結(jié)構(gòu)存在一個服務(wù)器資源配置的問題。因為登錄服處理的邏輯相對來說比較簡單,就是將玩家提交的帳號和密碼送到數(shù)據(jù)庫進(jìn)行驗證,和生成會話密鑰發(fā)送給游戲服和客戶端,操作完成后連接就會立即斷開,而且玩家在以后的游戲過程中不會再與登錄服打任何交道。這樣處理短連接的過程使得系統(tǒng)在大多數(shù)情況下都是比較空閑的,但是在某些時候,由于請求比較密集,比如開新服的時候,登錄服的負(fù)載又會比較大,甚至?xí)幚聿贿^來。 另外在實際的游戲運(yùn)營中,有些游戲世界很火爆,而有些游戲世界卻非常冷清,甚至沒有多少人玩的情況也是很常見的。所以,我們能否更合理地配置登錄服資源,使得整個大區(qū)內(nèi)的登錄服可以共享就成了下一步改進(jìn)的目標(biāo)。 服務(wù)器結(jié)構(gòu)探討 - 登錄服的負(fù)載均衡 回想一下我們在玩 wow 時的操作流程:運(yùn)行 wow.exe 進(jìn)入游戲后,首先就會要求我們輸入用戶名和密碼進(jìn)行驗證,驗證成功后才會出來游戲世界列表,之后是排隊進(jìn)入游戲世界,開始游戲. 可以看到跟前面的描述有個很明顯的不同,那就是要先驗證帳號再選擇游戲世界。這種結(jié)構(gòu)也就使得登錄服不是固定配備給個游戲世界,而是全區(qū)共有的。 我們可以試著從實際需求的角度來考慮一下這個問題。正如我們之前所描述過的那樣,登錄服在大多數(shù)情況下都是比較空閑的,也許我們的一個擁有20個游戲世界的大區(qū)僅僅使用10 臺或更少的登錄服即可滿足需求。而當(dāng)在開新區(qū)的時候,或許要配備40臺登錄服才能應(yīng)付那如潮水般涌入的玩家登錄請求。所以,登錄服在設(shè)計上應(yīng)該能滿足這種動態(tài)增刪的需求,我們可以在任何時候為大區(qū)增加或減少登錄服的部署。 當(dāng)然,在這里也不會存在要求添加太多登錄服的情況。還是拿開新區(qū)的情況來說,即使新增加登錄服滿足了玩家登錄的請求,游戲世界服的承載能力依然有限,玩家一樣只能在排隊系統(tǒng)中等待,或者是進(jìn)入到游戲世界中導(dǎo)致大家都卡。 另外,當(dāng)我們在增加或移除登錄服的時候不應(yīng)該需要對游戲世界服有所改動,也不會要求重啟世界服,當(dāng)然也不應(yīng)該要求客戶端有什么更新或者修改,一切都是在背后自動完成。 最后,有關(guān)數(shù)據(jù)持久化的問題也在這里考慮一下。一般來說,使用現(xiàn)有的商業(yè)數(shù)據(jù)庫系統(tǒng)比自己手工技術(shù)先進(jìn)要明智得多。我們需要持久化的數(shù)據(jù)有玩家的帳號及密碼,玩家創(chuàng)建的角色相關(guān)信息,另外還有一些游戲世界全局共有數(shù)據(jù)也需要持久化。 好了,需求已經(jīng)提出來了,現(xiàn)在來考慮如何將其實現(xiàn)。 對于負(fù)載均衡來說,已有了成熟的解決方案。一般最常用,也最簡單部署的應(yīng)該是基于 DNS 的負(fù)載均衡系統(tǒng)了,其通過在 DNS 中為一個域名配置多個 IP 地址來實現(xiàn)。最新的 DNS 服務(wù)已實現(xiàn)了根據(jù)服務(wù)器系統(tǒng)狀態(tài)來實現(xiàn)的動態(tài)負(fù)載均衡,也就是實現(xiàn)了真正意義上的負(fù)載均衡,這樣也就有效地解決了當(dāng)某臺登錄服當(dāng)機(jī)后,DNS 服務(wù)器不能立即做出反應(yīng)的問題。當(dāng)然,如果找不到這樣的解決方案,自己從頭打造一個也并不難。而且,通過 DNS 來實現(xiàn)的負(fù)載均衡已經(jīng)包含了所做的修改對登錄服及客戶端的透明。 而對于數(shù)據(jù)庫的應(yīng)用,在這種結(jié)構(gòu)下,登錄服及游戲世界服都會需要連接數(shù)據(jù)庫。從數(shù)據(jù)庫服務(wù)器的部署上來說,可以將帳號和角色數(shù)據(jù)都放在一個中心數(shù)據(jù)庫中,也可分為兩個不同的庫分別來處理,基到從物理上分到兩臺不同的服務(wù)器上去也行。 但是對于不同的游戲世界來說,其角色及游戲內(nèi)數(shù)據(jù)都是互相獨(dú)立的,所以一般情況下也就為每個游戲世界單獨(dú)配備一臺數(shù)據(jù)庫服務(wù)器,以減輕數(shù)據(jù)庫的壓力。所以,整體的服務(wù)器結(jié)構(gòu)應(yīng)該是一個大區(qū)有一臺帳號數(shù)據(jù)庫服務(wù)器,所有的登錄服都連接到這里。而每個游戲世界都有自己的游戲數(shù)據(jù)庫服務(wù)器,只允許本游戲世界內(nèi)的服務(wù)器連接。 最后,我們的服務(wù)器結(jié)構(gòu)就像這樣: 大區(qū)服務(wù)器 / | / | 登錄服1 登錄服2 世界服1 世界服2 | | | | | | 帳號數(shù)據(jù)庫 DBS DBS 這里既然討論到了大區(qū)及帳號數(shù)據(jù)庫,所以順帶也說一下關(guān)于激活大區(qū)的概念。wow 中一共有八個大區(qū),我們想要進(jìn)入某個大區(qū)游戲之前,必須到官網(wǎng)上激活這個區(qū),這是為什么呢? 一般來說,在各個大區(qū)帳號數(shù)據(jù)庫之上還有一個總的帳號數(shù)據(jù)庫,我們可以稱它為中心數(shù)據(jù)庫。比如我們在官網(wǎng)上注冊了一個帳號,這時帳號數(shù)據(jù)是只保存在中心數(shù)據(jù)庫上的。而當(dāng)我們要到一區(qū)去創(chuàng)建角色開始游戲的時候,在一區(qū)的帳號數(shù)據(jù)庫中并沒有我們的帳號數(shù)據(jù),所以,我們必須先到官網(wǎng)上做一次激活操作。這個激活的過程也就是從中心庫上把我們的帳號數(shù)據(jù)拷貝到所要到的大區(qū)帳號數(shù)據(jù)庫中。 服務(wù)器結(jié)構(gòu)探討 - 簡單的世界服實現(xiàn) 討論了這么久我們一直都還沒有進(jìn)入游戲世界服務(wù)器內(nèi)部,現(xiàn)在就讓我們來窺探一下里面的結(jié)構(gòu)吧。 對于現(xiàn)在大多數(shù) MMORPG 來說,游戲服務(wù)器要處理的基本邏輯有移動、聊天、技能、物品、任務(wù)和生物等,另外還有地圖管理與消息廣播來對其他高級功能做支撐。如縱隊、好友、公會、戰(zhàn)場和副本等,這些都是通過基本邏輯功能組合或擴(kuò)展而成。 在所有這些基礎(chǔ)邏輯中,與我們要討論的服務(wù)器結(jié)構(gòu)關(guān)系最緊密的當(dāng)屬地圖管理方式。決定了地圖的管理方式也就決定了我們的服務(wù)器結(jié)構(gòu),我們?nèi)匀幌葟淖詈唵蔚膶崿F(xiàn)方式開始說起。 回想一下我們曾戰(zhàn)斗過無數(shù)個夜晚的暗黑破壞神,整個暗黑的世界被分為了若干個獨(dú)立的小地圖,當(dāng)我們在地圖間穿越時,一般都要經(jīng)過一個叫做傳送門的裝置。世界中有些地圖間雖然在地理上是直接相連的,但我們發(fā)現(xiàn)其游戲內(nèi)部的邏輯卻是完全隔離的。可以這樣認(rèn)為,一塊地圖就是一個獨(dú)立的數(shù)據(jù)處理單元。 既然如此,我們就把每塊地圖都當(dāng)作是一臺獨(dú)立的服務(wù)器,他提供了在這塊地圖上游戲時的所有邏輯功能,至于內(nèi)部結(jié)構(gòu)如何劃分我們暫不理會,先把他當(dāng)作一個黑盒子吧。 當(dāng)兩個人合作做一件事時,我們可以以對等的關(guān)系相互協(xié)商著來做,而且一般也都不會有什么問題。當(dāng)人數(shù)增加到三個時,我們對等的合作關(guān)系可能會有些復(fù)雜,因為我們每個人都同時要與另兩個人合作協(xié)商。正如俗語所說的那樣,三個和尚可能會碰到?jīng)]水喝的情況。當(dāng)人數(shù)繼續(xù)增加,情況就變得不那么簡單了,我們得需要一個管理者來對我們的工作進(jìn)行分工、協(xié)調(diào)。游戲的地圖服務(wù)器之間也是這么回事。 一般來說,我們的游戲世界不可能會只有一塊或者兩塊小地圖,那順理成章的,也就需要一個地圖管理者。先稱它為游戲世界的中心服務(wù)器吧,畢竟是管理者嘛,大家都以它為中心。 中心服務(wù)器主要維護(hù)一張地圖 ID 到地圖服務(wù)器地址的映射表。當(dāng)我們要進(jìn)入某張地圖時,會從中心服上取得該地圖的 IP 和 port 告訴客戶端,客戶端主動去連接,這樣進(jìn)入他想要去的游戲地圖。在整個游戲過程中,客戶端始終只會與一臺地圖服務(wù)器保持連接,當(dāng)要切換地圖的時候,在獲取到新地圖的地址后,會先與當(dāng)前地圖斷開連接,再進(jìn)入新的地圖,這樣保證玩家數(shù)據(jù)在服務(wù)器上只有一份。 我們來看看結(jié)構(gòu)圖是怎樣的: 中心服務(wù)器 / / 登錄服 地圖1 地圖2 地圖 n | / / | / / 客戶端 很簡單,不是嗎。但是簡單并不表示功能上會有什么損失,簡單也更不能表示游戲不能賺錢。早期不少游戲也確實采用的就是這種簡單結(jié)構(gòu)。 服務(wù)器結(jié)構(gòu)探討 - 繼續(xù)世界服 都已經(jīng)看出來了,這種每切換一次地圖就要重新連接服務(wù)器的方式實在是不夠優(yōu)雅,而且在實際游戲運(yùn)營中也發(fā)現(xiàn),地圖切換導(dǎo)致的卡號,復(fù)制裝備等問題非常多,這里完全就是一個事故多發(fā)地段,如何避免這種頻繁的連接操作呢? 最直接的方法就是把那個圖倒轉(zhuǎn)過來就行了??蛻舳酥恍枰B接到中心服上,所有到地圖服務(wù)器的數(shù)據(jù)都由中心服來轉(zhuǎn)發(fā)。很完美的解決方案,不是嗎? 這種結(jié)構(gòu)在實際的部署中也遇到了一些挑戰(zhàn)。對于一般的 MMORPG 服務(wù)器來說,單臺服務(wù)器的承載量平均在2000左右,如果你的服務(wù)器很不幸地只能帶1000 人,沒關(guān)系,不少游戲都是如此;如果你的服務(wù)器上跑了3000多玩家依然比較流暢,那你可以自豪地告訴你的策劃,多設(shè)計些大量消耗服務(wù)器資源的玩法吧,比如大型國戰(zhàn)、公會戰(zhàn)爭等。 2000人,似乎我們的策劃朋友們不大愿意接受這個數(shù)字。我們將地圖服務(wù)器分開來原來也是想將負(fù)載分開,以多帶些客戶端,現(xiàn)在要所有的連接都從中心服上轉(zhuǎn)發(fā),那連接數(shù)又遇到單臺服務(wù)器的可最大承載量的瓶頸了。 這里有必要再解釋下這個數(shù)字。我知道,有人一定會說,才帶2000人,那是你水平不行,我隨便寫個TCP 服務(wù)器都可帶個五六千連接。問題恰恰在于你是隨便寫的,而 MMORPG 的服務(wù)器是復(fù)雜設(shè)計的。如果一個演示 socket API 用的 echo 服務(wù)器就能滿足 MMOG 服務(wù)器的需求,那寫服務(wù)器該是件多么愜意的事啊。但我們所遇到的事實是,服務(wù)器收到一個移動包后,要向周圍所有人廣播,而不是 echo 服務(wù)器那樣簡單的回應(yīng);服務(wù)器在收到一個連接斷開通知時要向很多人通知玩家退出事件,并將該玩家的資料寫入數(shù)據(jù)庫,而不是 echo 服務(wù)器那樣什么都不需要做;服務(wù)器在收到一個物品使用請求包后要做一系列的邏輯判斷以檢查玩家有沒有作弊;服務(wù)器上還啟動著很多定時器用來更新游戲世界的各種狀態(tài) 其實這么一比較,我們也看出資源消耗的所在了:服務(wù)器上大量的復(fù)雜的邏輯處理。再回過頭來看看我們想要實現(xiàn)的結(jié)構(gòu),我們既想要有一個唯一的入口,使得客戶端不用頻繁改變連接,又希望這個唯一入口的負(fù)載不會太大,以致于接受不了多少連接。 仔細(xì)看一看這個需求,我們想要的僅僅只是一臺管理連接的服務(wù)器,并不打算讓他承擔(dān)太多的游戲邏輯。既然如此,那五六千個連接也還有滿足我們的要求。至少在現(xiàn)在來說,一個游戲世界內(nèi),也就是一組服務(wù)器內(nèi)同時有五六千個在線的玩家還是件讓人很興奮的事。事實上,在大多數(shù)游戲的大部分時間里,這個數(shù)字也是很讓人眼紅的。 什么?你說夢幻、魔獸還有史先生的那個什么征途遠(yuǎn)不止這么點(diǎn)人了!噢,我說的是大多數(shù),是大多數(shù),不包括那些明星。你知道大陸現(xiàn)在有多少游戲在運(yùn)營嗎?或許你又該說,我們不該在一開始就把自己的目標(biāo)定的太低!好吧,我們還是先不談這個。 繼續(xù)我們的結(jié)構(gòu)討論。一般來說,我們把這臺負(fù)責(zé)連接管理的服務(wù)器稱為網(wǎng)關(guān)服務(wù)器,因為內(nèi)部的數(shù)據(jù)都要通過這個網(wǎng)關(guān)才能出去,不過從這臺服務(wù)器提供的功能來看,稱其為反向代理服務(wù)器可能更合適。我們也不在這個名字上糾纏了,就按大家通用的叫法,還是稱他為網(wǎng)關(guān)服務(wù)器吧。 網(wǎng)關(guān)之后的結(jié)構(gòu)我們依然可以采用之前描述的方案,只是,似乎并沒有必要為每一個地圖都開一個獨(dú)立的監(jiān)聽端口了。我們可以試著對地圖進(jìn)行一些劃分,由一個 Master Server 來管理一些更小的 Zone Server,玩家通過網(wǎng)關(guān)連接到 Master Server 上,而實際與地圖有關(guān)的邏輯是分派給更小的 Zone Server 去處理。 最后的結(jié)構(gòu)看起來大概是這樣的: Zone Server Zone Server / / Master Server Master Server / / / / Gateway Server / | / | / | Center Server | | Client 服務(wù)器結(jié)構(gòu)探討 - 最終的結(jié)構(gòu) 如果我們就此打住,可能馬上就會有人要嗤之以鼻了,就這點(diǎn)古董級的技術(shù)也敢出來現(xiàn)。好吧,我們還是把之前留下的問題拿出來解決掉吧。 一般來說,當(dāng)某一部分能力達(dá)不到我們的要求時,最簡單的解決方法就是在此多投入一點(diǎn)資源。既然想要更多的連接數(shù),那就再加一臺網(wǎng)關(guān)服務(wù)器吧。新增加了網(wǎng)關(guān)服后需要在大區(qū)服上做相應(yīng)的支持,或者再簡單點(diǎn),有一臺主要的網(wǎng)關(guān)服,當(dāng)其負(fù)載較高時,主動將新到達(dá)的連接重定向到其他網(wǎng)關(guān)服上。 而對于游戲服來說,有一臺還是多臺網(wǎng)關(guān)服是沒有什么區(qū)別的。每個代表客戶端玩家的對象內(nèi)部都保留一個代表其連接的對象,消息廣播時要求每個玩家對象使用自己的連接對象發(fā)送數(shù)據(jù)即可,至于連接是在什么地方,那是完全透明的。當(dāng)然,這只是一種簡單的實現(xiàn),也是普通使用的一種方案,如果后期想對消息廣播做一些優(yōu)化的話,那可能才需要多考慮一下。 既然說到了優(yōu)化,我們也稍稍考慮一下現(xiàn)在結(jié)構(gòu)下可能采用的優(yōu)化方案。 首先是當(dāng)前的 Zone Server 要做的事情太多了,以至于他都處理不了多少連接。這其中最消耗系統(tǒng)資源的當(dāng)屬生物的 AI 處理了,尤其是那些復(fù)雜的尋路算法,所以我們可以考慮把這部分 AI 邏輯獨(dú)立出來,由一臺單獨(dú)的 AI 服務(wù)器來承擔(dān)。 然后,我們可以試著把一些與地圖數(shù)據(jù)無關(guān)的公共邏輯放到 Master Server 上去實現(xiàn),這樣 Zone Server上只保留了與地圖數(shù)據(jù)緊密相關(guān)的邏輯,如生物管理,玩家移動和狀態(tài)更新等。 還有聊天處理邏輯,這部分與游戲邏輯沒有任何關(guān)聯(lián),我們也完全可以將其獨(dú)立出來,放到一臺單獨(dú)的聊天服務(wù)器上去實現(xiàn)。 最后是數(shù)據(jù)庫了,為了減輕數(shù)據(jù)庫的壓力,提高數(shù)據(jù)請求的響應(yīng)速度,我們可以在數(shù)據(jù)庫之前建立一個數(shù)據(jù)庫緩存服務(wù)器,將一些常用數(shù)據(jù)緩存在此,服務(wù)器與數(shù)據(jù)庫的通信都要通過這臺服務(wù)器進(jìn)行代理。緩存的數(shù)據(jù)會定時的寫入到后臺數(shù)據(jù)庫中。 好了,做完這些優(yōu)化我們的服務(wù)器結(jié)構(gòu)大體也就定的差不多了,暫且也不再繼續(xù)深入,更細(xì)化的內(nèi)容等到各個部分實現(xiàn)的時候再探討。 好比我們?nèi)タ匆粓鐾頃?,舞臺上演員們按著預(yù)定的節(jié)目單有序地上演著,但這就是整場晚會的全部嗎?顯然不止,在幕后還有太多太多的人在忙碌著,甚至在晚會前和晚會后都有。我們的游戲服務(wù)器也如此。 在之前描述的部分就如同舞臺上的演員,是我們能直接看到的,幕后的工作人員我們也來認(rèn)識一下。 現(xiàn)實中有警察來維護(hù)秩序,游戲中也如此,這就是我們常說的 GM。GM 可以采用跟普通玩家一樣的拉入方式來進(jìn)入游戲,當(dāng)然權(quán)限會比普通玩家高一些,也可以提供一臺 GM 服務(wù)器專門用來處理 GM 命令,這樣可以有更高的安全性,GM 服一般接在中心服務(wù)器上。 在以時間收費(fèi)的游戲中,我們還需要一臺計費(fèi)的服務(wù)器,這臺服務(wù)器一般接在網(wǎng)關(guān)服務(wù)器上,注冊玩家登錄和退出事件以記錄玩家的游戲時間。 任何為用戶提供服務(wù)的地方都會有日志記錄,游戲服務(wù)器當(dāng)然也不例外。從記錄玩家登錄的時間,地址,機(jī)器信息到游戲過程中的每一項操作都可以作為日志記錄下來,以備查錯及數(shù)據(jù)挖掘用。至于搜集玩家機(jī)器資料所涉及到的法律問題不是我們該考慮的。 差不多就這么多了吧,接下來我們會按照這個大致的結(jié)構(gòu)來詳細(xì)討論各部分的實現(xiàn)。 服務(wù)器結(jié)構(gòu)探討 - 一點(diǎn)雜談 再強(qiáng)調(diào)一下,服務(wù)器結(jié)構(gòu)本無所謂好壞,只有是否適合自己。我們在前面探討了一些在現(xiàn)在的游戲中見到過的結(jié)構(gòu),并盡我所知地分析了各自存在的一些問題和可以做的一些改進(jìn),希望其中沒有謬誤,如果能給大家也帶來些啟發(fā)那自然更好。 突然發(fā)現(xiàn)自己一旦羅嗦起來還真是沒完沒了。接下來先說說我在開發(fā)中遇到過的一些困惑和一基礎(chǔ)問題探討吧,這些問題可能有人與我一樣,也曾遇到過,或者正在被困擾中,而所要探討的這些基礎(chǔ)問題向來也是爭論比較多的,我們也不評價其中的好與壞,只做簡單的描述。 首先是服務(wù)器操作系統(tǒng),linux 與 windows 之爭隨處可見,其實在大多數(shù)情況下這不是我們所能決定的,似乎各大公司也基本都有了自己的傳統(tǒng),如網(wǎng)易的 freebsd,騰訊的 linux 等。如果真有權(quán)利去選擇的話,選自己最熟悉的吧。 決定了 OS 也就基本上確定了網(wǎng)絡(luò) IO 模型,windows 上的 IOCP 和 linux 下的 epool,或者直接使用現(xiàn)有的網(wǎng)絡(luò)框架,如 ACE 和 asio 等,其他還有些商業(yè)的網(wǎng)絡(luò)庫在國內(nèi)的使用好像沒有見到,不符合中國國情嘛。:) 然后是網(wǎng)絡(luò)協(xié)議的選擇,以前的選擇大多傾向于 UDP,為了可靠傳輸一般自己都會在上面實現(xiàn)一層封裝,而現(xiàn)在更普通的是直接采用本身就很可靠的 TCP,或者 TCP 與 UDP 的混用。早期選擇 UDP 的主要原因還是帶寬限制,現(xiàn)在寬帶普通的情況下 TCP 比 UDP 多出來的一點(diǎn)點(diǎn)開銷與開發(fā)的便利性相比已經(jīng)不算什么了。當(dāng)然,如果已有了成熟的可靠 UDP 庫,那也可以繼續(xù)使用著。 還有消息包格式的定義,這個曾在云風(fēng)的 blog 上展開過激烈的爭論。消息包格式定義包括三段,包長、消息碼和包體,爭論的焦點(diǎn)在于應(yīng)該是消息碼在前還是包長在前,我們也把這個當(dāng)作是信仰問題吧,有興趣的去云風(fēng)的 blog 上看看,論論。 另外早期有些游戲的包格式定義是以特殊字符作分隔的,這樣一個好處是其中某個包出現(xiàn)錯誤后我們的游戲還能繼續(xù)。但實際上,我覺得這是完全沒有必要的,真要出現(xiàn)這樣的錯誤,直接斷開這個客戶端的連接可能更安全。而且,以特殊字符做分隔的消息包定義還加大了一點(diǎn)點(diǎn)網(wǎng)絡(luò)數(shù)據(jù)量。 最后是一個純技術(shù)問題,有關(guān) socket 連接數(shù)的最大限制。開始學(xué)習(xí)網(wǎng)絡(luò)編程的時候我犯過這樣的錯誤,以為 port 的定義為 unsigned short,所以想當(dāng)然的認(rèn)為服務(wù)器的最大連接數(shù)為65535,這會是一個硬性的限制。而實際上,一個 socket 描述符在 windows 上的定義是 unsigned int,因此要有限制那也是四十多億,放心好了。 在服務(wù)器上 port 是監(jiān)聽用的,想象這樣一種情況,web server 在80端口上監(jiān)聽,當(dāng)一個連接到來時,系統(tǒng)會為這個連接分配一個 socket 句柄,同時與其在80端口上進(jìn)行通訊;當(dāng)另一個連接到來時,服務(wù)器仍然在80端口與之通信,只是分配的 socket 句柄不一樣。這個 socket 句柄才是描述每個連接的唯一標(biāo)識。按windows 網(wǎng)絡(luò)編程第二版上的說法,這個上限值配置影響。 好了,廢話說完了,下一篇,我們開始進(jìn)入登錄服的設(shè)計吧。 登錄服的設(shè)計 - 功能需求 正如我們在前面曾討論過的,登錄服要實現(xiàn)的功能相當(dāng)簡單,就是帳號驗證。為了便于描述,我們暫不引入那些討論過的優(yōu)化手段,先以最簡單的方式實現(xiàn),另外也將基本以 mangos 的代碼作為參考來進(jìn)行描述。想象一下帳號驗證的實現(xiàn)方法,最容易的那就是把用戶輸入的明文用帳號和密碼直接發(fā)給登錄服,服務(wù)器根據(jù)帳號從數(shù)據(jù)庫中取出密碼,與用戶輸入的密碼相比較。 這個方法存在的安全隱患實在太大,明文的密碼傳輸太容易被截獲了。那我們試著在傳輸之前先加一下密,為了服務(wù)器能進(jìn)行密碼比較,我們應(yīng)該采用一個可逆的加密算法,在服務(wù)器端把這個加密后的字串還原為原始的明文密碼,然后與數(shù)據(jù)庫密碼進(jìn)行比較。既然是一個可逆的過程,那外掛制作者總有辦法知道我們的加密過程,所以,這個方法仍不夠安全。 哦,如果我們只是希望密碼不可能被還原出來,那還不容易嗎,使用一個不可逆的散列算法就行了。用戶在登錄時發(fā)送給服務(wù)器的是明文的帳號和經(jīng)散列后的不可逆密碼串,服務(wù)器取出密碼后也用同樣的算法進(jìn)行散列后再進(jìn)行比較。比如,我們就用使用最廣泛的 md5算法吧。噢,不要管那個王小云的什么論文,如果我真有那么好的運(yùn)氣,早中500w 了,還用在這考慮該死的服務(wù)器設(shè)計嗎? 似乎是一個很完美的方案,外掛制作者再也偷不到我們的密碼了。慢著,外掛偷密碼的目的是什么?是為了能用我們的帳號進(jìn)游戲!如果我們總是用一種固定的算法來對密碼做散列,那外掛只需要記住這個散列后的字串就行了,用這個做密碼就可以成功登錄。 嗯,這個問題好解決,我們不要用固定的算法進(jìn)行散列就是了。只是,問題在于服務(wù)器與客戶端采用的散列算法得出的字串必須是相同的,或者是可驗證其是否匹配的。很幸運(yùn)的是,偉大的數(shù)學(xué)字們早就為我們準(zhǔn)備好了很多優(yōu)秀的這類算法,而且經(jīng)理論和實踐都證明他們也確實是足夠安全的。 這其中之一是一個叫做 SRP 的算法,全稱叫做 Secure Remote Password,即安全遠(yuǎn)程密碼。wow 使用的是第6版,也就是 SRP6算法。有關(guān)其中的數(shù)學(xué)證明,如果有人能向我解釋清楚,并能讓我真正弄明白的話,我將非常感激。不過其代碼實現(xiàn)步驟倒是并不復(fù)雜,mangos 中的代碼也還算清晰,我們也不再贅述。 登錄服除了帳號驗證外還得提供另一項功能,就是在玩家的帳號驗證成功后返回給他一個服務(wù)器列表讓他去選擇。這個列表的狀態(tài)要定時刷新,可能有新的游戲世界開放了,也可能有些游戲世界非常不幸地停止運(yùn)轉(zhuǎn)了,這些狀態(tài)的變化都要盡可能及時地讓玩家知道。不管發(fā)生了什么事,用戶都有權(quán)利知道,特別是對于付過費(fèi)的用戶來說,我們不該藏著掖著,不是嗎? 這個游戲世界列表的功能將由大區(qū)服來提供,具體的結(jié)構(gòu)我們在之前也描述過,這里暫不做討論。登錄服將從大區(qū)服上獲取到的游戲世界列表發(fā)給已驗證通過的客戶端即可。好了,登錄服要實現(xiàn)的功能就這些,很簡單,是吧。 確實是太簡單了,不過簡單的結(jié)構(gòu)正好更適合我們來看一看游戲服務(wù)器內(nèi)部的模塊結(jié)構(gòu),以及一些服務(wù)器共有組件的實現(xiàn)方法。這就留作下一篇吧。 服務(wù)器公共組件實現(xiàn) - mangos 的游戲主循環(huán) 當(dāng)閱讀一項工程的源碼時,我們大概會選擇從 main 函數(shù)開始,而當(dāng)開始一項新的工程時,第一個寫下的函數(shù)大多也是 main。那我們就先來看看,游戲服務(wù)器代碼實現(xiàn)中, main 函數(shù)都做了些什么。 由于我在讀技術(shù)文章時最不喜看到的就是大段大段的代碼,特別是那些直接 Ctrl+C 再 Ctrl+V 后未做任何修改的代碼,用句時髦的話說,一點(diǎn)技術(shù)含量都沒有!所以在我們今后所要討論的內(nèi)容中,盡量會避免出現(xiàn)直接的代碼,在有些地方確實需要代碼來表述時,也將會選擇使用偽碼。 先從 mangos 的登錄服代碼開始。mangos 的登錄服是一個單線程的結(jié)構(gòu),雖然在數(shù)據(jù)庫連接中可以開啟一個獨(dú)立的線程,但這個線程也只是對無返回結(jié)果的執(zhí)行類 SQL 做緩沖,而對需要有返回結(jié)果的查詢類SQL 還是在主邏輯線程中阻塞調(diào)用的。 登錄服中唯一的這一個線程,也就是主循環(huán)線程對監(jiān)聽的 socket 做 select 操作,為每個連接進(jìn)來的客戶端讀取其上的數(shù)據(jù)并立即進(jìn)行處理,直到服務(wù)器收到 SIGABRT 或 SIGBREAK 信號時結(jié)束。 所以,mangos 登錄服主循環(huán)的邏輯,也包括后面游戲服的邏輯,主循環(huán)的關(guān)鍵代碼其實是在SocketHandler 中,也就是那個 Select 函數(shù)中。檢查所有的連接,對新到來的連接調(diào)用 OnAccept 方法,有數(shù)據(jù)到來的連接則調(diào)用 OnRead 方法,然后 socket 處理器自己定義對接收到的數(shù)據(jù)如何處理。 很簡單的結(jié)構(gòu),也比較容易理解。 只是,在對性能要求比較高的服務(wù)器上,select 一般不會是最好的選擇。如果我們使用 windows 平臺,那 IOCP 將是首選;如果是 linux,epool 將是不二選擇。我們也不打算討論基于 IOCP 或是基于 epool 的服務(wù)器實現(xiàn),如果僅僅只是要實現(xiàn)服務(wù)器功能,很簡單的幾個 API 調(diào)用即可,而且網(wǎng)上已有很多好的教程;如果是要做一個成熟的網(wǎng)絡(luò)服務(wù)器產(chǎn)品,不是我?guī)灼唵蔚募夹g(shù)介紹文章所能達(dá)到。 另外,在服務(wù)器實現(xiàn)上,網(wǎng)絡(luò) IO 與邏輯處理一般會放在不同的線程中,以免耗時較長的 IO 過程阻塞住了需要立即反應(yīng)的游戲邏輯。 數(shù)據(jù)庫的處理也類似,會使用異步的方式,也是避免耗時的查詢過程將游戲服務(wù)器主循環(huán)阻塞住。想象一下,因某個玩家上線而發(fā)起的一次數(shù)據(jù)庫查詢操作導(dǎo)致服務(wù)器內(nèi)所有在線玩家都卡住不動將是多么恐怖的一件事! 另外還有一些如事件、腳本、消息隊列、狀態(tài)機(jī)、日志和異常處理等公共組件,我們也會在接下來的時間里進(jìn)行探討。 服務(wù)器公共組件實現(xiàn) - 繼續(xù)來說主循環(huán) 前面我們只簡單了解了下 mangos 登錄服的程序結(jié)構(gòu),也發(fā)現(xiàn)了一些不足之處,現(xiàn)在我們就來看看如何提供一個更好的方案。 正如我們曾討論過的,為了游戲主邏輯循環(huán)的流暢運(yùn)行,所有比較耗時的 IO 操作都會分享到單獨(dú)的線程中去做,如網(wǎng)絡(luò) IO,數(shù)據(jù)庫 IO 和日志 IO 等。當(dāng)然,也有把這些分享到單獨(dú)的進(jìn)程中去做的。 另外對于大多數(shù)服務(wù)器程序來說,在運(yùn)行時都是作為精靈進(jìn)程或服務(wù)進(jìn)程的,所以我們并不需要服務(wù)器能夠處理控制臺用戶輸入,我們所要處理的數(shù)據(jù)來源都來自網(wǎng)絡(luò)。 這樣,主邏輯循環(huán)所要做的就是不停要取消息包來處理,當(dāng)然這些消息包不僅有來自客戶端的玩家操作數(shù)據(jù)包,也有來自 GM 服務(wù)器的管理命令,還包括來自數(shù)據(jù)庫查詢線程的返回結(jié)果消息包。這個循環(huán)將一直持續(xù),直到收到一個通知服務(wù)器關(guān)閉的消息包。 主邏輯循環(huán)的結(jié)構(gòu)還是很簡單的,復(fù)雜的部分都在如何處理這些消息包的邏輯上。我們可以用一段簡單的偽碼來描述這個循環(huán)過程: while (Message* msg = getMessage() if (msg 為服務(wù)器關(guān)閉消息) break; 處理 msg 消息; 這里就有一個問題需要探討了,在 getMessage()的時候,我們應(yīng)該去哪里取消息?前面我們考慮過,至少會有三個消息來源,而我們還討論過,這些消息源的 IO 操作都是在獨(dú)立的線程中進(jìn)行的,我們這里的主線程不應(yīng)該直接去那幾處消息源進(jìn)行阻塞式的 IO 操作。 很簡單,讓那些獨(dú)立的 IO 線程在接收完數(shù)據(jù)后自己送過來就是了。好比是,我這里提供了一個倉庫,有很多的供貨商,他們有貨要給我的時候只需要交到倉庫,然后我再到倉庫去取就是了,這個倉庫也就是消息隊列。消息隊列是一個普通的隊列實現(xiàn),當(dāng)然必須要提供多線程互斥訪問的安全性支持,其基本的接口定義大概類似這樣: IMessageQueue void putMessage(Message*); Message* getMessage(); 網(wǎng)絡(luò) IO,數(shù)據(jù)庫 IO 線程把整理好的消息包都加入到主邏輯循環(huán)線程的這個消息隊列中便返回。有關(guān)消息隊列的實現(xiàn)和線程間消息的傳遞在 ACE 中有比較完全的代碼實現(xiàn)及描述,還有一些使用示例,是個很好的參考。 這樣的話,我們的主循環(huán)就很清晰了,從主線程的消息隊列中取消息,處理消息,再取下一條消息 服務(wù)器公共組件實現(xiàn) - 消息隊列 既然說到了消息隊列,那我們繼續(xù)來稍微多聊一點(diǎn)吧。 我們所能想到的最簡單的消息隊列可能就是使用 stl 的 list 來實現(xiàn)了,即消息隊列內(nèi)部維護(hù)一個 list 和一個互斥鎖,putMessage 時將 message 加入到隊列尾,getMessage 時從隊列頭取一個 message 返回,同時在 getMessage 和 putMessage 之前都要求先獲取鎖資源。 實現(xiàn)雖然簡單,但功能是絕對滿足需求的,只是性能上可能稍稍有些不盡如人意。其最大的問題在頻繁的鎖競爭上。 對于如何減少鎖競爭次數(shù)的優(yōu)化方案,Ghost Cheng 提出了一種。提供一個隊列容器,里面有多個隊列,每個隊列都可固定存放一定數(shù)量的消息。網(wǎng)絡(luò) IO 線程要給邏輯線程投遞消息時,會從隊列容器中取一個空隊列來使用,直到將該隊列填滿后再放回容器中換另一個空隊列。而邏輯線程取消息時是從隊列容器中取一個有消息的隊列來讀取,處理完后清空隊列再放回到容器中。 這樣便使得只有在對隊列容器進(jìn)行操作時才需要加鎖,而 IO 線程和邏輯線程在操作自己當(dāng)前使用的隊列時都不需要加鎖,所以鎖競爭的機(jī)會大大減少了。 這里為每個隊列設(shè)了個最大消息數(shù),看來好像是打算只有當(dāng) IO 線程寫滿隊列時才會將其放回到容器中換另一個隊列。那這樣有時也會出現(xiàn) IO 線程未寫滿一個隊列,而邏輯線程又沒有數(shù)據(jù)可處理的情況,特別是當(dāng)數(shù)據(jù)量很少時可能會很容易出現(xiàn)。Ghost Cheng 在他的描述中沒有講到如何解決這種問題,但我們可以先來看看另一個方案。 這個方案與上一個方案基本類似,只是不再提供隊列容器,因為在這個方案中只使用了兩個隊列,arthur 在他的一封郵件中描述了這個方案的實現(xiàn)及部分代碼。兩個隊列,一個給邏輯線程讀,一個給 IO 線程用來寫,當(dāng)邏輯線程讀完隊列后會將自己的隊列與 IO 線程的隊列相調(diào)換。所以,這種方案下加鎖的次數(shù)會比較多一些,IO 線程每次寫隊列時都要加鎖,邏輯線程在調(diào)換隊列時也需要加鎖,但邏輯線程在讀隊列時是不需要加鎖的。 雖然看起來鎖的調(diào)用次數(shù)是比前一種方案要多很多,但實際上大部分鎖調(diào)用都是不會引起阻塞的,只有在邏輯線程調(diào)換隊列的那一瞬間可能會使得某個線程阻塞一下。另外對于鎖調(diào)用過程本身來說,其開銷是完全可以忽略的,我們所不能忍受的僅僅是因為鎖調(diào)用而引起的阻塞而已。 兩種方案都是很優(yōu)秀的優(yōu)化方案,但也都是有其適用范圍的。Ghost Cheng 的方案因為提供了多個隊列,可以使得多個 IO 線程可以總工程師的,互不干擾的使用自己的隊列,只是還有一個遺留問題我們還不了解其解決方法。arthur 的方案很好的解決了上一個方案遺留的問題,但因為只有一個寫隊列,所以當(dāng)想要提供多個 IO 線程時,線程間互斥地寫入數(shù)據(jù)可能會增大競爭的機(jī)會,當(dāng)然,如果只有一個 IO 線程那將是非常完美的。 服務(wù)器公共組件實現(xiàn) - 環(huán)形緩沖區(qū) 消息隊列鎖調(diào)用太頻繁的問題算是解決了,另一個讓人有些苦惱的大概是這太多的內(nèi)存分配和釋放操作了。頻繁的內(nèi)存分配不但增加了系統(tǒng)開銷,更使得內(nèi)存碎片不斷增多,非常不利于我們的服務(wù)器長期穩(wěn)定運(yùn)行。也許我們可以使用內(nèi)存池,比如 SGI STL 中附帶的小內(nèi)存分配器。但是對于這種按照嚴(yán)格的先進(jìn)先出順序處理的,塊大小并不算小的,而且塊大小也并不統(tǒng)一的內(nèi)存分配情況來說,更多使用的是一種叫做環(huán)形緩沖區(qū)的方案,mangos 的網(wǎng)絡(luò)代碼中也有這么一個東西,其原理也是比較簡單的。 就好比兩個人圍著一張圓形的桌子在追逐,跑的人被網(wǎng)絡(luò) IO 線程所控制,當(dāng)寫入數(shù)據(jù)時,這個人就往前跑;追的人就是邏輯線程,會一直往前追直到追上跑的人。如果追上了怎么辦?那就是沒有數(shù)據(jù)可讀了,先等會兒唄,等跑的人向前跑幾步了再追,總不能讓游戲沒得玩了吧。那要是追的人跑的太慢,跑的人轉(zhuǎn)了一圈過來反追上追的人了呢?那您也先歇會兒吧。要是一直這么反著追,估計您就只能換一個跑的更快的追逐者了,要不這游戲還真沒法玩下去。 前面我們特別強(qiáng)調(diào)了,按照嚴(yán)格的先進(jìn)先出順序進(jìn)行處理,這是環(huán)形緩沖區(qū)的使用必須遵守的一項要求。也就是,大家都得遵守規(guī)定,追的人不能從桌子上跨過去,跑的人當(dāng)然也不允許反過來跑。至于為什么,不需要多做解釋了吧。 環(huán)形緩沖區(qū)是一項很好的技術(shù),不用頻繁的分配內(nèi)存,而且在大多數(shù)情況下,內(nèi)存的反復(fù)使用也使得我們能用更少的內(nèi)存塊做更多的事。 在網(wǎng)絡(luò) IO 線程中,我們會為每一個連接都準(zhǔn)備一個環(huán)形緩沖區(qū),用于臨時存放接收到的數(shù)據(jù),以應(yīng)付半包及粘包的情況。在解包及解密完成后,我們會將這個數(shù)據(jù)包復(fù)制到邏輯線程消息隊列中,如果我們只使用一個隊列,那這里也將會是個環(huán)形緩沖區(qū),IO 線程往里寫,邏輯線程在后面讀,互相追逐??梢俏覀兪褂昧饲懊娼榻B的優(yōu)化方案后,可能這里便不再需要環(huán)形緩沖區(qū)了,至少我們并不再需要他們是環(huán)形的了。因為我們對同一個隊列不再會出現(xiàn)同時讀和寫的情況,每個隊列在寫滿后交給邏輯線程去讀,邏輯線程讀完后清空隊列再交給 IO 線程去寫,一段固定大小的緩沖區(qū)即可。沒關(guān)系,這么好的技術(shù),在別的地方一定也會用到的。 服務(wù)器公共組件實現(xiàn) - 發(fā)包的方式 前面一直都在說接收數(shù)據(jù)時的處理方法,我們應(yīng)該用專門的 IO 線程,接收到完整的消息包后加入到主線程的消息隊列,但是主線程如何發(fā)送數(shù)據(jù)還沒有探討過。 一般來說最直接的方法就是邏輯線程什么時候想發(fā)數(shù)據(jù)了就直接調(diào)用相關(guān)的 socket API 發(fā)送,這要求服務(wù)器的玩家對象中保存其連接的 socket 句柄。但是直接 send 調(diào)用有時候有會存在一些問題,比如遇到系統(tǒng)的發(fā)送緩沖區(qū)滿而阻塞住的情況,或者只發(fā)送了一部分?jǐn)?shù)據(jù)的情況也時有發(fā)生。我們可以將要發(fā)送的數(shù)據(jù)先緩存一下,這樣遇到未發(fā)送完的,在邏輯線程的下一次處理時可以接著再發(fā)送。 考慮數(shù)據(jù)緩存的話,那這里這可以有兩種實現(xiàn)方式了,一是為每個玩家準(zhǔn)備一個緩沖區(qū),另外就是只有一個全局的緩沖區(qū),要發(fā)送的數(shù)據(jù)加入到全局緩沖區(qū)的時候同時要指明這個數(shù)據(jù)是發(fā)到哪個 socket 的。如果使用全局緩沖區(qū)的話,那我們可以再進(jìn)一步,使用一個獨(dú)立的線程來處理數(shù)據(jù)發(fā)送,類似于邏輯線程對數(shù)據(jù)的處理方式,這個獨(dú)立發(fā)送線程也維護(hù)一個消息隊列,邏輯線程要發(fā)數(shù)據(jù)時也只是把數(shù)據(jù)加入到這個隊列中,發(fā)送線程循環(huán)取包來執(zhí)行 send 調(diào)用,這時的阻塞也就不會對邏輯線程有任何影響了。 采用第二種方式還可以附帶一個優(yōu)化方案。一般對于廣播消息而言,發(fā)送給周圍玩家的數(shù)據(jù)都是完全相同的,我們?nèi)绻捎媒o每個玩家一個緩沖隊列的方式,這個數(shù)據(jù)包將需要拷貝多份,而采用一個全局發(fā)送隊列時,我們只需要把這個消息入隊一次,同時指明該消息包是要發(fā)送給哪些 socket 的即可。有關(guān)該優(yōu)化的說明在云風(fēng)描述其連接服務(wù)器實現(xiàn)的 blog 文章中也有講到,有興趣的可以去閱讀一下。 服務(wù)器公共組件實現(xiàn) - 狀態(tài)機(jī) 有關(guān) State 模式的設(shè)計意圖及實現(xiàn)就不從設(shè)計模式中摘抄了,我們只來看看游戲服務(wù)器編程中如何使用State 設(shè)計模式。 首先還是從 mangos 的代碼開始看起,我們注意到登錄服在處理客戶端發(fā)來的消息時用到了這樣一個結(jié)構(gòu)體: struct AuthHandler eAuthCmd cmd; uint32 status; bool (AuthSocket:*handler)(void); ; 該結(jié)構(gòu)體定義了每個消息碼的處理函數(shù)及需要的狀態(tài)標(biāo)識,只有當(dāng)前狀態(tài)滿足要求時才會調(diào)用指定的處理函數(shù),否則這個消息碼的出現(xiàn)是不合法的。這個 status 狀態(tài)標(biāo)識的定義是一個宏,有兩種有效的標(biāo)識,STATUS_CONNECTED 和 STATUS_AUTHED,也就是未認(rèn)證通過和已認(rèn)證通過。而這個狀態(tài)標(biāo)識的改變是在運(yùn)行時進(jìn)行的,確切的說是在收到某個消息并正確處理完后改變的。 我們再來看看設(shè)計模式中對 State 模式的說明,其中關(guān)于 State 模式適用情況里有一條,當(dāng)操作中含有龐大的多分支的條件語句,且這些分支依賴于該對象的狀態(tài),這個狀態(tài)通常用一個或多個枚舉變量表示。 描述的情況與我們這里所要處理的情況是如此的相似,也許我們可以試一試。那再看看 State 模式提供的解決方案是怎樣的,State 模式將每一個條件分支放入一個獨(dú)立的類中。 由于這里的兩個狀態(tài)標(biāo)識只區(qū)分出了兩種狀態(tài),所以,我們僅需要兩個獨(dú)立的類,用以表示兩種狀態(tài)即可。然后,按照 State 模式的描述,我們還需要一個 Context 類,也就是狀態(tài)機(jī)管理類,用以管理當(dāng)前的狀態(tài)類。稍作整理,大概的代碼會類似這樣: 狀態(tài)基類接口: StateBase void Enter() = 0; void Leave() = 0; void Process(Message* msg) = 0; ; 狀態(tài)機(jī)基類接口: MachineBase void ChangeState(StateBase* state) = 0; StateBase* m_curState; ; 我們的邏輯處理類會從 MachineBase 派生,當(dāng)取出數(shù)據(jù)包后交給當(dāng)前狀態(tài)處理,前面描述的兩個狀態(tài)類從 StateBase 派生,每個狀態(tài)類只處理該狀態(tài)標(biāo)識下需要處理的消息。當(dāng)要進(jìn)行狀態(tài)轉(zhuǎn)換時,調(diào)用MachineBase 的 ChangeState()方法,顯示地告訴狀態(tài)機(jī)管理類自己要轉(zhuǎn)到哪一個狀態(tài)。所以,狀態(tài)類內(nèi)部需要保存狀態(tài)機(jī)管理類的指針,這個可以在狀態(tài)類初始化時傳入。具體的實現(xiàn)細(xì)節(jié)就不做過多描述了。 使用狀態(tài)機(jī)雖然避免了復(fù)雜的判斷語句,但也引入了新的麻煩。當(dāng)我們在進(jìn)行狀態(tài)轉(zhuǎn)換時,可能會需要將一些現(xiàn)場數(shù)據(jù)從老狀態(tài)對象轉(zhuǎn)移到新狀態(tài)對象,這需要在定義接口時做一下考慮。如果不希望執(zhí)行拷貝,那么這里公有的現(xiàn)場數(shù)據(jù)也可放到狀態(tài)機(jī)類中,只是這樣在使用時可能就不那么優(yōu)雅了。 正如同在設(shè)計模式中所描述的,所有的模式都是已有問題的另一種解決方案,也就是說這并不是唯一的解決方案。放到我們今天討論的 State 模式中,就拿登錄服所處理的兩個狀態(tài)來說,也許用 mangos 所采用的遍歷處理函數(shù)的方法可能更簡單,但當(dāng)系統(tǒng)中的狀態(tài)數(shù)量增多,狀態(tài)標(biāo)識也變多的時候,State 模式就顯得尤其重要了。 比如在游戲服務(wù)器上玩家的狀態(tài)管理,還有在實現(xiàn) NPC 人工智能時的各種狀態(tài)管理,這些就留作以后的專題吧。 服務(wù)器公共組件 - 事件與信號 關(guān)于這一節(jié),這幾天已經(jīng)打了好幾遍草稿,總覺得說不清楚,也不好組織這些內(nèi)容,但是打鐵要趁熱,為避免熱情消退,先整理一點(diǎn)東西放這,好繼續(xù)下面的主題,以后如果有機(jī)會再回來完善吧。本節(jié)內(nèi)容欠考慮,希望大家多給點(diǎn)意見。 有些類似于 QT 中的 event 與 signal,我將一些動作請求消息定義為事件,而將狀態(tài)改變消息定義為信號。比如在 QT 應(yīng)用程序中,用戶的一次鼠標(biāo)點(diǎn)擊會產(chǎn)生一個鼠標(biāo)點(diǎn)擊事件加入到事件隊列中,當(dāng)處理此事件時可能會導(dǎo)致某個按鈕控件產(chǎn)生一個 clicked()信號。 對應(yīng)到我們的服務(wù)器上的一個例子,玩家登錄時會發(fā)給服務(wù)器一個請求登錄的數(shù)據(jù)包,服務(wù)器可將其當(dāng)作一個用戶登錄事件,該事件處理完后可能會產(chǎn)生一個用戶已登錄信號。 這樣,與 QT 類似,對于事件我們可以重定義其處理方法,甚至過濾掉某些事件使其不被處理,但對于信號我們只是收到了一個通知,有些類似于 Observe 模式中的觀察者,當(dāng)收到更新通知時,我們只能更新自己的狀態(tài),對剛剛發(fā)生的事件我不已不能做任何影響。 仔細(xì)來看,事件與信號其實并無多大差別,從我們對其需求上來說,都只要能注冊事件或信號響應(yīng)函數(shù),在事件或信號產(chǎn)生時能夠被通知到即可。但有一項區(qū)別在于,事件處理函數(shù)的返回值是有意義的,我們要根據(jù)這個返回值來確定是否還要繼續(xù)事件的處理,比如在 QT 中,事件處理函數(shù)如果返回 true,則這個事件處理已完成,QApplication 會接著處理下一個事件,而如果返回 false,那么事件分派函數(shù)會繼續(xù)向上尋找下一個可以處理該事件的注冊方法。信號處理函數(shù)的返回值對信號分派器來說是無意義的。 簡單點(diǎn)說,就是我們可以為事件定義過濾器,使得事件可以被過濾。這一功能需求在游戲服務(wù)器上是到處存在的。 關(guān)于事件和信號機(jī)制的實現(xiàn),網(wǎng)絡(luò)上的開源訓(xùn)也比較多,比如 FastDelegate,sigslot,boost:signal 等,其中 sigslot 還被 Google 采用,在 libjingle 的代碼中我們可以看到他是如何被使用的。 在實現(xiàn)事件和信號機(jī)制時或許可以考慮用同一套實現(xiàn),在前面我們就分析過,兩者唯一的區(qū)別僅在于返回值的處理上。 另外還有一個需要我們關(guān)注的問題是事件和信號處理時的優(yōu)先級問題。在 QT 中,事件因為都是與窗口相關(guān)的,所以事件回調(diào)時都是從當(dāng)前窗口開始,一級一級向上派發(fā),直到有一個窗口返回 true,截斷了事件的處理為止。對于信號的處理則比較簡單,默認(rèn)是沒有順序的,如果需要明確的順序,可以在信號注冊時顯示地指明槽的位置。 在我們的需求中,因為沒有窗口的概念,事件的處理也與信號類似,對注冊過的處理器要按某個順序依次回調(diào),所以優(yōu)先級的設(shè)置功能是需要的。 最后需要我們考慮的是事件和信號的處理方式。在 QT 中,事件使用了一個事件隊列來維護(hù),如果事件的處理中又產(chǎn)生了新的事件,那么新的事件會加入到隊列尾,直到當(dāng)前事件處理完畢后,QApplication 再去隊列頭取下一個事件來處理。而信號的處理方式有些不同,信號處理是立即回調(diào)的,也就是一個信號產(chǎn)生后,他上面所注冊的所有槽都會立即被回調(diào)。這樣就會產(chǎn)生一個遞歸調(diào)用的問題,比如某個信號處理器中又產(chǎn)生了一個信號,會使得信號的處理像一棵樹一樣的展開。我們需要注意的一個很重要的問題是會不會引起循環(huán)調(diào)用。 關(guān)于事件機(jī)制的考慮其實還很多,但都是一些不成熟的想法。在上面的文字中就同時出現(xiàn)了消息、事件和信號三個相近的概念,而在實際處理中,經(jīng)常發(fā)現(xiàn)三者不知道如何界定的情況,實際的情況比我在這里描述的要混亂的多。 這里也就當(dāng)是挖下一個坑,希望能夠有所交流。 再談登錄服的實現(xiàn) 離我們的登錄服實現(xiàn)已經(jīng)太遠(yuǎn)了,先拉回來一下。 關(guān)于登錄服、大區(qū)服及游戲世界服的結(jié)構(gòu)之前已做過探討,這里再把各自的職責(zé)和關(guān)系列一下。 GateWay/WorldServer GateWay/WodlServer LoginServer LoginServer DNSServer WorldServerMgr | | | | | - | | | internet | clients 其中 DNSServer 負(fù)責(zé)帶負(fù)載均衡的域名解析服務(wù),返回 LoginServer 的 IP 地址給客戶端。WorldServerMgr 維護(hù)當(dāng)前大區(qū)內(nèi)的世界服列表,LoginServer 會從這里取世界列表發(fā)給客戶端。LoginServer 處理玩家的登錄及世界服選擇請求。 GateWay/WorldServer 為各個獨(dú)立的世界服或者通過網(wǎng)關(guān)連接到后面的世界服。 在 mangos 的代碼中,我們注意到登錄服是從數(shù)據(jù)庫中取的世界列表,而在 wow 官方服務(wù)器中,我們卻會注意到,這個世界服列表并不是一開始就固定,而是動態(tài)生成的。當(dāng)每周一次的維護(hù)完成之后,我們可以很明顯的看到這個列表生成的過程。剛開始時,世界列表是空的,慢慢的,世界服會一個個加入進(jìn)來,而這里如果有世界服當(dāng)機(jī),他會顯示為離線,不
溫馨提示
- 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
- 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
- 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負(fù)責(zé)。
- 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請與我們聯(lián)系,我們立即糾正。
- 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時也不承擔(dān)用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。
最新文檔
- 2025年新能源汽車輕量化車身設(shè)計輕量化制造技術(shù)研究報告
- 物流金融服務(wù)在供應(yīng)鏈金融創(chuàng)新中的應(yīng)用場景與風(fēng)險分析報告
- 公司總部工程部管理制度
- 事業(yè)部及子公司管理制度
- 幼兒園電教設(shè)備管理制度
- 臺球廳消防安全管理制度
- 智障學(xué)生用餐管理制度
- 臨沂小企業(yè)財務(wù)管理制度
- 勞動合同付公司管理制度
- 德國職業(yè)經(jīng)理人管理制度
- 康復(fù)器具租賃協(xié)議書
- 2024年四川省成都市中考生物試卷(含答案與解析)
- 2025抖音財經(jīng)內(nèi)容生態(tài)報告
- 2025重慶理工職業(yè)學(xué)院輔導(dǎo)員考試題庫
- 跌倒墜床的評估及預(yù)防
- 2024北京朝陽區(qū)高一(下)期末物理試題和答案
- 麻將智力考試試題及答案
- 軌行區(qū)安全注意事項
- 印章刻制工序的質(zhì)量控制流程
- 幼兒園獲獎公開課:中班語言美術(shù)《有趣的西瓜皮》課件
- 室內(nèi)零星維修工程施工方案
評論
0/150
提交評論