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