在cocos creator中使用protobufjs(三)
在cocos creator中使用protobufjs(一) 一、proto文件的加载问题我遇到的第一个痛点就是proto文件的加载问题。有人可能会问,前面不是讲了怎么加载方法很简单的: ...
let builder = new protobuf.Builder();
protobuf.loadProtoFile('aaa.proto',builder);
protobuf.loadProtoFile('bbb.proto',builder);
...
protobufjs是一个很优秀的库,他提供的loadProtoFile接口简单直接,但是在真实的项目开发中会像是上面这样的吗?proto文件是一开始就设计好了,固定不变的吗?文件名会修改吗?文件会新增、删除吗? 痛点分析我只有第一天在cocos-js项目中使用proto时是将一个一个的proto文件名写死在loadProtoFile的参数中的,因为那是我中途参与的项目,当时我就发现了问题: 解决办法
我的解决办法是编写一个程序,扫描proto文件目录,生成一个文件列表的数组,从而完全解放人工操作。 //protoFiles.js 用脚本自动生成的文件
module.exports = [
res/proto/aaa.proto,res/proto/bbb.proto,res/proto/zzz.proto,res/proto/login/xxx.proto
...
]
//pbhelper.js 编写一个加载器
let protoFiles = require('protoFiles'); //导入自动生成的proto文件列表
...
loadProtoFile() {
let builder = new protobuf.Builder();
//遍历文件名,逐一加载
protoFiles.forEach((protoFile) => {
protobuf.loadProtoFile(protoFile,builder);
})
...
}
从此再也不用担心proto文件加载方面的问题了。 解放更多人工操作在编写proto扫描脚本的同时,还可以将proto文件同步到自己的工程目录中,以解决proto文件的手工复制粘贴问题,如果你还要更进一步,还可以将svn/git的拉取给做了。
Creator中的新发现最早在Creator中使用proto时我也是使用的上面的方法,但随着对Creator的了解越来越多,我就在想,Creator不是管理了我们所有的资源了吗?cc.loader.loadResDir不是要以加载一个目录下的所有资源,是否可以有更简单的办法?于是我尝试着去调试loadResDir函数有惊喜发现。 let files = [];
//xxx是assets/resources目录下的一个目录名
cc.loader._resources.getUuidArray('xxx',null,files);
//files会得到所有的文件名
cc.log(files);
通过这个发现,可以省去生成protoFiles.js的工作了。 二、proto对象的实例化问题proto对象的实例化是一个痛点,估计很多人会觉得有点小题大作。protobufjs不是提供了操作方法吗,那么简单: //实例化登录请求
let loginReq = new pb.LoginRep();
loginReq.account = 'zxh';
loginReq.password = '123456';
//假如net是封装好了的网络模块
net.send(pb.ActionCode.LOGIN,loginRsp,(data) => {
//收到数据,反序列化
let loginRsp = pb.LoginRsp.decode(data);
...
});
如果是做过网络开发的应该对上面的代码不难理解,这里还是简单的解释一下:
痛点分析
解决办法
如果能像下面一样是不是会更清爽: //使用工厂函数获得LoginReq对象
let req = pb.newReq(pb.ActionCode.LOGIN);
req.account = 'zxh';
req.password = '123456';
//在工厂函数时做个小动作:req.action = pb.ActionCode.LOGIN
//send时就不需要消息号参数了。
net.send(req,...);
通过pb.newReq隐藏协议细节,也不需要管消息的名字,用的什么protobuf库,返回的req上绑定上action消息号减少调用send时的重复参数,上层操作简单明了。 三、proto对象的反序列化问题我们再看下反序列化的场景 ...
//发送数据,net假如是封装好了的网络模块
net.send(pb.ActionCode.LOGIN,(data) => {
//发送的是登录请求,反序列化时要用登录响应,不然会失败
let loginRsp = pb.LoginRsp.decode(data);
...
});
痛点分析反序列化成为痛点有部分原因与实例化相同,而且当你收到一个响应时,该用那个proto对象去反序列化会杀死不少脑细包,特别是在设计协议消息名字时不注意规范时更容易出错。 解决办法
message PBMessage{
int32 action = 1; //消息号用于指明data字段(标识下层协议类型)
int32 sequence = 2; //请求序列
uint64 timestamp = 3; //时间戳
int32 userID = 4; //帐号
bytes data = 5; //请求或响应数据(序列化后的二进制数据)
}
其中的sequence字段是客户端向服务器发出一个请求时,生成的唯一ID。当服务器响应你这个请求时,传回这个sequence,通过这个sequence + action你就能确定你的响应消息对象,从而正确解码。 //收到网络数据
message(event) {
var pbMessage = pb.PBMessage.decode(event.data);
//从缓存对象中取出请求时的参数对象
var obj = this.cache[pbMessage.sequence];
//删除缓存数据
delete this.cache[pbMessage.sequence];
try{
//检测缓存数据是否存在
if (!obj) {
return;
}
//使用工厂创建响应对象
let rsp = pb.newRsp(obj.action,obj.data);
//调用请求时的回函数
obj.callback(rsp);
}catch(e) {
cc.log('处理响应错误');
}
}
这时响应函数就可以很轻松的处理业务了 //发送数据,net假如是封装好了的网络模块
net.send(loginReq,(loginRsp) => {
//直接访问响应对象,不需去解码了
this.label.string = loginRsp.player.name;
...
});
核心问题不论是解决实例化还是反序列化,最核心的问题是实现那两个工厂函数
而实现这两个工厂函数的前提是明确请求操作码、请求对象、响应对象,需要建立一个映射表,类似下面的定义 //proto中定义Action
enum ActionCode {
LOGIN: 1,LOGOUT: 2,}
//protoMap.js文件
protoMap = {
1: {
req: pb.LoginRes,rsp: pb.LoginRsp,}
...
}
有了protoMap工厂函数就简单了 //工厂函数
let protoMap = require('protoMap');
//请求工厂函数
newReq(action) {
let obj = protoMap[action];
let req = new obj.req();
req.action = action;
return req;
}
//响应工厂函数
newRsp(action,data) {
let obj = protoMap[action];
return obj.rsp.decode(data);
}
四、protoMap如何而来?我们的问题是不是都解决呢?如果你觉得都解决了,那是高兴的太早了。 痛点分析
解决办法
因为protoMap.js是根据proto的定义动态变化的,我采取的办法是通过一个程序去分析proto文件生成protoMap代码。不过这里为了让protoMap生成器不要太复杂,我在proto定义ActionCode时做了点小手脚 //proto中定义Action
enum ActionCode {
LOGIN: 1,//LoginReq;LoginRsp;
LOGOUT: 2,//LogoutReq;LogoutRsp;
}
在定义ActionCode时,我们为每一个消息码加上注释,第一个是请求,第二个是响应。 enum ActionCode {
LOGIN: 1,//Login
LOGOUT: 2,//Logout
}
通过在ActionCode中加点小手脚,再去解析这段文本,生成protoMap会简单很多了。在protoMap生成器中,可以去校验一下注释中写的请求、响应对象是否正确。 还有一种方案是在请求协议上添加注释: //action:1
message LoginReq {
...
}
//action:2
message LogoutReq {
...
}
这种方案我也在项目中使用过,也可以方便提取生成protoMap。 五、最后的痛关于protobuf在js中还剩下最后一个痛,那就是目前的IDE都不能支持proto对象属性的
let req = pb.newReq(pb.ActionCode.LOGIN);
req.useName = 'zxh'; //这里应该是userName被写成useName
req.pwd = '123456'; //这里应该是password被写成pwd
痛点分析
解决办法要解决这个问题我目前的办法是,将proto对象生成对应的js代码,如果还想做的更好,可以学习Creator那样,生成一个d.ts文件。 六、觉知你心中的痛在开发中不能觉知到开发体验,估计也很难觉知到用户体验,因为自己就是自己项目的用户。不能觉知到痛,如何去解决痛? (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |