skynet protobuf的使用

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" 

 

你可能感兴趣的:(skynet)