上一篇文章介紹sproto的構建流程(http://www.cnblogs.com/RainRill/p/8986572.html),這一篇文章介紹sproto如何使用,參考https://github.com/cloudwu/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也是互補操作。
1 -- lualib/sproto.lua 2 function sproto:request_encode(protoname, tbl) 3 ... 4 return core.encode(request,tbl) , p.tag 5 end 6 7 function sproto:response_encode(protoname, tbl) 8 ... 9 return core.encode(response,tbl) 10 end 11 12 13 14 function sproto:request_decode(protoname, ...) 15 ... 16 return core.decode(request,...) , p.name 17 end 18 19 function sproto:response_decode(protoname, ...) 20 ... 21 return core.decode(response,...) 22 end 23 24 25 sproto.pack = core.pack 26 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里。
1 // lualib/sproto/sproto.c
2 int sproto_encode(const struct sproto_type *st, void * buffer, int size, sproto_callback cb, void *ud) { 3 uint8_t * header = buffer; 4 uint8_t * data; 5 int header_sz = SIZEOF_HEADER + st->maxn * SIZEOF_FIELD; 6 data = header + header_sz; 7 ... 8 for (i=0;i<st->n;i++) { 9 struct field *f = &st->f[i]; 10 int type = f->type; 11 if (type & SPROTO_TARRAY) { 12 args.type = type & ~SPROTO_TARRAY; 13 sz = encode_array(cb, &args, data, size); 14 } else { 15 switch(type) { 16 case SPROTO_TINTEGER: 17 case SPROTO_TBOOLEAN: { 18 sz = cb(&args); 19 if (sz == sizeof(uint32_t)) { 20 if (u.u32 < 0x7fff) { 21 value = (u.u32+1) * 2; 22 sz = 2; // sz can be any number > 0
23 } else { 24 sz = encode_integer(u.u32, data, size); 25 } 26 } else if (sz == sizeof(uint64_t)) { 27 sz= encode_uint64(u.u64, data, size); 28 } else { 29 return -1; 30 } 31 break; 32 } 33 case SPROTO_TSTRUCT: 34 case SPROTO_TSTRING: 35 sz = encode_object(cb, &args, data, size); 36 break; 37 } 38 if (sz > 0) { 39 uint8_t * record; 40 int tag; 41 if (value == 0) { 42 data += sz; 43 size -= sz; 44 } 45 record = header+SIZEOF_HEADER+SIZEOF_FIELD*index; 46 tag = f->tag - lasttag - 1; 47 if (tag > 0) { 48 // skip tag
49 tag = (tag - 1) * 2 + 1; 50 if (tag > 0xffff) 51 return -1; 52 record[0] = tag & 0xff; 53 record[1] = (tag >> 8) & 0xff; 54 ++index; 55 record += SIZEOF_FIELD; 56 } 57 ++index; 58 record[0] = value & 0xff; 59 record[1] = (value >> 8) & 0xff; 60 lasttag = f->tag; 61 } 62 } 63 header[0] = index & 0xff; 64 header[1] = (index >> 8) & 0xff;
datasz = data - (header+header_sz);
data = header +header_sz;
memmove(header + SIZEOF_HEADER + index * SIZEOF_FIELD, data, datasz); 65 }
如果是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遞歸處理
1 // lualib-src/sproto/sproto.c 2 static int 3 encode_object(sproto_callback cb, struct sproto_arg *args, uint8_t *data, int size) { 4 int sz; 5 args->value = data+SIZEOF_LENGTH; 6 args->length = size-SIZEOF_LENGTH; 7 sz = cb(args); 8 ... 9 return fill_size(data, sz); 10 } 11 12 static inline int 13 fill_size(uint8_t * data, int sz) { 14 data[0] = sz & 0xff; 15 data[1] = (sz >> 8) & 0xff; 16 data[2] = (sz >> 16) & 0xff; 17 data[3] = (sz >> 24) & 0xff; 18 return sz + SIZEOF_LENGTH; 19 } 20 21 // lualib-src/sproto/lsproto.c 22 static int 23 encode(const struct sproto_arg *args) { 24 ... 25 case SPROTO_TSTRING: { 26 memcpy(args->value, str, sz); 27 ... 28 } 29 case SPROTO_TSTRUCT: { 30 ... 31 r = sproto_encode(args->subtype, args->value, args->length, encode, &sub); 32 } 33 }
如果是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解碼
1 // lualib-src/sproto/sproto.c 2 int 3 sproto_decode(const struct sproto_type *st, const void * data, int size, sproto_callback cb, void *ud) { 4 struct sproto_arg args; 5 int total = size; 6 uint8_t * stream; 7 uint8_t * datastream; 8 stream = (void *)data; 9 fn = toword(stream); 10 stream += SIZEOF_HEADER; 11 size -= SIZEOF_HEADER ; 12 datastream = stream + fn * SIZEOF_FIELD; 13 size -= fn * SIZEOF_FIELD; 14 args.ud = ud; 15 16 tag = -1; 17 for (i=0;i<fn;i++) { 18 uint8_t * currentdata; 19 struct field * f; 20 int value = toword(stream + i * SIZEOF_FIELD); 21 ++ tag; 22 if (value & 1) { 23 tag += value/2; 24 continue; 25 } 26 value = value/2 - 1; 27 currentdata = datastream; 28 if (value < 0) { 29 uint32_t sz; 30 sz = todword(datastream); 31 datastream += sz+SIZEOF_LENGTH; 32 size -= sz+SIZEOF_LENGTH; 33 } 34 f = findtag(st, tag); 35 36 args.tagname = f->name; 37 ... 38 if (value < 0) { 39 if (f->type & SPROTO_TARRAY) { 40 if (decode_array(cb, &args, currentdata)) { 41 return -1; 42 } 43 } else { 44 switch (f->type) { 45 case SPROTO_TINTEGER: { 46 ... 47 break; 48 } 49 case SPROTO_TSTRING: 50 case SPROTO_TSTRUCT: { 51 uint32_t sz = todword(currentdata); 52 args.value = currentdata+SIZEOF_LENGTH; 53 args.length = sz; 54 if (cb(&args)) 55 return -1; 56 break; 57 } 58 } 59 } else if (f->type != SPROTO_TINTEGER && f->type != SPROTO_TBOOLEAN) { 60 return -1; 61 } else { 62 uint64_t v = value; 63 args.value = &v; 64 args.length = sizeof(v); 65 cb(&args); 66 } 67 } 68 return total - size; 69 }
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里。
1 int 2 sproto_pack(const void * srcv, int srcsz, void * bufferv, int bufsz) { 3 uint8_t tmp[8]; 4 int i; 5 const uint8_t * ff_srcstart = NULL; 6 uint8_t * ff_desstart = NULL; 7 int ff_n = 0; 8 int size = 0; 9 const uint8_t * src = srcv; 10 uint8_t * buffer = bufferv; 11 for (i=0;i<srcsz;i+=8) { 12 int n; 13 int padding = i+8 - srcsz; 14 if (padding > 0) { 15 int j; 16 memcpy(tmp, src, 8-padding); 17 for (j=0;j<padding;j++) { 18 tmp[7-j] = 0; 19 } 20 src = tmp; 21 } 22 n = pack_seg(src, buffer, bufsz, ff_n); 23 bufsz -= n; 24 if (n == 10) { 25 // first FF 26 ff_srcstart = src; 27 ff_desstart = buffer; 28 ff_n = 1; 29 } else if (n==8 && ff_n>0) { 30 ++ff_n; 31 if (ff_n == 256) { 32 if (bufsz >= 0) { 33 write_ff(ff_srcstart, ff_desstart, 256*8); 34 } 35 ff_n = 0; 36 } 37 } else { 38 if (ff_n > 0) { 39 if (bufsz >= 0) { 40 write_ff(ff_srcstart, ff_desstart, ff_n*8); 41 } 42 ff_n = 0; 43 } 44 } 45 src += 8; 46 buffer += n; 47 size += n; 48 } 49 if(bufsz >= 0){ 50 if(ff_n == 1) 51 write_ff(ff_srcstart, ff_desstart, 8); 52 else if (ff_n > 1) 53 write_ff(ff_srcstart, ff_desstart, srcsz - (intptr_t)(ff_srcstart - (const uint8_t*)srcv)); 54 } 55 return size; 56 }
了解打包原理后,解包就是打包的逆過程,變得很容易了。調用sproto_unpack解包:
第11-27行,如果第一個字節是ff,計算出可直接拷貝的字節數n,然后拷貝到buffer。
第30-50行,計算第一個字節的每一位(總共8位),如果是1,復制跟隨的一個字節給buffer(32-41行);否則,設置buffer為0(42-49行)。
1 // lualib-src/sproto/sproto.c 2 int 3 sproto_unpack(const void * srcv, int srcsz, void * bufferv, int bufsz) { 4 const uint8_t * src = srcv; 5 uint8_t * buffer = bufferv; 6 int size = 0; 7 while (srcsz > 0) { 8 uint8_t header = src[0]; 9 --srcsz; 10 ++src; 11 if (header == 0xff) { 12 int n; 13 if (srcsz < 0) { 14 return -1; 15 } 16 n = (src[0] + 1) * 8; 17 if (srcsz < n + 1) 18 return -1; 19 srcsz -= n + 1; 20 ++src; 21 if (bufsz >= n) { 22 memcpy(buffer, src, n); 23 } 24 bufsz -= n; 25 buffer += n; 26 src += n; 27 size += n; 28 } else { 29 int i; 30 for (i=0;i<8;i++) { 31 int nz = (header >> i) & 1; 32 if (nz) { 33 if (srcsz < 0) 34 return -1; 35 if (bufsz > 0) { 36 *buffer = *src; 37 --bufsz; 38 ++buffer; 39 } 40 ++src; 41 --srcsz; 42 } else { 43 if (bufsz > 0) { 44 *buffer = 0; 45 --bufsz; 46 ++buffer; 47 } 48 } 49 ++size; 50 } 51 } 52 } 53 return size; 54 }
sproto協議是為lua量身定制的,非常適合用lua為腳本語言的框架。
