1. 程式人生 > >中小型手機棋牌網路遊戲服務端架構設計(帶原始碼)

中小型手機棋牌網路遊戲服務端架構設計(帶原始碼)

承接自己《中小型棋牌類網路遊戲服務端架構》博文,用Golang實現基礎架構邏輯後,準備再次談談我的想法。

已實現的邏輯與前文描述有幾點不同:
1. Gateway更名為Proxy,DBProxy更名為DB
2. Proxy同時持有與(Login, Game)不同型別伺服器的多條連線
3. DB不參與負載均衡,考慮是棋牌資料庫負載不高,即使需要擴充套件多個也可以通過不同伺服器配置指向不同的DB來擴充套件
4. 訊息頭格式以原始碼實現的為主
5. 心跳機制在不考慮客戶端的前提下,服務端會主動傳送心跳包,但並非總是特定間隔時間傳送

如果閱讀起來感覺晦澀難懂,那就不妨直接看原始碼

Network

包含Server,Client,RPC三個元件。

Server

網路服務端元件,持有監聽套接字,管理所有客戶端連線(map儲存,value可以儲存自定義值),一個客戶端連線到來起一個Goroutines接收訊息,呼叫Stop將等待所有Goroutines退出才返回,只通知業務邏輯“收到訊息”“連線關閉”兩種事件。

Client

網路客戶端元件,持有與服務端的連線,實現補償策略重連機制,只通知業務邏輯“客戶端連線成功”“客戶端收到訊息”兩種事件。

RPC

遠端過程呼叫,只實現同步呼叫,同一時間一個呼叫佔用一條連線,實現連線池,暫時沒有限制最大連線數。
注:服務端重啟,一次呼叫將會關閉所有已經建立的連線,建立新的連線完成呼叫,若服務端重啟歷時過長,此時呼叫將因為連線服務端失敗而返回錯誤。

Error

有必要詳說通用的錯誤機制,錯誤型別是跨伺服器跨網路的,可以直接返回error或MyError型別通知通訊呼叫方錯誤,錯誤會一級一級返回,Golang內建error型別終會建立相應的MyError自定義錯誤型別進行返回,RPC接收方收到訊息將會檢查若為錯誤訊息,則建立MyError返回標識呼叫失敗。

Server

Proxy(代理), Manager(管理), Login(登陸), Game(遊戲), DB(資料庫代理)。

Proxy

持有Server元件:接受客戶端連線,管理客戶端連線,事件到來通知業務邏輯。
持有Client元件:連線Manager伺服器,事件到來通知業務邏輯。
連線Manager成功,將會發送註冊服務訊息(編號,監聽地址,服務型別)。
客戶端訊息到來,檢查conn上是否繫結的有session,沒有則建立繫結,最後訊息總是交由session處理。
客戶端連線關閉,檢查conn上是否繫結的有session,若有則關閉session。
收到Manager通知服務訊息,合理更新本地可用服務資訊。

Session

會話目前共持有client(客戶端), login(登陸), game(遊戲)三個連線,存在最基本的目的就是訊息轉發。
session共有三種連線狀態,安全狀態(說明客戶端一直給服務端發訊息,平均間隔時間不超過十秒),警告狀態(客戶端至少最近十秒內沒有給服務端發訊息),死亡狀態(客戶端至少最近二十秒內沒有給服務端發訊息,且連服務端十秒前傳送的心跳包都不回覆),session收到客戶端訊息,總是將session狀態設定為安全狀態。
目前也只有這層實現心跳檢測保活(服務端和服務端之間是沒有心跳的),一個session起一個Goroutines間歇迴圈保活(原本也想所有session統一保活,最大的好處就是隻用起一個Goroutines,但是這樣必然要維護session集合,因為牽涉增加刪除session,必然又要加鎖,不想這樣所以採用了當前的方案),每十秒檢查一下session狀態,總是將狀態值遞減至死亡狀態,當session狀態是警告狀態,嘗試向客戶端傳送心跳包,當session狀態是死亡狀態,則關閉session。
總是在收到客戶端快速註冊訊息時,關閉原有與登陸建立的連線,重新建立連線,起一個Goroutines接收登陸服務端訊息,不在客戶端連線建立的時候連線登陸服務端,這樣做會多一層保護,防止攻擊者惡意建立連線,穿透至登陸服務端,這裡會重新序列化客戶端訊息,填充客戶端地址,畢竟只有這裡知道客戶端的真實地址,發向登陸服務端的其它型別訊息不允許先於快速註冊訊息,否則直接關閉session,接收登陸服務端快速註冊訊息返回時會解析訊息,取出使用者編號記錄到session裡,便於快速登陸游戲服務端時填充使用者編號。
總是在收到客戶端快速登陸訊息時,關閉原有與遊戲建立的連線,重新建立連線,起一個Goroutines接收遊戲服務端的訊息,客戶端快速登陸訊息只需要提供“遊戲型別(鬥地主)”“遊戲等級(新手房)”,這裡通過查詢對應服務建立正確連線,這裡會重新序列化訊息,新增“使用者編號”“時間戳”“簽名”,時間戳和簽名只是想保證遊戲服務端收到的快速登陸訊息一定是我的代理服務端發出的(詳細請參看原始碼,思路可參考微信公眾平臺接入驗證),發向遊戲服務端的其它型別訊息不允許先於快速登陸訊息,否則直接關閉session。
使用者斷開與代理的連線,將會觸發關閉session,通知保活Goroutines退出,關閉與登陸游戲之間的連線,兩個接收訊息Goroutines將退出,與客戶端的連線接下來會被釋放,不再有誰繫結該session,所以該session將會被GC(垃圾回收)。
不管接收登陸還是遊戲訊息失敗,接收Goroutines都會退出,將會關閉與客戶端連線,進而關閉session。
收到使用者登出訊息,session直接關閉與遊戲服務端的連線。

Manager

持有Server元件:接受客戶端(Proxy, Login, Game)連線,管理客戶端連線,事件到來通知業務邏輯。
維護兩個服務集合,一是所有服務(所有已開啟的服務),二是已選服務(負載均衡策略後選擇的服務)。
收到註冊服務訊息,記錄對應網路連線,所有服務表記錄該服務,若已選服務表中不存在類似服務,則同時已選服務表記錄該服務,若註冊服務型別是代理,則通知當前已選服務,用來初始化代理本地可用服務。
連線關閉,將通過連線查詢服務,所有服務表刪除該服務,已選服務表若存在該服務則刪除,嘗試從所有服務表中獲取相似服務,若獲取到則新增至已選服務表。
收到更新計數訊息,更新對應服務的計數值,只有當計數值高於容量閾值時才會觸發後面的邏輯,已選服務表若存在該服務則刪除,嘗試從所有服務表中獲取相似服務,若獲取到則新增至已選服務表,但若準備刪除和增加的是同一個服務,則不觸發前面的刪除新增服務邏輯。
收到開啟服務訊息,設定對應服務開啟標識,若已選服務表中不存在類似服務,則已選服務表記錄該服務。
收到關閉服務訊息,設定對應服務關閉標識,已選服務表若存在該服務則刪除,嘗試從所有服務表中獲取相似服務,若獲取到則新增至已選服務表。
前面有三處用到獲取相似服務,其實這裡實現了負載均衡,每次呼叫總會返回計數值最小的Proxy, Login服務,計數值最大的卻又不超過容量閾值的Game服務(若都超過閾值返回最小的),讓使用者儘可能在相同遊戲服務中玩,便於快速組桌開始遊戲。
前面提到的不管是增加已選服務,還是刪除已選服務,都會通知所有已註冊的代理。

Login

持有Server元件:接受Proxy模擬客戶端建立的連線,管理與Proxy建立的連線,事件到來通知業務邏輯。
持有Client元件:連線Manager伺服器,事件到來通知業務邏輯。
持有RPC元件:連線DB資料庫代理。
連線Manager成功,將會發送註冊服務訊息(編號,監聽地址,服務型別)。
收到快速註冊訊息,RPC同步呼叫請求資料庫代理,不管是查詢已有使用者還是建立新使用者,資料庫代理返回後,直接回復客戶端,其實是回覆給代理。

Game

持有Server元件:接受Proxy模擬客戶端建立的連線,管理與Proxy建立的連線,事件到來通知業務邏輯。
持有Client元件:連線Manager伺服器,事件到來通知業務邏輯。
持有RPC元件:連線DB資料庫代理。
連線Manager成功,將會發送註冊服務訊息(編號,監聽地址,遊戲型別,遊戲等級,服務型別)。
收到快速登陸訊息,通過校驗簽名確保是由Proxy發來的,RPC同步呼叫請求資料庫代理查詢使用者資訊,資料庫代理返回後,直接回復客戶端,其實是回覆給代理。

DB

持有Server元件:接受客戶端(Login, Game)連線,管理客戶端連線,事件到來通知業務邏輯。
這裡擴充套件了OnMessage的返回值,支援返回nil表示成功,返回內建error型別表示錯誤,返回自定義MyError型別表示成功或錯誤,返回其它資料結構表示回覆給客戶端的內容。
連線MySQL資料庫,連線Redis快取

注:已經實現定時器模組,定時時間精確到秒,主要邏輯只不過是對標準庫Timer、Ticker封裝管理而已,通過全域性唯一編號新增定時器,支援迴圈定時器,支援獲取到期剩餘時間。