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

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

服務器公共組件實現 -- 環形緩衝區數據庫

  消息隊列鎖調用太頻繁的問題算是解決了,另外一個讓人有些苦惱的大概是這太多的內存分配和釋放操做了。頻繁的內存分配不但增長了系統開銷,更使得內存碎片不斷增多,很是不利於咱們的服務器長期穩定運行。也許咱們可使用內存池,好比SGI STL中附帶的小內存分配器。可是對於這種按照嚴格的先進先出順序處理的,塊大小並不算小的,並且塊大小也並不統一的內存分配狀況來講,更多使用的是一種叫作環形緩衝區的方案,mangos的網絡代碼中也有這麼一個東西,其原理也是比較簡單的。編程

  就比如兩我的圍着一張圓形的桌子在追逐,跑的人被網絡IO線程所控制,當寫入數據時,這我的就往前跑;追的人就是邏輯線程,會一直往前追直到追上跑的人。若是追上了怎麼辦?那就是沒有數據可讀了,先等會兒唄,等跑的人向前跑幾步了再追,總不能讓遊戲沒得玩了吧。那要是追的人跑的太慢,跑的人轉了一圈過來反追上追的人了呢?那您也先歇會兒吧。要是一直這麼反着追,估計您就只能換一個跑的更快的追逐者了,要不這遊戲還真無法玩下去。設計模式

  前面咱們特別強調了,按照嚴格的先進先出順序進行處理,這是環形緩衝區的使用必須遵照的一項要求。也就是,你們都得遵照規定,追的人不能從桌子上跨過去,跑的人固然也不容許反過來跑。至於爲何,不須要多作解釋了吧。緩存

  環形緩衝區是一項很好的技術,不用頻繁的分配內存,並且在大多數狀況下,內存的反覆使用也使得咱們能用更少的內存塊作更多的事。安全

  在網絡IO線程中,咱們會爲每個鏈接都準備一個環形緩衝區,用於臨時存放接收到的數據,以應付半包及粘包的狀況。在解包及解密完成後,咱們會將這個數據包複製到邏輯線程消息隊列中,若是咱們只使用一個隊列,那這裏也將會是個環形緩衝區,IO線程往裏寫,邏輯線程在後面讀,互相追逐。可要是咱們使用了前面介紹的優化方案後,可能這裏便再也不須要環形緩衝區了,至少咱們並再也不須要他們是環形的了。由於咱們對同一個隊列再也不會出現同時讀和寫的狀況,每一個隊列在寫滿後交給邏輯線程去讀,邏輯線程讀完後清空隊列再交給IO線程去寫,一段固定大小的緩衝區便可。不要緊,這麼好的技術,在別的地方必定也會用到的。服務器

服務器公共組件實現 -- 發包的方式網絡

  前面一直都在說接收數據時的處理方法,咱們應該用專門的IO線程,接收到完整的消息包後加入到主線程的消息隊列,可是主線程如何發送數據尚未探討過。session

  通常來講最直接的方法就是邏輯線程何時想發數據了就直接調用相關的socket API發送,這要求服務器的玩家對象中保存其鏈接的socket句柄。可是直接send調用有時候有會存在一些問題,好比遇到系統的發送緩衝區滿而阻塞住的狀況,或者只發送了一部分數據的狀況也時有發生。咱們能夠將要發送的數據先緩存一下,這樣遇到未發送完的,在邏輯線程的下一次處理時能夠接着再發送。負載均衡

  考慮數據緩存的話,那這裏這能夠有兩種實現方式了,一是爲每一個玩家準備一個緩衝區,另外就是隻有一個全局的緩衝區,要發送的數據加入到全局緩衝區的時候同時要指明這個數據是發到哪一個socket的。若是使用全局緩衝區的話,那咱們能夠再進一步,使用一個獨立的線程來處理數據發送,相似於邏輯線程對數據的處理方式,這個獨立發送線程也維護一個消息隊列,邏輯線程要發數據時也只是把數據加入到這個隊列中,發送線程循環取包來執行send調用,這時的阻塞也就不會對邏輯線程有任何影響了。

  採用第二種方式還能夠附帶一個優化方案。通常對於廣播消息而言,發送給周圍玩家的數據都是徹底相同的,咱們若是採用給每一個玩家一個緩衝隊列的方式,這個數據包將須要拷貝多份,而採用一個全局發送隊列時,咱們只須要把這個消息入隊一次,同時指明該消息包是要發送給哪些socket的便可。有關該優化的說明在雲風描述其鏈接服務器實現的blog文章中也有講到,有興趣的能夠去閱讀一下。

