Websocek笔记三 egret + skynet使用 protobuffer

protocolbuffer是google 的一种数据交换的格式,它独立于语言,独立于平台。google 提供了多种语言的实现:java、c#c++、go 和 python,每一种实现都包含了相应语言的编译器以及库文件。由于它是一种二进制的格式,比使用 xml 进行数据交换快许多。可以把它用于分布式应用之间的数据通信或者异构环境下的数据交换。作为一种效率和兼容性都很优秀的二进制数据传输格式,可以用于诸如网络传输、配置文件、数据存储等诸多领域。 

这里使用 egret + skynet搭建了protocolbuffer传输的一个学习范例

1.Skynet使用protobuffer

安装PBC

$ cd skynet/3rd/
$ git clone https://github.com/cloudwu/pbc.git

编译安装

$ cd pbc
$ make

如果报错,安装protobuf

如果报如下错,表示protobuf未安装

make: protoc:命令未找到
Makefile:79: recipe for target 'build/addressbook.pb' failed
make: *** [build/addressbook.pb] Error 127

输入命令:

$ sudo apt-get install protobuf-c-compiler protobuf-compiler
$ protoc --version

编译:

$ make

工具编译:

$ cd ./binding/lua53
$ sudo make

如果报错,修改makefile

如果报以下错误:

$ make
gcc -O2 -fPIC -Wall -shared -o protobuf.so -I../.. -I/usr/local/include -L../../build pbc-lua.c -lpbc
pbc-lua.c:4:17: fatal error: lua.h: 没有那个文件或目录
compilation terminated.
Makefile:11: recipe for target 'protobuf.so' failed
make: *** [protobuf.so] Error 1

修改skynet/3rd/pbc/binding/lua53/makefile文件如下:

  CC = gcc
  CFLAGS = -O2 -fPIC -Wall
  LUADIR = ../../../lua   #这个路劲就是skynet/3rd/lua
  TARGET = protobuf.so
  
  .PHONY : all clean
  
  all : $(TARGET)
  
  $(TARGET) : pbc-lua53.c
      $(CC) $(CFLAGS) -shared -o $@ -I../.. -I$(LUADIR) -L../../build $^ -lpbc
  
  clean :
      rm -f $(TARGET)

复制文件

protobuf.so放在config文件中lua_cpath项配置的目录下面,同时将protobuf.lua放在config文件lua_path配置的目录下,就可以调用protobuf中的库方法

$ cp protobuf.so ../../../../luaclib/
$ cp protobuf.lua ../../../../lualib/

生成protobuffer文件

用sublime新建一个文件在skynet/protos/test.proto

syntax = "proto2";
package cs;
message Person{
	required string name = 1;
	required int32 id = 2;
	optional string email = 3;
 
	enum PhoneType {
		MOBILE = 0;
		HOME = 1;
		WORK = 2;
	}
 
	message PhoneNumber {
		required string number = 1;
		optional PhoneType type = 2 [default = HOME];
	}
	repeated PhoneNumber phone = 4;
}

将协议文件解析成.pb格式

protoc --descriptor_set_out=test.pb test.proto

使用protobuffer文件,范例1

使用方法:

local protobuf = require "protobuf" --引入文件protobuf.lua
--注册protobuffer文件
protobuf.register_file(protofile)
​
--根据注册的protofile中的类定义进行序列化,返回得到一个stringbuffer
protobuf.encode("package.message", { ... })
​
--根据注册的protofile中的类定义进行反序列化
protobuf.decode("package.message", stringbuffer)

示例代码:skynet/test/testpbc.lua

local skynet = require "skynet"
local protobuf = require "protobuf"

skynet.start(function()
	protobuf.register_file "./protos/test.pb"
	skynet.error("protobuf register: test.pb")

	stringbuffer = protobuf.encode("cs.test",
	{
		name = "xiaoming",
		age = 1,
		online = true,
		account = 888.88,
	})

	local data = protobuf.decode("cs.test",stringbuffer)
	skynet.error("------decode-----\nname=",data.name
	,",\name=",data.age
	,",\nemail=",data.email
	,".\nonline=",data.online
	,".\naccount=",data.account)
end)

运行结果: 

使用protobuffer文件,范例2

新建一个proto文件skynet/protos/person.proto

syntax = "proto2";
package cs;
message Person{
	required string name = 1;
	required int32 id = 2;
	optional string email = 3;

	enum PhoneType {
		MOBILE = 0;
		HOME = 1;
		WORK = 2;
	}

	message PhoneNumber {
		required string number = 1;
		optional PhoneType type = 2 [default = HOME];
	}
	repeated PhoneNumber phone = 4;
}

