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è)問題照慣例我要上圖了
原文
所有的數(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)的人不可能做出這樣的事情。下面是打包的流程
看清楚了沒有梆靖,沒用到的字節(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 方面的文章。