skynet源碼分析之sproto解析和構建


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文件的內容:

1 -- lualib/sprotoparser.lua
2 function sparser.parse(text, name)
3     local r = parser(text, name or "=text")
4     dump(r)
5     local data = encodeall(r)
6     sparser.dump(data)
7     return data
8 end

第3-4行,通過lpeg庫將.sproto文件分析轉化成一個lua表,部分結果如下,包含protocol和type兩大類。protocol里包含所有的協議,每個協議有request,response,tag三個key;type里包含所有的類型,每個類型有1個或多個域(field),每個field里包含name,tag,typename等信息。

 1 "protocol" = {
 2          "proto1" = {
 3              "request"  = "proto1.request"
 4              "response" = "proto1.response"
 5              "tag"      = 1001
 6          }
 7      }
 8 "type" = {
 9          "AddresBook" = {
10              1 = {
11                  "array"    = true
12                  "name"     = "person"
13                  "tag"      = 0
14                  "typename" = "Person"
15              }
16          }
17          "Person" = {
18              1 = {
19                  "name"     = "name"
20                  "tag"      = 0
21                  "typename" = "string"
22              }
23 ...

第5-6行,把lua表按特定格式組裝成二進制數據后,結果如下(每一行16個字節):

 1 02 00 00 00 00 00 65 01 - 00 00 32 00 00 00 02 00 
 2 00 00 00 00 0A 00 00 00 - 41 64 64 72 65 73 42 6F 
 3 6F 6B 1A 00 00 00 16 00 - 00 00 05 00 00 00 01 00 
 4 04 00 02 00 04 00 06 00 - 00 00 70 65 72 73 6F 6E 
 5 6E 00 00 00 02 00 00 00 - 00 00 06 00 00 00 50 65 
 6 72 73 6F 6E 5A 00 00 00 - 12 00 00 00 04 00 00 00 
 7 06 00 01 00 02 00 04 00 - 00 00 6E 61 6D 65 10 00 
 8 00 00 04 00 00 00 02 00 - 01 00 04 00 02 00 00 00 
 9 69 64 13 00 00 00 04 00 - 00 00 06 00 01 00 06 00 
10 05 00 00 00 65 6D 61 69 - 6C 15 00 00 00 05 00 00 
11 00 01 00 06 00 08 00 04 - 00 05 00 00 00 70 68 6F 
12 6E 65 4E 00 00 00 02 00 - 00 00 00 00 12 00 00 00 
13 50 65 72 73 6F 6E 2E 50 - 68 6F 6E 65 4E 75 6D 62 
14 65 72 2E 00 00 00 14 00 - 00 00 04 00 00 00 06 00 
15 01 00 02 00 06 00 00 00 - 6E 75 6D 62 65 72 12 00 
16 00 00 04 00 00 00 02 00 - 01 00 04 00 04 00 00 00 
17 74 79 70 65 2F 00 00 00 - 02 00 00 00 00 00 0E 00 
18 00 00 70 72 6F 74 6F 31 - 2E 72 65 71 75 65 73 74 
19 13 00 00 00 0F 00 00 00 - 04 00 00 00 02 00 01 00 
20 02 00 01 00 00 00 70 34 - 00 00 00 02 00 00 00 00 
21 00 0F 00 00 00 70 72 6F - 74 6F 31 2E 72 65 73 70 
22 6F 6E 73 65 17 00 00 00 - 13 00 00 00 05 00 00 00 
23 01 00 04 00 02 00 04 00 - 03 00 00 00 72 65 74 18 
24 00 00 00 14 00 00 00 04 - 00 00 00 D4 07 08 00 0A 
25 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個字節。

 1  -- lualib/sprotoparser.lua
 2 function packbytes(str)
 3     return string.pack("<s4",str)
 4 end
 5 
 6  local function encodeall(r)
 7      return packgroup(r.type, r.protocol)
 8  end
 9  
10  local function packgroup(t,p)
11      ...
12      tt = packbytes(table.concat(tt))
13      tp = packbytes(table.concat(tp))
14      result = {
15          "\2\0", -- 2fields
16          "\0\0", -- type array   (id = 0, ref = 0)
17          "\0\0", -- protocol array (id = 1, ref =1)
18  
19          tt,
20          tp,
21      }
22      return table.concat(result)
23  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)。

 1  -- lualib/sprotoparser.lua
 2  local function packtype(name, t, alltypes) -- 組裝每一個type
 3      ...
 4      local data
 5      if #fields == 0 then
 6          data = {
 7              "\1\0", -- 1 fields
 8              "\0\0", -- name (id = 0, ref = 0)
 9              packbytes(name),
10          }
11      else
12          data = {
13              "\2\0", -- 2 fields
14              "\0\0", -- name (tag = 0, ref = 0)
15              "\0\0", -- field[]      (tag = 1, ref = 1)
16              packbytes(name),
17              packbytes(table.concat(fields)),
18          }
19      end
20  
21      return packbytes(table.concat(data))
22  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"
     }
 }
 1 -- lualib/sprotoparser.lua
 2 local function packfield(f) -- 組裝每一個field
 3     local strtbl = {}
 4     if f.array then
 5         if f.key then
 6             table.insert(strtbl, "\6\0")  -- 6 fields
 7         else
 8             table.insert(strtbl, "\5\0")  -- 5 fields
 9         end
