Golang 游戲leaf系列(一) 概述與示例

基礎(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 包中)
image.png

每個模塊下一般有一個 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

在 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 的查找)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末牛隅,一起剝皮案震驚了整個濱河市炕柔,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌媒佣,老刑警劉巖匕累,帶你破解...
    沈念sama閱讀 217,509評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異默伍,居然都是意外死亡欢嘿,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評論 3 394
  • 文/潘曉璐 我一進店門也糊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來炼蹦,“玉大人,你說我怎么就攤上這事显设】虺冢” “怎么了?”我有些...
    開封第一講書人閱讀 163,875評論 0 354
  • 文/不壞的土叔 我叫張陵捕捂,是天一觀的道長瑟枫。 經(jīng)常有香客問我,道長指攒,這世上最難降的妖魔是什么慷妙? 我笑而不...
    開封第一講書人閱讀 58,441評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮允悦,結(jié)果婚禮上膝擂,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好架馋,可當我...
    茶點故事閱讀 67,488評論 6 392
  • 文/花漫 我一把揭開白布狞山。 她就那樣靜靜地躺著,像睡著了一般叉寂。 火紅的嫁衣襯著肌膚如雪萍启。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,365評論 1 302
  • 那天屏鳍,我揣著相機與錄音勘纯,去河邊找鬼。 笑死钓瞭,一個胖子當著我的面吹牛驳遵,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播山涡,決...
    沈念sama閱讀 40,190評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼堤结,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了佳鳖?” 一聲冷哼從身側(cè)響起霍殴,我...
    開封第一講書人閱讀 39,062評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎系吩,沒想到半個月后来庭,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,500評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡穿挨,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,706評論 3 335
  • 正文 我和宋清朗相戀三年月弛,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片科盛。...
    茶點故事閱讀 39,834評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡帽衙,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出贞绵,到底是詐尸還是另有隱情厉萝,我是刑警寧澤,帶...
    沈念sama閱讀 35,559評論 5 345
  • 正文 年R本政府宣布榨崩,位于F島的核電站谴垫,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏母蛛。R本人自食惡果不足惜翩剪,卻給世界環(huán)境...
    茶點故事閱讀 41,167評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望彩郊。 院中可真熱鬧前弯,春花似錦蚪缀、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至剃根,卻和暖如春哩盲,著一層夾襖步出監(jiān)牢的瞬間前方,已是汗流浹背狈醉。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留惠险,地道東北人苗傅。 一個月前我還...
    沈念sama閱讀 47,958評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像班巩,于是被迫代替她去往敵國和親渣慕。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,779評論 2 354

推薦閱讀更多精彩內(nèi)容

  • 自用收藏 原文:http://www.th7.cn/Program/IOS/201606/884245.shtml...
    西瓜皮奧特曼閱讀 2,183評論 0 16
  • 1.圖片瀏覽控件MWPhotoBrowser 實現(xiàn)了一個照片瀏覽器類似 iOS 自帶的相冊應(yīng)用抱慌,可顯示來自手機的圖...
    萬忍閱讀 1,500評論 0 6
  • 我知道 我們都想要牽了手就結(jié)婚的愛情抑进, 卻活在上了床也不一定有結(jié)果的年代强经。 可是我仍然希望不管男孩女孩,請你們一定...
    傾安閱讀 267評論 3 11
  • 10月7號寺渗,也就是昨天晚上匿情,許久不曾聯(lián)系過的初中同學,突然給我發(fā)來了信息信殊。她發(fā)了自己的微信號圖片炬称,要我加她,但我并...
    簡_小潔閱讀 164評論 0 0
  • 什么也沒有綁定涡拘,卻從沒想過簡書上的內(nèi)容會有人看玲躯,本就是想寫自己,學著梳理生活鳄乏, 目前還過得一片混亂跷车,一塌糊涂的自己...
    觀照生活閱讀 139評論 0 0