skynet原始碼分析之sproto使用方法 ——encode編碼與decode解碼、 pack打包與unpack解包
上一篇文章介紹sproto的構建流程,這一篇文章介紹sproto如何使用
A端主動給B端傳送請求:呼叫request_encode對lua表進行編碼,再用sproto.pack打包。
B端收到A端的請求:用sproto.unpack解包,再呼叫request_decode解碼成lua表。
B端給A端傳送返回包:用response_encode對lua表進行編碼,然後用sproto.pack打包。
A端收到B端的返回包:用sproto.unpack解包,再呼叫request_decode解碼成lua表。
不管是是request_encode還是response_encode,最終都會呼叫c層的encode介面,request_decode和response_decode都會呼叫c層decode介面。encode負責將lua資料表編碼成二進位制資料塊,而decode負責解碼,二者是互補操作。同樣,pack和unpack也是互補操作。
-- lualib/sproto.lua function sproto:request_encode(protoname, tbl) ... return core.encode(request,tbl) , p.tag end function sproto:response_encode(protoname, tbl) ... return core.encode(response,tbl) end function sproto:request_decode(protoname, ...) ... return core.decode(request,...) , p.name end function sproto:response_decode(protoname, ...) ... return core.decode(response,...) end sproto.pack = core.pack sproto.unpack = core.unpack
1. encode編碼
先放一個例子(在github上有),分析原始碼時會用到:
person { name = "Alice" , age = 13, marital = false }
03 00 (fn = 3)
00 00 (id = 0, value in data part)
1C 00 (id = 1, value = 13)
02 00 (id = 2, value = false)
05 00 00 00 (sizeof "Alice")
41 6C 69 63 65 ("Alice")
encode的目的是按指定協議型別將lua表裡的資料轉化成c中的型別,然後按特定格式編碼成一串二進位制資料塊。
最終呼叫sproto_encode api編碼,有5個引數:st,sproto指定型別的c結構;buffer、size,存放編碼結果的緩衝區和大小,如果緩衝區不夠,會擴充緩衝區,重新編碼;cb,對應lsproto.c中encode api,是一個c介面,負責獲取lua表中指定key的值,或陣列中指定索引位置的值;ud,額外資訊,包含lua與c之間互動用的虛擬棧、sproto中對應型別的c結構等。
第3-6行,編碼結果分兩部分:頭部header和資料data,header長度是固定的,等於2位元組field總數+field的數目*2位元組每個field長度。如下圖:header指標指向緩衝區首地址,data指向header+header_sz位置,接下
來編碼每個field資訊時,data指標會往後移動,而header指標保持不動。
第63-65行,將field的總數按大端格式打包長2位元組大小(示例中的03 00),data指向header+header_sz處,最後用memmove將頭部和資料塊連在一起。
接下來就是編碼每一個field資料,根據field型別做不同的處理:
第11-13行,如果是array,呼叫encode_array編碼,稍後介紹。
第33-37行,如果是string或自定義型別,呼叫encode_object編碼,稍後介紹。
第16-32行,如果是integer或boolean型別,呼叫cb(lsproto.c中的encode)獲取lua表中對應field名字的數值,儲存到args.value(即u中)。第21行,變數value等於(原來的值+1)*2,因為編碼後的0有特殊作用,為了區分原來值是0的情況。
第58-59行,最後將value按大端格式編碼2位元組,存到header指定的位置。比如示例中的1C 00,(13+1)*2=28=1C,02 00,(0+1)*2=2=02,注:lua中的false會編碼成0,true編碼成1。如果是array、string或自定義型別,value是0,編碼後是00 00,代表數值在data部分。
第47-56行,如果某些tag沒有設定值,需要把tag資訊編碼到header裡。
// lualib/sproto/sproto.c
int sproto_encode(const struct sproto_type *st, void * buffer, int size, sproto_callback cb, void *ud) {
uint8_t * header = buffer;
uint8_t * data;
int header_sz = SIZEOF_HEADER + st->maxn * SIZEOF_FIELD;
data = header + header_sz;
...
for (i=0;i<st->n;i++) {
struct field *f = &st->f[i];
int type = f->type;
if (type & SPROTO_TARRAY) {
args.type = type & ~SPROTO_TARRAY;
sz = encode_array(cb, &args, data, size);
} else {
switch(type) {
case SPROTO_TINTEGER:
case SPROTO_TBOOLEAN: {
sz = cb(&args);
if (sz == sizeof(uint32_t)) {
if (u.u32 < 0x7fff) {
value = (u.u32+1) * 2;
sz = 2; // sz can be any number > 0
} else {
sz = encode_integer(u.u32, data, size);
}
} else if (sz == sizeof(uint64_t)) {
sz= encode_uint64(u.u64, data, size);
} else {
return -1;
}
break;
}
case SPROTO_TSTRUCT:
case SPROTO_TSTRING:
sz = encode_object(cb, &args, data, size);
break;
}
if (sz > 0) {
uint8_t * record;
int tag;
if (value == 0) {
data += sz;
size -= sz;
}
record = header+SIZEOF_HEADER+SIZEOF_FIELD*index;
tag = f->tag - lasttag - 1;
if (tag > 0) {
// skip tag
tag = (tag - 1) * 2 + 1;
if (tag > 0xffff)
return -1;
record[0] = tag & 0xff;
record[1] = (tag >> 8) & 0xff;
++index;
record += SIZEOF_FIELD;
}
++index;
record[0] = value & 0xff;
record[1] = (value >> 8) & 0xff;
lasttag = f->tag;
}
}
header[0] = index & 0xff;
header[1] = (index >> 8) & 0xff; datasz = data - (header+header_sz); data = header +header_sz; memmove(header + SIZEOF_HEADER + index * SIZEOF_FIELD, data, datasz);
}
如果是string或自定義型別,呼叫encode_object編碼,4個引數是:cb,即lsproto.c中encode介面;args,額外引數;data,存放編碼結果的緩衝區,由4個位元組的長度+具體資料組成;size,緩衝區長度
第9行,填充4位元組的長度放到data的首地址處,比如示例中05 00 00 00
第5行,資料從data+SIZEOF_LENGTH開始存放,前4個位元組存放資料長度
第26行,如果是字串,拷貝字串到指定位置,比如示例中41 6C 69 63 65("Alice")
第31行,如果是自定義型別,對子型別再次呼叫sproto_encode遞迴處理
// lualib-src/sproto/sproto.c
static int
encode_object(sproto_callback cb, struct sproto_arg *args, uint8_t *data, int size) {
int sz;
args->value = data+SIZEOF_LENGTH;
args->length = size-SIZEOF_LENGTH;
sz = cb(args);
...
return fill_size(data, sz);
}
static inline int
fill_size(uint8_t * data, int sz) {
data[0] = sz & 0xff;
data[1] = (sz >> 8) & 0xff;
data[2] = (sz >> 16) & 0xff;
data[3] = (sz >> 24) & 0xff;
return sz + SIZEOF_LENGTH;
}
// lualib-src/sproto/lsproto.c
static int
encode(const struct sproto_arg *args) {
...
case SPROTO_TSTRING: {
memcpy(args->value, str, sz);
...
}
case SPROTO_TSTRUCT: {
...
r = sproto_encode(args->subtype, args->value, args->length, encode, &sub);
}
}
如果是array型別,呼叫encode_array進行編碼,遍歷陣列,對每一個元素進行編碼,同樣把資料長度編碼成4個位元組填充到前面。例如:
children = {
{ name = "Alice" , age = 13 },
{ name = "Carol" , age = 5 },
}
26 00 00 00 (sizeof children)
0F 00 00 00 (sizeof child 1)
02 00 (fn = 2)
00 00 (id = 0, value in data part)
1C 00 (id = 1, value = 13)
05 00 00 00 (sizeof "Alice")
41 6C 69 63 65 ("Alice")
0F 00 00 00 (sizeof child 2)
02 00 (fn = 2)
00 00 (id = 0, value in data part)
0C 00 (id = 1, value = 5)
05 00 00 00 (sizeof "Carol")
43 61 72 6F 6C ("Carol")
注:如果陣列元素是整數,在長度和資料之間會多用一個位元組用來標記是小整數(小於2^32)還是大整數,小整數用4個位元組(32位)存放,大整數用8個位元組(64位)存放,例如:
numbers = { 1,2,3,4,5 }
15 00 00 00 (sizeof numbers)
04 ( sizeof int32 )
01 00 00 00 (1)
02 00 00 00 (2)
03 00 00 00 (3)
04 00 00 00 (4)
05 00 00 00 (5)
小結:編碼後的二進位制資料塊由頭部和資料兩部分組成。頭部包含field總數,以及每個field值。資料部分由長度和具體的數值組成。如果field值為0,表示資料在資料部分(array、string或自定義型別);如果field值最後一位為1,表示該field沒資料;否則field值可直接轉化對應lua資料(integer或boolean型別)。
2. decode解碼
瞭解了encode編碼過程,decode解碼過程就是編碼的逆過程,將二進位制資料塊解碼成lua表。5個引數:st,sproto型別的c結構;data和size,待解碼的二進位制資料塊和長度;cb,是一個c介面,即lsproto.c中decode,負責將c型別的資料push到lua虛擬棧裡,然後供lua層使用;ud,額外引數,包括cb中需要用的lua虛擬棧。
第9-12行,獲取頭兩位元組表示field總數fn,stream指向頭部,datastream指向資料塊
第17行,對每一個field進行解碼
第20行,獲取field的值value。如果value最後一位為1,說明之後value/2個tag都沒資料(第22-25行);
第26行,計算value的實際值,currentdata指向當前資料塊(第27行)。如果小於0,說明是array、string或自定義型別,說明資料在資料部分,計算出資料長度sz,然後把datastream移到下一個field對應的資料塊的位置(28-33行)。
第34-37行,找出tag對應的field資訊,賦值給args,呼叫cb時根據args資訊進行相應轉化。
第61-66行,如果是integer或boolean型別,value即資料本身,呼叫cb,設定lua虛擬棧指定表的指定key的位置。
第49-58行,如果是string或自定義型別,先從資料部分中獲取資料(52行),再呼叫cb。
第39-42行,如果是array型別,呼叫decode_array解碼
// lualib-src/sproto/sproto.c
int
sproto_decode(const struct sproto_type *st, const void * data, int size, sproto_callback cb, void *ud) {
struct sproto_arg args;
int total = size;
uint8_t * stream;
uint8_t * datastream;
stream = (void *)data;
fn = toword(stream);
stream += SIZEOF_HEADER;
size -= SIZEOF_HEADER ;
datastream = stream + fn * SIZEOF_FIELD;
size -= fn * SIZEOF_FIELD;
args.ud = ud;
tag = -1;
for (i=0;i<fn;i++) {
uint8_t * currentdata;
struct field * f;
int value = toword(stream + i * SIZEOF_FIELD);
++ tag;
if (value & 1) {
tag += value/2;
continue;
}
value = value/2 - 1;
currentdata = datastream;
if (value < 0) {
uint32_t sz;
sz = todword(datastream);
datastream += sz+SIZEOF_LENGTH;
size -= sz+SIZEOF_LENGTH;
}
f = findtag(st, tag);
args.tagname = f->name;
...
if (value < 0) {
if (f->type & SPROTO_TARRAY) {
if (decode_array(cb, &args, currentdata)) {
return -1;
}
} else {
switch (f->type) {
case SPROTO_TINTEGER: {
...
break;
}
case SPROTO_TSTRING:
case SPROTO_TSTRUCT: {
uint32_t sz = todword(currentdata);
args.value = currentdata+SIZEOF_LENGTH;
args.length = sz;
if (cb(&args))
return -1;
break;
}
}
} else if (f->type != SPROTO_TINTEGER && f->type != SPROTO_TBOOLEAN) {
return -1;
} else {
uint64_t v = value;
args.value = &v;
args.length = sizeof(v);
cb(&args);
}
}
return total - size;
}
3. pack打包 與unpack解包
將lua表編碼成特定的二進位制資料塊後,再用pack打包。其原理是:每8個位元組為一組,打包後由第一個位元組+原資料不為0的位元組組成,第一個位元組的每一位為0時表示原位元組為0,否則就是跟隨的某個位元組。當第一個位元組是FF時,有特殊含義,假設下一位元組為N,表示接下來(N+1)*8個位元組都是原資料。例如:
unpacked (hex): 08 00 00 00 03 00 02 00 19 00 00 00 aa 01 00 00
packed (hex): 51 08 03 02 31 19 aa 01
51 = 0101 0001,從右到左數,表示該組第1,5,7個位置一次是08,03,02,其餘位置都是0。
呼叫sproto_pack打包,4個引數:srcv、srcsz原資料塊和長度;bufferv、bufsz存放打包後資料的緩衝區和長度。
第5-6行,ff_srcstart,ff_desstart分別指向ff代表的源地址和目的地址
第11行,8個一組進行打包
第17-19行,不足8個,用0填充
第22行,呼叫pack_seg,打包成特定格式,存放在buffer裡
第33,40行,如果ff_n>0,呼叫write_ff,按照ff的含義,重新打包,然後存放在buffer裡。
int
sproto_pack(const void * srcv, int srcsz, void * bufferv, int bufsz) {
uint8_t tmp[8];
int i;
const uint8_t * ff_srcstart = NULL;
uint8_t * ff_desstart = NULL;
int ff_n = 0;
int size = 0;
const uint8_t * src = srcv;
uint8_t * buffer = bufferv;
for (i=0;i<srcsz;i+=8) {
int n;
int padding = i+8 - srcsz;
if (padding > 0) {
int j;
memcpy(tmp, src, 8-padding);
for (j=0;j<padding;j++) {
tmp[7-j] = 0;
}
src = tmp;
}
n = pack_seg(src, buffer, bufsz, ff_n);
bufsz -= n;
if (n == 10) {
// first FF
ff_srcstart = src;
ff_desstart = buffer;
ff_n = 1;
} else if (n==8 && ff_n>0) {
++ff_n;
if (ff_n == 256) {
if (bufsz >= 0) {
write_ff(ff_srcstart, ff_desstart, 256*8);
}
ff_n = 0;
}
} else {
if (ff_n > 0) {
if (bufsz >= 0) {
write_ff(ff_srcstart, ff_desstart, ff_n*8);
}
ff_n = 0;
}
}
src += 8;
buffer += n;
size += n;
}
if(bufsz >= 0){
if(ff_n == 1)
write_ff(ff_srcstart, ff_desstart, 8);
else if (ff_n > 1)
write_ff(ff_srcstart, ff_desstart, srcsz - (intptr_t)(ff_srcstart - (const uint8_t*)srcv));
}
return size;
}
瞭解打包原理後,解包就是打包的逆過程,變得很容易了。呼叫sproto_unpack解包:
第11-27行,如果第一個位元組是ff,計算出可直接拷貝的位元組數n,然後拷貝到buffer。
第30-50行,計算第一個位元組的每一位(總共8位),如果是1,複製跟隨的一個位元組給buffer(32-41行);否則,設定buffer為0(42-49行)。
// lualib-src/sproto/sproto.c
int
sproto_unpack(const void * srcv, int srcsz, void * bufferv, int bufsz) {
const uint8_t * src = srcv;
uint8_t * buffer = bufferv;
int size = 0;
while (srcsz > 0) {
uint8_t header = src[0];
--srcsz;
++src;
if (header == 0xff) {
int n;
if (srcsz < 0) {
return -1;
}
n = (src[0] + 1) * 8;
if (srcsz < n + 1)
return -1;
srcsz -= n + 1;
++src;
if (bufsz >= n) {
memcpy(buffer, src, n);
}
bufsz -= n;
buffer += n;
src += n;
size += n;
} else {
int i;
for (i=0;i<8;i++) {
int nz = (header >> i) & 1;
if (nz) {
if (srcsz < 0)
return -1;
if (bufsz > 0) {
*buffer = *src;
--bufsz;
++buffer;
}
++src;
--srcsz;
} else {
if (bufsz > 0) {
*buffer = 0;
--bufsz;
++buffer;
}
}
++size;
}
}
}
return size;
}
本篇文章就寫到這了,
在2021年1月13/14號我會開一個四小時玩轉skynet訓練營,也就是兩個禮拜之後,現在已經開放報名,對遊戲開發感興趣的諸位同好可以訂閱一下,
訓練營內容大概如下:
1. 多核併發程式設計
2. 訊息佇列,執行緒池
3. actor訊息排程
4. 網路模組實現
5. 時間輪定時器實現
6. lua/c介面程式設計
7. skynet程式設計精要
8. demo演示actor程式設計思維
期待大家一起來打造遊戲開發的技術盛宴。
憑藉報名截圖可以進群973961276領取上一期skynet訓練營的錄播以及這期的預習資料哦!