服務器公共組件實現 -- 狀態機

  有關State模式的設計意圖及實現就不從設計模式中摘抄了,咱們只來看看遊戲服務器編程中如何使用State設計模式。

  首先仍是從mangos的代碼開始看起,咱們注意到登陸服在處理客戶端發來的消息時用到了這樣一個結構體:

  struct AuthHandler
  {
    eAuthCmd cmd;
    uint32 status;
    bool (AuthSocket::*handler)(void);
  };

  該結構體定義了每一個消息碼的處理函數及須要的狀態標識,只有當前狀態知足要求時纔會調用指定的處理函數,不然這個消息碼的出現是不合法的。這個status狀態標識的定義是一個宏,有兩種有效的標識,STATUS_CONNECTED和STATUS_AUTHED,也就是未認證經過和已認證經過。而這個狀態標識的改變是在運行時進行的,確切的說是在收到某個消息並正確處理完後改變的。

  咱們再來看看設計模式中對State模式的說明,其中關於State模式適用狀況裏有一條,當操做中含有龐大的多分支的條件語句,且這些分支依賴於該對象的狀態,這個狀態一般用一個或多個枚舉變量表示。

  描述的狀況與咱們這裏所要處理的狀況是如此的類似,也許咱們能夠試一試。那再看看State模式提供的解決方案是怎樣的,State模式將每個條件分支放入一個獨立的類中。

  因爲這裏的兩個狀態標識只區分出了兩種狀態,因此,咱們僅須要兩個獨立的類,用以表示兩種狀態便可。而後,按照State模式的描述,咱們還須要一個Context類,也就是狀態機管理類,用以管理當前的狀態類。稍做整理,大概的代碼會相似這樣:

  狀態基類接口:
  StateBase
  {
    void Enter() = 0;
    void Leave() = 0;
    void Process(Message* msg) = 0;
  };

  狀態機基類接口:
  MachineBase
  {
    void ChangeState(StateBase* state) = 0;

    StateBase* m_curState;
  };

  咱們的邏輯處理類會從MachineBase派生,當取出數據包後交給當前狀態處理,前面描述的兩個狀態類從StateBase派生,每一個狀態類只處理該狀態標識下須要處理的消息。當要進行狀態轉換時,調用MachineBase的ChangeState()方法,顯示地告訴狀態機管理類本身要轉到哪個狀態。因此,狀態類內部須要保存狀態機管理類的指針,這個能夠在狀態類初始化時傳入。具體的實現細節就不作過多描述了。

  使用狀態機雖然避免了複雜的判斷語句,但也引入了新的麻煩。當咱們在進行狀態轉換時,可能會須要將一些現場數據從老狀態對象轉移到新狀態對象,這須要在定義接口時作一下考慮。若是不但願執行拷貝,那麼這裏公有的現場數據也可放到狀態機類中,只是這樣在使用時可能就不那麼優雅了。

  正如同在設計模式中所描述的,全部的模式都是已有問題的另外一種解決方案,也就是說這並非惟一的解決方案。放到咱們今天討論的State模式中,就拿登陸服所處理的兩個狀態來講,也許用mangos所採用的遍歷處理函數的方法可能更簡單,但當系統中的狀態數量增多,狀態標識也變多的時候,State模式就顯得尤爲重要了。

  好比在遊戲服務器上玩家的狀態管理,還有在實現NPC人工智能時的各類狀態管理,這些就留做之後的專題吧。

服務器公共組件 -- 事件與信號

