protobuf编码详解

转自https://www.cnblogs.com/cobbliu/archive/2013/03/02/2940074.html


从protobuf如何将特定结构体序列化为二进制流的角度,看看为什么Protobuf如此之快;

一、示例

从例子入手是学习一门新工具的最佳方法。下面我们通过一个简单的例子看看我们如何用protobuf的C++接口序列化反序列化一个结构体。

1.编辑将要序列化的结构体描述文件Hello.proto

描述文件Hello.proto

每个结构体必须用message来描述,其中的每个字段的修饰符有required, repeated和optional三种,required表示该字段是必须的,repeated表示该字段可以重复出现,它描述的字段可以看做C语言中的数组,optional表示该字段可有可无。

同时,必须人为地为每个字段赋予一个标号field_number,如上图中的1,2,3,4所示。更详细的proto文件的编写规则见这里

2.用protoc工具“编译”Hello.proto
protoc工具使用的一般格式是:
protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/xxx.proto
其中SRC_DIR是proto文件所在的目录,DST_DIR是编译proto文件后生成的结构体处理文件的目录

image

之后会生成对结构体Hello.proto中描述的各字段做序列化反序列化的类

3.编写序列化进程write.cc

    Hello msg;
    msg.set_id(101);
    msg.set_flt(13.14);
    msg.set_str("hello");
    fstream output("./log",ios::out | ios::trunc | ios::binary);

    if(!msg.SerializeToOstream(&output)){
        cerr<<"Failed to write msg."<<endl;
        return -1;
    }

我们用set方法为结构体中的每个成员赋值,然后调用SerializeToOstream将结构体序列化到文件log中。

并编译它:

image

4,编写反序列化进程reader.cc

void listMsg(const Hello& msg)
{
  cout<<msg.id()<<endl;
  cout<<msg.flt()<<endl;
  cout<<msg.str()<<endl;
}

int main(int arge, char* argv[])
{
  Hello msg;
  fstream input("./log",ios::in | ios::binary);
  if (!msg.ParseFromIstream(&input)){
    cerr<<"Pailed to parse address book!"<<endl;
    return -1;
  } 
  listMsg(msg);
  return 0;
}

用ParseFromIstream将文件中的内容序列化到类Hello的对象msg中。

并编译它:


image

5.做序列化和反序列化操作

image

二、protocol buffer的数据类型

从第一节中的例子可以看出,用Protocol buffer时需要用户自定义自己的结构体,而且结构体中的定义规则要符合google制定的规则。结构体中每个字段都需要一个数据类型,protocol buffer支持的数据类型在源代码wire_format_lite.h中定义:

image

其中:
VARINT类数据表示要用variant编码对所传入的数据做压缩存储,variant编码细节见下一节。

FIXED32和FIXED64类数据不对用户传入的数据做variant压缩存储,只存储原始数据。

LENGTH_DELIMITED类数据主要针对string类型repeated类型嵌套类型,对这些类型编码时 ++ 需要存储他们的长度信息 ++ 。

START_GROUP是一个组(该组可以是嵌套类型,也可以是repeated类型)的开始标志

END_GROUP是一个组(该组可以是嵌套类型,也可以是repeated类型)的结束标志

每类数据包含的具体数据类型如下表所示:

WireType 表示类型
VARINT int32,int64,uint32,uint64,sint32,sint64,bool,enum
FIXED64 fixed64,sfixed64,double
LENGTH_DELIMITED string,bytes,embedded messages, packed repeadted field
START_GROUP group的开始标志
END_GROUP group的结束标志
FIXED32 fixed32,sfixed32,float

三、protocol buffer的编码

ProtocolBuffer的编码是尽其所能地将字段的元信息和字段的值压缩存储,并且字段的元信息中含有对这个字段描述的所有信息。
整个结构体序列化后抽象地看起来像下图这样:


image

可以看到,整个消息是以二进制流的方式存储,在这个二进制流中,逐个字段以定义的顺序仅仅相邻。每个字段中由元信息tag字段的值value组成。

其中tag是这样编码的:

  1. field_number << 3 | wire_tye
  2. 对上面得到的无符号类型整数做variant编码
    其中field_number第一节中提到的每个字段的标号,wire_type是第二节中提到的该字段的数据类型。

1. virant编码
variant编码是一种紧凑型数字编码,将元数据跟数字保存在一起,如下图所示是数字131415的variant编码:

variant编码示例

其中第一个字节的高位msb(Most Significant Bit)为1表示下一个字节还有有效数据,msb为0表示该字节中的后7位是最后一组有效数字。剃掉最高位的有效位组成真正的数字。

从上面可以看出,variant编码存储比较小的整数时很节省空间,小于等于127的数字可以用一个字节存储。但缺点是对于大于268,435,455(0xfffffff)的整数需要5个字节来存储。

对一个整数的variant编码的代码位于./src/google/protobuf/io/coded_stream.cc:WriteVarint32FallbackToArrayInline()函数中,摘录如下:

inline uint8* CodedOutputStream::WriteVarint32FallbackToArrayInline( 
    uint32 value, uint8* target) { 
  target[0] = static_cast<uint8>(value | 0x80); 
  if (value >= (1 << 7)) { 
    target[1] = static_cast<uint8>((value >>  7) | 0x80); 
    if (value >= (1 << 14)) { 
      target[2] = static_cast<uint8>((value >> 14) | 0x80); 
      if (value >= (1 << 21)) { 
        target[3] = static_cast<uint8>((value >> 21) | 0x80); 
        if (value >= (1 << 28)) { 
          target[4] = static_cast<uint8>(value >> 28); 
          return target + 5; 
        } else { 
          target[3] &= 0x7F; 
          return target + 4; 
        } 
      } else { 
        target[2] &= 0x7F; 
        return target + 3; 
      } 
    } else { 
      target[1] &= 0x7F; 
      return target + 2; 
    } 
  } else { 
    target[0] &= 0x7F; 
    return target + 1; 
  } 
}

整个结构体的序列化过程如下:
a. 调用Hello类的ByteSize()计算出序列化后的长度,分配该长度的空间,以备以后将每个字段填充到该空间中,示例中的长度计算公式是:
1+int32Size()+1+4+1+StringSize()
b. 调用Hello类的SerializeWithCachedSizes()对每个元素序列化

下面是对每一类元素的序列化编码详解:

2. int32/int64/uint32/uint64类型的编码
a. 计算长度 1+int32size(值)
b. 调用WireFormatLite::WriteInt32(...)将该字段的元信息和字段值写入到新空间中:

image

例如用户为int32传入值123,则该字段的存储如下:
第一个字节variant(1<<3|0)第二个字节variant(123)

3.String类型的编码
a. 计算长度 1 + variant(stringLength)+stringLength
b. 调用WireFormatLite::WriteString(…)将该字段的元信息、长度和值写入到新空间中

String类型的编码

4. float类型的编码
a. 计算长度1+4
b. 调用WireFormatLite::WriteFloat(...)将该字段的元信息和值写入到新空间中

float类型的编码

其中写float内存拷贝的代码非常精炼:

inline float WireFormatLite::DecodeFloat(uint32 value) { 
  union {float f; uint32 i;}; 
  i = value; 
  return f; 
}

5. 嵌套结构体编码
a. 计算长度 1+variant32(embedded长度)+embedded的长度
b. 调用WireFormatLite::WriteMessageMaybeToArray(…)将该字段的元信息、长度和值写入到新空间中

image

6. repeated类型字段编码
a. 计算长度 1*repeated个数 + variant32(repeated长度)+repeated长度
b. 调用WireFormatLite::WriteMessageMaybeToArray(…)将下图所示编码的值写入到新空间中

image

7. sint32,sint64类型字段编码
从int32编码中可以看出,当int32传入-1时所消耗的空间很大,所以结构体定义中引入了sint32和sint64类型的数据,这种数据采用一种叫zigzag的编码方式,使绝对值比较小的整数也占用比较小的字节。

zigzag编码的映射关系图如下:


image

它将原始类型为int32的数用uint32的数表示,当一个数的绝对值比较小时,将其用uint32表示,再采用variant编码存储就会比较节省空间。
对一个整数的zigzag编码也很巧妙:

inline uint32 WireFormatLite::ZigZagEncode32(int32 n) { 
  // Note:  the right-shift must be arithmetic 
  return (n << 1) ^ (n >> 31); 
}

总结

从上面的编码可以看出, protocol buffer压榨每一个没有真正用到的字节,使之序列化后的字节尽量少,清晰的数据编码和诸多的位操作使之变得很轻便简洁高效。同时它提供了很多编程语言的接口,可以广泛应用于RPC系统中。

但是,由于它将元信息编码到二进制位中,使得序列化后的数据可读性非常差(其实是没有可读性)。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 161,873评论 4 370
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,483评论 1 306
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,525评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,595评论 0 218
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,018评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,958评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,118评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,873评论 0 208
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,643评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,813评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,293评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,615评论 3 262
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,306评论 3 242
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,170评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,968评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,107评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,894评论 2 278

推荐阅读更多精彩内容