golang游戏服务器项目流程,Golang 游戏leaf系列(二) 网络消息流程概述

最开始接触到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()

}

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去读取数据了。

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值