關於這一節,這幾天已經打了好幾遍草稿,總以爲說不清楚,也很差組織這些內容,可是打鐵要趁熱,爲避免熱情消退,先整理一點東西放這,好繼續下面的主題,之後若是有機會再回來完善吧。本節內容欠考慮,但願你們多給點意見。

有些相似於QT中的event與signal,我將一些動做請求消息定義爲事件,而將狀態改變消息定義爲信號。好比在QT應用程序中,用戶的一次鼠標點擊會產生一個鼠標點擊事件加入到事件隊列中,當處理此事件時可能會致使某個按鈕控件產生一個clicked()信號。

對應到咱們的服務器上的一個例子,玩家登陸時會發給服務器一個請求登陸的數據包,服務器可將其看成一個用戶登陸事件,該事件處理完後可能會產生一個用戶已登陸信號。

這樣,與QT相似,對於事件咱們能夠重定義其處理方法,甚至過濾掉某些事件使其不被處理,但對於信號咱們只是收到了一個通知,有些相似於Observe模式中的觀察者,當收到更新通知時,咱們只能更新本身的狀態,對剛剛發生的事件我不已不能作任何影響。

仔細來看,事件與信號其實並沒有多大差異,從咱們對其需求上來講,都只要能註冊事件或信號響應函數,在事件或信號產生時可以被通知到便可。但有一項區別在於,事件處理函數的返回值是有意義的,咱們要根據這個返回值來肯定是否還要繼續事件的處理,好比在QT中,事件處理函數若是返回true,則這個事件處理已完成,QApplication會接着處理下一個事件,而若是返回false,那麼事件分派函數會繼續向上尋找下一個能夠處理該事件的註冊方法。信號處理函數的返回值對信號分派器來講是無心義的。

簡單點說,就是咱們能夠爲事件定義過濾器,使得事件能夠被過濾。這一功能需求在遊戲服務器上是處處存在的。

關於事件和信號機制的實現,網絡上的開源訓也比較多,好比FastDelegate,sigslot,boost::signal等,其中sigslot還被Google採用,在libjingle的代碼中咱們能夠看到他是如何被使用的。

在實現事件和信號機制時或許能夠考慮用同一套實現,在前面咱們就分析過,二者惟一的區別僅在於返回值的處理上。

另外還有一個須要咱們關注的問題是事件和信號處理時的優先級問題。在QT中,事件由於都是與窗口相關的,因此事件回調時都是從當前窗口開始,一級一級向上派發,直到有一個窗口返回true,截斷了事件的處理爲止。對於信號的處理則比較簡單,默認是沒有順序的,若是須要明確的順序,能夠在信號註冊時顯示地指明槽的位置。

在咱們的需求中,由於沒有窗口的概念,事件的處理也與信號相似,對註冊過的處理器要按某個順序依次回調,因此優先級的設置功能是須要的。

最後須要咱們考慮的是事件和信號的處理方式。在QT中,事件使用了一個事件隊列來維護,若是事件的處理中又產生了新的事件,那麼新的事件會加入到隊列尾,直到當前事件處理完畢後,QApplication再去隊列頭取下一個事件來處理。而信號的處理方式有些不一樣,信號處理是當即回調的,也就是一個信號產生後,他上面所註冊的全部槽都會當即被回調。這樣就會產生一個遞歸調用的問題,好比某個信號處理器中又產生了一個信號,會使得信號的處理像一棵樹同樣的展開。咱們須要注意的一個很重要的問題是會不會引發循環調用。

關於事件機制的考慮其實還不少,但都是一些不成熟的想法。在上面的文字中就同時出現了消息、事件和信號三個相近的概念,而在實際處理中,常常發現三者不知道如何界定的狀況,實際的狀況比我在這裏描述的要混亂的多。

這裏也就當是挖下一個坑,但願可以有所交流。

