在cocos creator中使用protobufjs(一)
在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();

  1. 在js中使用proto有个特点,proto对象一般IDE都没有代码提示和着色,在用调用proto对象解码时输入效率低下,还容易打错。
  2. 这句代码暴露了协议细节,如果pb.LoginRep改名了也不知道,代码会报错。
  3. 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,(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('处理响应错误');
    }        
}
  1. cache是缓存net.send时的参数包括:action、sequence、callback,其中sequence是自动生成的并以它为key。
  2. 当收到服务器数据时,先解码PBMessage,用解码后的sequence去查找出action。
  3. 使用action和data做为响应工厂函数的参数,反序列化出响应对象。
  4. 调用响应处理函数。

这时响应函数就可以很轻松的处理业务了

//发送数据,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中使用protobufjs(三)的更多相关文章

  1. ios – CGPath和UIBezierPath()有什么区别?

    目前,我正在努力制作一个自定义按钮,我有一个图像,并具有坐标,但我发现您可以通过使用CGPath类或UIBezierPath创建一个按钮/对象类.有人可以告诉我两者有什么区别?解决方法CGPath是CoreGraphics库的不透明类型,而UIBezierPath是UIKit中的Obj-C类.UIBezierPath是一个围绕CGPath的包装,具有更加面向对象的界面和一些方便的方法.使用CGPath可能会略微更快,因为它不必经过Obj-C,并且它具有更高级的功能,如CGPathApply.重要的是,UI

  2. swift 快速奔跑的兔几 本节的内容是:序列化与反序列化

    在cocoa中,我们经常需要向磁盘保存数据块,cocoa将这些数据块表示为NSData对象例如,有一个字符串,将其转换为NSData,可以使用如下方法:我们还可以将对象转化为数据。遵守协议NSCoding的对象可以转换为NSData对象,也可以从NSData对象中加载,方法如下:

  3. Swift中一个类中的枚举enum类型的数据该如何实现序列化NSCoder

    简述昨天在开发中遇到了这样一个问题,需要用NSUserDefaults持久化一些数据,其中需要保存一个自己定义的类对象。结束其实枚举本来就是一个Int,因此我们将其声明为Int型就可以根据Int值初始化了,以此实现序列化和反序列化。

  4. swift json的序列化和反序列化

    还有一点东西没写完,权作笔记参考:http://www.hangge.com/blog/cache/detail_983.html

  5. Swift中对象序列化的实现

    Swift中对象序列化的实现在swift中要使某个类可以序列化,只需要类实现NSCoding协议,并实现协议中的一个必要的构造函数和一个方法,分别对应序列化和反序列化的二个过程。

  6. Alamofire 4.0 迁移指南

    原文:Alamofire4.0MigrationGuide作者:cnoon译者:kemchenj译者注:最近打算把公司项目迁移到Swift3.0,顺手把Alamofire4.0的迁移指南翻译了,之前虽然读过一部分源码,但还是看到了很多新东西,新的Adapter和Retrier我都打算用到项目里,希望大家看完也能够有收获.Alamofire4.0是Alamofire最新的一个大版本更新,一个基于Sw

  7. [HandyJSON] 在Swift语言中处理JSON - 转换JSON和Model

    而HandyJSON是其中使用最舒服的一个库,本文将介绍用HandyJSON来进行Model和JSON间的互相转换。而HandyJSON另辟蹊径,采用Swift反射+内存赋值的方式来构造Model实例,规避了上述两个方案遇到的问题。所以我们要定义一个Mapping函数来做这两个支持:就这样,HandyJSON完美地帮我们进行了JSON到Model类的转换。把Model转换为JSON文本HandyJSON还提供了把Model类序列化为JSON文本的能力,简直无情。

  8. 数组 – 在swift中存储对数组的引用

    我错过了一些允许我这样做的Swift构造吗?你必须使用NSArray或NSMutableArray,因为SwiftArrays是值类型,所以任何赋值都会复制.

  9. android – 如何在Realm for Java中将RealmObject序列化为JSON?

    也就是说,使用RealmObject并将其序列化为JSON?它还应该序列化该对象内的任何RealmList.解决方法来自英国的基督徒在这里.RealmforAndroid目前没有任何此类方法,虽然核心数据库实际上支持JSON序列化,所以现在你要么必须手动操作,要么使用像GSON这样的第三方工具然而).

  10. android – GSON反序列化自定义对象数组

    我正在尝试使用GSON在Android中序列化/反序列化JSON.我有两个类看起来像这样:和:我正在使用GSON来序列化/反序列化数据.我像这样序列化:这将生成如下所示的JSON:我这样反序列化:我打电话的时候收到错误.我不知道这个错误意味着什么.我不认为自己做了任何严重的错误.有帮助吗?解决方法将您的代码更改为:使用Interfaces是一个很好的做法,GSON要求.Gson将javascript中的数组“[]”转换为LinkedList对象.在您的代码中,GSON尝试在_users字段中注入一个Lin

