[轉貼] 遊戲服務器架構一

來自:http://www.libing.net.cn/read.php/1724.htmphp

這裏討論的遊戲服務器架構大概是目前國內乃至世界上的網遊通用的一種架構了:
http://bbs.gameres.com/showthread.asp?threadid=93775
做者:qinglan


有段時間沒有研究技術了,此次正好看到了新版的mangos,較之之前我看的版本有了比較大的完善,因而再次瀏覽了下他的代碼,也藉此機會整理下我在遊戲服務器開發方面的一些心得,與你們探討。
  另外因爲爲避免與公司引發一些沒必要要的糾紛,我所描述的全都是經過google可以找到的資料,因此也能夠認爲我下面的內容都是網上所找資料的整理合集。在平時的開發中我也搜索過相關的中文網頁,不多有講遊戲服務器相關技術的,你們的討論主要仍是集中在3D相關技術,因此也但願我將開始的這幾篇文章可以起到拋磚引玉的做用,潛水的兄弟們也都上來透透氣。

  要描述一項技術或是一個行業,通常都會從其最古老的歷史開始提及,我本也想按着這個套路走,無奈本人乃一八零後小輩,沒有經歷過那些苦澀的卻使人羨慕的單機遊戲開發,也沒有響噹噹的拿的出手的優秀做品,因此也就只能就我所瞭解的一些技術作些簡單的描述。一來算是敦促本身對知識作個梳理,二來與你們探討的過程也可以找到我以前學習的不足和理解上的錯誤,最後呢,有可能的話也跟業內的同行們混個臉熟,哪天要是想換個工做了也好有我的幫忙介紹下。最後的理由有些俗了。

  關於遊戲開發,正如雲風在其blog上所說,遊戲項目始終只是個小工程,另外開發時間仍是個很重要的問題,因此軟件工程的思想及方法在大部分的遊戲公司中並不怎麼受歡迎。固然這也只是從我我的一些膚淺的瞭解所得,可能不夠充分。從遊戲開發的程序團隊的人員構成上也可看出來,基本只能算做是小開發團隊。有些工做室性質的開發團隊,那就更簡單了。

  我所瞭解的早些的開發團隊,其成員間沒有什麼嚴格的分工,你們憑興趣自由選擇一些模塊來負責,完成了再去負責另外一模塊,有其餘同事的工做須要接手或協助的也會當即轉入。因此遊戲開發人員基本都是多面手,從網絡到數據庫,從遊戲邏輯到圖形圖象,每一項都有所瞭解,並能實際應用。或者說都具備很是強的學習能力,在接手一項新的任務後能在很短的時間內對該領域的技術迅速掌握並消化,並且還能現炒現賣。固然,這也與早期2D遊戲的技術要求相對比較簡單,遊戲邏輯也沒有如今這般複雜有關。而更重要的多是,都是被逼出來的吧!:)

  好了,閒話少說,下一篇,也就是第一篇了,主題爲,服務器結構探討。


服務器結構探討 -- 最簡單的結構

  所謂服務器結構,也就是如何將服務器各部分合理地安排,以實現最初的功能需求。因此,結構本無所謂正確與錯誤;固然,優秀的結構更有助於系統的搭建,對系統的可擴展性及可維護性也有更大的幫助。

  好的結構不是一蹴而就的,並且每一個設計者心中的那把尺都不相同,因此這個優秀結構的定義也就沒有定論。在這裏,咱們不打算對現有遊戲結構作評價,而是試着從頭開始搭建一個咱們須要的MMOG結構。

  對於一個最簡單的遊戲服務器來講,它只須要可以接受來自客戶端的鏈接請求,而後處理客戶端在遊戲世界中的移動及交互,也即遊戲邏輯處理便可。若是咱們把這兩項功能集成到一個服務進程中,則最終的結構很簡單:

  client ----- server

  嗯,太簡單了點,這樣也敢叫服務器結構?好吧,如今咱們來往裏面稍稍加點東西,讓它看起來更像是服務器結構一些。

  通常來講,咱們在接入遊戲服務器的時候都會要提供一個賬號和密碼,驗證經過後才能進入。關於爲何要提供用戶名和密碼才能進入的問題咱們這裏不打算作過多討論,雲風曾對此也提出過相似的疑問,並給出了只用一個標識串就能進入的設想,有興趣的能夠去看看他們的討論。但無論是採用何種方式進入,照目前看來咱們的服務器起碼得提供一個賬號驗證的功能。

  咱們把觀察點先集中在一個大區內。在大多數狀況下,一個大區內都會有多組遊戲服,也就是多個遊戲世界可供選擇。簡單點來實現,咱們徹底能夠拋棄這個大區的概念,認爲一個大區也就是放在同一個機房的多臺服務器組,各服務器組間沒有什麼關係。這樣,咱們可爲每組服務器單獨配備一臺登陸服。最後的結構圖應該像這樣:

  loginServer   gameServer
     |           /
     |         /
     client

  該結構下的玩家操做流程爲,先選擇大區,再選擇大區下的某臺服務器,即某個遊戲世界,點擊進入後開始賬號驗證過程,驗證成功則進入了該遊戲世界。可是,若是玩家想要切換遊戲世界,他只能先退出當前遊戲世界,而後進入新的遊戲世界從新進行賬號驗證。

  早期的遊戲大都採用的是這種結構,有些遊戲在實現時採用了一些技術手段使得在切換遊戲服時不須要再次驗證賬號,但總體結構仍是未作改變。

  該結構存在一個服務器資源配置的問題。由於登陸服處理的邏輯相對來講比較簡單,就是將玩家提交的賬號和密碼送到數據庫進行驗證,和生成會話密鑰發送給遊戲服和客戶端,操做完成後鏈接就會當即斷開,並且玩家在之後的遊戲過程當中不會再與登陸服打任何交道。這樣處理短鏈接的過程使得系統在大多數狀況下都是比較空閒的,可是在某些時候,因爲請求比較密集,好比開新服的時候,登陸服的負載又會比較大,甚至會處理不過來。

  另外在實際的遊戲運營中,有些遊戲世界很火爆,而有些遊戲世界卻很是冷清,甚至沒有多少人玩的狀況也是很常見的。因此,咱們可否更合理地配置登陸服資源,使得整個大區內的登陸服能夠共享就成了下一步改進的目標。

