go leaf 从入坑到起飞

1 篇文章 0 订阅
1 篇文章 0 订阅

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

关于go leaf的下载

GitHub - name5566/leaf: A game server framework in Go (golang)

框架介绍

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

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

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

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

OnInit()     //初始化
OnDestroy()  //销毁
Run(closeSig chan bool) //运行,接收关闭信号

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

// 自定义的Module 继承于module.Skeleton
type Module struct {
	*module.Skeleton
}

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

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

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

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

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

例如:

前提有一份login.proto的协议文件

//login.proto文件
//注册请求
message RegisterReq{
  string Name = 1;            //用户
  string Password = 2;        //密码
}
//注册成功的结果反馈
message RegisterResp{
  uint32 Success = 1;         //0:失败 1:成功
}



//msg.go文件 主要是注册协议用的
import (
	"github.com/golang/protobuf/proto"
	//"github.com/name5566/leaf/network/json"
	"github.com/name5566/leaf/network/protobuf"
	protoMsg "server/msg/go" //protobuff的go代码
)
var ProcessorProto = protobuf.NewProcessor()
func init() {
    //将login.proto文件中的协议注册进来
	ProcessorProto.Register(&protoMsg.RegisterReq{})
	ProcessorProto.Register(&protoMsg.RegisterResp{})
}

//gate.go文件 主要是派发协议
func init() {
    //派发至login包内的ChanRPC进行处理
    msg.ProcessorProto.SetRouter(&protobuf.RegisterReq{}, login.ChanRPC)
    msg.ProcessorProto.SetRouter(&protobuf.RegisterRsp{}, login.ChanRPC)
}


//login包下面的external.go文件 ChanRPC的定义,其实它是来源于子包internal的ChanRPC
//这个协程是一个skeleton.ChanRPCServer,顾名思义就是RPC服务的的协程
var (
	Module  = new(internal.Module)
	ChanRPC = internal.ChanRPC
)


那么这个协程里到底是要执行什么,怎么去执行呢?
执行什么由用户说了算,于是在login的子包internal下新建一个处理文件handler.go

// internal下新建handler.go
func init() {
	// handleRegister是服务端对接收到的RegisterReq协议后的处理函数
	handleMsg(&protoMsg.RegisterReq{}, handleRegister) 
}
func handleMsg(m interface{}, h interface{}) {
	skeleton.RegisterChanRPC(reflect.TypeOf(m), h)
}
//注册请求的处理
func handleRegister(args []interface{}) {
    m := args[0].(*protoMsg.RegisterReq)
	a := args[1].(gate.Agent)
    result := .... //略过 (数据库查下该用户是否存在作为是否注册成功的依据)
    // 反馈注册结果
    a.WriteMsg(&protoMsg.RegisterResp{
			Success: result,
	})
}
到此,逻辑梳理结束

而在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、实现数据库功能(用户注册、金币增减等等)

待更新...

如有疑问,欢迎留言。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值