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" 

 

發佈了45 篇原創文章 · 獲贊 4 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章