go leaf 从入坑到起飞

之所以使用go leaf是因为其轻便,开发效率高不高,都是看个人的,好不好用,也是看个人的,咱们不予以置评,开始干活。

关于go leaf的下载

https://github.com/name5566/leaf

框架介绍

go leaf的框架介绍,网上可以搜索,这里跳过.

重要的事情,不妨多讲几遍。以下是个人想强调的。

首先go leaf里有样东西特别重要,这样东西叫:Module。

而这个Module是个interface类型,包含以下三种行为

OnInit()
OnDestroy()
Run(closeSig chan bool)

而用户可以定义这些Module,而继承于leaf框架 *module.Skeleton的,则表明是依赖Skeleton配置信息,且支持模块间通信的。

type Module struct {
	*module.Skeleton
}

最后这些Module,需交由leaf去执行。也就是在main.go中被leaf.Run遍历执行的。

	leaf.Run(
		gate.Module,//路由
		login.Module,//登录
		game.Module,//游戏
	)

刚开始使用时,有人会好奇: 这么多模块, 模块之间是怎么协同运作的呢?

关键在于,总要有其中一个模块站出来为人民服务,大家才能和睦共处呀。

于是乎,gate站了出来,它的工作非常单一,就是分派协议。

例如:

func init() {
    //login文件生成的代码
    msg.ProcessorProto.SetRouter(&protobuf.Register{}, login.ChanRPC)
    msg.ProcessorProto.SetRouter(&protobuf.RegisterRsp{}, login.ChanRPC)
    msg.ProcessorProto.SetRouter(&protobuf.Login{}, login.ChanRPC)
    msg.ProcessorProto.SetRouter(&protobuf.LoginRsp{}, login.ChanRPC)
    msg.ProcessorProto.SetRouter(&protobuf.EnterRoom{}, game.ChanRPC)
}

而在gate的模块初始化时,需要注意的是AgentChanRPC,因为在底层leaf是会触发两个ChanPRC:"NewAgent"和"CloseAgent"


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.ProcessorProto, //消息处理器对象(proto|json)
		AgentChanRPC:    game.ChanRPC,//包含agent的一个chan
	}

}

而如何处理呢,是由game.ChanRPC来处理。(game的chanrpc.go)

func init() {
	skeleton.RegisterChanRPC("NewAgent", rpcNewAgent)
	skeleton.RegisterChanRPC("CloseAgent", rpcCloseAgent)
	skeleton.RegisterChanRPC("OffLine", rpcOfflineAgent)
	//清场
	//AsyncChan.Register("clearUp", func(args []interface{}) {
	//
	//	log.Debug("clearUp:%v", args)
	//	//table := args[0].(*Table)
	//
	//}) // 广播消息 调用参考:game.ChanRPC.Go("Broadcast", agent, args)
}

func rpcNewAgent(args []interface{}) {
	a := args[0].(gate.Agent) //【模块间通信】跟路由之间的通信
	//GetClientManger().Append(INVALID, a)
	_ = a

}

所以,go leaf的大体流程就出来了。

【协议派发】- 【绑定处理接口】- 【在接口内实现逻辑】

msg.ProcessorProto.SetRouter(&protoMsg.Register{}, login.ChanRPC)
skeleton.RegisterChanRPC(reflect.TypeOf(m), h)

func h(args []interface{}){

   ....//

}

开始实战:

...

以下仅是个人实战所用

障碍一:go leaf如何使用protobuf??

我这边使用的是protobuffer的3.7.0版本,

第一步:咱们在msg目录下,创建一个proto子目录,并添加一个login.proto文件。

syntax = "proto3";
package go;

/info//
//个人信息
message UserInfo{
  uint64 UserID = 1;      //ID
  string Name = 2;        //用户
  string Account = 3;     //帐号
  string Password = 4;    //密码
  uint32 FaceID = 5;      //头像
  uint32 Gender = 6;      //性别
  uint32 Age = 7;         //年龄
  uint32 VIP = 8;         //VIP级别
  uint32 Level = 9;       //级别
  int64  Money = 10;      //金钱(余额)
  string PassPortID = 11;   //证件号
  string RealName = 12;     //真实名字
  string PhoneNum = 13;     //手机
  string Email = 14;        //邮箱
  string Address = 15;      //住址
  string Identity = 16;     //识别码(平台生成)
  uint64 AgentID = 17;        //代理标识(上级代理人)
  string ReferralCode = 18;   //推荐标识(推荐码,由邀请码生成)
  string ClientAddr = 19;     //连接地址(当前实际IP)
  string ServerAddr = 20;     //(跳转至该地址 由登录服务返回的真实服务器地址)
  string MachineCode = 21;    //机器序列
}
//注册
message RegisterReq{
  string Name = 1;            //用户
  string Password = 2;        //密码
  string SecurityCode = 3;    //验证码
  string MachineCode = 4;     //机器码
  string InvitationCode = 5;  //邀请码
  uint64 PlatformID = 6;      //需要注明平台ID (测试用: id == 1)
  //选填
  uint32 Gender = 7;       //性别
  uint32 Age = 8;          //年龄
  uint32 FaceID = 9;       //头像
  string PassPortID = 10;  //证件号
  string RealName = 11;    //真实名字
  string PhoneNum = 12;    //手机
  string Email = 13;       //邮箱
  string Address = 14;     //住址
}
message RegisterResp{
  UserInfo Info = 1;
}

第二步:将*.proto文件转成*.go文件。

#写一个批处理专门将proto目录下的文件 转成 go文件。
syntax = "proto3";
package go;

 在与main.go同级目录下,新建一个tools目录存放脚本 以下是生成proto转go文件的脚本 其他python文件可以略过。

@echo OFF
chcp  65001
@echo "-----------fix package name(本地化)------------------"
rem py  .\amend.py
timeout 1
md ..\msg\go
@echo "-----------Proto-file(待处理)------------------"
echo _generate.bat path : %~dp0
rem dir    %~dp0\..\msg\proto\*.proto /B > list.txt              
REM '待处理的Proto文件'
for  /f  %%a  in  (list.txt)  do (
echo 正在转换 %%a  
protoc -I=%~dp0\..\msg\proto\ --go_out=..\msg\go %%a
echo 忙碌中...
)

@echo "------------Go-file(已生成)--------------------"
for /R "..\msg\go" %%s in (*.go) do (@echo "creating->file:%%s")

@echo "------------c++代码(协议注册)--------------------"
rem py  .\convertCpp.py

@echo "------------若无操作 3秒后自动退出--------------------"
timeout 3
Exit

 

第三步:已经有了go文件,接下来就是将协议注册到protobuf的解析器当中。咱们想法独特,所以就在msg.go里做文章

package msg

import (
	"github.com/golang/protobuf/proto"
	"github.com/name5566/leaf/network/json"
	"github.com/name5566/leaf/network/protobuf"
	protoMsg "server/msg/go"
	"sync"
)

// 使用默认的 JSON 消息处理器(默认还提供了 protobuf 消息处理器)
var ProcessorProto = protobuf.NewProcessor()

func init() {
	//这里的注册顺序,必须,必须,必须与【客户端】一致
    RegisterMessage(&protoMsg.PacketData{})

}

//对外接口 【这里的注册函数并非线程安全】
func RegisterMessage(message proto.Message) {
	ProcessorProto.Register(message)
	//log.Debug("reg ID:%v",ProcessorProto.Register(message))
}

 第四步,在gate/router.go里去指派需要处理的协议消息。 需要处理的协议,是指由客户端主动发起的协议。

package gate

import (
	"server/login"
	"server/msg"
	protoMsg "server/msg/go"
)

//路由模块分发消息【模块间使用 ChanRPC 通讯,消息路由也不例外】
//注:需要解析的结构体才进行路由分派,即用客户端主动发起的
func init() {
    //派给login模块进行处理
	msg.ProcessorProto.SetRouter(&protoMsg.PacketData{}, login.ChanRPC) //[proto]
}

第五步:在指定的模块内处理消息。

//login/internal/handler.go
package internal

import (
	"github.com/golang/protobuf/proto"
	"github.com/name5566/leaf/gate"
	"reflect"
	. "server/base"
	protoMsg "server/msg/go"
)



func init() {
	// 向当前模块(login 模块)注册 Login 消息的消息处理函数 handleTest
	register(&protoMsg.PacketData{}, handleMsg)      //反馈--->用户信息(由客户端反馈过来的)
}

//注册模块间的通信
func register(m interface{}, h interface{}) {
	skeleton.RegisterChanRPC(reflect.TypeOf(m), h)
}



//处理消息
func handleMsg(args []interface{}) {
	m := args[0].(*protoMsg.PacketData)
	//a := args[1].(gate.Agent)
	log.Debug("msg: %v psw:%v", m.GetMainID(), m.GetSubID())

}

protobuf到此,流程走通。

障碍二:如何接受或发送字节,针对客户端。 因为go leaf在说明文档里,已经清楚说明了,自己是怎样发送protobuf的字节的。

//以go语言作为客户端
//封装消息
func packageMsg(message proto.Message) []byte{
	//
	data, _ := msg.ProcessorProto.Marshal(message)
	//...此处可先进行数据加密
	// len +id + data
	m := make([]byte, 4+len(data[1]))

	// 默认使用大端序
	binary.BigEndian.PutUint16(m, uint16(2+len(data[1])))

	//两个字节+数据
	copy(m[2:], data[0])
	copy(m[4:], data[1])

	return m
}

障碍三:protobuf 协议文件 更新后,导致客户端必须强制同步更新的问题?

由于go leaf的 消息注册机制 是根据proto文件里的message书写顺序,自动生成的。一旦生成,即会给消息体分配固定ID。

即message a{} 一旦在msg.go的

ProcessorProto.Register(message)//此处的返回值就是消息的ID

注册了,则底层的ID便固定下来,不容更改了。

所以,废弃的message一旦正式版发布后,不容废弃,最多把message的字段给删除。而且,新增的message必须以追加形式放在init()末。

msg.go里的
init(

    //'''
    //'''
RegisterMessage(&protoMsg.GameOverResp{})//新增
)

这样会引起文件体积膨胀。

如需彻底解决此问题,可修改注册函数为Register(id int32, msg message),提供主动设置msgID的接口。做一张映射表,记录所有历史的消息ID。新增时,在历史最大值中加一,并注册到ProcessorProto。

接下来,就是各位大侠按照业务逻辑实现需求了。

基本思路就是

1、接口类(如对战类、百人、益智类等等)

2、实现管理类(如客户端连接管理、玩家管理、子游戏管理等等)

3、实现数据库功能(用户注册、金币增减等等)

待更新...

如有疑问,欢迎留言。


版权声明:本文为po2656233原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。