sproto 數(shù)據(jù)格式圖解

sproto 也是 云風(fēng) 寫的一個(gè)開源的 數(shù)據(jù)描述語(yǔ)言 庫(kù),可以將數(shù)據(jù)進(jìn)行序列化和反序列化 主要用于數(shù)據(jù)存儲(chǔ)搭伤、通信協(xié)議訂制等方面只怎。和 Google公司開發(fā) protobuf 類似

至于為什么重造輪子,是因?yàn)?protobuf 作為國(guó)際大公司的產(chǎn)品怜俐,當(dāng)然不是想著如何在精簡(jiǎn)而是想著如何擴(kuò)大影響力身堡。也就是普適性。那么帶來的問題就是會(huì)有一些東西是咱們做游戲開發(fā)用不到的拍鲤。但是你又不得不為使用它而買單贴谎。而 云風(fēng) 一開始也是用的 protobuf 后面他也認(rèn)為是時(shí)候做下減法了。于是乎 sproto 就被造出來了殿漠。
它長(zhǎng)這樣的:

#定義數(shù)據(jù)結(jié)構(gòu):
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")

相對(duì)于 protobuf 赴精, sproto 更精簡(jiǎn),編解碼更快绞幌。那么做到這些東西并不是說 云風(fēng) 比Google公司厲害蕾哟,而是 sproto 去掉了一些游戲開發(fā)中不需要,或者不常用的特性莲蜘。更適合游戲開發(fā)使用谭确。準(zhǔn)確來說更適合使用 lua 進(jìn)行開發(fā)的游戲使用。因?yàn)? 云風(fēng) 還為 lua 做了一層 RPC 協(xié)議封裝票渠。其他語(yǔ)言的話就需要自己擼了逐哈,之前我就擼了一個(gè) js 版本的sproto,現(xiàn)在公司還在用问顷。效果還行昂秃。

那么上面定義的數(shù)據(jù)怎么被序列化成二進(jìn)制數(shù)據(jù)的禀梳,為了解釋清楚這個(gè)問題照慣例我要上圖了

sproto.jpg
原文
所有的數(shù)字編碼都是以小端方式(little-endian) 編碼。
打包的單位是一個(gè)結(jié)構(gòu)體(用戶定義的類型) 每個(gè)包分兩個(gè)部分:1\. 字段 2\. 數(shù)據(jù)塊

首先是一個(gè) word n肠骆,描述字段的個(gè)數(shù)算途,接下來有 n 個(gè) word 描述字段的內(nèi)容。這個(gè)結(jié)構(gòu)體的前半部分的長(zhǎng)度就是 (n+1) * 2 字節(jié)蚀腿。

字段的 tag 從 0 開始累加嘴瓤,每處理一個(gè)字段,將 tag 加一莉钙。

如果一個(gè)字段 v 為奇數(shù)廓脆,則把當(dāng)前 tag 加上 (v-1)/2 + 1 ,并繼續(xù)處理下一個(gè)字段值磁玉。 如果一個(gè)字段為 0 停忿,表示這個(gè)字段引用后面的一個(gè)數(shù)據(jù)塊。 如果一個(gè)字段不為 0(且為偶數(shù))蚊伞,這個(gè)字段的值為 v/2 - 1瞎嬉。(可以表示 [0, 32767] 的值)

接下來是被上面字段引用的數(shù)據(jù)塊。

數(shù)據(jù)塊用于描述字段中的大數(shù)據(jù)厚柳。它是由一個(gè) dword 長(zhǎng)度 + 字節(jié)串構(gòu)成氧枣。通常用來表示數(shù)組或結(jié)構(gòu)。大于 32767 的整數(shù)和負(fù)整數(shù)用 4 字節(jié)或 8 字節(jié)長(zhǎng)的數(shù)據(jù)塊表示(取決于需要和實(shí)現(xiàn))别垮。

數(shù)組的編碼就是把同一類型的數(shù)據(jù)依次打包成數(shù)據(jù)塊便监。如果是布爾數(shù)組,按 1 字節(jié)一個(gè)編碼碳想。如果是整數(shù)數(shù)組烧董,它比較特殊,會(huì)根據(jù)需要打包成 4 字節(jié)或 8 字節(jié)一個(gè)數(shù)字胧奔;第一字節(jié)是 4 或 8 逊移,指明后面的整數(shù)寬度。