随机推荐

  1. 【cocos2d-x 3.x 学习笔记】对象内存管理

    Cocos2d-x的内存管理cocos2d-x中使用的是上面的引用计数来管理内存,但是又增加了一些自己的特色。cocos2d-x中通过Ref类来实现引用计数,所有需要实现内存自动回收的类都应该继承自Ref类。下面是Ref类的定义:在cocos2d-x中创建对象通常有两种方式:这两中方式的差异可以参见我另一篇博文“对象创建方式讨论”。在cocos2d-x中提倡使用第二种方式,为了避免误用第一种方式,一般将构造函数设为protected或private。参考资料:[1]cocos2d-x高级开发教程2.3节[

  2. 利用cocos2dx 3.2开发消灭星星六如何在cocos2dx中显示中文

    由于编码的不同,在cocos2dx中的Label控件中如果放入中文字,往往会出现乱码。为了方便使用,我把这个从文档中获取中文字的方法放在一个头文件里面Chinese.h这里的tex_vec是cocos2dx提供的一个保存文档内容的一个容器。这里给出ChineseWords,xml的格式再看看ChineseWord的实现Chinese.cpp就这样,以后在需要用到中文字的地方,就先include这个头文件然后调用ChineseWord函数,获取一串中文字符串。

  3. 利用cocos2dx 3.2开发消灭星星七关于星星的算法

    在前面,我们已经在GameLayer中利用随机数初始化了一个StarMatrix,如果还不知道怎么创建星星矩阵请回去看看而且我们也讲了整个游戏的触摸事件的派发了。

  4. cocos2dx3.x 新手打包APK注意事项!

    这个在编译的时候就可以发现了比较好弄这只是我遇到的,其他的以后遇到再补充吧。。。以前被这两个问题坑了好久

  5. 利用cocos2dx 3.2开发消灭星星八游戏的结束判断与数据控制

    如果你看完之前的,那么你基本已经拥有一个消灭星星游戏的雏形。开始把剩下的两两互不相连的星星消去。那么如何判断是GameOver还是进入下一关呢。。其实游戏数据贯穿整个游戏,包括星星消除的时候要加到获得分数上,消去剩下两两不相连的星星的时候的加分政策等,因此如果前面没有做这一块的,最好回去搞一搞。

  6. 利用cocos2dx 3.2开发消灭星星九为游戏添加一些特效

    needClear是一个flag,当游戏判断不能再继续后,这个flag变为true,开始消除剩下的星星clearSumTime是一个累加器ONE_CLEAR_TIME就是每颗星星消除的时间2.连击加分信息一般消除一次星星都会有连击信息和加多少分的信息。其实这些combo标签就是一张图片,也是通过控制其属性或者runAction来实现。源码ComboEffect.hComboEffect.cpp4.消除星星粒子效果消除星星时,为了实现星星爆裂散落的效果,使用了cocos2d提供的粒子特效引擎对于粒子特效不了

  7. 02 Cocos2D-x引擎win7环境搭建及创建项目

    官网有搭建的文章,直接转载记录。环境搭建:本文介绍如何搭建Cocos2d-x3.2版本的开发环境。项目创建:一、通过命令创建项目前面搭建好环境后,怎样创建自己的Cocos2d-x项目呢?先来看看Cocos2d-x3.2的目录吧这就是Cocos2d-x3.2的目录。输入cocosnew项目名–p包名–lcpp–d路径回车就创建成功了例如:成功后,找到这个项目打开proj.win32目录下的Hello.slnF5成功了。

  8. 利用cocos2dx 3.2开发消灭星星十为游戏添加音效项目源码分享

    一个游戏,声音也是非常的重要,其实cocos2dx里面的简单音效引擎的使用是非常简单的。我这里只不过是用一个类对所有的音效进行管理罢了。Audio.hAudio.cpp好了,本系列教程到此结束,第一次写教程如有不对请见谅或指教,谢谢大家。最后附上整个项目的源代码点击打开链接

  9. 03 Helloworld

    程序都有一个入口点,在C++就是main函数了,打开main.cpp,代码如下:123456789101112131415161718#include"main.h"#include"AppDelegate.h"#include"cocos2d.h"USING_NS_CC;intAPIENTRY_tWinMain{UNREFERENCED_ParaMETER;UNREFERENCED_ParaMETER;//createtheapplicationinstanceAppDelegateapp;return

  10. MenuItemImage*图标菜单创建注意事项

    学习cocos2dx,看的是cocos2d-x3.x手游开发实例详解,这本书错误一大把,本着探索求知勇于发现错误改正错误的精神,我跟着书上的例子一起调试,当学习到场景切换这个小节的时候,出了个错误,卡了我好几个小时。

返回
顶部