服務器結構探討 -- 登陸服的負載均衡

  回想一下咱們在玩wow時的操做流程:運行wow.exe進入遊戲後,首先就會要求咱們輸入用戶名和密碼進行驗證,驗證成功後纔會出來遊戲世界列表,以後是排隊進入遊戲世界,開始遊戲...

  能夠看到跟前面的描述有個很明顯的不一樣,那就是要先驗證賬號再選擇遊戲世界。這種結構也就使得登陸服不是固定配備給個遊戲世界,而是全區共有的。

  咱們能夠試着從實際需求的角度來考慮一下這個問題。正如咱們以前所描述過的那樣,登陸服在大多數狀況下都是比較空閒的,也許咱們的一個擁有20個遊戲世界的大區僅僅使用10臺或更少的登陸服便可知足需求。而當在開新區的時候,或許要配備40臺登陸服才能應付那如潮水般涌入的玩家登陸請求。因此,登陸服在設計上應該能知足這種動態增刪的需求,咱們能夠在任什麼時候候爲大區增長或減小登陸服的部署。

  固然,在這裏也不會存在要求添加太多登陸服的狀況。仍是拿開新區的狀況來講,即便新增長登陸服知足了玩家登陸的請求,遊戲世界服的承載能力依然有限,玩家同樣只能在排隊系統中等待,或者是進入到遊戲世界中致使你們都卡。

  另外,當咱們在增長或移除登陸服的時候不該該須要對遊戲世界服有所改動,也不會要求重啓世界服,固然也不該該要求客戶端有什麼更新或者修改,一切都是在背後自動完成。

  最後,有關數據持久化的問題也在這裏考慮一下。通常來講,使用現有的商業數據庫系統比本身手工技術先進要明智得多。咱們須要持久化的數據有玩家的賬號及密碼,玩家建立的角色相關信息,另外還有一些遊戲世界全局共有數據也須要持久化。

  好了,需求已經提出來了,如今來考慮如何將其實現。

  對於負載均衡來講,已有了成熟的解決方案。通常最經常使用,也最簡單部署的應該是基於DNS的負載均衡系統了,其經過在DNS中爲一個域名配置多個IP地址來實現。最新的DNS服務已實現了根據服務器系統狀態來實現的動態負載均衡,也就是實現了真正意義上的負載均衡,這樣也就有效地解決了當某臺登陸服當機後,DNS服務器不能當即作出反應的問題。固然,若是找不到這樣的解決方案,本身從頭打造一個也並不難。並且,經過DNS來實現的負載均衡已經包含了所作的修改對登陸服及客戶端的透明。

  而對於數據庫的應用,在這種結構下,登陸服及遊戲世界服都會須要鏈接數據庫。從數據庫服務器的部署上來講,能夠將賬號和角色數據都放在一箇中心數據庫中,也可分爲兩個不一樣的庫分別來處理,基到從物理上分到兩臺不一樣的服務器上去也行。

  可是對於不一樣的遊戲世界來講,其角色及遊戲內數據都是互相獨立的,因此通常狀況下也就爲每一個遊戲世界單獨配備一臺數據庫服務器,以減輕數據庫的壓力。因此,總體的服務器結構應該是一個大區有一臺賬號數據庫服務器,全部的登陸服都鏈接到這裏。而每一個遊戲世界都有本身的遊戲數據庫服務器,只容許本遊戲世界內的服務器鏈接。

  最後,咱們的服務器結構就像這樣:

               大區服務器        
          /     |       \
            /       |        \
            登陸服1   登陸服2   世界服1   世界服2
         \         |         |       |  
          \       |         |         |
          賬號數據庫         DBS     DBS

  這裏既然討論到了大區及賬號數據庫,因此順帶也說一下關於激活大區的概念。wow中一共有八個大區,咱們想要進入某個大區遊戲以前,必須到官網上激活這個區,這是爲何呢?

  通常來講,在各個大區賬號數據庫之上還有一個總的賬號數據庫,咱們能夠稱它爲中心數據庫。好比咱們在官網上註冊了一個賬號,這時賬號數據是隻保存在中心數據庫上的。而當咱們要到一區去建立角色開始遊戲的時候,在一區的賬號數據庫中並無咱們的賬號數據,因此,咱們必須先到官網上作一次激活操做。這個激活的過程也就是從中心庫上把咱們的賬號數據拷貝到所要到的大區賬號數據庫中。

服務器結構探討 -- 簡單的世界服實現

  討論了這麼久咱們一直都尚未進入遊戲世界服務器內部,如今就讓咱們來窺探一下里面的結構吧。

  對於如今大多數MMORPG來講,遊戲服務器要處理的基本邏輯有移動、聊天、技能、物品、任務和生物等,另外還有地圖管理與消息廣播來對其餘高級功能作支撐。如縱隊、好友、公會、戰場和副本等,這些都是經過基本邏輯功能組合或擴展而成。

  在全部這些基礎邏輯中,與咱們要討論的服務器結構關係最緊密的當屬地圖管理方式。決定了地圖的管理方式也就決定了咱們的服務器結構,咱們仍然先從最簡單的實現方式開始提及。

  回想一下咱們曾戰鬥過無數個夜晚的暗黑破壞神,整個暗黑的世界被分爲了若干個獨立的小地圖,當咱們在地圖間穿越時,通常都要通過一個叫作傳送門的裝置。世界中有些地圖間雖然在地理上是直接相連的,但咱們發現其遊戲內部的邏輯倒是徹底隔離的。能夠這樣認爲,一塊地圖就是一個獨立的數據處理單元。

  既然如此,咱們就把每塊地圖都看成是一臺獨立的服務器,他提供了在這塊地圖上游戲時的全部邏輯功能,至於內部結構如何劃分咱們暫不理會,先把他看成一個黑盒子吧。

  當兩我的合做作一件事時,咱們能夠以對等的關係相互協商着來作,並且通常也都不會有什麼問題。當人數增長到三個時,咱們對等的合做關係可能會有些複雜,由於咱們每一個人都同時要與另兩我的合做協商。正如俗語所說的那樣,三個和尚可能會碰到沒水喝的狀況。當人數繼續增長,狀況就變得不那麼簡單了,咱們得須要一個管理者來對咱們的工做進行分工、協調。遊戲的地圖服務器之間也是這麼回事。

  通常來講,咱們的遊戲世界不可能會只有一塊或者兩塊小地圖,那瓜熟蒂落的,也就須要一個地圖管理者。先稱它爲遊戲世界的中心服務器吧,畢竟是管理者嘛,你們都以它爲中心。

  中心服務器主要維護一張地圖ID到地圖服務器地址的映射表。當咱們要進入某張地圖時,會從中心服上取得該地圖的IP和port告訴客戶端,客戶端主動去鏈接,這樣進入他想要去的遊戲地圖。在整個遊戲過程當中,客戶端始終只會與一臺地圖服務器保持鏈接,當要切換地圖的時候,在獲取到新地圖的地址後,會先與當前地圖斷開鏈接,再進入新的地圖,這樣保證玩家數據在服務器上只有一份。

  咱們來看看結構圖是怎樣的:

              中心服務器
           /       \         \
         /         \         \
       登陸服     地圖1     地圖2   地圖n
         \         |         /       /
           \       |         /       /
                客戶端

  很簡單,不是嗎。可是簡單並不表示功能上會有什麼損失,簡單也更不能表示遊戲不能賺錢。早期很多遊戲也確實採用的就是這種簡單結構。

服務器結構探討 -- 繼續世界服

  都已經看出來了,這種每切換一次地圖就要從新鏈接服務器的方式實在是不夠優雅,並且在實際遊戲運營中也發現,地圖切換致使的卡號,複製裝備等問題很是多,這裏徹底就是一個事故多發地段,如何避免這種頻繁的鏈接操做呢?

  最直接的方法就是把那個圖倒轉過來就好了。客戶端只須要鏈接到中心服上,全部到地圖服務器的數據都由中心服來轉發。很完美的解決方案,不是嗎?

  這種結構在實際的部署中也遇到了一些挑戰。對於通常的MMORPG服務器來講,單臺服務器的承載量平均在2000左右,若是你的服務器很不幸地只能帶1000人,不要緊,很多遊戲都是如此;若是你的服務器上跑了3000多玩家依然比較流暢,那你能夠自豪地告訴你的策劃,多設計些大量消耗服務器資源的玩法吧,好比大型國戰、公會戰爭等。

  2000人,彷佛咱們的策劃朋友們不大願意接受這個數字。咱們將地圖服務器分開來原來也是想將負載分開,以多帶些客戶端,如今要全部的鏈接都從中心服上轉發,那鏈接數又遇到單臺服務器的可最大承載量的瓶頸了。

  這裏有必要再解釋下這個數字。我知道,有人必定會說,才帶2000人,那是你水平不行,我隨便寫個TCP服務器均可帶個五六千鏈接。問題偏偏在於你是隨便寫的,而MMORPG的服務器是複雜設計的。若是一個演示socket API用的echo服務器就能知足MMOG服務器的需求,那寫服務器該是件多麼愜意的事啊。

  但咱們所遇到的事實是,服務器收到一個移動包後,要向周圍全部人廣播,而不是echo服務器那樣簡單的迴應;服務器在收到一個鏈接斷開通知時要向不少人通知玩家退出事件,並將該玩家的資料寫入數據庫,而不是echo服務器那樣什麼都不須要作;服務器在收到一個物品使用請求包後要作一系列的邏輯判斷以檢查玩家有沒有做弊;服務器上還啓動着不少定時器用來更新遊戲世界的各類狀態......

  其實這麼一比較,咱們也看出資源消耗的所在了:服務器上大量的複雜的邏輯處理。再回過頭來看看咱們想要實現的結構,咱們既想要有一個惟一的入口,使得客戶端不用頻繁改變鏈接,又但願這個惟一入口的負載不會太大,以至於接受不了多少鏈接。

  仔細看一看這個需求,咱們想要的僅僅只是一臺管理鏈接的服務器,並不打算讓他承擔太多的遊戲邏輯。既然如此,那五六千個鏈接也還有知足咱們的要求。至少在如今來講,一個遊戲世界內,也就是一組服務器內同時有五六千個在線的玩家仍是件讓人很興奮的事。事實上,在大多數遊戲的大部分時間裏,這個數字也是很讓人眼紅的。

  什麼?你說夢幻、魔獸還有史先生的那個什麼征途遠不止這麼點人了!噢,我說的是大多數,是大多數,不包括那些明星。你知道大陸如今有多少遊戲在運營嗎?或許你又該說,咱們不應在一開始就把本身的目標定的過低!好吧,咱們仍是先不談這個。

  繼續咱們的結構討論。通常來講,咱們把這臺負責鏈接管理的服務器稱爲網關服務器,由於內部的數據都要經過這個網關才能出去,不過從這臺服務器提供的功能來看,稱其爲反向代理服務器可能更合適。咱們也不在這個名字上糾纏了,就按你們通用的叫法,仍是稱他爲網關服務器吧。

  網關以後的結構咱們依然能夠採用以前描述的方案,只是,彷佛並無必要爲每個地圖都開一個獨立的監聽端口了。咱們能夠試着對地圖進行一些劃分,由一個Master Server來管理一些更小的Zone Server,玩家經過網關鏈接到Master Server上,而實際與地圖有關的邏輯是分派給更小的Zone Server去處理。

  最後的結構看起來大概是這樣的:

         Zone Server         Zone Server
                 \             /
                 \           /
                 Master Server           Master Server
                     /       \                   /
                   /         \                 /
         Gateway Server         \               /
             |         \         \             /
             |         \         \           /
             |               Center Server
             |
             |
           Client

服務器結構探討 -- 最終的結構

  若是咱們就此打住,可能立刻就會有人要嗤之以鼻了,就這點古董級的技術也敢出來現。好吧,咱們仍是把以前留下的問題拿出來解決掉吧。

  通常來講,當某一部分能力達不到咱們的要求時,最簡單的解決方法就是在此多投入一點資源。既然想要更多的鏈接數,那就再加一臺網關服務器吧。新增長了網關服後須要在大區服上作相應的支持,或者再簡單點,有一臺主要的網關服,當其負載較高時,主動將新到達的鏈接重定向到其餘網關服上。

  而對於遊戲服來講,有一臺仍是多臺網關服是沒有什麼區別的。每一個表明客戶端玩家的對象內部都保留一個表明其鏈接的對象,消息廣播時要求每一個玩家對象使用本身的鏈接對象發送數據便可,至於鏈接是在什麼地方,那是徹底透明的。固然,這只是一種簡單的實現,也是普通使用的一種方案,若是後期想對消息廣播作一些優化的話,那可能才須要多考慮一下。

  既然說到了優化,咱們也稍稍考慮一下如今結構下可能採用的優化方案。

  首先是當前的Zone Server要作的事情太多了,以致於他都處理不了多少鏈接。這其中最消耗系統資源的當屬生物的AI處理了,尤爲是那些複雜的尋路算法,因此咱們能夠考慮把這部分AI邏輯獨立出來,由一臺單獨的AI服務器來承擔。

  而後,咱們能夠試着把一些與地圖數據無關的公共邏輯放到Master Server上去實現,這樣Zone Server上只保留了與地圖數據緊密相關的邏輯,如生物管理,玩家移動和狀態更新等。

  還有聊天處理邏輯,這部分與遊戲邏輯沒有任何關聯,咱們也徹底能夠將其獨立出來,放到一臺單獨的聊天服務器上去實現。

  最後是數據庫了,爲了減輕數據庫的壓力,提升數據請求的響應速度,咱們能夠在數據庫以前創建一個數據庫緩存服務器,將一些經常使用數據緩存在此,服務器與數據庫的通訊都要經過這臺服務器進行代理。緩存的數據會定時的寫入到後臺數據庫中。

  好了,作完這些優化咱們的服務器結構大致也就定的差很少了,暫且也再也不繼續深刻,更細化的內容等到各個部分實現的時候再探討。

  比如咱們去看一場晚會,舞臺上演員們按着預約的節目單有序地上演着,但這就是整場晚會的所有嗎?顯然不止,在幕後還有太多太多的人在忙碌着,甚至在晚會前和晚會後都有。咱們的遊戲服務器也如此。

  在以前描述的部分就如同舞臺上的演員,是咱們能直接看到的,幕後的工做人員咱們也來認識一下。

  現實中有警察來維護秩序,遊戲中也如此,這就是咱們常說的GM。GM能夠採用跟普通玩家同樣的拉入方式來進入遊戲,固然權限會比普通玩家高一些,也能夠提供一臺GM服務器專門用來處理GM命令,這樣能夠有更高的安全性,GM服通常接在中心服務器上。

  在以時間收費的遊戲中,咱們還須要一臺計費的服務器,這臺服務器通常接在網關服務器上,註冊玩家登陸和退出事件以記錄玩家的遊戲時間。

  任何爲用戶提供服務的地方都會有日誌記錄,遊戲服務器固然也不例外。從記錄玩家登陸的時間,地址,機器信息到遊戲過程當中的每一項操做均可以做爲日誌記錄下來,以備查錯及數據挖掘用。至於蒐集玩家機器資料所涉及到的法律問題不是咱們該考慮的。

  差很少就這麼多了吧,接下來咱們會按照這個大體的結構來詳細討論各部分的實現。

服務器結構探討 -- 一點雜談

  再強調一下,服務器結構本無所謂好壞,只有是否適合本身。咱們在前面探討了一些在如今的遊戲中見到過的結構,並盡我所知地分析了各自存在的一些問題和能夠作的一些改進,但願其中沒有謬誤,若是能給你們也帶來些啓發那天然更好。

  忽然發現本身一旦羅嗦起來還真是沒完沒了。接下來先說說我在開發中遇到過的一些困惑和一基礎問題探討吧,這些問題可能有人與我同樣,也曾遇到過,或者正在被困擾中,而所要探討的這些基礎問題向來也是爭論比較多的,咱們也不評價其中的好與壞,只作簡單的描述。

  首先是服務器操做系統,linux與windows之爭隨處可見,其實在大多數狀況下這不是咱們所能決定的,彷佛各大公司也基本都有了本身的傳統,如網易的freebsd,騰訊的linux等。若是真有權利去選擇的話,選本身最熟悉的吧。

  決定了OS也就基本上肯定了網絡IO模型,windows上的IOCP和linux下的epool,或者直接使用現有的網絡框架,如ACE和asio等,其餘還有些商業的網絡庫在國內的使用好像沒有見到,不符合中國國情嘛。:)

  而後是網絡協議的選擇,之前的選擇大多傾向於UDP,爲了可靠傳輸通常本身都會在上面實現一層封裝,而如今更普通的是直接採用自己就很可靠的TCP,或者TCP與UDP的混用。早期選擇UDP的主要緣由仍是帶寬限制,如今寬帶普通的狀況下TCP比UDP多出來的一點點開銷與開發的便利性相比已經不算什麼了。固然,若是已有了成熟的可靠UDP庫,那也能夠繼續使用着。

  還有消息包格式的定義,這個曾在雲風的blog上展開過激烈的爭論。消息包格式定義包括三段,包長、消息碼和包體,爭論的焦點在於應該是消息碼在前仍是包長在前,咱們也把這個看成是信仰問題吧,有興趣的去雲風的blog上看看,論論。

  另外早期有些遊戲的包格式定義是以特殊字符做分隔的,這樣一個好處是其中某個包出現錯誤後咱們的遊戲還能繼續。但實際上,我以爲這是徹底沒有必要的,真要出現這樣的錯誤,直接斷開這個客戶端的鏈接可能更安全。並且,以特殊字符作分隔的消息包定義還加大了一點點網絡數據量。

  最後是一個純技術問題,有關socket鏈接數的最大限制。開始學習網絡編程的時候我犯過這樣的錯誤,覺得port的定義爲unsigned short,因此想固然的認爲服務器的最大鏈接數爲65535,這會是一個硬性的限制。而實際上,一個socket描述符在windows上的定義是unsigned int,所以要有限制那也是四十多億,放心好了。

  在服務器上port是監聽用的,想象這樣一種狀況,web server在80端口上監聽,當一個鏈接到來時,系統會爲這個鏈接分配一個socket句柄,同時與其在80端口上進行通信;當另外一個鏈接到來時,服務器仍然在80端口與之通訊,只是分配的socket句柄不同。這個socket句柄纔是描述每一個鏈接的惟一標識。按windows網絡編程第二版上的說法,這個上限值配置影響。

  好了,廢話說完了,下一篇,咱們開始進入登陸服的設計吧。

登陸服的設計 -- 功能需求

  正如咱們在前面曾討論過的,登陸服要實現的功能至關簡單,就是賬號驗證。爲了便於描述,咱們暫不引入那些討論過的優化手段,先以最簡單的方式實現,另外也將基本以mangos的代碼做爲參考來進行描述。

  想象一下賬號驗證的實現方法,最容易的那就是把用戶輸入的明文用賬號和密碼直接發給登陸服,服務器根據賬號從數據庫中取出密碼,與用戶輸入的密碼相比較。

  這個方法存在的安全隱患實在太大,明文的密碼傳輸太容易被截獲了。那咱們試着在傳輸以前先加一下密,爲了服務器能進行密碼比較,咱們應該採用一個可逆的加密算法,在服務器端把這個加密後的字串還原爲原始的明文密碼,而後與數據庫密碼進行比較。既然是一個可逆的過程,那外掛製做者總有辦法知道咱們的加密過程,因此,這個方法仍不夠安全。

  哦,若是咱們只是但願密碼不可能被還原出來,那還不容易嗎,使用一個不可逆的散列算法就好了。用戶在登陸時發送給服務器的是明文的賬號和經散列後的不可逆密碼串,服務器取出密碼後也用一樣的算法進行散列後再進行比較。好比,咱們就用使用最普遍的md5算法吧。噢,不要管那個王小云的什麼論文,若是我真有那麼好的運氣,早中500w了,還用在這考慮該死的服務器設計嗎?

  彷佛是一個很完美的方案,外掛製做者再也偷不到咱們的密碼了。慢着,外掛偷密碼的目的是什麼?是爲了能用咱們的賬號進遊戲!若是咱們老是用一種固定的算法來對密碼作散列,那外掛只須要記住這個散列後的字串就好了,用這個作密碼就能夠成功登陸。

  嗯,這個問題好解決,咱們不要用固定的算法進行散列就是了。只是,問題在於服務器與客戶端採用的散列算法得出的字串必須是相同的,或者是可驗證其是否匹配的。很幸運的是,偉大的數學字們早就爲咱們準備好了不少優秀的這類算法,並且經理論和實踐都證實他們也確實是足夠安全的。

  這其中之一是一個叫作SRP的算法,全稱叫作Secure Remote Password,即安全遠程密碼。wow使用的是第6版,也就是SRP6算法。有關其中的數學證實,若是有人能向我解釋清楚,並能讓我真正弄明白的話,我將很是感激。不過其代碼實現步驟卻是並不複雜,mangos中的代碼也還算清晰,咱們也再也不贅述。

  登陸服除了賬號驗證外還得提供另外一項功能,就是在玩家的賬號驗證成功後返回給他一個服務器列表讓他去選擇。這個列表的狀態要定時刷新,可能有新的遊戲世界開放了,也可能有些遊戲世界很是不幸地中止運轉了,這些狀態的變化都要儘量及時地讓玩家知道。無論發生了什麼事,用戶都有權利知道,特別是對於付過費的用戶來講,咱們不應藏着掖着,不是嗎?

  這個遊戲世界列表的功能將由大區服來提供,具體的結構咱們在以前也描述過,這裏暫不作討論。登陸服將從大區服上獲取到的遊戲世界列表發給已驗證經過的客戶端便可。好了,登陸服要實現的功能就這些,很簡單,是吧。

  確實是太簡單了,不過簡單的結構正好更適合咱們來看一看遊戲服務器內部的模塊結構,以及一些服務器共有組件的實現方法。這就留做下一篇吧。

