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"