最后龙填,數(shù)據(jù)中的 0 將被壓縮的胳泉。壓縮算法見[上一篇 blog](http://blog.codingnow.com/2014/07/ejoyproto.html) 。

這就是數(shù)據(jù)編碼后的樣子岩遗,細(xì)心的同學(xué)不難看出數(shù)據(jù)段中扇商,描述長(zhǎng)度的類型都是32位,可能有些人會(huì)問都用32位來描述長(zhǎng)度是不是太浪費(fèi)了宿礁。

別擔(dān)心案铺,以云大這種追求極簡(jiǎn)的人不可能做出這樣的事情。下面是打包的流程

pack.jpg

看清楚了沒有梆靖,沒用到的字節(jié)其實(shí)是被壓縮了的控汉。

下面也貼一下產(chǎn)生上面數(shù)據(jù)的代碼笔诵。注意,sproto.c 并沒有給上面的數(shù)據(jù)分配內(nèi)存姑子,而是由調(diào)用層去分配嗤放,并且 sproto.c 只是 做了數(shù)據(jù)長(zhǎng)度的寫入。數(shù)據(jù)內(nèi)容

都是 調(diào)用層去做的 默認(rèn)是 lsproto.c 給 lua 用的壁酬,當(dāng)然你也可以自己寫你喜歡的。

代碼很多恨课,但是你看下面的編碼函數(shù)就夠了舆乔,解碼就是反向操作。

sproto.c

int
sproto_encode(const struct sproto_type *st, void * buffer, int size, sproto_callback cb, void *ud) {
   //回調(diào)時(shí)候回傳到上層的結(jié)構(gòu)體
   struct sproto_arg args;
   //頭部段指針
   uint8_t * header = buffer;
   //當(dāng)前數(shù)據(jù)寫入的位置
   uint8_t * data;
   //頭部段總長(zhǎng)度
   int header_sz = SIZEOF_HEADER + st->maxn * SIZEOF_FIELD;
   int i;
   //當(dāng)前寫到第幾個(gè)字段
   int index;
   //上次的tag
   int lasttag;
   int datasz;
   if (size < header_sz)
       return -1;
   args.ud = ud;
   //先把 buffer 分成 header部分(2個(gè)字節(jié)) | header_sz(st->maxn * SIZEOF_FIELD) | data(數(shù)據(jù)段--可擴(kuò)展的)
   data = header + header_sz;
   size -= header_sz;
   //有數(shù)據(jù)字段的數(shù)量索引
   index = 0;
   lasttag = -1;
   for (i=0;i<st->n;i++) {
       struct field *f = &st->f[i];
       int type = f->type;
       //只有字段類型為整形并且小于0xEFFF才有改變
       //如果=0則這個(gè)字段的值被打包到數(shù)據(jù)段
       int value = 0;
       //這字段寫入數(shù)據(jù)的長(zhǎng)度
       int sz = -1;
       args.tagname = f->name;
       args.tagid = f->tag;
       args.subtype = f->st;
       args.mainindex = f->key;
       args.extra = f->extra;
       if (type & SPROTO_TARRAY) {
           args.type = type & ~SPROTO_TARRAY;
           sz = encode_array(cb, &args, data, size);
           //數(shù)組先寫入4個(gè)字節(jié)表示長(zhǎng)度
           //后面的解析和下面的類似剂公,復(fù)雜數(shù)據(jù)前面加4個(gè)字節(jié)長(zhǎng)度
           //sz 就是已經(jīng)寫了多少字節(jié)
       } else {
           args.type = type;
           args.index = 0;
           switch(type) {
           case SPROTO_TINTEGER:
           case SPROTO_TBOOLEAN: {
               union {
                   uint64_t u64;
                   uint32_t u32;
               } u;
               args.value = &u;
               args.length = sizeof(u);
               sz = cb(&args);
               if (sz < 0) {
                   if (sz == SPROTO_CB_NIL)
                       continue;
                   if (sz == SPROTO_CB_NOARRAY)    // no array, don't encode it
                       return 0;
                   return -1;  // sz == SPROTO_CB_ERROR
               }
               if (sz == SIZEOF_INT32) {
                   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_INT64) {
                   sz= encode_uint64(u.u64, data, size);
               } else {
                   return -1;
               }
               break;
           }
           case SPROTO_TSTRUCT:
           case SPROTO_TSTRING:
               //寫入長(zhǎng)度后還是遞歸調(diào)用到這邊
               sz = encode_object(cb, &args, data, size);
               //data 前4個(gè)字節(jié)放總長(zhǎng)度希俩,后面的放到 args->value
               //上層邏輯按各自的數(shù)據(jù)結(jié)構(gòu)寫入到 args->value
               //sz 是這次一共用了多少個(gè)字節(jié)
               break;
           }
       }
      
       if (sz < 0)
           return -1;
       if (sz > 0) {
           //record 如果為 0 則表示數(shù)據(jù)放在了 數(shù)據(jù)段
           //record 如果為 基數(shù) 則表示跳過若干個(gè)字段
           //record 如果為 偶素 則表示數(shù)據(jù)為小整形
           uint8_t * record;
           //tag 的意義就是打包的數(shù)據(jù)中有部分字段可能沒有數(shù)據(jù)
           //需要記錄跳過幾個(gè)字段。并且把跳過的信息也記錄在描述字段中纲辽,用基數(shù)值來表示
           //描述字段偶數(shù)值就是小整形和bool
           //描述字段值為0就是把數(shù)據(jù)放在了數(shù)據(jù)段上
           
           int tag;
           if (value == 0) {
               data += sz;
               size -= sz;
           }
           record = header+SIZEOF_HEADER+SIZEOF_FIELD*index;
           tag = f->tag - lasttag - 1;
           
           //兩個(gè)不連續(xù)的字段中,需要額外加2個(gè)字節(jié)的跳過信息
           if (tag > 0) {
               // skip tag
               tag = (tag - 1) * 2 + 1;
               //這里返回 -1 重新分配 buffer 內(nèi)存颜武,大于 ENCODE_MAXSIZE 會(huì)報(bào)錯(cuò)和結(jié)束
               if (tag > 0xffff)
                   return -1;
               record[0] = tag & 0xff;
               record[1] = (tag >> 8) & 0xff;
               ++index;
               record += SIZEOF_FIELD;
           }
           //如果有寫入數(shù)據(jù)
           ++index;
           // value 為 0 數(shù)據(jù)在數(shù)據(jù)段
           record[0] = value & 0xff;
           record[1] = (value >> 8) & 0xff;
           //為了計(jì)算空字段 記錄上一個(gè) tag 等于 當(dāng)前 tag
           lasttag = f->tag;
       }
   }
   //如果全部的字段都沒有數(shù)據(jù)這里就是 0x00 0x00
   header[0] = index & 0xff;
   header[1] = (index >> 8) & 0xff;
   //計(jì)算用掉的數(shù)據(jù)部分長(zhǎng)度
   datasz = data - (header + header_sz);
   data = header + header_sz;
   //如果有空的字段就收縮
   if (index != st->maxn) {
       //header部分(2個(gè)字節(jié)) | 收縮這部分 header_sz(st->maxn * SIZEOF_FIELD)| data
       memmove(header + SIZEOF_HEADER + index * SIZEOF_FIELD, data, datasz);
   }
   //返回寫入數(shù)據(jù)的總長(zhǎng)度
   return SIZEOF_HEADER + index * SIZEOF_FIELD + datasz;
}

下次再寫一篇 sproto rpc 方面的文章。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末拖吼,一起剝皮案震驚了整個(gè)濱河市鳞上,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌吊档,老刑警劉巖篙议,帶你破解...
    沈念sama閱讀 218,941評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異怠硼,居然都是意外死亡鬼贱,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門香璃,熙熙樓的掌柜王于貴愁眉苦臉地迎上來这难,“玉大人,你說我怎么就攤上這事葡秒∫雠遥” “怎么了?”我有些...
    開封第一講書人閱讀 165,345評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵眯牧,是天一觀的道長(zhǎng)糖权。 經(jīng)常有香客問我,道長(zhǎng)炸站,這世上最難降的妖魔是什么星澳? 我笑而不...
    開封第一講書人閱讀 58,851評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮旱易,結(jié)果婚禮上禁偎,老公的妹妹穿的比我還像新娘腿堤。我一直安慰自己,他們只是感情好如暖,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評(píng)論 6 392
  • 文/花漫 我一把揭開白布笆檀。 她就那樣靜靜地躺著,像睡著了一般盒至。 火紅的嫁衣襯著肌膚如雪酗洒。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,688評(píng)論 1 305
  • 那天枷遂,我揣著相機(jī)與錄音樱衷,去河邊找鬼。 笑死酒唉,一個(gè)胖子當(dāng)著我的面吹牛矩桂,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播痪伦,決...
    沈念sama閱讀 40,414評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼侄榴,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了网沾?” 一聲冷哼從身側(cè)響起癞蚕,我...
    開封第一講書人閱讀 39,319評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎辉哥,沒想到半個(gè)月后涣达,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,775評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡证薇,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年度苔,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片浑度。...
    茶點(diǎn)故事閱讀 40,096評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡寇窑,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出箩张,到底是詐尸還是另有隱情甩骏,我是刑警寧澤,帶...
    沈念sama閱讀 35,789評(píng)論 5 346
  • 正文 年R本政府宣布先慷,位于F島的核電站饮笛,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏论熙。R本人自食惡果不足惜福青,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧无午,春花似錦媒役、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至次泽,卻和暖如春穿仪,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背意荤。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評(píng)論 1 271
  • 我被黑心中介騙來泰國(guó)打工啊片, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人袭异。 一個(gè)月前我還...
    沈念sama閱讀 48,308評(píng)論 3 372
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像炬藤,于是被迫代替她去往敵國(guó)和親御铃。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評(píng)論 2 355