10     else
11         table.insert(strtbl, "\4\0")    -- 4 fields
12     end
13     table.insert(strtbl, "\0\0")    -- name (tag = 0, ref an object)
14     if f.buildin then
15         table.insert(strtbl, packvalue(f.buildin))      -- buildin (tag = 1)
16         if f.extra then
17             table.insert(strtbl, packvalue(f.extra))        -- f.buildin can be integer or string
18         else
19             table.insert(strtbl, "\1\0")    -- skip (tag = 2)
20         end
21         table.insert(strtbl, packvalue(f.tag))          -- tag (tag = 3)
22     else
23         table.insert(strtbl, "\1\0")    -- skip (tag = 1)
24         table.insert(strtbl, packvalue(f.type))         -- type (tag = 2)
25         table.insert(strtbl, packvalue(f.tag))          -- tag (tag = 3)
26     end
27     if f.array then
28         table.insert(strtbl, packvalue(1))      -- array = true (tag = 4)
29     end
30     if f.key then
31         table.insert(strtbl, packvalue(f.key)) -- key tag (tag = 5)
32     end
33     table.insert(strtbl, packbytes(f.name)) -- external object (name)
34     return packbytes(table.concat(strtbl))
35 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
1
local function packproto(name, p, alltypes) -- 組裝每一個proto 2 if p.request then 3 local request = alltypes[p.request] 4 if request == nil then 5 error(string.format("Protocol %s request type %s not found", name, p.request)) 6 end 7 request = request.id 8 end 9 local tmp = { 10 "\4\0", -- 4 fields 11 "\0\0", -- name (id=0, ref=0) 12 packvalue(p.tag), -- tag (tag=1) 13 } 14 if p.request == nil and p.response == nil and p.confirm == nil then 15 tmp[1] = "\2\0" -- only two fields 16 else 17 if p.request then 18 table.insert(tmp, packvalue(alltypes[p.request].id)) -- request typename (tag=2) 19 else 20 table.insert(tmp, "\1\0")-- skip this field (request)
21 22 end 23 if p.response then 24 table.insert(tmp, packvalue(alltypes[p.response].id)) -- request typename (tag=3) 25 elseif p.confirm then 26 tmp[1] = "\5\0" -- add confirm field 27 table.insert(tmp, "\1\0") 28 -- skip this field (response) 29 table.insert(tmp, packvalue(1)) -- confirm = true 30 else 31 tmp[1] = "\3\0" -- only three fields 32 end 33 end 34 35 table.insert(tmp, packbytes(name)) 36 37 return packbytes(table.concat(tmp)) 38 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這個數組里

 1 // lualib/sproto/sproto.c
 2 struct sproto *
 3 sproto_create(const void * proto, size_t sz) {
 4     ...
 5     if (create_from_bundle(s, proto, sz) == NULL) {
 6         pool_release(&s->memory);
 7         return NULL;
 8     }
 9     return s;
10 }
11 
12 static struct sproto *
13 create_from_bundle(struct sproto *s, const uint8_t* stream, size_t sz) {
14     ...
15     int fn = struct_field(stream, sz);
16     int i;
17     ...
18     for (i=0;i<fn;i++) {
19         int value = toword(stream + i*SIZEOF_FIELD);
20         int n;
21         if (value != 0)
22             return NULL;
23         n = count_array(content);
24         if (n<0)
25            return NULL;
26         if (i == 0) {
27             typedata = content+SIZEOF_LENGTH;
28             s->type_n = n;
29             s->type = pool_alloc(&s->memory, n * sizeof(*s->type));
30         } else {
31             protocoldata = content+SIZEOF_LENGTH;
32             s->protocol_n = n;
33             s->proto = pool_alloc(&s->memory, n * sizeof(*s->proto));
34         }
35         content += todword(content) + SIZEOF_LENGTH;
36     }
37 
38     for (i=0;i<s->type_n;i++) {
39         typedata = import_type(s, &s->type[i], typedata);
40         if (typedata == NULL) {
41             return NULL;
42         }
43     }
44     for (i=0;i<s->protocol_n;i++) {
45         protocoldata = import_protocol(s, &s->proto[i], protocoldata);
46         if (protocoldata == NULL) {
47             return NULL;
48         }
49     }
50 
51     return s;
52 }

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如何使用。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM