首页 > 代码库 > Protobuf使用规范分享

Protobuf使用规范分享

一、Protobuf 的优点

  Protobuf 有如 XML,不过它更小、更快、也更简单。它以高效的二进制方式存储,比 XML 小 3 到 10 倍,快 20 到 100 倍。你可以定义自己的数据结构,然后使用代码生成器生成的代码来读写这个数据结构。你甚至可以在无需重新部署程序的情况下更新数据结构。只需使用 Protobuf 对数据结构进行一次描述,即可利用各种不同语言或从各种不同数据流中对你的结构化数据轻松读写。
   有两项技术保证了采用 Protobuf 的程序能获得相对于 XML 极大的性能提高。
第一点,我们可以考察 Protobuf 序列化后的信息内容。您可以看到 Protocol Buffer 信息的表示非常紧凑,这意味着消息的体积减少,自然需要更少的资源。比如网络上传输的字节数更少,需要的 IO 更少等,从而提高性能。

第二点,我们需要理解 Protobuf 封解包的大致过程,从而理解为什么会比 XML 快很多。详细看以下链接 http://www.ibm.com/developerworks/cn/linux/l-cn-gpb/

另外,它有一个非常棒的特性,即“向后”兼容性好,人们不必破坏已部署的、依靠“老”数据格式的程序就可以对数据结构进行升级。这样您的程序就可以不必担心因为消息结构的改变而造成的大规模的代码重构或者迁移的问题。因为添加新的消息中的 field 并不会引起已经发布的程序的任何改变。
Protobuf 语义更清晰,无需类似 XML 解析器的东西(因为 Protobuf 编译器会将 .proto 文件编译生成对应的数据访问类以对 Protobuf 数据进行序列化、反序列化操作)。 使用 Protobuf 无需学习复杂的文档对象模型,Protobuf 的编程模式比较友好,简单易学,同时它拥有良好的文档和示例,对于喜欢简单事物的人们而言,Protobuf 比其他的技术更加有吸引力。

二、Protobuf消息定义

  消息由至少一个字段组合而成,类似于C语言中的结构。每个字段都有一定的格式。
  字段格式:限定修饰符① | 数据类型② | 字段名称③ | = | 字段编码值④ | 字段默认值⑤
  1)限定修饰符包含 required\optional\repeated
  Required: 表示是一个必须字段,必须相对于发送方,在发送消息之前必须设置该字段的值,对于接收方,必须能够识别该字段的意思。发送之前没有设置required字段或者无法识别required字段都会引发编解码异常,导致消息被丢弃。至于为什么感兴趣的自己可以到protobuf官网深入研究其编解码原理。http://code.google.com/p/protobuf/
  Optional:表示是一个可选字段,可选对于发送方,在发送消息时,可以有选择性的设置或者不设置该字段的值。对于接收方,如果能够识别可选字段就进行相应的处理,如果无法识别,则忽略该字段,消息中的其它字段正常处理。---因为optional字段的特性,很多接口在升级版本中都把后来添加的字段都统一的设置为optional字段,这样老的版本无需升级程序也可以正常的与新的软件进行通信,只不过新的字段无法识别而已,因为并不是每个节点都需要新的功能,因此可以做到按需升级和平滑过渡。
  Repeated:表示该字段可以包含0,N个元素。其特性和optional一样,但是每一次可以包含多个值。可以看作是在传递一个数组的值。
  
  2)数据类型 
  Protobuf定义了一套基本数据类型。几乎都可以映射到C\C++\Java等语言的基础数据类型. 
  



  另外,有一点特意强调一下:
  关于 fixed32 和int32的区别。fixed32的打包效率比int32的效率高,但是使用的空间一般比int32多。因此一个属于时间效率高,一个属于空间效率高。根据项目的实际情况,一般选择fixed32,如果遇到对传输数据量要求比较苛刻的环境,可以选择int32.
  3)字段编码值
  有了该值,通信双方才能互相识别对方的字段。当然相同的编码值,其限定修饰符和数据类型必须相同.
编码值的取值范围为 1~2^32(4294967296)。其中 1~15的编码时间和空间效率都是最高的,编码值越大,其编码的时间和空间效率就越低(相对于1-15),当然一般情况下相邻的2个值编码效率的是相同的,除非2个值恰好实在4字节,12字节,20字节等的临界区。比如15和16.
有一点需要强调,消息中的字段的编码值无需连续,只要是合法的,并且不能在同一个消息中有字段包含相同的编码值。

三、protobuf编解码原理

1) ProtoBuf编码基础——Varints, varints是一种将一个整数序列化为一个或者多个Bytes的方法,越小的整数,使用的Bytes越少。
Varints的基本规则是:
(a) 每个Byte的最高位(msb)是标志位,如果该位为1,表示该Byte后面还有其它Byte,如果该位为0,表示该Byte是最后一个Byte。
(b)每个Byte的低7位是用来存数值的位
(c)Varints方法用Litte-Endian(小端)字节序 
2)ProtoBuf中消息的编码规则:
(a)每条消息(message)都是有一系列的key-value对组成的, key和value分别采用不同的编码方式。
(b)对某一条件消息(message)进行编码的时候,是把该消息中所有的key-value对序列化成二进制字节流;而解码的时候,解码程序读入二进制的字节流,解析出每一个key-value对,如果解码过程中遇到识别不出来的类型,直接跳过。这样的机制,保证了即使该消息添加了新的字段,也不会影响旧的编/解码程序正常工作。 
(c)key由两部分组成,一部分是字段编码值(field_num),另一部分是字段类型(wire_type)。key = field_num << 3 | wire_type

 

类型含义用于
0Varintint32, int64, uint32, uint64, sint32, sint64, bool, enum
164-bitfixed64, sfixed64, double
2Length-delimitedstring, bytes, embedded messages, packed repeated fields
3Start groupgroups (deprecated)
4End groupgroups (deprecated)
532-bitfixed32, sfixed32, float

(d)varint类型(wire_type=0)的编码,与第(1)部分中介绍的方法基本一致,但是int32, int64和sint32,sint64有些特别之处:int32和int64就是简单的按varints方法来编码,所以像-1、-2这样负数也会占比较多的Bytes。于是sint32和sint64采用了一种改进的方法:先采用Zigzag方法将所有的整数(正数、0和负数)一一映射到所有的无符号数上,然 后再采用varints编码方法进行编码。Zigzag映射函数为:
Zigzag(n) = (n << 1) ^ (n >> 31), n为sint32时
Zigzag(n) = (n << 1) ^ (n >> 63), n为sint64时
(f)64-bit(wire_type=1)和32-bit(wire_type=5)的编码方式就比较简单了,直接在key后面跟上64bits或32bits,采用Little-Endian(小端)字节序。
(g)length-delimited(wire_type=2)的编码方式:key+length+content, key的编码方式是统一的,length采用varints编码方式,content就是由length指定的长度的Bytes。
(h)wire_type=3和4的现在已经不推荐使用了,因此这里也不再做介绍。
如果希望更深入的理解它,可以从代码上面去进一步研究,主要接口有protobuf_c_message_unpack、protobuf_c_message_pack、protobuf_c_message_free_unpacked等,路径/wns/commonlibs/3party/protobuf-c-0.15/protobuf-c-0.15-low-memory-version/src/google/protobuf-c


实际使用过程当中,常常出现一些使用不当的情况导致了程序异常,下面列举常见的几个情况:
1、新增字段限定修饰符设置为required(项目投入运营以后涉及到版本升级时的新增消息字段如果使用了required,需要全网统一升级)
2、多个版本同时在开发时,往同一个结构里边加入相同字段编码值的新optional字段(要知道字段编码值正是处于这种兼容性的考虑)
第2种情况会导致版本兼容性问题,代码示例如下:

发送端(AP),发送函数:

void send_person_info_to_wac() 
{ 
    Wns__PersonInfo person = WNS__PERSON_INFO__INIT; 

    person.name = "sanzer"; 
    person.gender = 1; 
    person.has_option = 1; 
    person.option = 5; 
        
    wns_ipc_to_local_direct(WNS__MODID, 0, 
        WNS__MSGID, 
        (ProtobufCMessage *)&person); 
}

proto定义:

package wns; 
person_info{ 
	requried string name = 1; 
	required int32 gender = 2; 
	optioned int32 option = 3; 
}

接收端(WAC),接收函数:

int32_t show_person_info_cb(const void *buf, int32_t len, 
                                   const ProtobufCMessage *msg, 
                                   const struct wns_cmd_ipc_hdr_t *proxy_hdr) 
{ 
    assert(msg); 
    Wns__PersonInfo *person = (Wns__PersonInfo *)msg;
    return 0;
}
// 注册回调函数: 
wns_ipc_reg_callback(WNS__MSGID, show_person_info_cb, 
    &wns__person_info__descriptor, AUTO_FREE);

proto定义: 

package wns;
person_info{
	require string name = 1;
	require int32 gender = 2;
	optioned double option = 3;
}

分别编译两种proto,并将动态库拷贝到指定目录。在接收端(wac)和发送端(ap)分别运行各自程序,然后观察接收端解析的数据格式。

结果证明接收端与发送端option字段,都采用了相同的字段编码值(3),但是不同的数据属性(发送端:int32,接收端:double),这种情况下,接收端将会解析失败。

四、protobuf使用规范

为了更好的使用它,现制定以下规范:
1、不要修改已经存在的字段编码值
2、新增字段必须为optional或repeated,否则无法保证新老程序在互相传递消息时的消息兼容性。
3、在原有的消息中,不能移除已经存在的required字段,optional和repeated类型的字段可以被移除,但是他们之前使用的标签号必须被保留,不能被新的字段重用。
4、新增字段标签号可以不连续但不能重复。


Protobuf使用规范分享