Golang 游戲leaf系列(二) 網(wǎng)絡(luò)消息流程概述

最開始接觸到Leaf颜武,就是被它的網(wǎng)絡(luò)消息功能吸引的璃搜。那么先看看這部分功能吧。從文檔中得知:

Leaf 可以單獨(dú)使用 TCP 協(xié)議或 WebSocket 協(xié)議鳞上,也可以同時(shí)使用兩者这吻,換而言之,服務(wù)器可以同時(shí)接受 TCP 連接和 WebSocket 連接篙议,對開發(fā)者而言消息來自 TCP 還是 WebSocket 是完全透明的唾糯。

一、network和gate

這個(gè)功能在源碼中是如何實(shí)現(xiàn)的呢,看看network目錄下tcp開頭的移怯,和ws開頭的拒名,有xx_conn,xx_msg,xx_server,正好各有3個(gè)文件芋酌。在conn.go里有個(gè)Conn interface,所以xx_conn肯定是實(shí)現(xiàn)這個(gè)接口的兩個(gè)不同類型雁佳。按照這個(gè)思路脐帝,順便看一下processor.go里的解析器接口,也是有json.go和protobuf.go兩種實(shí)現(xiàn)糖权。

type Conn interface {
    ReadMsg() ([]byte, error)
    WriteMsg(args ...[]byte) error
    LocalAddr() net.Addr
    RemoteAddr() net.Addr
    Close()
    Destroy()
}
1.gate目錄

然后xx_conn這兩種連接方式堵腹,要對外透明,是封裝在gate包下面星澳,一起使用的疚顷。先看一下agent.go:

type Agent interface {
    WriteMsg(msg interface{})
    LocalAddr() net.Addr
    RemoteAddr() net.Addr
    Close()
    Destroy()
    UserData() interface{}
    SetUserData(data interface{})
}

在gate.go里,會(huì)有一個(gè)agent 結(jié)構(gòu)體來實(shí)現(xiàn)Agent接口禁偎。除了Agent接口中的方法腿堤,agent還實(shí)現(xiàn)了Run方法和OnClose方法。

type agent struct {
    conn     network.Conn
    gate     *Gate
    userData interface{}
}

這個(gè)結(jié)構(gòu)體又引入了一個(gè)Gate如暖,這是啥笆檀?在gate.go里也能找到:

type Gate struct {
    MaxConnNum      int
    PendingWriteNum int
    MaxMsgLen       uint32
    Processor       network.Processor
    AgentChanRPC    *chanrpc.Server

    // websocket
    WSAddr      string
    HTTPTimeout time.Duration
    CertFile    string
    KeyFile     string

    // tcp
    TCPAddr      string
    LenMsgLen    int
    LittleEndian bool
}

看起來有一些配置參數(shù),還有一個(gè)數(shù)據(jù)解析器Processor盒至,和AgentChanRPC *chanrpc.Server酗洒,看一下怎么用的吧。
Gate只有兩個(gè)方法枷遂,OnDestroy目前是空的樱衷,還有一個(gè)是Run,不出意外的話酒唉,應(yīng)該是解析那些配置參數(shù)矩桂,啟動(dòng)服務(wù):

func (gate *Gate) Run(closeSig chan bool) {
    var wsServer *network.WSServer
    if gate.WSAddr != "" {
        wsServer = new(network.WSServer)
        wsServer.Addr = gate.WSAddr
        wsServer.MaxConnNum = gate.MaxConnNum
        wsServer.PendingWriteNum = gate.PendingWriteNum
        wsServer.MaxMsgLen = gate.MaxMsgLen
        wsServer.HTTPTimeout = gate.HTTPTimeout
        wsServer.CertFile = gate.CertFile
        wsServer.KeyFile = gate.KeyFile
        wsServer.NewAgent = func(conn *network.WSConn) network.Agent {
            a := &agent{conn: conn, gate: gate}
            if gate.AgentChanRPC != nil {
                gate.AgentChanRPC.Go("NewAgent", a)
            }
            return a
        }
    }

    var tcpServer *network.TCPServer
    if gate.TCPAddr != "" {
        tcpServer = new(network.TCPServer)
        tcpServer.Addr = gate.TCPAddr
        tcpServer.MaxConnNum = gate.MaxConnNum
        tcpServer.PendingWriteNum = gate.PendingWriteNum
        tcpServer.LenMsgLen = gate.LenMsgLen
        tcpServer.MaxMsgLen = gate.MaxMsgLen
        tcpServer.LittleEndian = gate.LittleEndian
        tcpServer.NewAgent = func(conn *network.TCPConn) network.Agent {
            a := &agent{conn: conn, gate: gate}
            if gate.AgentChanRPC != nil {
                gate.AgentChanRPC.Go("NewAgent", a)
            }
            return a
        }
    }

    if wsServer != nil {
        wsServer.Start()
    }
    if tcpServer != nil {
        tcpServer.Start()
    }
    <-closeSig
    if wsServer != nil {
        wsServer.Close()
    }
    if tcpServer != nil {
        tcpServer.Close()
    }
}

這里啟動(dòng)了兩種不同類型的Server,closeSig那個(gè)暫時(shí)忽略不說。Server使用NewAgent回調(diào)黔州,把gate傳走了耍鬓,呃,有點(diǎn)懵逼流妻。還是回到官方例子中看看整個(gè)使用流程吧

二牲蜀、 官方例子LeafServer中的Module
1.Module初始化

首先在main.go中

    leaf.Run(
        game.Module,
        gate.Module,
        login.Module,
    )

這里gate.Module實(shí)際上是由gate包里的external.go暴露出來的(這也是leaf的使用習(xí)慣,所有module都這樣暴露)绅这。

//src/server/gate/external.go
type Module struct {
    *gate.Gate
}

func (m *Module) OnInit() {
    m.Gate = &gate.Gate{
        MaxConnNum:      conf.Server.MaxConnNum,
        PendingWriteNum: conf.PendingWriteNum,
        MaxMsgLen:       conf.MaxMsgLen,
        WSAddr:          conf.Server.WSAddr,
        HTTPTimeout:     conf.HTTPTimeout,
        CertFile:        conf.Server.CertFile,
        KeyFile:         conf.Server.KeyFile,
        TCPAddr:         conf.Server.TCPAddr,
        LenMsgLen:       conf.LenMsgLen,
        LittleEndian:    conf.LittleEndian,
        Processor:       msg.Processor,
        AgentChanRPC:    game.ChanRPC,
    }
}

匿名結(jié)構(gòu)體Gate了涣达,又額外實(shí)現(xiàn)一個(gè)OnInit方法,感覺像是有一個(gè)IModule這樣的接口呢,找一找:
在源碼的module.go中度苔,確實(shí)找到了:

type Module interface {
    OnInit()
    OnDestroy()
    Run(closeSig chan bool)
}

結(jié)合上面第一部分說的Gate實(shí)現(xiàn)了OnDestroy和Run方法匆篓,官方例子中的gate/external.go確是實(shí)現(xiàn)了Module接口。注意其OnInit中寇窑,除了一堆屬性從conf配置中讀取鸦概,還引入了msg.Processor,這明顯是個(gè)網(wǎng)絡(luò)消息解析器甩骏。然后game.ChanRPC窗市,這看起來是轉(zhuǎn)到game模塊去了,所以在一開始main.go中的leaf.Run中饮笛,也是先傳入的game.Module咨察,然后才是gate.Module。

