一个解决tcp+jsb+protobuf的demo

前言

先说一下为什么写这个教程。由于新项目打算用creator开发,服务器用的是c++,底层通信用的是tcp+protobuf,所以就要用到creator的定制引擎。由于之前没用过creator,刚开始做项目就要用定制引擎,对js也不太了解,所以这个过程中经历了非常多的坑。
写这个教程就是为了给那些和我有同样经历的同学,帮你们少走一些弯路,同时提供一个还算凑合的解决方案,一个完整的demo,包含定制引擎相关操作,js与c++互调,数据如何往来,以及protobuf相关操作,bytes和repeat的案例。但不包含tcp通信相关代码,demo中只是在模拟这个操作,这个涉及到加解密以及和心跳包等,本教程不提供。
如果你的也是使用c++实现tcp,然后通过jsb供js调用,通信协议使用的protobuf,那这个demo刚好适合你参考。
再说一下demo的适用场景。不适用于web和微信小程序,仅适用于安卓ios和windows。
本人的开发环境操作系统:windows。mac下同样适用。

qq:1101502561
gitbub: 项目源码

准备工作

1.creator1.8.2。
2.vs2015,xcode。
3.eclipse or android studio以及对应的sdk ndk。
4.python 2.7.14版本,32位。不要用2.7.3等低版本的python,也不要用64位python。这是第一个坑,如果低版本python,定制c++引擎的时候,执行python download-deps.py会出错。原因是低版本的openssl库版本过低,py下载远程zip包时会报错。如果用64位也会有的地方执行不成功。
下载地址:https://www.python.org/downloads/release/python-2714/
5.git。下载地址: https://git-scm.com/
6.node.js。下载地址:https://nodejs.org/en/

一、定制引擎

官网定制引擎流程:
http://docs.cocos.com/creator/manual/zh/advanced-topics/engine-customization.html

第一步,把github服务器上的定制引擎代码clone到本地

首先在本地创建一个定制引擎的根目录。我的在F:\svn\client。然后命令行进入这个目录。
使用git命令把需求的分支clone本地仓库。
创建js引擎仓库
git clone -b v1.8-release https://github.com/cocos-creator/engine.git
创建c++引擎仓库
git clone -b v1.8-release https://github.com/cocos-creator/cocos2d-x-lite.git

我用的是1.8.2的creator,所以clone的是1.8-release分支。
等待clone完成。中间还出错了两次。从github上下载代码速度很慢,这里有个方法可以提高速度。https://blog.csdn.net/u013360850/article/details/77145661
然后命令行进入js引擎的根目录。
执行官网教程给出的命令。
安装编译依赖

安装 gulp 构建工具
npm install -g gulp
在命令行中进入引擎路径
npm install
进行修改然后编译

接下来您可以定制引擎修改了,修改之后请在命令行中执行:
gulp build

来编译将引擎源码编译到 bin 目录下。

然后进入c++引擎根目录。

安装编译依赖
npm install
下载依赖包,需要提前配置好 python
python download-deps.py
同步子 repo,需要提前配置好 git
git submodule update --init

当时到第二个命令的时候一直执行失败,最后是python版本的问题,弄了半天才搞好。
如果你按照我之前说的,是通过git clone命令下载的代码,那第三个命令是可以正确执行的。如果不是的话,请重新从github上clone吧。
以上完成后,才算是把完整的c++引擎下载的本地。

由于我的项目构建发布用的是link模式,所以不执行 gulp gen-libs命令。

依次执行以下命令生成模拟器。每次修改c++代码后都要c++引擎目录执行一下命令。
通过 cocos console 生成模拟器

gulp sign-simulator
gulp gen-simulator
gulp update-simulator-config

gulp sign-simulator 是 1.7.0 中的新增命令,只有 Mac 需要运行。

creator的偏好设置中,修改原生开发环境配置。

箭头指示的位置修改为从github上clone的项目路径。
到目前为止,定制引擎配置完成。新建creator项目点击预览正常运行。

二、JSB绑定

官网jsb绑定教程:

http://docs.cocos.com/creator/manual/zh/advanced-topics/jsb/JSB2.0-learning.html
使用的是JSB2.0的自动绑定。

绑定流程:

首先写好要绑定的c++类。
项目的github:项目链接

demo中写了两个需要绑定的类。CNetClient和CNetResponse。
我选择把绑定的文件和cocos引擎默认需要绑定的类文件放在了一起。
在cocos2d-x-lite\cocos\scripting\js-bindings目录下新建了一个tools文件夹,把编写到c++文件放到了这里。

然后编写自动绑定的脚本。
绑定脚本在cocos2d-x-lite\tools\tojs目录下。

