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 파일을 어셈블합니다. 매개 변수 텍스트는 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 행에서 .sproto 파일은 lpeg 라이브러리를 통해 분석되고 lua 테이블로 변환됩니다. 결과의 일부는 프로토콜과 유형의 두 범주를 포함하여 다음과 같습니다. 프로토콜에는 모든 프로토콜이 포함되며 각 프로토콜에는 요청, 응답 및 태그의 세 가지 키가 있습니다. 유형에는 모든 유형이 포함되며 각 유형에는 하나 이상의 필드가 있습니다. 각 필드에는 이름, 태그, 유형 이름 및 기타 정보가 포함됩니다.
"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
이 결과를 통해 sproto 조립 프로세스를 반대로 수행합니다 (아래 결과 참조).
2-4 행에서 문자열은 "<s4"형식에 따라 압축됩니다. 즉, 문자열 길이가 4 바이트를 차지하고 리틀 엔디안 형식에 따라 헤더에 압축되고 문자열 내용이 추가됩니다.
14-22 행에서 결과의 처음 6 바이트는 "\ 2 \ 0 \ 0 \ 0 \ 0 \ 0"이고 다음은 유형 (tt)의 어셈블리 결과와 프로토콜 (tp)의 어셈블리 결과입니다. 결과 7-10 바이트는 65 01 00 00이며, 이는 유형 357 (6 * 2 ^ 4 + 5 + 1 * 2 ^ 16)의 조합 된 길이입니다. 결과는 368 바이트부터 시작하는 프로토콜의 어셈블리 결과이며 368-371 바이트는 18 00 00 00이며 이는 프로토콜의 어셈블리 결과가 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
형식 어셈블리 결과에는 총 357 바이트 (11-367)가 있습니다. 모든 형식을 오름차순 사전 순서로 탐색 한 다음 어셈블리를 위해 packtype을 호출합니다.
첫 번째 유형은 "AddresBook"입니다. result11-14 바이트는 32 00 00 00입니다. 즉, "AddresBook"의 어셈블리 결과는 1 개의 Field가 있으므로 50 바이트 (3 * 2 ^ 4 + 2) (21 행)입니다. , 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), 결과의 21-34 바이트입니다. 35에서 38까지의 다음 4 바이트 인 1A 00 00 00은 "AddresBook", 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
39 번째 -42 번째 결과의 4 바이트 16 00 00 00 필드 어셈블리 프로세스는 첫 번째 필드 22 (1 * 2 ^ 4 + 6)의 어셈블리 결과 길이, 즉 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". 결과의 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
다음으로 다른 유형을 순서대로 어셈블하고 유형을 어셈블 한 다음 packproto를 호출하여 각 프로토를 어셈블합니다. result372-375의 4 바이트는 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)입니다. 결과의 마지막 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) 모든 유형을 순서대로 조합하고 먼저 각 유형의 이름을 조합 한 다음 필드를 조합합니다.
(3) 모든 프로토 타입을 차례로 조립합니다.
최종적으로 어셈블 된 바이너리 블록은 N 개의 유형과 N 개의 프로토로 구성됩니다. 각 유형에는 이름과 N 개의 필드가 포함됩니다. 각 필드에는 이름, 빌드, 유형, 태그 및 배열과 같은 정보가 포함됩니다. 각 프로토에는 이름, 태그, 요청, 응답 및 기타 정보. 필드, 유형 또는 proto에 관계없이 다음 바이트의 정보를 나타 내기 위해 일부 바이트 접두사가 추가됩니다. 형식은 다음과 같습니다.
2. sproto 건설 과정
.sproto 파일이 바이너리 블록으로 어셈블 될 때 sproto 구성은 바이너리 블록을 구문 분석하는 것입니다. 조립 과정을 이해 한 후 분석 과정은 조립 과정을 반대로하고 최종적으로 분석 결과를 c 구조에 저장하는 것입니다. lua 레이어 newproto를 통해 api create_from_bundle은 결국 세 가지 매개 변수를 사용하여 sproto를 빌드하기 위해 호출됩니다. s 생성 후의 sproto는이 구조에 저장되고, 이진 데이터 블록은 스트림에 의해 조립되고 sz 길이입니다.
19 행, struct_field api가 접두사를 계산하고 접두사가 다른 다음 데이터는 다른 의미를 갖습니다.
23 행, count_array api는 총 유형 수 계산, 총 proto 수 계산 및 각 유형의 총 필드 수와 같은 수를 계산합니다.
26-29 행, 총 유형 수 s-> type_n 저장
31-34 행, 총 프로토스 수 s-> protocol_n 저장
38-43 행, import_type api를 통해 각 유형의 데이터를 구성하고 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를 호출하여 사용할 모든 lua VM로드 (loadproto)에 대해 전역 어레이 G_sproto에 sproto를 저장합니다.
이것이 sproto의 분석 및 구축 과정입니다. 다음으로 sproto 사용법에 대한 기사를 쓸 것입니다.
2021 년 1 월 13 일 14 일, 2 주 후인 4 시간짜리 스카이 넷 트레이닝 캠프를 열 예정입니다. 등록은 현재 진행 중입니다. 게임 개발에 관심이있는 분은 구독 할 수 있습니다.
훈련 캠프 내용은 대략 다음과 같습니다.
1. 멀티 코어 동시 프로그래밍
2. 메시지 큐, 스레드 풀
3. 액터 메시지 스케줄링
4. 네트워크 모듈 구현
5. 타임 휠 타이머 구현
6. Lua / c 인터페이스 프로그래밍
7. 스카이 넷 프로그래밍 필수 사항
8. 데모는 액터 프로그래밍 사고를 보여줍니다.
게임 개발을위한 기술 축제를 만들기 위해 함께 일하는 모든 사람을 기대합니다.
등록 스크린 샷을 통해 그룹 973961276에 입장하여 마지막 스카이 넷 훈련 캠프의 녹화 및 방송과이 기간 동안의 미리보기 자료를받을 수 있습니다!