skynet源码分析之sproto使用方法

摘要:
A端主动给B端发送请求:调用request_encode对lua表进行编码,再用sproto.pack打包。不管是是request_encode还是response_encode,最终都会调用c层的encode接口,request_decode和response_decode都会调用c层decode接口。encode负责将lua数据表编码成二进制数据块,而decode负责解码,二者是互补操作。

上一篇文章介绍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 functionsproto:request_encode(protoname, tbl)
 3...
 4     returncore.encode(request,tbl) , p.tag
 5 end
 6 
 7 functionsproto:response_encode(protoname, tbl)
 8...
 9     returncore.encode(response,tbl)
10 end
11 
12 
13 
14 functionsproto:request_decode(protoname, ...)
15...
16     returncore.decode(request,...) , p.name
17 end
18 
19 functionsproto:response_decode(protoname, ...)
20...
21     returncore.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 indata 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位置,接下skynet源码分析之sproto使用方法第1张

来编码每个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                 caseSPROTO_TINTEGER:
17                 caseSPROTO_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                 caseSPROTO_TSTRUCT:
34                 caseSPROTO_TSTRING:
35                     sz = encode_object(cb, &args, data, size);
36                     break;
37 }
38             if (sz > 0) {
39                 uint8_t *record;
40                 inttag;
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, intsize) {
4      intsz;
5      args->value = data+SIZEOF_LENGTH;
6      args->length = size-SIZEOF_LENGTH;
7      sz =cb(args);
8 ...
9      returnfill_size(data, sz);
10 }
11  
12  static inline int
13  fill_size(uint8_t * data, intsz) {
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     caseSPROTO_TSTRING: {
26         memcpy(args->value, str, sz);
27 ...
28 }
29     caseSPROTO_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     structsproto_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                 caseSPROTO_TINTEGER: {
46 ...
47                     break;
48 }
49                 caseSPROTO_TSTRING:
50                 caseSPROTO_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, intbufsz) {
3     uint8_t tmp[8];
4     inti;
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         intn;
13         int padding = i+8 -srcsz;
14         if (padding > 0) {
15             intj;
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     returnsize;
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, intbufsz) {
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             intn;
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              inti;
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     returnsize;
54 }

sproto协议是为lua量身定制的,非常适合用lua为脚本语言的框架。

免责声明:文章转载自《skynet源码分析之sproto使用方法》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇AES的数学基础Java Stream() 两个对象数组根据ID获取交集下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

使用NODEJS+REDIS开发一个消息队列以及定时任务处理

作者:RobanLee 原创文章,转载请注明: 萝卜李 http://www.robanlee.com 源码在这里: https://github.com/robanlee123/RobCron时间有限,就不详细注释,有问题或者意见欢迎@我,也欢迎大家批评指正. 本文所必须的一些资料如下: 1. NODEJS ==> 可以去NODEJS.ORG下载...

java爬虫(四)利用Jsoup获取需要登陆的网站中的内容(无验证码的登录)

一、实现原理 登录之后进行数据分析,精确抓取数据。根据上篇文章的代码,我们不仅获取了cookies,还获取了登录之后返回的网页源码,此时有如下几种种情况:(1)若我们所需的数据就在登录之后返回的源码里面,那么我们就可以直接通过Jsoup去解析源码了,然后利用Jsoup的选择器功能去筛选出我们需要的信息;(2)若需要的数据是需要通过请求源码里的链接得到,那么...

【原】用Java编写第一个区块链(一)

  写这篇随笔主要是尝试帮助自己了解如何学习区块链技术开发。  【本文禁止任何形式的全文粘贴式转载,本文来自zacky31的随笔】 目标: 创建一个最基本的“区块链” 实现一个简单的挖矿系统 前提: 对面向对象编程有一定的基础 注意: 值得注意的是,这不会是一个完整的功能,恰恰相反,这是一个概念证明的实例,可以帮助您进一步了解区块链。 准备:   我将...

Vuex 模块化与项目实例 (2.0)

Vuex 强调使用单一状态树,即在一个项目里只有一个 store,这个 store 集中管理了项目中所有的数据以及对数据的操作行为。但是这样带来的问题是 store 可能会非常臃肿庞大不易维护,所以就需要对状态树进行模块化的拆分。 首先贴出一个逻辑比较复杂的H5项目:DEMO & 源码 该项目主要包括 banner、feeds、profile 三个...

Java-API:javax.servlet.http.HttpServletResponse

ylbtech-Java-API:javax.servlet.http.HttpServletResponse 1.返回顶部 1、 javax.servlet.httpInterface HttpServletResponse All Superinterfaces: ServletResponse All Known Implementing...

PHP socket 接收 java端口 netty 网络字节序

java 服务端测试代码: @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throwsException { buffer.writeShort(5); buffer.writeI...