[前言]
关于如何使用生成proto的exe工具, 参考我的另一篇文章:
https://blog.csdn.net/liuyongjie1992/article/details/115185967
我发现生成lua版本的proto和生成C#版的调用方式不太一样,因此开一片文章记录一下
新建一个bat文件执行这一句话即可调用lua的生成工具
.\protoclient.exe --proto_path=./proto --lua_out=./genpath ./proto/TestProto.proto
.\protoclient.exe是调用当前目录的protoclient.exe文件
--proto_path=./proto是proto的文件夹,请注意是文件夹路径
--lua_out=./genpath 输出后的proto路径
./proto/TestProto.proto找到我们要输入的proto
上述操作不出意外可以顺利生成TestProto_pb.lua文件
很显然lua和C#的exe输入的语句是不一样的
C#版本的执行语句为
.\protogen.exe -i:xxx.proto -o:.\genpath\xxx.cs
[Lua版本的pb使用方法]
以上文的TestMessage.proto为例
生成的lua版pb长这样子
下面这句话是业务代码如何使用Message,项目里各种UI需要向服务器请求数据展示自己的内容,TestProto_pb.TestMessage()就是消息体,表示你想请求哪个内容。
--这是一个lua函数
function HowUseProtoMessage()
--提示:在使用TestProto_pb之前别忘了require,下文会介绍如何在项目中批处理多个require
local msg = TestProto_pb.TestMessage()
msg.id = 1;
msg.name = "测试message"
--!!!这句话很关键,把一个message序列化成string
local NetString = msg:SerializeToString();
--如果你想把这个msg发给服务器,那么就把这个string发过去
NetMgr.SendMsg(NetString)
end
下面开始进阶版 lua pb的使用
项目中我们通常会给Proto里面的mssage加上id,这样前后端通信的时候就知道你传的到底是个毛线,从而方便分发,添加id的方式有两种。提示!!!:无论protoid是3个数的组合还是1个数,这个protoid都是批处理工具按照一定规则生成出来的枚举,不要手动去添加这个id。
我们给proto加上一个枚举用来做标识,第一种方式是mrid,groupid,unitid三个数的组合,第二种是protoid一个数的。
前后端在通信的时候可以从包头中提取出 第一种或者第二种 id从而得出该数据包要用哪个message解析。
【项目启动时注册消息回调】
下面的Reg函数是在项目启动时调用,所有业务都要根据protoid记录消息体、回调函数、错误处理函数。
--[[注册消息处理函数
proto 消息类,示例Module_XXX_pb.XX
handler 处理函数
errorHandler 错误处理函数(错误码不为0时会调用)
--]]
function Reg(proto, handler, errorHandler)
local mid = proto.protoid -- proto.MRID;
if mMsgCallBacks[mid] then
local callBack = mMsgCallBacks[mid];
local callBackMeta = getmetatable(callBack.proto());
local newCallBackMeta = getmetatable(proto());
GameLog.LogModuleError("msg handler repeat %s %s %d", callBackMeta._descriptor.name, newCallBackMeta._descriptor.name, mid);
else
local callBack = {};
callBack.proto = proto;
callBack.handler = handler;
callBack.errorHandler = errorHandler;
mMsgCallBacks[mid] = callBack;
end
end
可以在项目启动时,调用一个统一的lua文件把所有业务的回调全部注册。
--所有需要接受服务器通讯的协议都要注册哦,下面这个注册只是举个例子
GameNet.Reg(Module_Item_pb.SCItemGetDataRe, ItemMgr.OnGetItemData)
上面注册完了协议回调,那么客户端就等待服务器数据啦。
下面的代码就是收到服务器数据后,C#层把数据传递给lua,请注意,lua不支持byte[]数组,因此数据流会被转化成LuaByte,最终就是个string。
--参数解释:
--参数1:protoid是我们自定义的id
--参数2:data是C#层接收到的byte[]数组转换成的string
function OnRecvMsg(protoid, data)
if mMsgCallBacks[protoid] then
local callBack = mMsgCallBacks[protoid]; -- mMsgCallBacks是什么在上文注册中已讲明
local msg = callBack.proto();--proto()拿到消息体,此时msg是个空消息体
if msg == nil then
GameLog.LogModuleError(NetModule.LOG_TAG,"Failed to OnRecvMsg, cannot find proto for protoid= %d",protoid);
end
local flag, errorMsg = xpcall(msg.ParseFromString, traceback, msg, data);
--调用了ParseFromString后,上文的msg不再是空消息体啦,服务器数据已经序列化进去了
if flag then--没有错误
if callBack.handler then
--xpcall调用回调函数,并把消息体当做参数传入该函数,该msg就是服务发过来的message
local flag, errorMsg = xpcall(callBack.handler, traceback, msg);
if not flag then
OnRecvFail(tid, errorMsg, msg);
end
else
OnRecvFail(tid, "handler is nil", msg);
end
end
end
end
下面是UI业务层随便一个小例子
-- 下面是UI层业务接受proto消息体的回调函数,msg就是proto里的message
function OnGetItemData(msg)
local id = msg.id
local name = msg.name
--数据拿到,下面开始刷UI啦
end
本篇文章到此,lua版pb的使用教程已经完成啦。但是!!!还没有说明protoid的生成规则以及pb文件的批处理require
[protoid]批处理生成
protoid的枚举值不要自己生成,这个前文有提到过
先给大家展示一下我们项目的proto,这么多proto如果要手动添加一个唯一的id显然是不可能的。
[生成protoid的本质]
朋友们!生成protoid的本质是什么,其实就是在message里加一个枚举,这样的好处是,当我们拿到这个消息体时,直接通过 TestMessage.protoid就可以拿到这个枚举值。建议大家再回到上面的文章看一下消息回调注册的地方,加深理解。
如何在原有proto的基础上,加上一个枚举?这就很简单啦,直接用批处理工具写入这句话就好
下面就开始介绍如何批处理写入枚举值,我个人首选python写批处理,真的好用,python工程怎么创建以及环境部署看我的另一篇文章:
https://blog.csdn.net/liuyongjie1992/article/details/112378661
顺便安利一下这个系列的文章,Python版本的打表工具
下面开始讲生成protoid的核心思路:
1:哪些message不需要生成protoid?经常用proto的人都应该知道,很多被引用的message是用来做数据包体,而不是消息包体,我们批处理工具里自己定义了一些头标识,当这个message名前两个字母带着两个大写字母且匹配,那么就给改message生成protoid
msgtype = {
'CS':1,
'SC':1,
'SS':1,
'SD':1,
'DS':1,
'CF':1,
'FC':1,
'SF':1,
'FF':1,
'RR':1,
'RC':1,
'CO':1,
'SO':1,
'FO':1,
'OO':1,
'WS':1,
'TS':1,
'TT':1,
'ST':1,
'SW':1,
'CT':1,
'WC':1,
'OC':1,
'OS':1,
'FS':1,
'TF':1,
'WF':1,
'FT':1,
'CW':1,
'TC':1,
}
这是我们项目中的战斗proto为例,可以看到该message是以SC开头的,那么该message就要生成protoid,而该message内的CampData是一个引用数据体,CampData没有以我们定义的message头开始,那么该message就不会生成protoid
//[返回][游戏服]进入战斗结果
message SCEnterBattle
{
optional int64 battleId = 1;
repeated CampData camps = 2; // 战斗阵营数据 (仅站位和card_sid)
optional int32 totalChapterNum = 3; // 回目数量
optional int32 battleSid = 4; // 战役配置id
repeated FightPlayerInfo playerInfo = 5;
optional int32 result = 6; // 结果 0:成功 其他:返回对应错误码
optional int32 isReady = 7; // 0没准备 1已准备 (用于战前断线重连)
optional int32 isFixedArray = 8; // 是否固定阵容0不是 1是
optional int64 bettleBeginTime = 9; // 0 不限制, > 0 战斗开始时间
}
//阵营数据
message CampData
{
optional int32 id = 1; //阵营id
optional int32 campType = 2; //阵营类型 0:玩家 1:怪物 2:裁判
optional int32 energyType = 3; //鬼火类型 0:阵营 1:单体
optional int32 energyValue = 4; //鬼火当前值(阵营鬼火)
optional int32 energyProgressValue = 5; //鬼火进度当前值
optional int32 energyRound = 6; //第几次恢复鬼火
repeated UnitData units = 7; //战斗站位数据
repeated UnitData cardpool = 8; //卡池中数据
optional int32 speed = 9; //阵营速度
}
2:在python工程中遍历所有proto(1步骤被引用的除外),遍历每一个proto的每一行,每检测到一个message消息体,就给消息体添加一个枚举,并且protoid+1向上累积。举个例子,假设这个proto里有10个message,那么该协议内的protoid就是1-10,当遍历到下一个proto时,该proto的message内生成的枚举值就要从11开始啦。这样想是不是非常的简单。遍历到最后,每一个message都有一个唯一的protoid啦
3:步骤2的实现方法可以,但非常愚蠢,因为开发者不希望每一次都把所有proto都生成一遍,因为我正在开发一个UI功能,我只修改这一个proto,却导致我需要把所有proto的protoid都生成一遍,因此有一个简单的解决办法,就是给每个proto定义一个段位
def gengid(modulename):
gids = {
"Module_AiPet.proto" : 1,
"Module_Bag.proto" : 2,
"Module_Buff.proto" : 3,
"Module_Chat.proto" : 4,
"Module_Coin.proto" : 5,
"Module_DaySign.proto" : 6,
"Module_Faction.proto" : 7,
"Module_Friend.proto" : 8,
"Module_Gem.proto" : 9,
}
注意:每创建一个proto的时候就要手动添加一个段,这样我们生成protoid的时候就不会扰动其他的proto里的protoid,python文件定义的段作为千位,如果是第100个proto,那么它的段位就是100000+ ***后面的个十百位可以让你的proto内定义999个消息体,很显然已经够用了。举个例子:假设是第100个proto里的第65个消息体,那么该message的protoid就是100065。到此为止,protoid的生成教程就完成啦。
[protoid]lua批处理require
批量处理require和上述批量处理protoid可以在一个python工程中连续完成,也可以分步完成, 因为他们之间没有必然联系。
#这是一个python函数,该函数会生成一个AllPB.lua文件,当客户端require该lua文件后,会require所有我们批处理生成的XXXX_pb.lua文件
#这是一个python函数,该函数会生成一个AllPB.lua文件,当客户端require该lua文件后,会require所有我们批处理生成的XXXX_pb.lua文件
def export_require(lua_path):
outPath = os.path.abspath(lua_path + "/AllPB.lua")
fileObj = open(outPath,"wb")
fileObj.write("--this file is generated by tools, do not edit\n\n")
fileObj.write("module(...,package.seeall)\n")
fileObj.write("function InitModule()\n")
fileObj.write(" ------------register NetMsg pb------------\n")
for file in os.listdir(lua_path + "\\NetMsg"):
if file[-3:] == "lua":
fileObj.write("\trequire \"Logic/Proto/NetMsg/"+ file[:-4] +"\"\n")
fileObj.write("end\n")
fileObj.close()
分析上述代码,也不难看出该函数干了啥
--this file is generated by tools, do not edit
module(...,package.seeall)
function InitModule()
------------register NetEnum pb------------
------------register NetStruct pb------------
------------register NetMsg pb------------
require "Logic/Proto/NetMsg/Module_Achievement_pb"
require "Logic/Proto/NetMsg/Module_AsyncArena_pb"
require "Logic/Proto/NetMsg/Module_Award_pb"
require "Logic/Proto/NetMsg/Module_Buildings_pb"
require "Logic/Proto/NetMsg/Module_CenturyAdventure_pb"
require "Logic/Proto/NetMsg/Module_Chat_pb"
require "Logic/Proto/NetMsg/Module_ClientConfig_pb"
require "Logic/Proto/NetMsg/Module_CommonInfo_pb"
require "Logic/Proto/NetMsg/Module_Condition_pb"
require "Logic/Proto/NetMsg/Module_Cross_pb"
require "Logic/Proto/NetMsg/Module_Currencys_pb"
require "Logic/Proto/NetMsg/NetStruct_Item_pb"
require "Logic/Proto/NetMsg/NetStruct_Math_pb"
require "Logic/Proto/NetMsg/NetStruct_Thing_pb"
end
最后展示一下AllPB.lua长这样,在项目启动的时候require该文件,则自动require所有生成的pb,是不是很nice!!!!
[完结语]
到此为止,lua版Proto生成与使用教程全部结束,并且还补充了protoid的生成方式。