绑定脚本的字段意义参考官网教程。字段不多,理解意思就可以自己写了。

在genbindings.py文件中添加新写的脚本。

这些完成之后就可以通过执行命令自动生成绑定了。
进入到genbindings.py所在的目录cocos2d-x-lite\tools\tojs。
执行 python genbindings.py。
这个时候可能会报错,因为python没有安装PyYAML和Cheetah。下载并安装。(安装python的时候要安装在c根目录下)
PyYAML官网::http://www.pyyaml.org/wiki/PyYAML
Cheetah官网:http://cheetahtemplate.org/
安装之后再执行命令。如果类和脚本没有问题会提示

添加生成和jsb文件c++项目中。
进入cocos2d-x-lite\tools\simulator\frameworks\runtime-src目录下,这个是creator模拟器的工程项目。安卓,ios,win都在这里。
打开proj.win32下的simulator.sln。
在libjscocos2d项目下的auto目录添加我们自动绑定成功的c++文件。如下图:

然后打开simulator项目。在jsb_module_register.cpp里添加注册类到js环境。
首先添加头文件引用

#include “cocos/scripting/js-bindings/auto/jsb_cnet_client_auto.hpp”
#include “cocos/scripting/js-bindings/auto/jsb_cnet_response_auto.hpp”

然后在jsb_register_all_modules函数下添加注册。

se->addRegisterCallback(register_all_cnet_client);
se->addRegisterCallback(register_all_cnet_response);

在安卓和ios项目下,同样添加c++文件的引用。篇幅有限,不在赘述。
刚刚我们只是把模拟器的项目添加了注册js的代码。新建项目的时候,新项目的c++工程下需要再修改一下jsb_module_register.cpp。

更新模拟器
每次修改c++代码都要调用执行下面命令重新生成模拟器。

通过 cocos console 生成模拟器

gulp sign-simulator
gulp gen-simulator
gulp update-simulator-config
进入cocos2dx-lite目录执行以上命令。注意执行的时候关闭vs2015和creator,否则可能会出错。建议执行命令之前先运行模拟器项目编译一下,编译完成在执行,这样可以忽略掉一些低级错误。

自此,定制引擎部分就完成了,我们可以在js代码中直接使用CNetClient和CNetResponse类。
使用的时候请加上if(cc.sys.isNative)。否则可能会编译不过。

我们还可以在谷歌浏览器输入以下网址调试我们的代码。
chrome-devtools://devtools/bundled/inspector.html?v8only=true&ws=127.0.0.1:5086/00010002-0003-4004-8005-000600070008

二、js和c++传递数据

为了简化流程,我在项目中只用c++提供一些基础的类供js调用,c++不主动调用js,需要调用的地方通过一些回调函数来传递。js可以通过调用c++的接口来传递一个函数指针。c++保存这个函数指针,在需要的时候调用它。(尽量在主ui线程中去调用)。
由于项目使用的是tcp。传递数据肯定会涉及到二进制流,我们可以用cocos2d::Data来传递数据,jsb自动绑定是可以直接绑定Data的。在js中对应的数据结构是Uint8Array,发送的时候传Uint8Array即可。
传递Data
c++接口:

void CNetClient::sendData(int32_t tag, uint16_t mainCmd, uint16_t subCmd, cocos2d::Data data)
{
SocketManager::getInstance()->sendData(tag,mainCmd,subCmd,data.getBytes(),data.getSize());
}

void CNetClient::sendCmd(int32_t tag, uint16_t mainCmd, uint16_t subCmd)
{
SocketManager::getInstance()->sendData(tag, mainCmd, subCmd, NULL, 0);
}

js调用:

sendData: function(main,sub,data) {
    console.log(`[BaseManager][sendData]--tag:${this._tag},main:${main},sub:${sub}`);
    if (this && this._CNetClient) {
        if (data == null) {
            this._CNetClient.sendCmd(this._tag,main,sub);
        } else {
            this._CNetClient.sendData(this._tag,main,sub,data);
        }
    }
},

传递函数指针
c++接口

void CNetClient::connectServer(int32_t tag, std::string ipAddr, const std::function<bool(const CNetResponse*)> callback)
{
ConnectCallback connect = CONNECT_CALLBACK(CNetClient::OnConnect, this);
CloseCallback close = CLOSE_CALLBACK(CNetClient::OnClose, this);
RecvCallback tcpRecv = RECV_CALLBACK(CNetClient::OnTcpRecv, this);

SocketManager::getInstance()->connectServer(tag, ipAddr, connect, close, tcpRecv);
m_callbackMap[tag] = callback;
}

