在cocos creator中使用protobufjs(三)
2017-08-01 05:15
615 查看
在cocos creator中使用protobufjs(一)
在cocos creator中使用protobufjs(二)
通过前面两篇我们探索了如何在creator中使用protobuf,并且让其能正常工作在浏览器、JSB上,最后聊到protobuf在js项目中使用上的一些痛点。这篇博文我要把这些痛点一条一条地扳开,分析为什么它让我痛,以及我的治疗方案。
protobufjs是一个很优秀的库,他提供的loadProtoFile接口简单直接,但是在真实的项目开发中会像是上面这样的吗?proto文件是一开始就设计好了,固定不变的吗?文件名会修改吗?文件会新增、删除吗?
1. 路径名、文件较长容易写错字。
2. 项目开发中协议会不断新增,会写漏,少加载了proto文件。
3. 某些原因会修改proto文件名,原来加载的没及时修改,加载时会出错。
4. 人工手写这个加载文件会很累,效率低下,容易出错,在文件众多的情况下极度消耗脑细胞。
我的解决办法是编写一个程序,扫描proto文件目录,生成一个文件列表的数组,从而完全解放人工操作。
从此再也不用担心proto文件加载方面的问题了。
总结一下脚本要做的事:
1.从svn或git获取最新的proto文件(svn: svn up, git: git pull origin master)
2.将proto文件同步到工程目录
3.扫描工程目录中的proto文件,生成一个文件列表数组
通过这个发现,可以省去生成protoFiles.js的工作了。
如果是做过网络开发的应该对上面的代码不难理解,这里还是简单的解释一下:
1.xxxRep是客户端请求消息,xxxRsp 是服务器响应消息,成对的设计请求、响应协议比较好管理。
2.pb.ActionCode.LOGIN是一个常量定义,是设计的请求操作码,用于服务器识别你发的消息是登录请求,而不是其它,不然序列化后的二进制内容服务器无法反序列化。
3.这里没有出现客户端proto对象的序列化操作,因为可以封装到net.send函数中,所以它不足以成为一个痛点。
4.net.send中的回调函数是客户端响应处理函数,通过参数获得服务器发送的数据,因为二进制数据,所以需要用pb.LoginRsp.decode(data)进行反序列化。
在js中使用proto有个特点,proto对象一般IDE都没有代码提示和着色,在用调用proto对象解码时输入效率低下,还容易打错。
这句代码暴露了协议细节,如果pb.LoginRep改名了也不知道,代码会报错。
net.send(pb.ActionCode.LOGIN, loginReq, () => { }) 明明已经是发送的登录消息了,为什么还需要一个操作码呢?感觉有些累赘、重复。
如果能像下面一样是不是会更清爽:
通过pb.newReq隐藏协议细节,也不需要管消息的名字,用的什么protobuf库,返回的req上绑定上action消息号减少调用send时的重复参数,上层操作简单明了。
除了设计工厂函数外,还需要定义pb.ActionCode.LOGIN,让它能被IDE自动提示、代码补全,文本着色,我们会省心很多。
2.请求\响应唯一序列号
3.工厂模式
通信协议头是客户端、服务器在收到二进制数据时,可以使用一个固定的协议结构去反序列也称之为解码。 解码后可以获得基本的数据,比如路由号、时间戳、用户ID、下层协议数据(二进制)等,大概如下:
其中的sequence字段是客户端向服务器发出一个请求时,生成的唯一ID。当服务器响应你这个请求时,传回这个sequence,通过这个sequence + action你就能确定你的响应消息对象,从而正确解码。
cache是缓存net.send时的参数包括:action、sequence、callback,其中sequence是自动生成的并以它为key。
当收到服务器数据时,先解码PBMessage,用解码后的sequence去查找出action。
使用action和data做为响应工厂函数的参数,反序列化出响应对象。
调用响应处理函数。
这时响应函数就可以很轻松的处理业务了
let req = newReq(action);
let rsp = newRsp(action, data);
而实现这两个工厂函数的前提是明确请求操作码、请求对象、响应对象,需要建立一个映射表,类似下面的定义
有了protoMap工厂函数就简单了
目前protoMap.js文件是需要人手工去编写的,同样的问题又来了。
2.手工编写这个protoMap.js文件在协议新增、修改、删除时容易出错。
3.出了错问题还很不好找,只有在调用到的地方才能暴露问题。
因为protoMap.js是根据proto的定义动态变化的,我采取的办法是通过一个程序去分析proto文件生成protoMap代码。不过这里为了让protoMap生成器不要太复杂,我在proto定义ActionCode时做了点小手脚
在定义ActionCode时,我们为每一个消息码加上注释,第一个是请求,第二个是响应。
如果在设计协议时,能有严格的规范可以将注释写的简单些。
通过在ActionCode中加点小手脚,再去解析这段文本,生成protoMap会简单很多了。在protoMap生成器中,可以去校验一下注释中写的请求、响应对象是否正确。
还有一种方案是在请求协议上添加注释:
这种方案我也在项目中使用过,也可以方便提取生成protoMap。
自动补全,代码提示,文本着色
2.没有自动补全需要多打很多字。
3.没有函数着色,敲出来的代码心里不踏实。
在cocos creator中使用protobufjs(二)
通过前面两篇我们探索了如何在creator中使用protobuf,并且让其能正常工作在浏览器、JSB上,最后聊到protobuf在js项目中使用上的一些痛点。这篇博文我要把这些痛点一条一条地扳开,分析为什么它让我痛,以及我的治疗方案。
一、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的参数中的,因为那是我中途参与的项目,当时我就发现了问题:1. 路径名、文件较长容易写错字。
2. 项目开发中协议会不断新增,会写漏,少加载了proto文件。
3. 某些原因会修改proto文件名,原来加载的没及时修改,加载时会出错。
4. 人工手写这个加载文件会很累,效率低下,容易出错,在文件众多的情况下极度消耗脑细胞。
解决办法
编写代码来生成代码我的解决办法是编写一个程序,扫描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的拉取给做了。总结一下脚本要做的事:
1.从svn或git获取最新的proto文件(svn: svn up, git: git pull origin master)
2.将proto文件同步到工程目录
3.扫描工程目录中的proto文件,生成一个文件列表数组
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); ... });
如果是做过网络开发的应该对上面的代码不难理解,这里还是简单的解释一下:
1.xxxRep是客户端请求消息,xxxRsp 是服务器响应消息,成对的设计请求、响应协议比较好管理。
2.pb.ActionCode.LOGIN是一个常量定义,是设计的请求操作码,用于服务器识别你发的消息是登录请求,而不是其它,不然序列化后的二进制内容服务器无法反序列化。
3.这里没有出现客户端proto对象的序列化操作,因为可以封装到net.send函数中,所以它不足以成为一个痛点。
4.net.send中的回调函数是客户端响应处理函数,通过参数获得服务器发送的数据,因为二进制数据,所以需要用pb.LoginRsp.decode(data)进行反序列化。
痛点分析
let loginReq = new pb.LoginRep();在js中使用proto有个特点,proto对象一般IDE都没有代码提示和着色,在用调用proto对象解码时输入效率低下,还容易打错。
这句代码暴露了协议细节,如果pb.LoginRep改名了也不知道,代码会报错。
net.send(pb.ActionCode.LOGIN, loginReq, () => { }) 明明已经是发送的登录消息了,为什么还需要一个操作码呢?感觉有些累赘、重复。
解决办法
工厂模式如果能像下面一样是不是会更清爽:
//使用工厂函数获得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时的重复参数,上层操作简单明了。
除了设计工厂函数外,还需要定义pb.ActionCode.LOGIN,让它能被IDE自动提示、代码补全,文本着色,我们会省心很多。
三、proto对象的反序列化问题
我们再看下反序列化的场景... //发送数据,net假如是封装好了的网络模块 net.send(pb.ActionCode.LOGIN, loginReq, (data) => { //发送的是登录请求,反序列化时要用登录响应,不然会失败 let loginRsp = pb.LoginRsp.decode(data); ... });
痛点分析
反序列化成为痛点有部分原因与实例化相同,而且当你收到一个响应时,该用那个proto对象去反序列化会杀死不少脑细包,特别是在设计协议消息名字时不注意规范时更容易出错。解决办法
1.设计通信协议头2.请求\响应唯一序列号
3.工厂模式
通信协议头是客户端、服务器在收到二进制数据时,可以使用一个固定的协议结构去反序列也称之为解码。 解码后可以获得基本的数据,比如路由号、时间戳、用户ID、下层协议数据(二进制)等,大概如下:
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('处理响应错误'); } }
cache是缓存net.send时的参数包括:action、sequence、callback,其中sequence是自动生成的并以它为key。
当收到服务器数据时,先解码PBMessage,用解码后的sequence去查找出action。
使用action和data做为响应工厂函数的参数,反序列化出响应对象。
调用响应处理函数。
这时响应函数就可以很轻松的处理业务了
//发送数据,net假如是封装好了的网络模块 net.send(loginReq, (loginRsp) => { //直接访问响应对象,不需去解码了 this.label.string = loginRsp.player.name; ... });
核心问题
不论是解决实例化还是反序列化,最核心的问题是实现那两个工厂函数let req = newReq(action);
let rsp = newRsp(action, data);
而实现这两个工厂函数的前提是明确请求操作码、请求对象、响应对象,需要建立一个映射表,类似下面的定义
//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文件是需要人手工去编写的,同样的问题又来了。
痛点分析
1 一个项目与服务器的请求少则几十个,多则上百上千,手工方式维护protoMap的难度大。2.手工编写这个protoMap.js文件在协议新增、修改、删除时容易出错。
3.出了错问题还很不好找,只有在调用到的地方才能暴露问题。
解决办法
编写代码来生成代码因为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
痛点分析
1.js中没有代码提示容易笔误,而且问题大多数在运行到代码那一刻才会暴露出来。2.没有自动补全需要多打很多字。
3.没有函数着色,敲出来的代码心里不踏实。
解决办法
要解决这个问题我目前的办法是,将proto对象生成对应的js代码,如果还想做的更好,可以学习Creator那样,生成一个d.ts文件。六、觉知你心中的痛
在开发中不能觉知到开发体验,估计也很难觉知到用户体验,因为自己就是自己项目的用户。不能觉知到痛,如何去解决痛?相关文章推荐
- cocos creator中使用protobuf(dcodeIO/protobuf.js 5.0)
- 在cocos creator中使用protobufjs(二)
- 在cocos creator中使用protobufjs(四)
- 在cocos creator中使用protobufjs(一)
- 白鹭egret配合protobufjs的使用
- 在TS语言H5项目中使用Protobufjs(二) - Layaair腊鸭引擎篇
- 在node中使用protobuf.js
- ProtoBuf.js 使用技巧
- cocos creator: js中实现protobuf的打包和解析
- WebSocket中关于使用ProtoBuf传输数据介绍js部分
- 在TS语言H5项目中使用Protobufjs(一) - Egret白鹭引擎篇
- ProtoBuf.js 使用技巧
- 基于Netty5.0高级案例二之WebSocket中关于使用ProtoBuf传输数据介绍js部分
- WebSocket中关于使用ProtoBuf传输数据介绍js部分
- 使用netty+zookeeper+protobuf实现一个RPC过程
- Unity跨iOS、Android平台使用protobuf-net的方法
- java序列化/反序列化之xstream、protobuf、protostuff 的比较与使用例子
- 使用protobuf和socket实现服务器间消息的传递
- [Unity3D]简单使用Protobuf-net(一)
- protobuf 在lua中使用(3)