Golang 游戏leaf系列(二) 网络消息流程概述

栏目: Go · 发布时间: 4年前

内容简介:最开始接触到Leaf,就是被它的网络消息功能吸引的。那么先看看这部分功能吧。从文档中得知:Leaf 可以单独使用 TCP 协议或 WebSocket 协议,也可以同时使用两者,换而言之,服务器可以同时接受 TCP 连接和 WebSocket 连接,对开发者而言消息来自 TCP 还是 WebSocket 是完全透明的。这个功能在源码中是如何实现的呢,看看network目录下tcp开头的,和ws开头的,有xx_conn,xx_msg,xx_server,正好各有3个文件。在conn.go里有个Conn inte

最开始接触到Leaf,就是被它的网络消息功能吸引的。那么先看看这部分功能吧。从文档中得知:

Leaf 可以单独使用 TCP 协议或 WebSocket 协议,也可以同时使用两者,换而言之,服务器可以同时接受 TCP 连接和 WebSocket 连接,对开发者而言消息来自 TCP 还是 WebSocket 是完全透明的。

一、network和gate

这个功能在源码中是如何实现的呢,看看network目录下tcp开头的,和ws开头的,有xx_conn,xx_msg,xx_server,正好各有3个文件。在conn.go里有个Conn interface,所以xx_conn肯定是实现这个接口的两个不同类型。按照这个思路,顺便看一下processor.go里的解析器接口,也是有json.go和protobuf.go两种实现。

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里,会有一个agent 结构体来实现Agent接口。除了Agent接口中的方法,agent还实现了Run方法和OnClose方法。

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

这个结构体又引入了一个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
}

看起来有一些配置参数,还有一个数据解析器Processor,和AgentChanRPC *chanrpc.Server,看一下怎么用的吧。

Gate只有两个方法,OnDestroy目前是空的,还有一个是Run,不出意外的话,应该是解析那些配置参数,启动服务:

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()
    }
}

这里启动了两种不同类型的Server,closeSig那个暂时忽略不说。Server使用NewAgent回调,把gate传走了,呃,有点懵逼。还是回到官方例子中看看整个使用流程吧

二、 官方例子 LeafServer 中的Module

1.Module初始化

首先在main.go中

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

这里gate.Module实际上是由gate包里的external.go暴露出来的(这也是leaf的使用习惯,所有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,
    }
}

匿名结构体Gate了,又额外实现一个OnInit方法,感觉像是有一个IModule这样的接口呢,找一找:

在源码的module.go中,确实找到了:

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

结合上面第一部分说的Gate实现了OnDestroy和Run方法,官方例子中的gate/external.go确是实现了Module接口。注意其OnInit中,除了一堆属性从conf配置中读取,还引入了msg.Processor,这明显是个网络消息解析器。然后game.ChanRPC,这看起来是转到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是怎么运行起来的

再次回到源码module.go,节选一部分代码过来

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 首先会在同一个 goroutine 中按模块注册顺序执行模块的 OnInit 方法,等到所有模块 OnInit 方法执行完成后则为每一个模块启动一个 goroutine 并执行模块的 Run 方法。最后,游戏服务器关闭时(Ctrl + C 关闭游戏服务器)将按模块注册相反顺序在同一个 goroutine 中执行模块的 OnDestroy 方法。

三、综述

1.流程

现在来理一理思路。从main.go里开始,leaf.Run注册并运行了game,gate,login三个module。重点关注gate这个module,这个module通过组合方式实现了Module接口,即自己项目里实现OnInit方法,通过匿名结构体gate.Gate在源码里实现OnDestroy和Run方法。其中,OnInit方法里把gate.Gate制造出来了,部分属性读取conf的配置,Processor指定成自己项目的消息解析,AgentChanRPC指定了自己项目里的game模块。

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

然后按照流程继续走,gate模块的OnInit执行完,就要去执行Run了。这个方法在本文第一部分就看过了,当时卡在一个懵逼的地方:

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
}

现在有点感觉了吧,也就是说tcpServer执行NewAgent时,看单词名字意思是一个新连接事件发生时,实际会转交给gate.AgentChanRPC去执行,也就是例子中的game.ChanRPC。转交方式是 .Go("NewAgent", a) ,就像抛出一个事件一样,有一个名称,有一个参数。可以去game模块的chanrpc.go验证一下

//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什么时候执行NewAgent

在gate的Run方法中,提到了tcpServer会根据参数生成并运行

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

然后去tcp_server.go看一下

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

init和run细节有点多,先忽略掉吧。我们是来找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()
}()

首先这段代码是在一个for循环中的,也就是收到tcp消息时,才会执行。具体基础知识参考 Golang socket websocket 。agent在拿到具体的tcpConn,会执行自己的Run方法,回到源码gate.go的agent结构体可以看到:

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)
    ...
}

开始使用相应的Processor去读取数据了。

本篇暂时先到这里,还有许多细节,留待后续系列慢慢深究。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

并行算法的设计与分析

并行算法的设计与分析

陈国良 / 2009-8 / 66.00元

第3版在修订版的基础上进行了大幅度的修订,新增加3章、重写3章,改写8章。《普通高等教育十一五国家级规划教材·并行算法的设计与分析(第3版)》系统深入地讨论了计算机领域中诸多计算问题的并行算法的设计和分析方法。在着重介绍各种并行计算模型上的常用和典型的并行算法的同时,也力图反映本学科的最新成就、学科前沿和发展趋势。 全书共分二十章,包括基础篇4章(绪论、设计技术、前缀计算、排序和选择网络),......一起来看看 《并行算法的设计与分析》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器