//leaf.go
func Run(mods ...module.Module) {
    ...

    log.Release("Leaf %v starting up", version)

    // module
    for i := 0; i < len(mods); i++ {
        module.Register(mods[i])
    }
    module.Init()
    ...
2.module是怎么運(yùn)行起來的

再次回到源碼module.go福青,節(jié)選一部分代碼過來

type module struct {
    mi       Module
    closeSig chan bool
    wg       sync.WaitGroup
}

var mods []*module

func Register(mi Module) {
    m := new(module)
    m.mi = mi
    m.closeSig = make(chan bool, 1)

    mods = append(mods, m)
}

func Init() {
    for i := 0; i < len(mods); i++ {
        mods[i].mi.OnInit()
    }

    for i := 0; i < len(mods); i++ {
        m := mods[i]
        m.wg.Add(1)
        go run(m)
    }
}

func run(m *module) {
    m.mi.Run(m.closeSig)
    m.wg.Done()
}

看到這些摄狱,是不是想起來官方文檔說的這段話:

Leaf 首先會(huì)在同一個(gè) goroutine 中按模塊注冊順序執(zhí)行模塊的 OnInit 方法,等到所有模塊 OnInit 方法執(zhí)行完成后則為每一個(gè)模塊啟動(dòng)一個(gè) goroutine 并執(zhí)行模塊的 Run 方法无午。最后媒役,游戲服務(wù)器關(guān)閉時(shí)(Ctrl + C 關(guān)閉游戲服務(wù)器)將按模塊注冊相反順序在同一個(gè) goroutine 中執(zhí)行模塊的 OnDestroy 方法。

三宪迟、綜述
1.流程

現(xiàn)在來理一理思路刊愚。從main.go里開始,leaf.Run注冊并運(yùn)行了game,gate,login三個(gè)module踩验。重點(diǎn)關(guān)注gate這個(gè)module鸥诽,這個(gè)module通過組合方式實(shí)現(xiàn)了Module接口,即自己項(xiàng)目里實(shí)現(xiàn)OnInit方法箕憾,通過匿名結(jié)構(gòu)體gate.Gate在源碼里實(shí)現(xiàn)OnDestroy和Run方法牡借。其中,OnInit方法里把gate.Gate制造出來了袭异,部分屬性讀取conf的配置钠龙,Processor指定成自己項(xiàng)目的消息解析,AgentChanRPC指定了自己項(xiàng)目里的game模塊御铃。

...
Processor:       msg.Processor,
AgentChanRPC:    game.ChanRPC,
...

然后按照流程繼續(xù)走碴里,gate模塊的OnInit執(zhí)行完,就要去執(zhí)行Run了上真。這個(gè)方法在本文第一部分就看過了咬腋,當(dāng)時(shí)卡在一個(gè)懵逼的地方:

tcpServer.NewAgent = func(conn *network.TCPConn) network.Agent {
    a := &agent{conn: conn, gate: gate}
    if gate.AgentChanRPC != nil {
        gate.AgentChanRPC.Go("NewAgent", a)
    }
    return a
}

現(xiàn)在有點(diǎn)感覺了吧,也就是說tcpServer執(zhí)行NewAgent時(shí)睡互,看單詞名字意思是一個(gè)新連接事件發(fā)生時(shí)根竿,實(shí)際會(huì)轉(zhuǎn)交給gate.AgentChanRPC去執(zhí)行陵像,也就是例子中的game.ChanRPC。轉(zhuǎn)交方式是.Go("NewAgent", a)寇壳,就像拋出一個(gè)事件一樣醒颖,有一個(gè)名稱,有一個(gè)參數(shù)壳炎∨⑶福可以去game模塊的chanrpc.go驗(yàn)證一下

//game.internal.chanrpc.go
func init() {
    skeleton.RegisterChanRPC("NewAgent", rpcNewAgent)
    skeleton.RegisterChanRPC("CloseAgent", rpcCloseAgent)
}

func rpcNewAgent(args []interface{}) {
    a := args[0].(gate.Agent)
    _ = a
}

func rpcCloseAgent(args []interface{}) {
    a := args[0].(gate.Agent)
    _ = a
}
2.tcpServer什么時(shí)候執(zhí)行NewAgent

在gate的Run方法中,提到了tcpServer會(huì)根據(jù)參數(shù)生成并運(yùn)行

...
if tcpServer != nil {
    tcpServer.Start()
}
...

然后去tcp_server.go看一下

func (server *TCPServer) Start() {
    server.init()
    go server.run()
}

init和run細(xì)節(jié)有點(diǎn)多匿辩,先忽略掉吧疏日。我們是來找NewAgent的,終于在run中找到了:

...
tcpConn := newTCPConn(conn, server.PendingWriteNum, server.msgParser)
agent := server.NewAgent(tcpConn)
go func() {
    agent.Run()

    // cleanup
    tcpConn.Close()
    server.mutexConns.Lock()
    delete(server.conns, conn)
    server.mutexConns.Unlock()
    agent.OnClose()

    server.wgConns.Done()
}()

首先這段代碼是在一個(gè)for循環(huán)中的撒汉,也就是收到tcp消息時(shí),才會(huì)執(zhí)行涕滋。具體基礎(chǔ)知識(shí)參考Golang socket websocket睬辐。agent在拿到具體的tcpConn,會(huì)執(zhí)行自己的Run方法宾肺,回到源碼gate.go的agent結(jié)構(gòu)體可以看到:

type agent struct {
    conn     network.Conn
    gate     *Gate
    userData interface{}
}

func (a *agent) Run() {
    for {
        data, err := a.conn.ReadMsg()
        if err != nil {
            log.Debug("read message: %v", err)
            break
        }

        if a.gate.Processor != nil {
            msg, err := a.gate.Processor.Unmarshal(data)
    ...
}

開始使用相應(yīng)的Processor去讀取數(shù)據(jù)了溯饵。

本篇暫時(shí)先到這里,還有許多細(xì)節(jié)锨用,留待后續(xù)系列慢慢深究丰刊。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市增拥,隨后出現(xiàn)的幾起案子啄巧,更是在濱河造成了極大的恐慌,老刑警劉巖掌栅,帶你破解...
    沈念sama閱讀 219,490評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件秩仆,死亡現(xiàn)場離奇詭異,居然都是意外死亡猾封,警方通過查閱死者的電腦和手機(jī)澄耍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來晌缘,“玉大人齐莲,你說我怎么就攤上這事×谆” “怎么了选酗?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長岳枷。 經(jīng)常有香客問我星掰,道長多望,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任氢烘,我火速辦了婚禮怀偷,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘播玖。我一直安慰自己椎工,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布蜀踏。 她就那樣靜靜地躺著维蒙,像睡著了一般。 火紅的嫁衣襯著肌膚如雪果覆。 梳的紋絲不亂的頭發(fā)上颅痊,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天,我揣著相機(jī)與錄音局待,去河邊找鬼斑响。 笑死,一個(gè)胖子當(dāng)著我的面吹牛钳榨,可吹牛的內(nèi)容都是我干的舰罚。 我是一名探鬼主播,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼薛耻,長吁一口氣:“原來是場噩夢啊……” “哼营罢!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起饼齿,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤饲漾,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后缕溉,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體能颁,經(jīng)...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年倒淫,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了伙菊。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,137評論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡敌土,死狀恐怖镜硕,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情返干,我是刑警寧澤兴枯,帶...
    沈念sama閱讀 35,819評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站矩欠,受9級(jí)特大地震影響财剖,放射性物質(zhì)發(fā)生泄漏悠夯。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,482評論 3 331
  • 文/蒙蒙 一躺坟、第九天 我趴在偏房一處隱蔽的房頂上張望沦补。 院中可真熱鬧,春花似錦咪橙、人聲如沸夕膀。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽产舞。三九已至,卻和暖如春菠剩,著一層夾襖步出監(jiān)牢的瞬間易猫,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評論 1 272
  • 我被黑心中介騙來泰國打工具壮, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留准颓,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,409評論 3 373
  • 正文 我出身青樓嘴办,卻偏偏與公主長得像,于是被迫代替她去往敵國和親买鸽。 傳聞我的和親對象是個(gè)殘疾皇子涧郊,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,086評論 2 355