skynet源码分析之sproto使用方法
上一篇文章介绍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为脚本语言的框架。