服務器公共組件實現 -- mangos的遊戲主循環

  當閱讀一項工程的源碼時,咱們大概會選擇從main函數開始,而當開始一項新的工程時,第一個寫下的函數大多也是main。那咱們就先來看看,遊戲服務器代碼實現中,main函數都作了些什麼。

  因爲我在讀技術文章時最不喜看到的就是大段大段的代碼,特別是那些直接Ctrl+C再Ctrl+V後未作任何修改的代碼,用句時髦的話說,一點技術含量都沒有!因此在咱們從此所要討論的內容中,儘可能會避免出現直接的代碼,在有些地方確實須要代碼來表述時,也將會選擇使用僞碼。

  先從mangos的登陸服代碼開始。mangos的登陸服是一個單線程的結構,雖然在數據庫鏈接中能夠開啓一個獨立的線程,但這個線程也只是對無返回結果的執行類SQL作緩衝,而對須要有返回結果的查詢類SQL仍是在主邏輯線程中阻塞調用的。

  登陸服中惟一的這一個線程,也就是主循環線程對監聽的socket作select操做,爲每一個鏈接進來的客戶端讀取其上的數據並當即進行處理,直到服務器收到SIGABRT或SIGBREAK信號時結束。

  因此,mangos登陸服主循環的邏輯,也包括後面遊戲服的邏輯,主循環的關鍵代碼實際上是在SocketHandler中,也就是那個Select函數中。檢查全部的鏈接,對新到來的鏈接調用OnAccept方法,有數據到來的鏈接則調用OnRead方法,而後socket處理器本身定義對接收到的數據如何處理。

  很簡單的結構,也比較容易理解。


  只是,在對性能要求比較高的服務器上,select通常不會是最好的選擇。若是咱們使用windows平臺,那IOCP將是首選;若是是linux,epool將是不二選擇。咱們也不打算討論基於IOCP或是基於epool的服務器實現,若是僅僅只是要實現服務器功能,很簡單的幾個API調用便可,並且網上已有不少好的教程;若是是要作一個成熟的網絡服務器產品,不是我幾篇簡單的技術介紹文章所能達到。

  另外,在服務器實現上,網絡IO與邏輯處理通常會放在不一樣的線程中,以避免耗時較長的IO過程阻塞住了須要當即反應的遊戲邏輯。

  數據庫的處理也相似,會使用異步的方式,也是避免耗時的查詢過程將遊戲服務器主循環阻塞住。想象一下,因某個玩家上線而發起的一次數據庫查詢操做致使服務器內全部在線玩家都卡住不動將是多麼恐怖的一件事!

  另外還有一些如事件、腳本、消息隊列、狀態機、日誌和異常處理等公共組件,咱們也會在接下來的時間裏進行探討。

服務器公共組件實現 -- 繼續來講主循環

  前面咱們只簡單瞭解了下mangos登陸服的程序結構,也發現了一些不足之處,如今咱們就來看看如何提供一個更好的方案。

  正如咱們曾討論過的,爲了遊戲主邏輯循環的流暢運行,全部比較耗時的IO操做都會分享到單獨的線程中去作,如網絡IO,數據庫IO和日誌IO等。固然,也有把這些分享到單獨的進程中去作的。

  另外對於大多數服務器程序來講,在運行時都是做爲精靈進程或服務進程的,因此咱們並不須要服務器可以處理控制檯用戶輸入,咱們所要處理的數據來源都來自網絡。

  這樣,主邏輯循環所要作的就是不停要取消息包來處理,固然這些消息包不只有來自客戶端的玩家操做數據包,也有來自GM服務器的管理命令,還包括來自數據庫查詢線程的返回結果消息包。這個循環將一直持續,直到收到一個通知服務器關閉的消息包。

  主邏輯循環的結構仍是很簡單的,複雜的部分都在如何處理這些消息包的邏輯上。咱們能夠用一段簡單的僞碼來描述這個循環過程:

    while (Message* msg = getMessage())
    {
      if (msg爲服務器關閉消息)
        break;
      處理msg消息;
    }

  這裏就有一個問題須要探討了,在getMessage()的時候,咱們應該去哪裏取消息?前面咱們考慮過,至少會有三個消息來源,而咱們還討論過,這些消息源的IO操做都是在獨立的線程中進行的,咱們這裏的主線程不該該直接去那幾處消息源進行阻塞式的IO操做。

  很簡單,讓那些獨立的IO線程在接收完數據後本身送過來就是了。比如是,我這裏提供了一個倉庫,有不少的供貨商,他們有貨要給個人時候只須要交到倉庫,而後我再到倉庫去取就是了,這個倉庫也就是消息隊列。消息隊列是一個普通的隊列實現,固然必需要提供多線程互斥訪問的安全性支持,其基本的接口定義大概相似這樣:

    IMessageQueue
    {
      void putMessage(Message*);
      Message* getMessage();
    }

  網絡IO,數據庫IO線程把整理好的消息包都加入到主邏輯循環線程的這個消息隊列中便返回。有關消息隊列的實現和線程間消息的傳遞在ACE中有比較徹底的代碼實現及描述,還有一些使用示例,是個很好的參考。

  這樣的話,咱們的主循環就很清晰了,從主線程的消息隊列中取消息,處理消息,再取下一條消息......

服務器公共組件實現 -- 消息隊列

  既然說到了消息隊列,那咱們繼續來稍微多聊一點吧。

  咱們所能想到的最簡單的消息隊列可能就是使用stl的list來實現了,即消息隊列內部維護一個list和一個互斥鎖,putMessage時將message加入到隊列尾,getMessage時從隊列頭取一個message返回,同時在getMessage和putMessage以前都要求先獲取鎖資源。

  實現雖然簡單,但功能是絕對知足需求的,只是性能上可能稍稍有些不盡如人意。其最大的問題在頻繁的鎖競爭上。

  對於如何減小鎖競爭次數的優化方案,Ghost Cheng提出了一種。提供一個隊列容器,裏面有多個隊列,每一個隊列均可固定存放必定數量的消息。網絡IO線程要給邏輯線程投遞消息時,會從隊列容器中取一個空隊列來使用,直到將該隊列填滿後再放回容器中換另外一個空隊列。而邏輯線程取消息時是從隊列容器中取一個有消息的隊列來讀取,處理完後清空隊列再放回到容器中。

  這樣便使得只有在對隊列容器進行操做時才須要加鎖,而IO線程和邏輯線程在操做本身當前使用的隊列時都不須要加鎖,因此鎖競爭的機會大大減小了。

  這裏爲每一個隊列設了個最大消息數,看來好像是打算只有當IO線程寫滿隊列時纔會將其放回到容器中換另外一個隊列。那這樣有時也會出現IO線程未寫滿一個隊列,而邏輯線程又沒有數據可處理的狀況,特別是當數據量不多時可能會很容易出現。Ghost Cheng在他的描述中沒有講到如何解決這種問題,但咱們能夠先來看看另外一個方案。

  這個方案與上一個方案基本相似,只是再也不提供隊列容器,由於在這個方案中只使用了兩個隊列,arthur在他的一封郵件中描述了這個方案的實現及部分代碼。兩個隊列,一個給邏輯線程讀,一個給IO線程用來寫,當邏輯線程讀完隊列後會將本身的隊列與IO線程的隊列相調換。因此,這種方案下加鎖的次數會比較多一些,IO線程每次寫隊列時都要加鎖,邏輯線程在調換隊列時也須要加鎖,但邏輯線程在讀隊列時是不須要加鎖的。

  雖然看起來鎖的調用次數是比前一種方案要多不少,但實際上大部分鎖調用都是不會引發阻塞的,只有在邏輯線程調換隊列的那一瞬間可能會使得某個線程阻塞一下。另外對於鎖調用過程自己來講,其開銷是徹底能夠忽略的,咱們所不能忍受的僅僅是由於鎖調用而引發的阻塞而已。

  兩種方案都是很優秀的優化方案,但也都是有其適用範圍的。Ghost Cheng的方案由於提供了多個隊列,可使得多個IO線程能夠總工程師的,互不干擾的使用本身的隊列,只是還有一個遺留問題咱們還不瞭解其解決方法。arthur的方案很好的解決了上一個方案遺留的問題,但由於只有一個寫隊列,因此當想要提供多個IO線程時,線程間互斥地寫入數據可能會增大競爭的機會,固然,若是隻有一個IO線程那將是很是完美的。linux

相關文章
相關標籤/搜索