您好,登錄后才能下訂單哦!
本篇內容主要講解“經典游戲服務器端架構實例分析”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“經典游戲服務器端架構實例分析”吧!
現代電子游戲,基本上都會使用一定的網絡功能。從驗證正版,到多人交互等等,都需要架設一些專用的服務器,以及編寫在服務器上的程序。因此,游戲服務器端軟件的架構,本質上也是游戲服務器這個特定領域的軟件架構。
軟件架構的分析,可以通過不同的層面入手。比較經典的軟件架構描述,包含了以下幾種架構:
運行時架構——這種架構關心如何解決運行效率問題,通常以程序進程圖、數據流圖為表達方式。在大多數開發團隊的架構設計文檔中,都會包含運行時架構,說明這是一種非常重要的設計方面。這種架構也會顯著的影響軟件代碼的開發效率和部署效率。本文主要討論的是這種架構。
邏輯架構——這種架構關心軟件代碼之間的關系,主要目的是為了提高軟件應對需求變更的便利性。人們往往會以類圖、模塊圖來表達這種架構。這種架構設計在需要長期運營和重用性高的項目中,有至關重要的作用。因為軟件的可擴展性和可重用度基本是由這個方面的設計決定的。特別是在游戲領域,需求變更的頻繁程度,在多個互聯網產業領域里可以說是最高的。本文會涉及一部分這種架構的內容,但不是本文的討論重點。
物理架構——關心軟件如何部署,以機房、服務器、網絡設備為主要描述對象。
數據架構——關心軟件涉及的數據結構的設計,對于數據分析挖掘,多系統協作有較大的意義。
開發架構——關心軟件開發庫之間的關系,以及版本管理、開發工具、編譯構建的設計,主要為了提高多人協作開發,以及復雜軟件庫引用的開發效率。現在流行的集成構建系統就是一種開發架構的理論。
服務器端軟件的本質,是一個會長期運行的程序,并且它還要服務于多個不定時,不定地點的網絡請求。所以這類軟件的特點是要非常關注穩定性和性能。這類程序如果需要多個協作來提高承載能力,則還要關注部署和擴容的便利性;同時,還需要考慮如何實現某種程度容災需求。由于多進程協同工作,也帶來了開發的復雜度,這也是需要關注的問題。
功能約束,是架構設計決定性因素。一個萬能的架構,必定是無能的架構。一個優秀的架構,則是正好把握了對應業務領域的核心功能產生的。游戲領域的功能特征,于服務器端系統來說,非常明顯的表現為幾個功能的需求:
對于游戲數據和玩家數據的存儲
對玩家客戶端進行數據廣播
把一部分游戲邏輯在服務器上運算,便于游戲更新內容,以及防止外掛。
針對以上的需求特征,在服務器端軟件開發上,我們往往會關注軟件對電腦內存和CPU的使用,以求在特定業務代碼下,能盡量滿足承載量和響應延遲的需求。最基本的做法就是“時空轉換”,用各種緩存的方式來開發程序,以求在CPU時間和內存空間上取得合適的平衡。在CPU和內存之上,是另外一個約束因素:網卡。網絡帶寬直接限制了服務器的處理能力,所以游戲服務器架構也必定要考慮這個因素。
對于游戲服務器架構設計來說,最重要的是利用游戲產品的需求約束,從而優化出對此特定功能最合適的“時-空”架構。并且最小化對網絡帶寬的占用。
[圖-游戲服務器的分析模型]
基于上述的分析模型,對于游戲服務端架構,最重要的三個部分就是,如何使用CPU、內存、網卡的設計:
內存架構:主要決定服務器如何使用內存,以保證盡量少的內存泄漏的可能,以及最大化利用服務器端內存來提高承載量,降低服務延遲。
調度架構:設計如何使用進程、線程、協程這些對于CPU調度的方案。選擇同步、異步等不同的編程模型,以提高服務器的穩定性和承載量。同時也要考慮對于開發帶來的復雜度問題。現在出現的虛擬化技術,如虛擬機、docker、云服務器等,都為調度架構提供了更多的選擇。
通信模式:決定使用何種方式通訊。網絡通訊包含有傳輸層的選擇,如TCP/UDP;據表達層的選擇,如定義協議;以及應用層的接口設計,如消息隊列、事件分發、遠程調用等。
本文的討論,也主要是集中于對以上三個架構的分析。
最早的游戲服務器是比較簡單的,如UO《網絡創世紀》的服務端一張3.5寸軟盤就能存下。基本上只是一個廣播和存儲文件的服務器程序。后來由于國內的外掛、盜版流行,各游戲廠商開始以MUD為模型,建立主要運行邏輯在服務器端的架構。這種架構在MMORPG類產品的不斷更新中發揚光大,從而出現了以地圖、視野等分布要素設計的分布式游戲服務器。而在另外一個領域,休閑游戲,天然的需要集中超高的在線用戶,所以全區型架構開始出現。現代的游戲服務器架構,基本上都希望能結合承載量和擴展性的有點來設計,從而形成了更加豐富多樣的形態。
本文的討論主要是選取這些比較典型的游戲服務器模型,分析其底層各種選擇的優點和缺點,希望能探討出更具廣泛性,更高開發效率的服務器模型。
分服模型是游戲服務器中最典型,也是歷久最悠久的模型。其特征是游戲服務器是一個個單獨的世界。每個服務器的帳號是獨立的,而且只用同一服務器的帳號才能產生線上交互。在早期服務器的承載量達到上限的時候,游戲開發者就通過架設更多的服務器來解決。這樣提供了很多個游戲的“平行世界”,讓游戲中的人人之間的比較,產生了更多的空間。所以后來以服務器的開放、合并形成了一套成熟的運營手段。一個技術上的選擇最后導致了游戲運營方式的模式,是一個非常有趣的現象。
[圖-單進程調度模型]
同步-動態多線程:每接收一個用戶會話,就建立一個線程。這個用戶會話往往就是由客戶端的TCP連接來代表,這樣每次從socket中調用讀取或寫出數據包的時候,都可以使用阻塞模式,編碼直觀而簡單。有多少個游戲客戶端的連接,就有多少個線程。但是這個方案也有很明顯的缺點,就是服務器容易產生大量的線程,這對于內存占用不好控制,同時線程切換也會造成CPU的性能損失。更重要的多線程下對同一塊數據的讀寫,需要處理鎖的問題,這可能讓代碼變的非常復雜,造成各種死鎖的BUG,影響服務器的穩定性。
同步-多線程池:為了節約線程的建立和釋放,建立了一個線程池。每個用戶會話建立的時候,向線程池申請處理線程的使用。在用戶會話結束的時候,線程不退出,而是向線程池“釋放”對此線程的使用。線程池能很好的控制線程數量,可以防止用戶暴漲下對服務器造成的連接沖擊,形成一種排隊進入的機制。但是線程池本身的實現比較復雜,而“申請”、“施放”線程的調用規則需要嚴格遵守,否則會出現線程泄露,耗盡線程池。
異步-單線程/協程:在游戲行業中,采用Linux的epoll作為網絡API,以期得到高性能,是一個常見的選擇。游戲服務器進程中最常見的阻塞調用就是網路IO,因此在采用epoll之后,整個服務器進程就可能變得完全沒有阻塞調用,這樣只需要一個線程即可。這徹底解決了多線程的鎖問題,而且也簡化了對于并發編程的難度。但是,“所有調用都不得阻塞”的約束,并不是那么容易遵守的,比如有些數據庫的API就是阻塞的;另外單進程單線程只能使用一個CPU,在現在多核多CPU的服務器情況下,不能充分利用CPU資源。異步編程由于是基于“回調”的方式,會導致要定義很多回調函數,并且把一個流程里面的邏輯,分別寫在多個不同的回調函數里面,對于代碼閱讀非常不理。——針對這種編碼問題,協程(Coroutine)能較好的幫忙,所以現在比較流行使用異步+協程的組合。不管怎樣,異步-單線程模型由于性能好,無需并發思維,依然是現在很多團隊的首選。
異步-固定多線程:這是基于異步-單線程模型進化出來的一種模型。這種模型一般有三類線程:主線程、IO線程、邏輯線程。這些線程都在內部以全異步的方式運行,而他們之間通過無鎖消息隊列通信。
多進程游戲服務器
多進程的游戲服務器系統,最早起源于對于性能問題需求。由于單進程架構下,總會存在承載量的極限,越是復雜的游戲,其單進程承載量就越低,因此開發者們一定要突破進程的限制,才能支撐更復雜的游戲。
一旦走上多進程之路,開發者們還發現了多進程系統的其他一些好處:能夠利用上多核CPU能力;利用操作系統的工具能更仔細的監控到運行狀態、更容易進行容災處理。多進程系統比較經典的模型是“三層架構”:
在多進程架構下,開發者一般傾向于把每個模塊的功能,都單獨開發成一個進程,然后以使用進程間通信來協調處理完整的邏輯。這種思想是典型的“管道與過濾器”架構模式思想——把每個進程看成是一個過濾器,用戶發來的數據包,流經多個過濾器銜接而成的管道,最后被完整的處理完。由于使用了多進程,所以首選使用單進程單線程來構造其中的每個進程。這樣對于程序開發來說,結構清晰簡單很多,也能獲得更高的性能。
[圖-對象樹架構]
在Objective C語言中,有所謂autorealse的特性,這種特性實際上是一種引用計數的技術。由于能配合在某個調度模型下,所以使用起來會比較簡單。同樣的思想,有些開發者會使用一些智能指針,配合自己寫的框架,在完整的業務邏輯調用后一次性清理相關內存。
[圖-預分配內存池]
不過這樣做,同樣有一些缺點:首先是不太好部署,比如你想在某個資源較小的虛擬機上部署一套用來測試,可能一位內沒改內存池的大小,導致啟動不成功。每次更換環境都需要修改這個配置。其次,是所有的用到的類對象,都要在根節點對象那里有個指針或者引用,否則就可能泄漏內存。由于對于非基本類型的對象,我們一般不喜歡用拷貝的方式來作為函數的參數和返回值,而指針和應用所指向的內存,如果不能new的話,只能是現成的某個對象的成員屬性。這回導致程序越復雜,這類的成員屬性就越多,這些屬性在代碼維護是一個不小的負擔。
要解決以上的缺點,可以修改內存池的實現,為動態增長,但是具備上限的模型,每次從內存池中“獲取”對象的時候才new。這樣就能避免在小內存機器上啟動不了的問題。對于對象屬性復雜的問題,一般上需要好好的按面向對象的原則規劃代碼,做到盡量少用僅僅表示函數參數和返回值的屬性,而是主要是記錄對象的“業務狀態”屬性為主,多花點功夫在構建游戲的數據模型上。
在多進程的系統中,進程間如何通訊是一個至關重要的問題,其性能和使用便利性,直接決定了多進程系統的技術效能。
Socket通訊
TCP/IP協議是一種通用的、跨語言、跨操作系統、跨機器的通訊方案。這也是開發者首先想到的一種手段。在使用上,有使用TCP和UDP兩個選擇。一般我們傾向在游戲系統中使用TCP,因為游戲數據的邏輯相關性比較強,UDP由于可能存在的丟包和重發處理,在游戲邏輯上的處理一般比較復雜。由于多進程系統的進程間網絡一般情況較好,UDP的性能優勢不會特別明顯。
要使用TCP做跨進程通訊,首先就是要寫一個TCP Server,做端口監聽和連接管理;其次需要對可能用到的通信內容做協議定制;最后是要編寫編解碼和業務邏輯轉發的邏輯。這些都完成了之后,才能真正的開始用來作為進程間通信手段。
使用Socket編程的好處是通用性廣,你可以用來實現任何的功能,和任何的進程進行協作。但是其缺點也異常明顯,就是開發量很大。雖然現在有一些開源組件,可以幫你簡化Socket Server的編寫工作,簡化連接管理和消息分發的處理,但是選擇目標建立連接、定制協議編解碼這兩個工作往往還是要自己去做。游戲的特點是業務邏輯變化很多,導致協議修改的工作量非常大。因此我們除了直接使用TCP/IP socket以外,還有很多其他的方案可以嘗試。
[圖-消息隊列]
需要注意的是,有些開發者缺乏經驗,使用了數據庫,如MySQL,或者是NFS這類運行效率比較低的媒介作為隊列的存儲者。這在功能上雖然可以行得通,但是操作一頻繁,就難以發揮作用了。如以前有一些手機短信應用系統,就用MySQL來存儲“待發送”的短信。
消息隊列雖然非常好用,但是我們還是要自己對消息進行編解碼,并且分發給所需要的處理程序。在消息到處理程序之間,存在著一個轉換和對應的工作。由于游戲邏輯的繁多,這種對應工作完全靠手工編碼,是比較容易出錯的。所以這里還有進一步的改進空間。
遠程調用
有一些開發者會希望,在編碼的時候完全屏蔽是否跨進程在進行調用,完全可以好像調用本地函數或者本地對象的方法一樣。于是誕生了很多遠程調用的方案,最經典的有Corba方案,它試圖實現能在不同語言的代碼直接,實現遠程調用。JAVA虛擬機自帶了RMI方案的支持,在JAVA進程之間遠程調用是比較方便的。在互聯網的環境下,還有各種Web Service方案,以HTTP協議作為承載,WSDL作為接口描述。
使用遠程調用的方案,最大好處是開發的便捷,你只需要寫一個函數,就能在任何一個其他進程上對此函數進行調用。這對游戲開發來說,就解決了多進程方案最大的一個開發效率問題。但是這種便捷是有成本的:一般來說,遠程調用的性能會稍微差一點,因為需要用一套統一的編解碼方案。如果你使用的是C/C++這類靜態語言,還需要使用一種IDL語言來先描述這種遠程函數的接口。但是這些困難帶來的好處,在游戲開發領域還是非常值得的。
[圖-服務器狀態管理]
盡管用簡單的目錄服務器可以實現大部分容災和擴容的需求,但是如果被訪問進程的內存中有數據存在,那么問題就比較復雜了。對于容災來說,新的進程必須要有辦法重建那個“失效”了的進程內存中的數據,才可能完成容災功能;對于擴容功能來說,新加入的進程,也必須能把需要的數據載入到自己的內存中才行,而這些數據,可能已經存在于其他平行的進程中,如何把這部分數據轉移過來,是一個比較耗費性能和需要編寫相當多代碼的工作。——所以一般我們喜歡對“無狀態”的進程來做擴容和容災。
到此,相信大家對“經典游戲服務器端架構實例分析”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。