将协议文件解析成.pb格式:

protoc --descriptor_set_out=person.pb person.proto

示例代码 testpbc.lua

local skynet = require "skynet"
local protobuf = require "protobuf"

skynet.start(function()
	protobuf.register_file "./protos/person.pb"
	skynet.error("protobuf register: person.pb")

	stringbuffer = protobuf.encode("cs.Person",
	{
		name = "xiaoming",
		id = 1,
		email = "xiaoming@163.com",
		phone = {
			{
				number = "1388888888",
				type = "MOBILE",
			},
			{
				number = "8888888",
			},
			{
				number = "87878787",
				type = "WORK",
			},

		}
	})

	local data = protobuf.decode("cs.Person",stringbuffer)
	skynet.error("decode name="..data.name..",id="..data.id..",email="..data.email)
	skynet.error("decode phone.type="..data.phone[1].type..",phone.number="..data.phone[1].number)
	skynet.error("decode phone.type="..data.phone[2].type..",phone.number="..data.phone[2].number)
	skynet.error("decode phone.type="..data.phone[3].type..",phone.number="..data.phone[3].number)
end)

运行结果 

protobuf数据类型

2.Egret使用protobuf

egret官方提供的工具

官方github梗概:

安装 node.js  以及 npm

登陆官网(http://nodejs.org/),便可以看到首页的“INSTALL”按钮,直接点击就会自动下载安装了

cmd安装protobuf


npm install protobufjs@6.8.4 -g
npm install @egret/protobuf -g

 

控制台切换到Test项目目录,执行拷贝

例如我的目录:E:\Projects\EgretProjects\protobufTest

切换到e盘:

E:

进入目标目录 :

cd E:\Projects\EgretProjects\protobufTest

 执行拷贝操作:

pb-egret add

 

新建一个person.proto文件

放在项目目录/protobuf/protofile/person.proto

syntax = "proto2";
package cs;
message Person{
	required string name = 1;
	required int32 id = 2;
	optional string email = 3;

	enum PhoneType {
		MOBILE = 0;
		HOME = 1;
		WORK = 2;
	}

	message PhoneNumber {
		required string number = 1;
		optional PhoneType type = 2 [default = HOME];
	}
	repeated PhoneNumber phone = 4;
}

生成可以直接引用的库文件

之前的控制台执行:

pb-egret generate

这里的含义是,把之前准备好的协议文件,person.proto,转换为随项目加载的js静态代码文件。

之前有一种把.proto文件当成资源动态加载进来,然后解析使用的方法,有加载慢的缺点,所以被这个方法替代了。

配置 egretProperties.json

好像是自动会配置好protobuf相关的依赖

{
		"engineVersion": "5.2.13",
		"compilerVersion": "5.2.13",
		"template": {},
		"target": {
				"current": "web"
		},
		"modules": [
				{
						"name": "egret"
				},
				{
						"name": "game"
				},
				{
						"name": "tween"
				},
				{
						"name": "assetsmanager"
				},
				{
						"name": "socket"
				},
				{
						"name": "eui"
				},
				{
						"name": "promise"
				},
				{
						"name": "protobuf-library",
						"path": "protobuf/library"
				},
				{
						"name": "protobuf-bundles",
						"path": "protobuf/bundles"
				}
		]
}

重新编译引擎

使用方法

在Main.ts中进行测试:

        //init cs.Person
        let person: cs.Person = new cs.Person();
        let pwirter:protobuf.Writer = new protobuf.Writer();
        person.name = "123";
        console.log("protobuf生成打印", person.name);

        //encode cs.Person
        let sendByte = cs.Person.encode(person).finish();

        //websocket send
        let byteArray:egret.ByteArray = new egret.ByteArray(sendByte);
        let socket:egret.WebSocket = new egret.WebSocket();
        socket.writeBytes(byteArray);
        socket.flush();

        //decode cs.Person
        let decPerson:cs.Person = cs.Person.decode(sendByte);
        console.log("protobuf解码",decPerson.name);  //输出

 

3.Skynet与Egret客户端测试

服务端代码

(添加了websocket库的引用。skynet的使用以及websocket的使用,可以参考作者之前的博客)

skynet/test/testpbc.lua

local skynet = require "skynet"
local socket = require "skynet.socket"
local websocket = require "websocket"
local httpd = require "http.httpd"
local urllib = require "http.url"
local sockethelper = require "http.sockethelper"
local protobuf = require "protobuf"

local handler = {}
function handler.on_open(ws)
	--encord Message
    print(string.format("%d::open", ws.id))
    --register Timer
    skynet.fork(function()

    	protobuf.register_file "./protos/person.pb"
		skynet.error("protobuf register: person.pb")
        while true do
            -- ws:send_text("heart" .. "from server")
			stringbuffer = protobuf.encode("cs.Person",
			{
				name = "xiaoming",
				id = 1,
				email = "xiaoming@163.com",
				phone = {
					{
						number = "1388888888",
						type = "MOBILE",
					},
					{
						number = "8888888",
					},
					{
						number = "87878787",
						type = "WORK",
					},
				}
			})
            ws:send_binary(stringbuffer)
            skynet.sleep(500)
        end
    end)
end

function handler.on_message(ws, message)
	--decord Message
    print(string.format("%d receive:%s", ws.id, message))
    local data = protobuf.decode("cs.Person",message)
	skynet.error("decode name="..data.name..",id="..data.id..",email="..data.email)
	skynet.error("decode phone.type="..data.phone[1].type..",phone.number="..data.phone[1].number)
	skynet.error("decode phone.type="..data.phone[2].type..",phone.number="..data.phone[2].number)
	skynet.error("decode phone.type="..data.phone[3].type..",phone.number="..data.phone[3].number)

end

function handler.on_close(ws, code, reason)
    print(string.format("%d close:%s  %s", ws.id, code, reason))
end

local function handle_socket(id)
    -- limit request body size to 8192 (you can pass nil to unlimit)
    local code, url, method, header, body = httpd.read_request(sockethelper.readfunc(id), 8192)
    if code then
        
        if header.upgrade == "websocket" then
            local ws = websocket.new(id, header, handler)
            ws:start()
        end
    end


end

skynet.start(function()
    local address = "0.0.0.0:8001"
    skynet.error("Listening "..address)
    local id = assert(socket.listen(address))
    socket.start(id , function(id, addr)
       socket.start(id)
       pcall(handle_socket, id)
    end)
end)

其中handler.on_open(ws)函数包含了向客户端发送encord消息的相关代码。

handler.on_message(ws, message)函数包含了服务端收到客户端消息,并进行decode的相关代码

客户端代码

 

白鹭Test项目的目录: \src\ProtobufTest.ts

class ProtobuffTest {
    private webSocket: egret.WebSocket;

    public constructor(){
        this.initSocket();
        // this.initTest();
    }

    private initSocket(){
        //官方示例WebSocket
        this.webSocket = new egret.WebSocket();
        this.webSocket.type = egret.WebSocket.TYPE_BINARY;        
        this.webSocket.addEventListener(egret.ProgressEvent.SOCKET_DATA, this.onReceiveMessage, this);
        this.webSocket.addEventListener(egret.Event.CONNECT, this.onSocketOpen, this);
        this.webSocket.connect("192.168.137.151", 8001);//这里我填写的skynet虚拟机的ip地址,以及skynet配置的端口
    }

    /**
     * 收到信息后
     */
    private onReceiveMessage():void{
        //创建 ByteArray 对象
        var byte:egret.ByteArray = new egret.ByteArray();
        //读取数据
        this.webSocket.readBytes(byte);
        let decPerson:cs.Person = cs.Person.decode(byte.bytes);
        console.log("protobuf解码",decPerson);  //输出
    }

    /**
     * 连接成功之后,发送一条protobuf协议
     */
    private onSocketOpen():void{
        console.log("连接成功!发送protobuf信息!");
        let person: cs.Person = new cs.Person();
        let pwirter:protobuf.Writer = new protobuf.Writer();
        person.name = "123";
        //encode cs.Person
        let sendByte = cs.Person.encode(person).finish();
        //websocket send
        let byteArray:egret.ByteArray = new egret.ByteArray(sendByte);
        this.webSocket.writeBytes(byteArray);
        this.webSocket.flush();
    }
}

在Main.ts的createGameScene()函数中添加上测试脚本:

    /**
     * 创建游戏场景
     * Create a game scene
     */
    private createGameScene() {
        //启动测试
        let test:ProtobuffTest = new ProtobuffTest();
    }

运行测试

客户端:

服务端:

源代码:

在我的资源目录

4.客户端精简protobuf生成的js文件

打开protobuf-bundles.js,会发现Person里有有以下方法 

create
encode
encodeDelimited
decode
decodeDelimited
verify

我们可以对该生成规则进行精简,在生成js文件时,生成指定的方法,减少文件大小.

精简生成文件

打开客户端项目protobuf/pbconfig.json

修改代码如下:(意思就是不生成这些函数)

接着用终端重新进入客户端项目目录,执行: 

pb-egret generate

接着会自动把 \protobuf\protofile\person.proto的内容生成相应的js文件

 

打开protobuf-bundles.d.ts发现没有之前create,verify这些多余的函数了

接着重新编译引擎,重新启动项目

5.给protobuf的字节流添加信息头

由于客户端需要对接收到的encode字节流进行判断,这是proto中的什么类型,然后才进行decode。所以服务端这边encode好的字节流的头部需要添加一个unsigned short字节流,代表消息类型,或者需要添加其他的什么信息都可以。

Skynet服务端代码

关键lua代码:

			protobuf.register_file "./protos/person.pb"
			skynet.error("protobuf register: person.pb")
			stringbuffer = protobuf.encode("cs.Person",
			{
				name = "xiaoming",
				id = 1,
				email = "xiaoming@163.com",
				phone = {
					{
						number = "1388888888",
						type = "MOBILE",
					},
					{
						number = "8888888",
					},
					{
						number = "87878787",
						type = "WORK",
					},
				}
			})
			local x = string.pack("H",1230);
			stringbuffer = x..stringbuffer;
			ws:send_binary(stringbuffer)

其中stringbuffer是一个encode好的protobuf字节流,而x是一个字节流化的unsigned Short数字1230.

测试用例代码:/skynet/test/testpbc.lua

local skynet = require "skynet"
local socket = require "skynet.socket"
local websocket = require "websocket"
local httpd = require "http.httpd"
local urllib = require "http.url"
local sockethelper = require "http.sockethelper"
local protobuf = require "protobuf"

local handler = {}
function handler.on_open(ws)
	--encord Message
    print(string.format("%d::open", ws.id))
    --register Timer
    skynet.fork(function()

    	protobuf.register_file "./protos/person.pb"
		skynet.error("protobuf register: person.pb")
        while true do
            -- ws:send_text("heart" .. "from server")
			stringbuffer = protobuf.encode("cs.Person",
			{
				name = "xiaoming",
				id = 1,
				email = "xiaoming@163.com",
				phone = {
					{
						number = "1388888888",
						type = "MOBILE",
					},
					{
						number = "8888888",
					},
					{
						number = "87878787",
						type = "WORK",
					},
				}
			})
			local x = string.pack("H",1230);
			stringbuffer = x..stringbuffer;
            ws:send_binary(stringbuffer)
            skynet.sleep(500)
        end
    end)
end

function handler.on_message(ws, message)
	--decord Message
    print(string.format("%d receive:%s", ws.id, message))
    local data = protobuf.decode("cs.Person",message)
	skynet.error("decode name="..data.name..",id="..data.id..",email="..data.email)
	skynet.error("decode phone.type="..data.phone[1].type..",phone.number="..data.phone[1].number)
	skynet.error("decode phone.type="..data.phone[2].type..",phone.number="..data.phone[2].number)
	skynet.error("decode phone.type="..data.phone[3].type..",phone.number="..data.phone[3].number)

end

function handler.on_close(ws, code, reason)
    print(string.format("%d close:%s  %s", ws.id, code, reason))
end

local function handle_socket(id)
    -- limit request body size to 8192 (you can pass nil to unlimit)
    local code, url, method, header, body = httpd.read_request(sockethelper.readfunc(id), 8192)
    if code then
        
        if header.upgrade == "websocket" then
            local ws = websocket.new(id, header, handler)
            ws:start()
        end
    end


end

skynet.start(function()
    local address = "0.0.0.0:8001"
    skynet.error("Listening "..address)
    local id = assert(socket.listen(address))
    socket.start(id , function(id, addr)
       socket.start(id)
       pcall(handle_socket, id)
    end)
end)

Egreit客户端代码

核心代码

    /**
     * 收到信息后
     */
    private onReceiveMessage():void{
        //创建 ByteArray 对象
        var WholeBytes:egret.ByteArray = new egret.ByteArray();
        //读取数据
        this.webSocket.readBytes(WholeBytes);
        //由于skynet lua 中string.pack打包的字节流是LITTLE_ENDIAN小端模式,而protobuf采用大端模式,所以消息头和消息体需要分开读取.

        //读取消息头   
        let headBytes:egret.ByteArray = new egret.ByteArray();
        headBytes.endian = egret.Endian.LITTLE_ENDIAN;//设置消息头读取方式为小端
        WholeBytes.readBytes(headBytes,null,2);//这里第三个参数2填写的是UnsignedShort的长度,16bit = 2 * Uni8-bite,这个可以让服务端发单独发送一个数据,这边打印WholeBytes查看长度length
        var mainId = headBytes.readUnsignedShort();
        console.log("解码的type=",mainId,typeof(mainId));

        //读取消息体,默认大端模式
        let bodyBytes:egret.ByteArray = new egret.ByteArray();
        WholeBytes.readBytes(bodyBytes);
        let decPerson:cs.Person = cs.Person.decode(bodyBytes.bytes);
        console.log("protobuf解码",decPerson);  //输出
    }

由于skynet lua 中string.pack打包的字节流是LITTLE_ENDIAN小端模式,而protobuf打包后的字节流采用大端模式,所以消息头和消息体需要分开读取.

测试用例代码:

\src\ProtobufTest.ts

class ProtobuffTest {
    private webSocket: egret.WebSocket;

    public constructor(){
        this.initSocket();
        // this.initTest();
    }

    private initSocket(){
        //官方示例WebSocket
        this.webSocket = new egret.WebSocket();
        this.webSocket.type = egret.WebSocket.TYPE_BINARY;        
        this.webSocket.addEventListener(egret.ProgressEvent.SOCKET_DATA, this.onReceiveMessage, this);
        this.webSocket.addEventListener(egret.Event.CONNECT, this.onSocketOpen, this);
        this.webSocket.connect("192.168.137.151", 8001);//192.168.137.151 vware  47.100.42.13 Aliyun
    }

    /**
     * 收到信息后
     */
    private onReceiveMessage():void{
        //创建 ByteArray 对象
        var WholeBytes:egret.ByteArray = new egret.ByteArray();
        //读取数据
        this.webSocket.readBytes(WholeBytes);
        //由于skynet lua 中string.pack打包的字节流是LITTLE_ENDIAN小端模式,而protobuf采用大端模式,所以消息头和消息体需要分开读取.

        //读取消息头   
        let headBytes:egret.ByteArray = new egret.ByteArray();
        headBytes.endian = egret.Endian.LITTLE_ENDIAN;//设置消息头读取方式为小端
        WholeBytes.readBytes(headBytes,null,2);//这里第三个参数2填写的是UnsignedShort的长度,16bit = 2 * Uni8-bite,这个可以让服务端发单独发送一个数据,这边打印WholeBytes查看长度length
        var mainId = headBytes.readUnsignedShort();
        console.log("解码的type=",mainId,typeof(mainId));

        //读取消息体,默认大端模式
        let bodyBytes:egret.ByteArray = new egret.ByteArray();
        WholeBytes.readBytes(bodyBytes);
        let decPerson:cs.Person = cs.Person.decode(bodyBytes.bytes);
        console.log("protobuf解码",decPerson);  //输出
    }

    /**
     * 连接成功之后,发送一条protobuf协议
     */
    private onSocketOpen():void{
        console.log("连接成功!发送protobuf信息!");
        let person: cs.Person = new cs.Person();
        let pwirter:protobuf.Writer = new protobuf.Writer();
        person.name = "123";
        //encode cs.Person
        let sendByte = cs.Person.encode(person).finish();
        //websocket send
        let byteArray:egret.ByteArray = new egret.ByteArray(sendByte);
        this.webSocket.writeBytes(byteArray);
        this.webSocket.flush();
    }

    private initTest():void{
        let person: cs.Person = new cs.Person();
        let pwirter:protobuf.Writer = new protobuf.Writer();
        person.name = "123";

        console.log("protobuf生成打印", person.name);

        //encode cs.Person
        let sendByte = cs.Person.encode(person).finish();
        //websocket send
        let byteArray:egret.ByteArray = new egret.ByteArray(sendByte);
        let socket:egret.WebSocket = new egret.WebSocket();
        socket.writeBytes(byteArray);
        socket.flush();

        //decode cs.Person
        let decPerson:cs.Person = cs.Person.decode(sendByte);
        console.log("protobuf解码",decPerson.name);  //输出
    }
}

在Main.ts中测试:

    /**
     * 创建游戏场景
     * Create a game scene
     */
    private createGameScene() {
        //启动测试
        let test:ProtobuffTest = new ProtobuffTest();
    }

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值