上一篇(游戲服務器之網關)說了一些網關大致功能,這次說說具體的實現.
網關需要與客戶端保證連接。這里網關使用Netty4來做為網絡通信框架。它也是目前在Java游戲服務器開發中,長連接使用最多的框架。
1,管理與客戶端的連接
客戶端連接到網關之后,並且驗證過之后,我們需要把連接的channel和用戶綁定起來,這樣方便使用用戶id查詢到它和客戶端的連接,就可以給客戶端返回消息了。因為是需要管理所有的客戶端連接。所以會涉及到多線程的操作。在每個連接驗證成功之后,會在當前連接的channel之中添加用戶id和channel的映射到一個map集合中。簡單點這個map可以是ConcurrentHashMap,它是線程安全的。但是如果並發理大的話,由於ConcurrentHashMap使用了鎖會產生大量的上下文切換。所以我們這里采用無鎖的單線程模式實現。即同一個用戶id的操作都會放到同一個線程中去執行,而不加鎖。
2,消息的定義
這里說的消息定義是指客戶端與服務器長連接的情況下,使用消息進行數據交互。一個消息代表客戶端發出的一個請求,或服務器返回給客戶端的一個響應。
由於服務器由網關和邏輯服務組成,所以消息又分為外部消息和內部消息,和客戶端交互的叫外部消息,在服務器內部交互的叫內部消息。
外部請求消息的組成
包頭信息:
- 消息Id:消息的唯一id,用於區分每個消息的邏輯意義,知道這個消息是干什么的
- 消息發送時間
- 是否壓縮
- 是否加密
- 服務類型,這個主要用於分布式服務。
- crc32校驗碼,用於保證消息的完整性。
- 消息序列Id: 每個用戶登陸之后,從給服務器發送第一條消息起,開始累計計數,叫序列id,Id主要用於服務器判斷消息的唯一性,對消息做等冪處理。
包體信息
- 消息體,向服務器發送的請求內容,這個部分可以根據自己的業務需要自行設計,可以是json,也可以是由protobuf序列化組成的。而且這部分信息可以加密碼,壓縮。
外部返回消息組成
- 消息序列Id: 每個用戶登陸之后,從給服務器發送第一條消息起,開始累計計數,叫序列id,Id主要用於服務器判斷消息的唯一性,對消息做等冪處理。
- 消息Id:消息的唯一id,處理於區分每個消息
- 服務類型:返回消息的服務類型。
- 服務器發送時間
- 是否壓縮
- 錯誤碼:如果服務器處理的消息有錯誤,以錯誤碼的形式返回給客戶端,在這里定義返回的錯誤碼
- 消息體,向服務器發送的請求內容,這個部分是由protobuf或json序列化組成的。
3,消息編碼與解碼
通信協議就是一份約定。在網絡通信中,所有的數據都是以二進制的形式傳輸的,不管是客戶端還是服務器端,在收到二進制的消息之后,都需要把二進制轉化為明文的形式,以方便在代碼里面使用。把明文數據轉化為二進制的過程叫序列化的過程,把二進制轉化為明文的過程中反序列化的過程,也叫編碼和解碼。由於我們使用的是netty的tcp協議,它是一種流協議,沒有明確的界限,所以我們需要知道我們每次傳輸的數據的大小,每次收到消息后,只有得到整個包才能正解的反序列化出來。這就需要處理好斷包和粘包的總理了。不過在服務器端,netty已幫我們實現好了,我們只需要配置一下就可以了。
請求協議編碼解碼格式:
數據總長度(short(2)) + 消息序列號(int(4)) + 消息發送時間(long(8)) +消息id(ishort(2)) + 服務類型(short(2)) + 是否壓縮(1)+ 是否加密(1) + crc32 (32) + 消息體
數據總長度:等於所有的位置占用字節數的總和 = 2 + 4 + 8 + 2 + 2 + 1 + 1 + 32 + 消息體長度
返回協議的編碼解碼格式:
數據總長度(short(2)) + 消息序列號(int(4)) + 消息id(short(2)) + 服務類型(short(2))+ 服務器發送時間(8)+ 是否壓縮(1) + 錯誤碼(short(2)) + 消息體
數據總長度:等於所有的位置占用字節數的總和 = 2 + 4 + 2 + 2 + 8+ 1 + 2 + 消息體長度
網關消息向服務器轉發
網關與業務服務之間是一種一對多的關系,當網關收到客戶端消息的時候,需要把它轉發或叫路由到業務服務上面,而且為了使業務服務可以動態擴展,當多個業務服務啟動或關閉時,網關都應該能感應到。所以需要有一個服務發現的服務。為了統一框架,我們采用springcloud中的Eureka做為服務的注冊和發現。每個服務都有一個服務類型和服務id,兩者組成一個唯的標識符標記一個唯一服務。服務類型表示服務提供哪些功能,比如活動服務,但是有時候為了動態擴展,當服務壓力過大時,一活動服務不能滿足要求,可能需要啟動兩台活動服務,為了區別服務實例,需要一個服務id。所以同個服務類型,可以由多個服務id的服務組成,這樣可以做動態負載。
為了提高性能,網關與業務服務之間采用異步的通信方式,而且為了防止客戶端請求負載的波動,需要緩存部分請求。所以可以使用消息隊列負責網關與服務器之間的通信,使用現成的消息中間件,可以減少對底層網絡的關注,減少網絡層的開發時間,減少bug的出現。消息隊列還可以緩存消息,起到消峰的作用。還可以使用消息的訂閱、發布功能,方便的解決網關與業務服務一對多的問題。
當網關收到客戶端的消息時,需要知道消息是屬於哪個服務類型的,根據服務類型,找到這個服務類型所有的服務實例,然后根據角色id% 實例數量路由消息到一個服務實例上。我使用阿里的Rockketmq做為消息中間件。網關向業務服務發送消息就是publish一個消息,需要一個publsh topic和唯一的tag, 這個tag可以標識一個唯一的消息,它由:prefix + messageId + serverType + serverId ,而業務服務在啟動的時候,會監聽這個服務處理的消息,監聽同樣的topic和tag。
業務服務向網關發送消息
當業務服務處理完請求之后,需要給網關返回結果,再由網關把結果返回給客戶端。網關監聽一個公開的topic ,它的tag就是網關實例Id。 這樣就算有多個網關的話,也可以准確的返回消息。所以業務服務收到的消息中要攜帶消息來源的服務實例id.這樣業務服務就可以publish topic到網關了。
內部消息的通信協議
網關與業務服務之間交互的消息格式編碼與解碼:
- roleId (long)
- userId (long)
- 消息序列號(int)
- 消息Id (short)
- 服務類型 (short)
- 消息發送時間(long)
- 消息來源的服務實例Id (short)
- 消息要到達的服務實例id (short)
- 錯誤碼(short)
- ip字符串長度
- 客戶端的ip地址 (string)
- 消息體(byte[])
消息的封裝和具體實現在GitHub:https://github.com/youxijishu/xinyue-game-frame/tree/master/game-frame-server/game-network-message