skynet提供一套與客戶端通訊的協議sproto,設計簡單,有利于lua使用,參考官方wiki https://github.com/cloudwu/skynet/wiki/Sproto,本篇介紹組裝".sproto"檔案以及sproto構建流程,之后,會另寫一篇介紹sproto的使用方法,
1. 組裝.sproto檔案流程
以下面簡單的test.sproto檔案為例介紹.sproto檔案組裝流程:
-- test.sproto
.Person {
name 0 : string
id 1 : integer
email 2 : string
.PhoneNumber {
number 0 : string
type 1 : integer
}
phone 3 : *PhoneNumber
}
.AddresBook {
person 0 : *Person
}
proto1 1001 {
request {
p 0 : integer
}
response {
ret 0 : *Person
}
}
通過sparser.parse api組裝.sproto檔案,引數text即test.sproto檔案的內容:
-- lualib/sprotoparser.lua
function sparser.parse(text, name)
local r = parser(text, name or "=text")
dump(r)
local data = encodeall(r)
sparser.dump(data)
return data
end
第3-4行,通過lpeg庫將.sproto檔案分析轉化成一個lua表,部分結果如下,包含protocol和type兩大類,protocol里包含所有的協議,每個協議有request,response,tag三個key;type里包含所有的型別,每個型別有1個或多個域(field),每個field里包含name,tag,typename等資訊,
"protocol" = {
"proto1" = {
"request" = "proto1.request"
"response" = "proto1.response"
"tag" = 1001
}
}
"type" = {
"AddresBook" = {
1 = {
"array" = true
"name" = "person"
"tag" = 0
"typename" = "Person"
}
}
"Person" = {
1 = {
"name" = "name"
"tag" = 0
"typename" = "string"
}
...
第5-6行,把lua表按特定格式組裝成二進制資料后,結果如下(每一行16個位元組):
02 00 00 00 00 00 65 01 - 00 00 32 00 00 00 02 00
00 00 00 00 0A 00 00 00 - 41 64 64 72 65 73 42 6F
6F 6B 1A 00 00 00 16 00 - 00 00 05 00 00 00 01 00
04 00 02 00 04 00 06 00 - 00 00 70 65 72 73 6F 6E
6E 00 00 00 02 00 00 00 - 00 00 06 00 00 00 50 65
72 73 6F 6E 5A 00 00 00 - 12 00 00 00 04 00 00 00
06 00 01 00 02 00 04 00 - 00 00 6E 61 6D 65 10 00
00 00 04 00 00 00 02 00 - 01 00 04 00 02 00 00 00
69 64 13 00 00 00 04 00 - 00 00 06 00 01 00 06 00
05 00 00 00 65 6D 61 69 - 6C 15 00 00 00 05 00 00
00 01 00 06 00 08 00 04 - 00 05 00 00 00 70 68 6F
6E 65 4E 00 00 00 02 00 - 00 00 00 00 12 00 00 00
50 65 72 73 6F 6E 2E 50 - 68 6F 6E 65 4E 75 6D 62
65 72 2E 00 00 00 14 00 - 00 00 04 00 00 00 06 00
01 00 02 00 06 00 00 00 - 6E 75 6D 62 65 72 12 00
00 00 04 00 00 00 02 00 - 01 00 04 00 04 00 00 00
74 79 70 65 2F 00 00 00 - 02 00 00 00 00 00 0E 00
00 00 70 72 6F 74 6F 31 - 2E 72 65 71 75 65 73 74
13 00 00 00 0F 00 00 00 - 04 00 00 00 02 00 01 00
02 00 01 00 00 00 70 34 - 00 00 00 02 00 00 00 00
00 0F 00 00 00 70 72 6F - 74 6F 31 2E 72 65 73 70
6F 6E 73 65 17 00 00 00 - 13 00 00 00 05 00 00 00
01 00 04 00 02 00 04 00 - 03 00 00 00 72 65 74 18
00 00 00 14 00 00 00 04 - 00 00 00 D4 07 08 00 0A
00 06 00 00 00 70 72 6F - 74 6F 31
通過這個結果(下面稱為result)反推sproto組裝流程:
第2-4行,按“<s4”格式打包字串,即字串長度占4個位元組,按小端格式打包在頭部,再加上字串內容,
第14-22行,result前6個位元組分別是"\2\0\0\0\0\0",接下來分別是type的組裝結果(tt)和protocol的組裝結果(tp),result7-10個位元組是65 01 00 00,為type組裝后的長度357(6*2^4+5+1*2^16), result從368個位元組開始是protocol的組裝結果,368-371個位元組是18 00 00 00,表示protocol的組裝結果有24個位元組(1*2^4+8),即最后24個位元組,
-- lualib/sprotoparser.lua
function packbytes(str)
return string.pack("<s4",str)
end
local function encodeall(r)
return packgroup(r.type, r.protocol)
end
local function packgroup(t,p)
...
tt = packbytes(table.concat(tt))
tp = packbytes(table.concat(tp))
result = {
"\2\0", -- 2fields
"\0\0", -- type array (id = 0, ref = 0)
"\0\0", -- protocol array (id = 1, ref =1)
tt,
tp,
}
return table.concat(result)
end
type組裝結果共有357個位元組(11-367):按字典升序遍歷所有type,依次呼叫packtype進行組裝,
第一個type是“AddresBook”:result11-14個位元組是32 00 00 00,表示“AddresBook”組裝結果有50個位元組(3*2^4+2)(第21行),因為有1個field,result15-20個位元組是02 00 00 00 00 00(第13-15行),緊接著是第16行packbytes("AddresBook"),長度是10,結果是 0A 00 00 00 41(A) 64(d) 64(d) 72(r) 65(e) 73(s) 42(B) 6F(o) 6F(o) 6B(k),即result的21-34個位元組,接下來第35-38的四個位元組1A 00 00 00,是"AddresBook"的所有field組裝后長度26(1*2^4+10),
-- lualib/sprotoparser.lua
local function packtype(name, t, alltypes) -- 組裝每一個type
...
local data
if #fields == 0 then
data = {
"\1\0", -- 1 fields
"\0\0", -- name (id = 0, ref = 0)
packbytes(name),
}
else
data = {
"\2\0", -- 2 fields
"\0\0", -- name (tag = 0, ref = 0)
"\0\0", -- field[] (tag = 1, ref = 1)
packbytes(name),
packbytes(table.concat(fields)),
}
end
return packbytes(table.concat(data))
end
field組裝流程,result第39-42的4個位元組16 00 00 00,是第一個field的組裝結果長度22(1*2^4+6),即result的43-64個位元組,由AddresBook的資料可知,組裝流程是:
第8行, 05 00
第13行,00 00
第23行,01 00
第24行,04 00, f.type=1
第25行,02 00, f.tag=0
第28行,04 00
第33行,06 00 00 00 70(p) 65(e) 72(r) 73(s) 6F(o) 6E(n),name="person",正好對應result的43-64個位元組,
"AddresBook" = {
1 = {
"array" = true
"name" = "person"
"tag" = 0
"typename" = "Person"
}
}
-- lualib/sprotoparser.lua
local function packfield(f) -- 組裝每一個field
local strtbl = {}
if f.array then
if f.key then
table.insert(strtbl, "\6\0") -- 6 fields
else
table.insert(strtbl, "\5\0") -- 5 fields
end
else
table.insert(strtbl, "\4\0") -- 4 fields
end
table.insert(strtbl, "\0\0") -- name (tag = 0, ref an object)
if f.buildin then
table.insert(strtbl, packvalue(f.buildin)) -- buildin (tag = 1)
if f.extra then
table.insert(strtbl, packvalue(f.extra)) -- f.buildin can be integer
or string
else
table.insert(strtbl, "\1\0") -- skip (tag = 2)
end
table.insert(strtbl, packvalue(f.tag)) -- tag (tag = 3)
else
table.insert(strtbl, "\1\0") -- skip (tag = 1)
table.insert(strtbl, packvalue(f.type)) -- type (tag = 2)
table.insert(strtbl, packvalue(f.tag)) -- tag (tag = 3)
end
if f.array then
table.insert(strtbl, packvalue(1)) -- array = true (tag = 4)
end
if f.key then
table.insert(strtbl, packvalue(f.key)) -- key tag (tag = 5)
end
table.insert(strtbl, packbytes(f.name)) -- external object (name)
return packbytes(table.concat(strtbl))
end
接下來,依次組裝其他type,組裝完type,然后呼叫packproto組裝每一個proto,result372-375四個位元組14 00 00 00,表示"proto1"組裝后的長度20(1*2^4+4),
"proto1" = {
"request" = "proto1.request"
"response" = "proto1.response"
"tag" = 1001
}
組裝流程如下:
第10-12行, 04 00 00 00 D4 07
第18行,08 00 (alltypes[p.request].id=3)
第24行,0A 00 (alltypes[p.response].id=4)
第35行,name="proto1",長度是6,打包后是 06 00 00 00 70(p) 72(r) 6F(o) 74(t) 6F(0) 31(1),正好對應result的最后20個位元組,
-- lualib/sprotoparser.lua
local function packproto(name, p, alltypes) -- 組裝每一個proto
if p.request then
local request = alltypes[p.request]
if request == nil then
error(string.format("Protocol %s request type %s not found", name, p.request))
end
request = request.id
end
local tmp = {
"\4\0", -- 4 fields
"\0\0", -- name (id=0, ref=0)
packvalue(p.tag), -- tag (tag=1)
}
if p.request == nil and p.response == nil and p.confirm == nil then
tmp[1] = "\2\0" -- only two fields
else
if p.request then
table.insert(tmp, packvalue(alltypes[p.request].id)) -- request typename (tag=2)
else
table.insert(tmp, "\1\0")-- skip this field (request)
end
if p.response then
table.insert(tmp, packvalue(alltypes[p.response].id)) -- request typename (tag=3)
elseif p.confirm then
tmp[1] = "\5\0" -- add confirm field
table.insert(tmp, "\1\0")
-- skip this field (response)
table.insert(tmp, packvalue(1)) -- confirm = true
else
tmp[1] = "\3\0" -- only three fields
end
end
table.insert(tmp, packbytes(name))
return packbytes(table.concat(tmp))
end
小結:組裝.sproto檔案流程如下:
(1). 用lpeg庫決議.sproto檔案內容,把資訊保存在一個lua表里
(2). 依次組裝所有types,對每一個type先組裝名稱,再組裝它的fields
(3). 依次組裝所有protos
最后組裝出的二進制塊是由N個type和N個proto組成,每個type又包含name和N個field,每個field包含name、buildin、type、tag、array等資訊;每個proto包含name、tag、request、response等資訊,不論是field,type還是proto,都會加一些位元組前綴,用來表示接下來位元組的資訊,格式如下:
2. sproto構建流程
當把.sproto檔案組裝成二進制塊后,sproto構建就是決議這個二進制塊,了解了組裝程序后,決議程序就是把組裝程序倒過來,最后把決議結果保存在c結構里,通過lua層newproto,最侄訓呼叫到create_from_bundle 這個api來構建sproto,三個引數:s構建后的sproto保存在這個結構里,stream組裝的二進制資料塊,sz長度,
第19行,struct_field api計算前綴,不同的前綴接下來的資料含義不同
第23行,count_array api計算數目,比如計算types的總數,計算protos的總數,每個type中fields的總數
第26-29行,保存types總數s->type_n
第31-34行,保存protos總數s->protocol_n
第38-43行,通過import_type api構建每一個type的資料,保存在s->type這個陣列里
第44-49行,通過import_protocol api構建每一個proto的資料,保存在s->proto這個陣列里
// lualib/sproto/sproto.c
struct sproto *
sproto_create(const void * proto, size_t sz) {
...
if (create_from_bundle(s, proto, sz) == NULL) {
pool_release(&s->memory);
return NULL;
}
return s;
}
static struct sproto *
create_from_bundle(struct sproto *s, const uint8_t* stream, size_t sz) {
...
int fn = struct_field(stream, sz);
int i;
...
for (i=0;i<fn;i++) {
int value = toword(stream + i*SIZEOF_FIELD);
int n;
if (value != 0)
return NULL;
n = count_array(content);
if (n<0)
return NULL;
if (i == 0) {
typedata = content+SIZEOF_LENGTH;
s->type_n = n;
s->type = pool_alloc(&s->memory, n * sizeof(*s->type));
} else {
protocoldata = content+SIZEOF_LENGTH;
s->protocol_n = n;
s->proto = pool_alloc(&s->memory, n * sizeof(*s->proto));
}
content += todword(content) + SIZEOF_LENGTH;
}
for (i=0;i<s->type_n;i++) {
typedata = import_type(s, &s->type[i], typedata);
if (typedata == NULL) {
return NULL;
}
}
for (i=0;i<s->protocol_n;i++) {
protocoldata = import_protocol(s, &s->proto[i], protocoldata);
if (protocoldata == NULL) {
return NULL;
}
}
return s;
}
sproto資料結構如下:
// lualib/sproto/sproto.c
struct sproto { // 整個sproto結構
struct pool memory;
int type_n; // types總數
int protocol_n; // proto總數
struct sproto_type * type; // N個type資訊
struct protocol * proto; // N個proto資訊
};
struct sproto_type { // 單個type結構
const char * name; // 名稱
int n; // fields實際個數
int base; //如果tag是連續的,為最小的tag,否則是-1
int maxn; //fields實際個數+最小的tag+不連續的tag個數,比如tag依次是1,3,5,則maxn=3+1+2=6
struct field *f; // N個field資訊
};
struct field { // 單個field結構
int tag; //唯一的tag
int type; // 型別,可以是內置的integer,string,boolean,也可以是自定義的type,也可以是陣列
const char * name; // 名稱
struct sproto_type * st; //如果是自定義的型別,st指向這個型別
int key;
int extra;
};
struct protocol { //單個proto結構
const char *name; //名稱
int tag; //唯一的tag
int confirm; // confirm == 1 where response nil
struct sproto_type * p[2]; //request,response的型別
};
通過sproto_dump api,列印出構建后的sproto的資訊如下:
=== 5 types ===
AddresBook 1 0 1
person (0) *Person
Person 4 0 4
name (0) string
id (1) integer
email (2) string
phone (3) *Person.PhoneNumber
Person.PhoneNumber 2 0 2
number (0) string
type (1) integer
proto1.request 1 0 1
p (0) integer
proto1.response 1 0 1
ret (0) *Person
=== 1 protocol ===
proto1 (1001) request:proto1.request response:proto1.response
構建成功后,呼叫saveproto將sproto保存在全域陣列G_sproto中,供所有lua VM加載(loadproto)使用,
這就是sproto的決議和構建流程,接下來會寫一篇文章介紹sproto如何使用,
在2021年1月13/14號我會開一個四小時玩轉skynet訓練營,也就是兩個禮拜之后,現在已經開放報名,對游戲開發感興趣的諸位同好可以訂閱一下,
訓練營內容大概如下:
1. 多核并發編程
2. 訊息佇列,執行緒池
3. actor訊息調度
4. 網路模塊實作
5. 時間輪定時器實作
6. lua/c介面編程
7. skynet編程精要
8. demo演示actor編程思維
期待大家一起來打造游戲開發的技術盛宴,
憑借報名截圖可以進群973961276領取上一期skynet訓練營的錄播以及這期的預習資料哦!
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/242945.html
標籤:其他
上一篇:golang 猜數字小游戲
