因爲大型多人在線遊戲服務器理論上須要支持無限多的玩家,因此對服務器端是一個很是大的考驗。服務器必須是安全的,可維護性高的,可伸縮性高的,可負載均衡的,支持高併發請求的。面對這些需求,咱們在設計服務器的時候就須要慎重考慮,特別是架構的設計,若是前期設計很差,最後面臨的極可能是重構。算法
一款遊戲服務器的架構都是慢慢從小變大的,不可能一會兒就上來一個完善的服務器構架,目前流行的說法是遊戲先上線,再擴展。因此說咱們在作架構的時候,必定要把底層的基礎組件作好,方便之後擴展,可是剛開始的時候留出一些接口,並不實現它,未來遊戲業務的發展,再慢慢擴展。固然,若是前期設計的很差,後期業務擴展了,但架構沒辦法擴展,只能加班加點搞了。數據庫
面對龐大的數據量咱們想到的惟一個解決方案就是分而治之,即採用分佈式的方式去解決它。把緊湊獨立的功能單獨拿出來作。分擔到不一樣的物理服務器上面去運行。並且作到能夠動態擴展。這就須要咱們考慮好模塊的劃分,儘可能要業務獨立,關聯性低。api
前期,因爲遊戲須要儘快上線,開發週期短,咱們須要把服務儘快的跑起來,這個時候的目標應該是儘快完成測試版本開發,單臺服務器支持的人數能夠稍微低一些,可是當人數暴漲時,咱們能夠能過多開幾組服務來支持新增漲的用戶量,便可以平衡擴展就能夠了。到後期咱們再把具體的模塊單獨拿出來支持,好比前期邏輯服務器上包括:活動,關卡,揹包,技能,好友管理等。後期咱們能夠把好友,揹包管理或其它的單獨作一個服務進程,部署在不一樣的物理服務器上面。咱們先按分區的服務進行設計,後面在部署的時候能夠部署爲世界服務器,下面是一個前期的架構圖,安全
本文來自遊戲技術網:http://www.youxijishu.com服務器
下面咱們從每一個服務器的功能提及:網絡
負責用戶的登錄驗證,若是有註冊功能的話,也能夠放在這裏。通常手機遊戲直接走sdk驗證。網頁遊戲和客戶端遊戲會有註冊功能,也能夠叫用戶管理服務。架構
負責接收客戶端的用戶登錄請求,驗證帳號的合法性,是否在黑名單(被封號的用戶),是否在白名單(通常是測試帳號,服務未開啓時也能夠進入)。若是是sdk登錄,此服務向第三方服務發起回調請求。併發
使用加密的傳輸協議,見通訊協議部分。負載均衡
白名單是給內部測試人員使用的,在服務器未開啓的狀態下,白名單的用戶能夠提早進入遊戲進行遊戲測試。異步
黑名單的用戶是禁止登錄的,通常這是一些被封號的用戶,拒絕登錄。
服務器使用私鑰解密密碼,進行驗證,若是是sdk登錄,則直接向第三方服務發起回調。
當用戶登錄驗證成功以後,服務器端須要生成一個登錄令牌token,這個token具備時效性,當用戶客戶端拿到這個token以後,若是在必定時間內沒有登錄遊戲成功,那麼這個token將失敗,用戶須要從新申請token,token存儲在登錄服務這,向外提供用戶是否已登錄的接口,其它服務器想驗證若是是否登錄,就拿那個服務收到的token來此驗證。
當用戶登錄成功以後,顯示最近登錄的角色信息。
用戶登錄成功以後,請求公告服務器,獲取最新的公告,公告服務先根據token和Userid驗證用戶是否已登錄,公告有可能根據渠道的不一樣,顯示不一樣的公告。因此 公告必定是要能夠根據渠道編輯的。
當用戶登錄成功以後,請求服務器分區列表服務器,顯示當前全部的大區列表。
2.1 驗證用戶是否已登錄
向登錄服務器請求驗證是否已登錄。
2.1 大區列表顯示
大區列表信息中只顯示大區id和大區名稱。這樣作是爲了安全考慮,不一次性把大區對應的網關ip和端口暴露出來,也能夠減小網絡的傳輸量。
2.2 用戶點擊選擇某個大區,客戶端拿到大區id再向選區服務請求獲取此大區對應的網關ip地址和端口。根據負載算法計算得出。
2.3 網關的選擇
選區服務會維護一份網關的配置列表。一個大區對應一到多個網關,當配置有多個網關時,須要定時檢測各個網關是否鏈接正常,若是發現有網關鏈接不上,須要把大區對應的網關信息設置爲無效,再也不參與網關的分配,併發出報警。
通常對於網關的選擇,可使用用戶id求餘法加虛網關節點法。這樣在網關節點數量固定的狀況下,一個用戶老是會被分配到同一個網關上面。可是若是隻是使用求餘法的話,可能會形成用戶分佈不均衡,這裏能夠經過增長網關的虛擬節點(其它就是增長某個網關的權重,讓用戶多來一些到這個網關上面),這個能夠參考哈稀一致性算法。包括後面說到的一個網關對應多個邏輯服務器,也可使用一樣的方法。這部分能夠抽象出來一個模塊使用。
2.4 選區服務對內要提供修改服務器狀態的接口,好比維護中...
4.1 創建鏈接
收到客戶端的創建鏈接請求以後,記錄此channel和對應的鏈接創建時間。並設置若是在必定時間內未收到登錄請求,則斷開鏈接。返回給客戶端登錄超時。
4.2 登錄請求
收到登錄請求後,移除記錄的channelid信息,向登錄服務器驗證用戶是否已登錄過,並向外廣播用戶角色登錄成功的消息。
4.3 登錄成功後,接收網關的其它的消息
4.4 客戶端消息合法性驗證
在向邏輯服務器轉發消息以前驗證消息的合法性,具體驗證方法見協議安全驗 證。
4.5 將客戶端消息轉發送到對應的邏輯服務器。
5.1 協議序列化和返回序列化
能夠直接使用protobuf,直接對協議進行序列化和反序列化。
5.2 協議組成
5.2.1 包頭構成
包總長度,加密字符串長度,加密字符串內容,userId,playerId,版本號,內包內容。
5.2.2 包體組成
請求的邏輯信息,是protobuf後對應的二進制數據。
包總長度 |
加密內容 |
UserId |
playerId |
請求序列id |
版本號 |
內包內容 |
|
Int |
64 |
Long |
Long |
Long |
int |
varchar |
|
4 |
64 |
8 |
8 |
8 |
4 |
變長 |
|
若是協議明文傳輸的話,被篡改的風險就很是大,因此咱們要對傳輸協議進行加密傳輸,因爲協議內容大小不固定,爲了保證效率,採用對稱加密算法,首先客戶端使用AES的公鑰對消息內容加密(上表中userid以後的信息),客戶端把加密後的報文發送到服務器端。AES的公鑰在用戶第一次鏈接時獲取。
儘管咱們對消息作了加密,但也不是萬無一失的,爲了進一步確保消息沒有被篡改,咱們須要對消息的完整性進行檢測,使用數字摘要的方式,首先客戶端對userid及以後的協議信息進行AES加密,加密以後取它的md5值,md5值用於驗證數據的完整性。這個md5值會被傳送到服務器,若是協議信息被修改了,那個md5就會不一樣。
爲了防止非法用戶修改協議內容後,模擬客戶端操做從新生成新的數字摘要信息,咱們對生成的數字摘要信息進行二次加密,此次使用RSA的公鑰對md5的值進行加密,將加密的內容和其它信息一塊兒發送到服務器。服務器根據ip向登錄服務器拿到AES的公鑰和RSA的私鑰,先用RSA 私鑰取出客戶端加密的md5值,服務器端計算userid後面的數據的md5值,若是兩個md5值同樣,說明安全的。若是不同,說明用戶是非法的,加入黑名單。由於RAS使用公鑰加密,必須使用對應的私鑰才能解密,並且不一樣的公鑰對應的私鑰不一樣,這樣就算非法用戶從新生成了數字摘要,在服務器端也是驗證不經過的。
當服務器收到報文後,對報文進行數子摘要驗證經過以後,服務器端使用用戶本身對應的AES的公鑰,解密數據,得到明文數據。爲了保證安全,每一個用戶的AES公鑰可能不同。
發佈訂閱是一種分佈式的解耦方式,它使用模塊更加獨立,模塊間的數據交互更加方便,發佈訂閱模式是一種一對多的關係,發佈方不關心誰訂閱了它,只要想得到它發佈的消息的服務,均可以去訂閱它。發佈方式是異步的,它加強了系統的處理性能,增長了系統的吞吐量。目前的大多數消息隊列都支持發佈訂閱模式,好比rabbitmq,activemq,kafka等消息隊列。發佈訂閱服務能夠單獨部署,加強了系統的擴展性和穩定性。
在服務器內部不一樣的服務有時候須要信息交互。爲了方便服務之間的調用,咱們引入了RPC的概念。客戶端調用一個api以後,底層會把此調用發送到遠程的服務上處理,遠程服務處理完以後再返回結果。rpc的做用就是封裝底層協議的序列化和反序列化,它讓用戶感受不到調用被髮送到了遠程服務,而感受仍是在本地同樣
7.1 同步rpc
當調用一個同步的rpc以後,結果並非馬上返回,而是在等待rpc服務器端的返回。同步rpc能夠直接使用帶同步的socket實現。或者http請求。另外一種方式是調用rpc方法以後,在本地自旋,直到服務端返回。
7.2 異步rpc
異步rpc調用以後,結果是馬上返回的,它的處理方式是把業務放在回調方法裏面,而不是一直佔用線程在那裏等待數據的返回,這樣就能夠記空閒的線程去處理另外的消息,當消息從服務器端返回後,會去調用那個回調方法。
如今大多數的遊戲都是分區分服的,通過一段時間的運營以後,有些老的大區可能在線人數很是的少了,爲了節約成本,首先會在一臺物理機器上運行多個大區對應的進程,再過一段時間,可能須要把不一樣區的數據合併起來到一個數據庫中。而對用戶來講是感受不到變化的。
爲何說合服要提交設計好呢?由於若是設計很差,後期在合服的時候會遇到不少問題, 好比用戶惟一主鍵問題,表與表主鍵關聯重複問題,那麼在合服存在的狀況下,如何保證用戶的惟一性呢,也就是我一個用戶在兩個大區都創建了帳號,這個時候userid是同樣的,還有一個角色id,若是角色id不是全局惟一的,也可能重複。而角色id若是參與了表外鍵設計,一重複數據就亂了。
首先,要保證用戶的惟一性。並且各個表的外鍵引用也必須是惟一的,即合服以後不會再發生改變。那麼有幾個鍵須要全局惟一,userid(用戶id),roleId(角色id),爲了區分用戶原來所在的區,須要記錄角色所在的大區id,因此一個userid和一個大區id來肯定一個惟一的角色id,而角色的其它信息使用角色id作外鍵引用。這樣合服就能夠直接把兩個庫的數據合併到一塊兒了。
這個只是用角色數據舉個例子,在數據庫中,凡是獨立存在的,最好都使用全局惟一id,好比公會,每一個服都會有公會,但每一個服的公會id不能都是從一開始,即不能使用數據庫自增的方式