再談登陸服的實現

    離咱們的登陸服實現已經太遠了,先拉回來一下。
   
    關於登陸服、大區服及遊戲世界服的結構以前已作過探討,這裏再把各自的職責和關係列一下。

        GateWay/WorldServer   GateWay/WodlServer LoginServer LoginServer DNSServer WorldServerMgr
                |                     |                     |                 |            |
      ---------------------------------------------------------------------------------------------
                                             | | |
                                             internet
                                                |
                                              clients

    其中DNSServer負責帶負載均衡的域名解析服務,返回LoginServer的IP地址給客戶端。WorldServerMgr維護當前大區內的世界服列表,LoginServer會從這裏取世界列表發給客戶端。LoginServer處理玩家的登陸及世界服選擇請求。GateWay/WorldServer爲各個獨立的世界服或者經過網關鏈接到後面的世界服。

    在mangos的代碼中,咱們注意到登陸服是從數據庫中取的世界列表,而在wow官方服務器中,咱們卻會注意到,這個世界服列表並非一開始就固定,而是動態生成的。當每週一次的維護完成以後,咱們能夠很明顯的看到這個列表生成的過程。剛開始時,世界列表是空的,慢慢的,世界服會一個個加入進來,而這裏若是有世界服當機,他會顯示爲離線,不會從列表中刪除。可是當下一次服務器再維護後,全部的世界服都不存在了,所有從新開始添加。

    從上面的過程描述中,咱們很容易想到利用一個臨時的列表來保存世界服信息,這也是咱們增長WorldServerMgr服務器的目的所在。GateWay/WorldServer在啓動時會自動向WorldServerMgr註冊本身,這樣就把本身所表明的遊戲世界添加到世界列表中了。相似的,若是DNSServer也可讓LoginServer本身去註冊,這樣在臨時LoginServer時就不須要去改動DNSServer的配置文件了。

    WorldServerMgr內部的實現很簡單,監聽一個固定的端口,接受來自WorldServer的主動鏈接,並檢測其狀態。這裏能夠用一個心跳包來實現其狀態的檢測,若是WorldServer的鏈接斷開或者在規定時間內未收到心跳包,則將其狀態更新爲離線。另外WorldServerMgr還處理來自LoginServer的列表請求。因爲世界列表並不常變化,因此LoginServer沒有必要每次發送世界列表時都到WorldServerMgr上去取,LoginServer徹底能夠本身維護一個列表,當WorldServerMgr上的列表發生變化時,WorldServerMgr會主動通知全部的LoginServer也更新一下本身的列表。這個或許就能夠用前面描述過的事件方式,或者就是觀察者模式了。

    WorldServerMgr實現所要考慮的內容就這些,咱們再來看看LoginServer,這纔是咱們今天要重點討論的對象。

    前面探討一些服務器公共組件,那咱們這裏也應該試用一下,不能只是停留在理論上。先從狀態機開始,前面也說過了,登陸服上的鏈接會有兩種狀態,一是賬號密碼驗證狀態,一是服務器列表選擇狀態,其實還有另一個狀態咱們不曾討論過,由於它與咱們的登陸過程並沒有多大關係,這就是升級包發送狀態。三個狀態的轉換流程大體爲:

        LogonState -- 驗證成功 -- 版本檢查 -- 版本低於最新值 -- 轉到UpdateState
                                          |
                                           -- 版本等於最新值 -- 轉到WorldState

    這個版本檢查的和決定下一個狀態的過程是在LogonState中進行的,下一個狀態的選擇是由當前狀態來決定。密碼驗證的過程使用了SRP6協議,具體過程就很少作描述,每一個遊戲使用的方式也都不大同樣。而版本檢查的過程就更無值得探討的東西,一個if-else便可。

    升級狀態其實就是文件傳輸過程,文件發送完畢後通知客戶端開始執行升級文件並關閉鏈接。世界選擇狀態則提供了一個列表給客戶端,其中包括了全部遊戲世界網關服務器的IP、PORT和當前負載狀況。若是客戶端一直鏈接着,則該狀態會以每5秒一次的頻率不停刷新列表給客戶端,固然是否值得這樣作仍是有待商榷。

    整個過程彷佛都沒有值得探討的內容,可是,尚未完。當客戶端選擇了一個世界以後該怎麼辦?wow的作法是,當客戶端選擇一個遊戲世界時,客戶端會主動去鏈接該世界服的IP和PORT,而後進入這個遊戲世界。與此同時,與登陸服的鏈接尚未斷開,直到客戶端確實鏈接上了選定的世界服而且走完了排隊過程爲止。這是一個很必要的設計,保證了咱們在因意外狀況鏈接不上世界服或者發現世界服正在排隊而想換另一個試試時不會須要從新進行密碼驗證。

    可是咱們所要關注的還不是這些,而是客戶端去鏈接遊戲世界的網關服時服務器該如何識別咱們。打個比方,有個不自覺的玩家不遵照遊戲規則,沒有去驗證賬號密碼就直接跑去鏈接世界服了,就如同一個不自覺的乘客沒有換登機牌就直接跑到登機口同樣。這時,乘務員會客氣地告訴你要先換登機牌,那登機牌又從哪來?檢票口換的,人家會先驗明你的身份,確認後纔會發給你登機牌。同樣的處理過程,咱們的登陸服在驗明客戶端身份後,也會發給客戶端一個登機牌,這個登機牌還有一個學名,叫作session key。

    客戶端拿着這個session key去世界服網關處就可正確登陸了嗎?彷佛仍是有個疑問,他怎麼知道我這個key是否是造假的?沒辦法,中國的假貨太多,咱們不得不處處都考慮假貨的問題。方法很簡單,去找給他登機牌的那個檢票員問一下,這張牌是否是他發的不就得了。但是,那麼多的LoginServer,要一個個問下來,這效率也過低了,後面排的長隊必定會開始叫喚了。那麼,LoginServer將這個key存到數據庫中,讓網關服本身去數據庫驗證?彷佛也是個可行的方案。

    若是以爲這樣給數據庫帶來了太大的壓力的話,也能夠考慮相似WorldServerMgr的作法,用一個臨時的列表來保存,甚至能夠將這個列表就保存到WorldServerMgr上,他正好是全區惟一的。這兩種方案的本質並沒有差異,只是看你願意將負載放在哪裏。而無論在哪裏,這個查詢的壓力都是有點大的,想一想,全區全部玩家呢。因此,咱們也能夠試着考慮一種新的方案,一種不須要去全區惟一一個入口查詢的方案。

    那咱們將這些session key分開存儲不就得了。一個可行的方案是,讓任意時刻只有一個地方保存一個客戶端的session key,這個地方多是客戶端當前正鏈接着的服務器,也能夠是它正要去鏈接的服務器。讓咱們來詳細描述一下這個過程,客戶端在LoginServer上驗證經過時,LoginServer爲其生成了本次會話的session key,但只是保存在當前的LoginServer上,不會存數據庫,也不會發送給WorldServerMgr。若是客戶端這時想要去某個遊戲世界,那麼他必須先通知當前鏈接的LoginServer要去的服務器地址,LoginServer將session key安全轉移給目標服務器,轉移的意思是要確保目標服務器收到了session key,本地保存的要刪除掉。轉移成功後LoginServer通知客戶端再去鏈接目標服務器,這時目標服務器在驗證session key合法性的時候就不須要去別處查詢了,只在本地保存的session key列表中查詢便可。

    固然了,爲了session key的安全,全部的服務器在收到一個新的session key後都會爲其設一個有效期,在有效期事後還沒來認證的,則該session key會被自動刪除。同時,全部服務器上的session key在鏈接關閉後必定會被刪除,保證一個session key真正只爲一次鏈接會話服務。

    可是,很顯然的,wow並無採用這種方案,由於客戶端在選擇世界服時並無向服務器發送要求確認的消息。wow中的session key應該是保存在一個相似於WorldServerMgr的地方,或者如mangos同樣,就是保存在了數據庫中。無論是怎樣一種方式,瞭解了其過程,代碼實現都是比較簡單的,咱們就再也不贅述了。

    有關登陸服的討論或許該告一段落了吧。

相關文章
相關標籤/搜索