js调用设置回调函数:

if (this&&this._CNetClient) {
var self = this;
this._func = function(response){
self.onSocketEvent(response);
}.bind(self);

        this._server = server;
        this._CNetClient.connectServer(this._tag,this._server,this._func);
    }

为了方便c++回调js方便,封装了一个CNetResponse类用来传递数据。
类的内容很简单,只是一些set,get函数。

class CNetResponse {
public:
CNetResponse();

~CNetResponse();

cocos2d::Data getTcpData();

void setTcpData(unsigned char* buf, uint32_t size);

CC_SYNTHESIZE(int32_t, _tag, Tag);
CC_SYNTHESIZE(int32_t, _type, Type);
CC_SYNTHESIZE(uint16_t, _mainCmd, Main);
CC_SYNTHESIZE(uint16_t, _subCmd, Sub);
CC_SYNTHESIZE(uint32_t, _size, Size);
CC_SYNTHESIZE(std::string, _string, String);

private:
cocos2d::Data _data;
};

c++去填充这个response,js去读其中的数据。

let type = response.getType();
let main = response.getMain();
let sub = response.getSub();
let size = response.getSize();

具体内容参考c++类以及js相关调用的实现。
js有number和string,string对应了std::string。
还可以参考creator官网给出的一些方案来传递更多的数据类型。
http://docs.cocos.com/creator/manual/zh/advanced-topics/jsb/JSB2.0-learning.html

三,protobuf的使用

项目中的引用的protobuf最开始是使用奎神的pbkiller。pbkiller是基于protobufjs5.x的。写的过程中发现低版本protobufjs中对bytes,repeated,int64的使用太麻烦了。然后果断放弃了pbkiller,使用了最新的protobufjs6.8.6.
protobufjs的github:https://github.com/dcodeIO/protobuf.js
可以通过npm install -g protobufjs命令去获取。
也可以自己动手集成google protobuf。
https://github.com/google/protobuf
参考:http://forum.cocos.com/t/cocoscreator-protobuf/61045。不再赘述。
demo中提供了bytes和repeats的使用方法供参考。

bytes的使用

protobuf的bytes对应的是js里的Uint8Array。直接创建Uint8Array,然后去填充相关的object。在调用protobuf对象的encode生成实例,调用实例的finish生成二进制流,也就是js的Uint8Array。
protobuf的定义:

message BytesTest {
uint32 id = 1;
bytes buf = 2;
}

js调用生成相关的代码:

    let buffer = new Uint8Array(10);
    for (let index = 0; index < buffer.length; index++) {
        buffer[index] = index + 1;
    }
    let testBytes = {id:2, buf: buffer};
    let uint8Array = cmd.BytesTest.encode(testBytes).finish();
    this.sendData(cmd.main.kLogon,cmd.sub.kBytesReq,uint8Array);

Repeat的使用

protobuf的定义:

message RepeatItem {
uint32 id = 1;
string text = 2;
}

message RepeatTest {
repeated RepeatItem items = 1;
}

js调用生成相关的代码:

    let array = {items:[{id:3,text:'333'},{id:4,text:'444'},{id:5,text:'555'}]};
    let uint8Array = cmd.RepeatTest.encode(array).finish();
    this.sendData(cmd.main.kLogon,cmd.sub.kRepeatReq,uint8Array);

最后提示一下

导入protobuf.js的时候,要选择导入为插件,并勾选允许编辑器加载,否则构建发布的时候会报错。

15赞

支持好帖。。。。

好贴,楼主用心了写了这么多,不过为什么不用websocket反而花了这么久去写了一个底层的网络模块呢。。其实让服务器支持下weboscket就okay了感觉,这么写H5方便是不是就不行了,不过没自己写过学下下 ,再次感谢楼主

1赞

服务器底层是c++的tcp,我也习惯用tcp去写客户端通信。有现成的底层通信库。

python download-deps.py python 2.7.14 报这个错误<urlopen error [SSL: TLSV1_ALERT_PROTOCOL_VERSION] tlsv1 alert protocol version
升级python版本吗 ?

竟然先做ios和windows:scream:

是不是32位的python,还有python版本的问题。不过你也可以直接去github上下载相关的代码放到本地。

CNetResponse类在iphone里有个bug,如果有用到这种方式的,请废除掉,不然iphone会闪退的。如遇到问题请联系qq1101502561

好帖:slightly_smiling:

Mark

很详细,先赞为敬:+1::+1::+1::+1::+1:

mark

赞一个!!!

mark

这个是真的强 建议 作为 单独的一个版本 发

mark

mark

服务器直接用websocket不就完事了

mark

mark