基礎(chǔ)知識參考Golang 游戲架構(gòu)簡介
本系列參考github leaf的官方wiki及issues
本文參考
Leaf 代碼閱讀
Leaf 游戲服務(wù)器框架簡介
一、概述
1.Leaf 服務(wù)器的設(shè)計
Leaf專注于游戲服務(wù)器陈辱,因此與一些Web服務(wù)器開發(fā)的設(shè)計和考慮有所不同沛贪。
在一些游戲服務(wù)器中利赋,采用的是分布式架構(gòu)隐砸,即服務(wù)器整體被劃分為不同的模塊,各個模塊承擔不同的功能幽纷,而模塊之間通過TCP進行交互友浸。這樣的架構(gòu)能夠保證服務(wù)器能夠在多臺機器上部署收恢,單點故障不至于讓服務(wù)整體崩潰伦意。但是這種服務(wù)器有其自身的開發(fā)難度聪全,而且有時候做好模塊劃分并不容易离钝。
Leaf是一體式的框架卵渴,連最外圍的接入服務(wù)器也被整合在一起奖恰。雖然Leaf中間也劃分了不同模塊瑟啃,但是他們是通過InnerRpc進行通訊的蛹屿。介于現(xiàn)在手游興起坟瓢,單機性能提升折联,不少游戲服務(wù)器所需要的性能并不十分苛刻诚镰,所以Leaf在這方面的簡潔與易于開發(fā)有很大的優(yōu)勢清笨。
一個 Leaf 開發(fā)的游戲服務(wù)器由多個模塊組成(例如 LeafServer)抠艾,模塊有以下特點:
- 每個模塊運行在一個單獨的 goroutine 中
- 模塊間通過一套輕量的 RPC 機制通訊(leaf/chanrpc)
Leaf 不建議在游戲服務(wù)器中設(shè)計過多的模塊。無論封裝多么精巧齐苛,跨 goroutine 的調(diào)用總不能像直接的函數(shù)調(diào)用那樣簡單直接脸狸,因此除非必要我們不要構(gòu)建太多的模塊炊甲,模塊間不要太頻繁的交互卿啡。模塊在 Leaf 中被設(shè)計出來最主要是用于劃分功能而非利用多核颈娜,Leaf 認為在模塊內(nèi)按需使用 goroutine 才是多核利用率問題的解決之道
游戲服務(wù)器在啟動時進行模塊的注冊蛹磺,例如:
leaf.Run(
game.Module,
gate.Module,
login.Module,
)
這里按順序注冊了 game萤捆、gate俗或、login 三個模塊。每個模塊都需要實現(xiàn)接口:
type Module interface {
OnInit()
OnDestroy()
Run(closeSig chan bool)
}
Leaf 首先會在同一個 goroutine 中按模塊注冊順序執(zhí)行模塊的 OnInit 方法帅腌,等到所有模塊 OnInit 方法執(zhí)行完成后則為每一個模塊啟動一個 goroutine 并執(zhí)行模塊的 Run 方法狞膘。最后,游戲服務(wù)器關(guān)閉時(Ctrl + C 關(guān)閉游戲服務(wù)器)將按模塊注冊相反順序在同一個 goroutine 中執(zhí)行模塊的 OnDestroy 方法臣镣。
與一些Web服務(wù)器不同,Leaf運行的數(shù)據(jù)絕大部分都在內(nèi)存里面弃舒,雖然提供了Mongo模塊聋呢,但是做實時交互的數(shù)據(jù)一般是保存在內(nèi)存中的削锰。Mongo只是為了持久化一些用戶數(shù)據(jù)。這與有些無狀態(tài)毕莱,靠數(shù)據(jù)庫做數(shù)據(jù)交互的Web服務(wù)器有很大不同器贩。
二颅夺、官方示例
參考
Leaf 游戲服務(wù)器框架簡介,介紹了官方的leafServer
1.消息處理
首先定義一個 JSON 格式的消息(protobuf 類似)蛹稍。Processor 為消息的處理器(可由用戶自定義)吧黄,這里我們使用 Leaf 默認提供的 JSON 消息處理器并嘗試添加一個名字為 Hello 的消息:
//LeafServer msg/msg.go
package msg
import (
"github.com/name5566/leaf/network/json"
)
// 使用默認的 JSON 消息處理器(默認還提供了 protobuf 消息處理器)
var Processor = json.NewProcessor()
func init() {
// 這里我們注冊了一個 JSON 消息 Hello
Processor.Register(&Hello{})
}
// 一個結(jié)構(gòu)體定義了一個 JSON 消息的格式
// 消息名為 Hello
type Hello struct {
Name string
}
客戶端發(fā)送到游戲服務(wù)器的消息需要通過 gate 模塊路由,簡而言之唆姐,gate 模塊決定了某個消息具體交給內(nèi)部的哪個模塊來處理。這里厦酬,我們將 Hello 消息路由到 game 模塊中胆描。打開 LeafServer gate/router.go,敲入如下代碼:
package gate
import (
"server/game"
"server/msg"
)
func init() {
// 這里指定消息 Hello 路由到 game 模塊
// 模塊間使用 ChanRPC 通訊仗阅,消息路由也不例外
msg.Processor.SetRouter(&msg.Hello{}, game.ChanRPC)
}
一切就緒昌讲,我們現(xiàn)在可以在 game 模塊中處理 Hello 消息了。打開 LeafServer game/internal/handler.go减噪,敲入如下代碼:
package internal
import (
"github.com/name5566/leaf/log"
"github.com/name5566/leaf/gate"
"reflect"
"server/msg"
)
func init() {
// 向當前模塊(game 模塊)注冊 Hello 消息的消息處理函數(shù) handleHello
handler(&msg.Hello{}, handleHello)
}
func handler(m interface{}, h interface{}) {
skeleton.RegisterChanRPC(reflect.TypeOf(m), h)
}
func handleHello(args []interface{}) {
// 收到的 Hello 消息
m := args[0].(*msg.Hello)
// 消息的發(fā)送者
a := args[1].(gate.Agent)
// 輸出收到的消息的內(nèi)容
log.Debug("hello %v", m.Name)
// 給發(fā)送者回應(yīng)一個 Hello 消息
a.WriteMsg(&msg.Hello{
Name: "client",
})
}
到這里短绸,一個簡單的范例就完成了。為了更加清楚的了解消息的格式筹裕,我們從 0 編寫一個最簡單的測試客戶端醋闭。
Leaf 中,當選擇使用 TCP 協(xié)議時朝卒,在網(wǎng)絡(luò)中傳輸?shù)南⒍紩褂靡韵赂袷剑?/p>
--------------
| len | data |
--------------
其中:
- len 表示了 data 部分的長度(字節(jié)數(shù))证逻。len 本身也有長度,默認為 2 字節(jié)(可配置)抗斤,len 本身的長度決定了單個消息的最大大小
- data 部分使用 JSON 或者 protobuf 編碼(也可自定義其他編碼方式)
測試客戶端同樣使用 Go 語言編寫:
package main
import (
"encoding/binary"
"net"
)
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:3563")
if err != nil {
panic(err)
}
// Hello 消息(JSON 格式)
// 對應(yīng)游戲服務(wù)器 Hello 消息結(jié)構(gòu)體
data := []byte(`{
"Hello": {
"Name": "leaf"
}
}`)
// len + data
m := make([]byte, 2+len(data))
// 默認使用大端序
binary.BigEndian.PutUint16(m, uint16(len(data)))
copy(m[2:], data)
// 發(fā)送消息
conn.Write(m)
}
執(zhí)行此測試客戶端囚企,游戲服務(wù)器輸出:
2015/09/25 07:41:03 [debug ] hello leaf
2015/09/25 07:41:03 [debug ] read message: read tcp 127.0.0.1:3563->127.0.0.1:54599: wsarecv: An existing connection was forcibly closed by the remote host.
測試客戶端發(fā)送完消息以后就退出了,此時和游戲服務(wù)器的連接斷開瑞眼,相應(yīng)的龙宏,游戲服務(wù)器輸出連接斷開的提示日志(第二條日志,日志的具體內(nèi)容和 Go 語言版本有關(guān))伤疙。
除了使用 TCP 協(xié)議外银酗,還可以選擇使用 WebSocket 協(xié)議(例如開發(fā) H5 游戲)。Leaf 可以單獨使用 TCP 協(xié)議或 WebSocket 協(xié)議徒像,也可以同時使用兩者黍特,換而言之,服務(wù)器可以同時接受 TCP 連接和 WebSocket 連接厨姚,對開發(fā)者而言消息來自 TCP 還是 WebSocket 是完全透明的⌒瞥海現(xiàn)在,我們來編寫一個對應(yīng)上例的使用 WebSocket 協(xié)議的客戶端:
<script type="text/javascript">
var ws = new WebSocket('ws://127.0.0.1:3653')
ws.onopen = function() {
// 發(fā)送 Hello 消息
ws.send(JSON.stringify({Hello: {
Name: 'leaf'
}}))
}
</script>
保存上述代碼到某 HTML 文件中并使用(任意支持 WebSocket 協(xié)議的)瀏覽器打開谬墙。在打開此 HTML 文件前今布,首先需要配置一下 LeafServer 的 bin/conf/server.json 文件经备,增加 WebSocket 監(jiān)聽地址(WSAddr):
{
"LogLevel": "debug",
"LogPath": "",
"TCPAddr": "127.0.0.1:3563",
"WSAddr": "127.0.0.1:3653",
"MaxConnNum": 20000
}
重啟游戲服務(wù)器后,方可接受 WebSocket 消息:
2015/09/25 07:50:03 [debug ] hello leaf
在 Leaf 中使用 WebSocket 需要注意的一點是:Leaf 總是發(fā)送二進制消息而非文本消息部默。
2.模塊結(jié)構(gòu)
一般來說(而非強制規(guī)定)侵蒙,從代碼結(jié)構(gòu)上,一個 Leaf 模塊:
- 放置于一個目錄中(例如 game 模塊放置于 game 目錄中)
- 模塊的具體實現(xiàn)放置于 internal 包中(例如 game 模塊的具體實現(xiàn)放置于 game/internal 包中)
每個模塊下一般有一個 external.go 的文件傅蹂,顧名思義表示模塊對外暴露的接口纷闺,這里以 game 模塊的 external.go 文件為例:
package game
import (
"server/game/internal"
)
var (
// 實例化 game 模塊
Module = new(internal.Module)
// 暴露 ChanRPC
ChanRPC = internal.ChanRPC
)
首先,模塊會被實例化份蝴,這樣才能注冊到 Leaf 框架中(詳見 LeafServer main.go)犁功,另外,模塊暴露的 ChanRPC 被用于模塊間通訊婚夫。
進入 game 模塊的內(nèi)部(LeafServer game/internal/module.go):
package internal
import (
"github.com/name5566/leaf/module"
"server/base"
)
var (
skeleton = base.NewSkeleton()
ChanRPC = skeleton.ChanRPCServer
)
type Module struct {
*module.Skeleton
}
func (m *Module) OnInit() {
m.Skeleton = skeleton
}
func (m *Module) OnDestroy() {
}
模塊中最關(guān)鍵的就是 skeleton(骨架)浸卦,skeleton 實現(xiàn)了 Module 接口的 Run 方法并提供了:
- ChanRPC
- goroutine
- 定時器
3.Leaf ChanRPC
由于 Leaf 中,每個模塊跑在獨立的 goroutine 上案糙,為了模塊間方便的相互調(diào)用就有了基于 channel 的 RPC 機制限嫌。一個 ChanRPC 需要在游戲服務(wù)器初始化的時候進行注冊(注冊過程不是 goroutine 安全的),例如 LeafServer 中 game 模塊注冊了 NewAgent 和 CloseAgent 兩個 ChanRPC:
package internal
import (
"github.com/name5566/leaf/gate"
)
func init() {
skeleton.RegisterChanRPC("NewAgent", rpcNewAgent)
skeleton.RegisterChanRPC("CloseAgent", rpcCloseAgent)
}
func rpcNewAgent(args []interface{}) {
}
func rpcCloseAgent(args []interface{}) {
}
使用 skeleton 來注冊 ChanRPC时捌。RegisterChanRPC 的第一個參數(shù)是 ChanRPC 的名字怒医,第二個參數(shù)是 ChanRPC 的實現(xiàn)奢讨。這里的 NewAgent 和 CloseAgent 會被 LeafServer 的 gate 模塊在連接建立和連接中斷時調(diào)用稚叹。ChanRPC 的調(diào)用方有 3 種調(diào)用模式:
- 同步模式,調(diào)用并等待 ChanRPC 返回
- 異步模式禽笑,調(diào)用并提供回調(diào)函數(shù)入录,回調(diào)函數(shù)會在 ChanRPC 返回后被調(diào)用
- Go 模式蛤奥,調(diào)用并立即返回佳镜,忽略任何返回值和錯誤
gate 模塊這樣調(diào)用 game 模塊的 NewAgent ChanRPC(這僅僅是一個示例,實際的代碼細節(jié)復(fù)雜的多):
game.ChanRPC.Go("NewAgent", a)
這里調(diào)用 NewAgent 并傳遞參數(shù) a凡桥,我們在 rpcNewAgent 的參數(shù) args[0] 中可以取到 a(args[1] 表示第二個參數(shù)蟀伸,以此類推)。
更加詳細的用法可以參考 leaf/chanrpc缅刽。需要注意的是啊掏,無論封裝多么精巧,跨 goroutine 的調(diào)用總不能像直接的函數(shù)調(diào)用那樣簡單直接衰猛,因此除非必要我們不要構(gòu)建太多的模塊迟蜜,模塊間不要太頻繁的交互。模塊在 Leaf 中被設(shè)計出來最主要是用于劃分功能而非利用多核啡省,Leaf 認為在模塊內(nèi)按需使用 goroutine 才是多核利用率問題的解決之道娜睛。
三髓霞、Leaf 服務(wù)器的主要模塊
1.ChanRpc
Rpc是遠程過程調(diào)用的簡稱,原來是通過Tcp手段使得一個本地的函數(shù)調(diào)用畦戒,將調(diào)用信息傳遞給其他服務(wù)器執(zhí)行方库,并通過Tcp返回結(jié)果的一種技術(shù)手段,在分布式中障斋,這種調(diào)用十分常見纵潦。但是這里的ChanRpc是在不同協(xié)程中進行函數(shù)調(diào)用,其實現(xiàn)的手段是Chan垃环,所以成為ChanRpc邀层。你可以將不同的功能模塊實現(xiàn)為ChanRpc并提供給其他模塊調(diào)用。
2.Cluster
Cluster 主要是管理集群遂庄,但是Leaf本身專注的還是單機服務(wù)被济,所以這個模塊的功能現(xiàn)在還沒有實現(xiàn)。
3.Conf
Conf 是Leaf的配置管理模塊涧团。里面主要是Leaf啟動的一些必要信息只磷。
4.Console
Console 模塊為Leaf管理提供了一個終端接口,你可以使用Telnet連接上去動態(tài)的修改參數(shù)泌绣,或者指向命令钮追。其內(nèi)部實現(xiàn)了Help, CpuProf, Prof命令,并提供擴展阿迈,可以方便的添加其他命令元媚。另外,擴展命令是通過ChanRpc實現(xiàn)的苗沧。
5.DB
DB模塊提供里Mongo支持刊棕,也可以在這里聚合其他DB模塊。
6.Gate
Gate 模塊為Leaf提供接入功能待逞。這個模塊的功能很重要甥角,是服務(wù)器的入口。它能同時監(jiān)聽TcpSocket和WebSocket识樱。主要流程是在接入連接的時候創(chuàng)建一個Agent嗤无,并將這個Agent通知給AgentRpc。其核心其實是一個TcpServer和WebScoketServer怜庸,他的協(xié)議函數(shù)能夠?qū)ocket字節(jié)流分包当犯,封裝為Msg傳遞給Agent。其工作流可以查看Server模塊割疾。
客戶端發(fā)送到游戲服務(wù)器的消息需要通過 gate 模塊路由嚎卫,簡而言之,gate 模塊決定了某個消息具體交給內(nèi)部的哪個模塊來處理宏榕。
7.Go
用于創(chuàng)建能夠被 Leaf 管理的 goroutine拓诸。Go模塊是對golang中g(shù)o提供一些額外功能胸懈。Go提供回調(diào)功能,LinearContext提供順序調(diào)用功能恰响。善用 goroutine 能夠充分利用多核資源趣钱,Leaf 提供的 Go 機制解決了原生 goroutine 存在的一些問題:
- 能夠恢復(fù) goroutine 運行過程中的錯誤
- 游戲服務(wù)器會等待所有 goroutine 執(zhí)行結(jié)束后才關(guān)閉
- 非常方便的獲取 goroutine 執(zhí)行的結(jié)果數(shù)據(jù)
- 在一些特殊場合保證 goroutine 按創(chuàng)建順序執(zhí)行
我們來看一個例子(可以在 LeafServer 的模塊的 OnInit 方法中測試):
log.Debug("1")
// 定義變量 res 接收結(jié)果
var res string
skeleton.Go(func() {
// 這里使用 Sleep 來模擬一個很慢的操作
time.Sleep(1 * time.Second)
// 假定得到結(jié)果
res = "3"
}, func() {
log.Debug(res)
})
log.Debug("2")
上面代碼執(zhí)行結(jié)果如下:
2015/08/27 20:37:17 [debug ] 1
2015/08/27 20:37:17 [debug ] 2
2015/08/27 20:37:18 [debug ] 3
這里的 Go 方法接收 2 個函數(shù)作為參數(shù),第一個函數(shù)會被放置在一個新創(chuàng)建的 goroutine 中執(zhí)行胚宦,在其執(zhí)行完成之后首有,第二個函數(shù)會在當前 goroutine 中被執(zhí)行。由此枢劝,我們可以看到變量 res 同一時刻總是只被一個 goroutine 訪問井联,這就避免了同步機制的使用。Go 的設(shè)計使得 CPU 得到充分利用您旁,避免操作阻塞當前 goroutine烙常,同時又無需為共享資源同步而憂心。
更加詳細的用法可以參考 leaf/go鹤盒。
8.Log
Leaf 的 log 系統(tǒng)支持多種日志級別:
- Debug 日志蚕脏,非關(guān)鍵日志
- Release 日志,關(guān)鍵日志
- Error 日志侦锯,錯誤日志
- Fatal 日志驼鞭,致命錯誤日志
Debug < Release < Error < Fatal(日志級別高低)
在 LeafServer 中,bin/conf/server.json 可以配置日志級別尺碰,低于配置的日志級別的日志將不會輸出挣棕。Fatal 日志比較特殊,每次輸出 Fatal 日志之后游戲服務(wù)器進程就會結(jié)束亲桥,通常來說洛心,只在游戲服務(wù)器初始化失敗時使用 Fatal 日志。
我們還可以通過配置 LeafServer conf/conf.go 的 LogFlag 來在日志中輸出文件名和行號:
LogFlag = log.Lshortfile
可用的 LogFlag 見:https://golang.org/pkg/log/#pkg-constants
更加詳細的用法可以參考 leaf/log题篷。
9.Module
Module 為Leaf提供模塊化支持词身。Skeleton是Leaf的整體骨架,它聚合了Leaf中其他一些異步調(diào)用模塊的功能悼凑,使得各模塊之間能夠協(xié)同工作偿枕。
10.Network
Network是Leaf的網(wǎng)絡(luò)部分,這部分比較大户辫,而且包含一個json和protobuf解包模塊。
11.Recordfile
Recordfile 提供序列化和反序列化為文本的功能嗤锉。Leaf 的 recordfile 是基于 CSV 格式(范例見這里)渔欢。recordfile 用于管理游戲配置數(shù)據(jù)。在 LeafServer 中使用 recordfile 非常簡單:
- 將 CSV 文件放置于 bin/gamedata 目錄中
- 在 gamedata 模塊中調(diào)用函數(shù) readRf 讀取 CSV 文件
范例:
// 確保 bin/gamedata 目錄中存在 Test.txt 文件
// 文件名必須和此結(jié)構(gòu)體名稱相同(大小寫敏感)
// 結(jié)構(gòu)體的一個實例映射 recordfile 中的一行
type Test struct {
// 將第一列按 int 類型解析
// "index" 表明在此列上建立唯一索引
Id int "index"
// 將第二列解析為長度為 4 的整型數(shù)組
Arr [4]int
// 將第三列解析為字符串
Str string
}
// 讀取 recordfile Test.txt 到內(nèi)存中
// RfTest 即為 Test.txt 的內(nèi)存鏡像
var RfTest = readRf(Test{})
func init() {
// 按索引查找
// 獲取 Test.txt 中 Id 為 1 的那一行
r := RfTest.Index(1)
if r != nil {
row := r.(*Test)
// 輸出此行的所有列的數(shù)據(jù)
log.Debug("%v %v %v", row.Id, row.Arr, row.Str)
}
}
更加詳細的用法可以參考 leaf/recordfile瘟忱。
12.Timer
Timer主要是提供一個Cron功能的定時器服務(wù)奥额,其中Timer是time.AfterFunc的封裝苫幢,是為了方便聚合到Skeleton中。
Go 語言標準庫提供了定時器的支持:
func AfterFunc(d Duration, f func()) *Timer
AfterFunc 會等待 d 時長后調(diào)用 f 函數(shù)垫挨,這里的 f 函數(shù)將在另外一個 goroutine 中執(zhí)行韩肝。Leaf 提供了一個相同的 AfterFunc 函數(shù),相比之下九榔,f 函數(shù)在 AfterFunc 的調(diào)用 goroutine 中執(zhí)行哀峻,這樣就避免了同步機制的使用:
skeleton.AfterFunc(5 * time.Second, func() {
// ...
})
另外,Leaf timer 還支持 cron 表達式哲泊,用于實現(xiàn)諸如“每天 9 點執(zhí)行”剩蟀、“每周末 6 點執(zhí)行”的邏輯。
更加詳細的用法可以參考 leaf/timer切威。
13.Leaf 服務(wù)器的其他設(shè)施
主要是util提供的一些功能
deepcopy
進行深拷貝育特,建立數(shù)據(jù)快照,并同步到數(shù)據(jù)庫中先朦。map
對原始map封裝缰冤,提供協(xié)程安全的訪問。rand
對原始rand的封裝喳魏,提供一些高級的隨機函數(shù)锋谐。semaphore
用chan實現(xiàn)的信號量。
四截酷、與cocos creator 結(jié)合的一個示例
參考
leaf 和cocos creator 游戲?qū)崙?zhàn)(一)使用protobuf完成通訊
leaf 和cocos creator 游戲?qū)崙?zhàn)(二)注冊與登陸
這個例子在官方的leafServer基礎(chǔ)上改的涮拗,下載代碼后很容易就能把前后端跑通。主要區(qū)別是使用了protobufjs通訊方式迂苛,與leafserver相同部分不再復(fù)述三热。另外,前端cocos部分代碼參見原文三幻。
1.在msg文件夾下新增lobby.proto文件
syntax = "proto3";
package msg;
enum Result {
REGISTER_SUCCESS=0;
REGISTER_FAIL=1;
LOGIN_SUCCESS=2;
LOGIN_FAIL=3;
}
message Test {
string Test = 2;
}
// 用戶登陸協(xié)議
message UserLogin {
string LoginName = 1; // 用戶名
string LoginPW =2; // 密碼
}
...
生成相應(yīng)的lobby.pb.go文件就漾,基礎(chǔ)知識可以參考Golang protobuf
2.msg.go里換成protobuf解析器
package msg
import (
"github.com/name5566/leaf/network/protobuf"
"fmt"
)
// 使用 Protobuf 消息處理器
var Processor = protobuf.NewProcessor()
func init() {
id1 := Processor.Register(&Test{})
fmt.Println("id1:",id1)
id2 := Processor.Register(&UserLogin{})
fmt.Println("id2:",id2)
id3 := Processor.Register(&UserRegister{})
fmt.Println("id3:",id3)
id4 := Processor.Register(&UserResult{})
fmt.Println("id4:",id4)
id5 := Processor.Register(&UserST{})
fmt.Println("id5:",id5)
}
這里打印的id是不是消息ID呢,有點困惑
3.消息ID
在 Leaf 中念搬,默認的 Protobuf Processor 將一個完整的 Protobuf 消息定義為如下格式:
-------------------------
| id | protobuf message |
-------------------------
其中 id 為 2 個字節(jié)抑堡。如果你選擇使用 TCP 協(xié)議時,在網(wǎng)絡(luò)中傳輸?shù)南⒏袷饺缦拢?/p>
-------------------------------
| len | id | protobuf message |
-------------------------------
如果你選擇使用 WebSocket 協(xié)議時朗徊,發(fā)送的消息格式如下:
-------------------------
| id | protobuf message |
-------------------------
其中 len 默認為兩個字節(jié)首妖,len 和 id 默認使用網(wǎng)絡(luò)字節(jié)序∫遥客戶端需要按此格式進行編碼有缆。
首先,Protobuf id 是 Leaf 的 Protobuf Processor 自動生成的。生成規(guī)則是從 0 開始棚壁,第一個注冊的消息 ID 為 0杯矩,第二個注冊的消息 ID 為 1,以此類推袖外。
//leaf/network/protobuf.go的Register方法
i := new(MsgInfo)
i.msgType = msgType
p.msgInfo = append(p.msgInfo, i)
id := uint16(len(p.msgInfo) - 1)
p.msgID[msgType] = id
return id
可以看到每注冊一個就append到切片里史隆,然后取len做為id.
4.客戶端如何正確處理消息 ID?
由于消息 ID 是服務(wù)器生成的曼验,因此建議服務(wù)器導(dǎo)出消息 ID 給客戶端使用泌射。這里提供一個簡單的思路,假定客戶端使用 Lua蚣驼,這時候服務(wù)器可以導(dǎo)出消息 ID 為一個 Lua 源文件魄幕,此源文件中包含一個 table,可能內(nèi)容如下:
msg = {
[0] = "msg.MsgA",
[1] = "msg.MsgB",
[2] = "msg.MsgC",
}
Lua 客戶端加載此 Lua 源文件颖杏,得到 msg table纯陨,然后就可以從消息 ID(網(wǎng)絡(luò)中過來的消息)獲取到消息了。當然留储,各位同學需要按照自己的實際情況來做實際的處理翼抠。
為了導(dǎo)出消息 ID 給客戶端,Leaf 的 Protobuf Processor 提供了方法:
func (p *Processor) Range(f func(id uint16, t reflect.Type))
通過此方法获讳,我們可以遍歷所有 Protobuf 消息阴颖,然后(比如說按上述方式)導(dǎo)出消息 ID 給客戶端使用。
5.為什么客戶端解析出來的 id 很大丐膝?
一般來說量愧,id 很大是因為字節(jié)序問題。Leafserver 默認使用大端序帅矗,這個設(shè)定可以配置偎肃,具體修改 Leafserver 中 conf/conf.go 中 LittleEndian 配置,true 表示使用小端序浑此,false 表示使用大端序累颂。
6.issues 關(guān)于protobuf msg id自動生成的設(shè)計
Q:初步接觸leaf,對這個id還是不能理解為什么要自動生成凛俱?這個id是由服務(wù)器消息注冊順序決定的紊馏,那如何做到向后兼容呢?比如游戲客戶端發(fā)布了1.0版本蒲犬,消息id m1.0. 而后服務(wù)端注冊順序發(fā)生變化了朱监,這樣客戶端消息不是不兼容了嗎?每次都強制客戶端升級暖哨?
A:
說一下為什么選擇自動生成的方式:
- 自動生成的方式簡單不容易出錯
- 如果服務(wù)端消息發(fā)生改變客戶端不改變的情況很少
- Leaf 的自動生成方案可以支持消息兼容的問題
這里主要說一下如何做到服務(wù)器和客戶端消息版本不一致赌朋。消息注冊的順序決定了消息的 ID凰狞。保持兼容的幾種情況:
- 某個消息服務(wù)器不再使用的時候篇裁。保留消息注冊沛慢,不進行消息路由
- 服務(wù)器需要增加某個消息的時候。把消息注冊到最后的位置
- 這樣可以保證新的消息兼容舊的客戶端达布。
7.issues protobuf Register allows set message id
出于以下幾點原因团甲,這個修改將不被合并:
- 修改了接口,改變了對之前程序的兼容性:之前使用自動生成 ID 的程序不但服務(wù)器需要全部重新定義 ID黍聂,并且客戶端也要做相關(guān)的改動
- 設(shè)置 ID 的需求:一定要設(shè)置 ID 不是一個充分的應(yīng)該必須支持的需求躺苦,實際中,自動 ID 更加方便和可靠产还,容易使用匹厘,不容易出錯,完全的自動化(更多討論參考 issues)
- 內(nèi)部改為 map 而不是用數(shù)組的方式不符合性能最優(yōu)的思路:使用 protobuf 而不是 JSON 等其他編解碼方式脐区,在一個程度上表示了對性能的極度熱衷愈诚,ID 和消息的關(guān)聯(lián),使用數(shù)組給人更加舒服的心理感受(因為數(shù)組的訪問可以秒殺 map 的查找)