之所以使用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、实现数据库功能(用户注册、金币增减等等)
待更新...
如有疑问,欢迎留言。