protobuf 的lua版本的安装编译和使用参考文章:
https://blog.csdn.net/qq769651718/article/details/79435298
protobuf的协议定义。
---c2s.proto
syntax = "proto2";
package c2s; //定义包名
message test { //定义消息结构
required string name = 1; //name为string类型,并且是必须的,1表示第一个字段
required int32 age = 2; //age为int32类型,并且是必须的
optional string email = 3; //email为string类型,并且是可选的
required bool online = 4; //online为bool类型,并且是必须的
required double account = 5; //account为doubleg类型,并且是必须的
}
序列化封包代码。
local protobuf = require "protobuf" --引入文件protobuf.lua
protobuf.register_file "./protos/c2s.pb" --注册pb文件
local xserialize = {}
--[[
big endian
head
2 byte body size
2 byte protonamesize
n byte protoname
body
2 byte datasize
n byte data
@desc: 将lua格式的协议序列化为二进制数据
]]
function xserialize.encode( name,data )
local stringbuffer = protobuf.encode(name, data) -- protobuf序列化 返回lua string
local body = string.pack(">s2s2",name,stringbuffer) -- 打包包体 协议名 + 协议数据
local head = string.pack(">I2",#body) -- 打包包体长度
return head .. body -- 包体长度 + 协议名 + 协议数据
end
--[[
@desc: 将二进制数据反序列化为lua string
--@msg: C Point
@return:协议名字,协议数据
]]
function xserialize.decode( msg )
--- 前两个字节在netpack.filter 已经解析
local proto_name,stringbuffer = string.unpack(">s2s2",msg)
local body = protobuf.decode(proto_name, stringbuffer)
return proto_name,body
end
return xserialize
可以看到使用包头+包体的封包格式。按大端方式进行编码。
包头 = 包体大小(2字节)
包体 = 协议名字长度(两字节) + 协议名(n字节) + 协议数据大小(两字节) +协议数据(n字节)
客户端代码:启动个协程,序列化为二进制数据直接发送。
local skynet = require "skynet"
local socket = require "skynet.socket"
local xserialize = require "xserialize"
local function send_data(fd)
while true do
local data = xserialize.encode("c2s.test",
{
name = "xiaoming",
age = 1,
email = "[email protected]",
online = true,
account = 888.88,
})
-- 发送数据
socket.write(fd,data)
skynet.sleep(100)
end
end
skynet.start(function ()
local fd = socket.open("0.0.0.0", 8001)
skynet.fork(send_data,fd)
end
)
gateserver:收到客户端的socket数据,调用netpack.filter进行解包,缺包和粘包都在filter中进行处理。dispatch_msg中的msg 就是包体的内容所以在xserialize.encode并不需要处理包头数据。gateserver 将包体转发到agent。
skynet.register_protocol {
name = "socket",
id = skynet.PTYPE_SOCKET, -- PTYPE_SOCKET = 6
unpack = function ( msg, sz )
return netpack.filter( queue, msg, sz)
end,
dispatch = function (_, _, q, type, ...)
queue = q
if type then
MSG[type](...)
end
end
}
local function dispatch_msg(fd, msg, sz)
if connection[fd] then
handler.message(fd, msg, sz)
else
skynet.error(string.format("Drop message from fd (%d) : %s", fd,
netpack.tostring(msg,sz)))
end
end
agent:收到包体数据,将 C point 转换为 lua 二进制字符串,然后调用xserialize.decode对包体进行解包,返回协议的全名和协议数据。我这里的处理是用协议名作为cmd,去调用协议的逻辑处理函数。
local skynet = require("skynet")
local xserialize = require "xserialize"
skynet.register_protocol {
name = "client",
id = skynet.PTYPE_CLIENT,
unpack = skynet.tostring, --- 将C point 转换为lua 二进制字符串
}
local CMD = {}
local REQ = {}
function REQ.test(message)
for key,value in pairs(message) do
skynet.error(key,":",value)
end
end
function CMD.close( )
skynet.exit()
end
--- 分发消息
local function dispatch_message(msg)
--- 反序列化二进制string数据
local pack_name,data = xserialize.decode(msg) -- pack_name = c2s.test
local sub_name = pack_name:match(".+%.(%w+)$") -- sub_name = test
if REQ[sub_name] then
local ret = REQ[sub_name](data)
else
skynet.error(name,"not impl")
end
end
skynet.start(function ()
skynet.dispatch("lua", function (session, address, cmd, ...)
local f = CMD[cmd]
if f then
skynet.ret(skynet.pack(f(address, ...)))
end
end)
skynet.dispatch("client", function (session, address, msg)
dispatch_message(msg)
end)
end
)
解码流程总结:
gateserver收到完整的数据包 ,调用netpack.filter进行缺包粘包处理,如果数据包完整,将包体内容(c point)转发到agent.agent收到后调用skynet.tostring将C point转换为lua 二进制string,然后xserialize.decode将二进制字符串解码为lua 数据。
我们在进行包体封装时将协议名封装进去,解包时通过协议名来确定协议的类型进行解包。由于协议名是不定长的,增加了网络传输量,可以使用4byte的msgid,来作为协议的唯一标识。只要建立一个msgid 和协议名的映射就可以了。
协议msgid 定义文件
--- 协议定义
local M = {}
local msg_map = {}
--- 客户端请求协议(协议名 --> 协议Id)
M.c2s =
{
MIN = 10000,
test = 10001, --这个名字要和proto名字声明的一样
--todo
MAX = 20000,
}
--- 服务器发送到客户端的协议(协议名 --> 协议Id)
M.s2c =
{
MIN = 30000,
-- todo
MAX = 40000,
}
--- 映射 协议Id--->协议名
for pack_name,pack_list in pairs(M) do
for sub_name,msgid in pairs(pack_list) do
msg_map[msgid] = pack_name .. "." .. sub_name
end
end
function M.get_name(msgid)
return msg_map[msgid]
end
return M
客户端修改
local msgdef = require "msgdefine"
local function send_data(fd)
while true do
local data = xserialize.encode(msgdef.c2s.test,
{
name = "xiaoming",
age = 1,
email = "[email protected]",
online = true,
account = 888.88,
})
-- 发送数据
socket.write(fd,data)
skynet.sleep(100)
end
end
序列化修改
local protobuf = require "protobuf" --引入文件protobuf.lua
protobuf.register_file "./protos/c2s.pb"
local msgdef = require "msgdefine"
local skynet = require "skynet"
local xserialize = {}
--[[
big endian
head
2 byte body size
body
2 byte msgid
2 byte datasize
n byte data
@desc: 将lua格式的协议序列化为二进制数据
]]
function xserialize.encode( msgid,data )
local msg_name = msgdef.get_name(msgid)
skynet.error(msg_name)
local stringbuffer = protobuf.encode(msg_name, data) -- protobuf序列化 返回lua string
local body = string.pack(">I2s2",msgid,stringbuffer) -- 打包包体 协议id + 协议数据
local head = string.pack(">I2",#body) -- 打包包体长度
return head .. body -- 包体长度 + 协议名 + 协议数据
end
--[[
@desc: 将二进制数据反序列化为lua string
--@msg: C Point
@return:协议名字,协议数据
]]
function xserialize.decode( msg )
--- 前两个字节在netpack.filter 已经解析
local msgid,stringbuffer = string.unpack(">I2s2",msg)
local proto_name = msgdef.get_name(msgid)
local body = protobuf.decode(proto_name, stringbuffer)
return proto_name,body
end
return xserialize
消息分发有两种方式可选择:
1.将消息id绑定一个逻辑处理函数
2.将包名,例如 c2s.test 转为 c2s_test ,c2s_test就是逻辑处理函数的函数名
更推荐使用第一种,只不过这种方式需要手动的register(msgid,callback),第二种每次收到消息时都要进行一次字符串的转换相对耗时
对于 protobuf.register_file "./protos/c2s.pb" 文件的注册,可以用批处理来处理
@echo off
setlocal enabledelayedexpansion
cd %~dp0
rem .proto file path
cd ./protos
rem out path
set out_file=../mytest/script/register.lua
set head=local protobuf=require "protobuf"
set register_str=protobuf.register_file "./protos/filename.pb"
echo %head% >%out_file%
for /R %%s in (*.proto) do (
set name1=%%~ns
rem echo !name1! >> %out_file%
echo %register_str:filename=!name1!% >> %out_file%
)
pause
处理后输出:
local protobuf=require "protobuf"
protobuf.register_file "./protos/c2s.pb"
protobuf.register_file "./protos/test.pb"