Websocek笔记三 egret + skynet使用 protobuffer
protocolbuffer是google 的一种数据交换的格式,它独立于语言,于平台。google 提供了多种语言的实现:java、、、go 和 ,每一种实现都包含了相应语言的以及库文件。由于它是一种二进制的格式,比使用 进行数据交换快许多。可以把它用于之间的数据通信或者异构环境下的数据交换。作为一种效率和兼容性都很优秀的二进制数据传输格式,可以用于诸如网络传输、配置文件、数据存储等诸多领域。
这里使用 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 = "[email protected]",
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
官方github梗概:
安装 node.js 以及 npm
登陆官网(),便可以看到首页的“INSTALL”按钮,直接点击就会自动下载安装了
cmd安装protobuf
npm install [email protected] -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 = "[email protected]",
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 = "[email protected]",
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 = "[email protected]",
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();
}