1 关于protobuf及安装
protocol buffers 是一种语言无关、平台无关、可扩展的序列化结构数据的方法,它可用于(数据)通信协议、数据存储等。
Protocol Buffers 是一种灵活,高效,自动化机制的结构数据序列化方法-可类比 XML,但是比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更为简单。
你可以定义数据的结构,然后使用特殊生成的源代码轻松的在各种数据流中使用各种语言进行编写和读取结构数据。你甚至可以更新数据结构,而不破坏由旧数据结构编译的已部署程序。
简单来讲, ProtoBuf 是结构数据序列化[1] 方法,可简单类比于 XML[2],其具有以下特点:
语言无关、平台无关。即 ProtoBuf 支持 Java、C++、Python 等多种语言,支持多个平台
高效。即比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更为简单
扩展性、兼容性好。你可以更新数据结构,而不影响和破坏原有的旧程序
安装protobuf包
wget https://github.com/protocolbuffers/protobuf/releases/download/v2.6.1/protobuf-2.6.1.tar.gz --no-check-certificate
tar -xvf protobuf-2.6.1.tar.gz
cd protobuf-2.6.1
./configure
make -j8
make check
sudo make install
sudo ldconfig
2 使用protobuf
2.1 创建.proto文件 ,定义数据结构
// 例1: 在 xxx.proto 文件中定义 Example1 message
message Example1 {
optional string stringVal = 1;
optional bytes bytesVal = 2;
message EmbeddedMessage {
int32 int32Val = 1;
string stringVal = 2;
}
optional EmbeddedMessage embeddedExample1 = 3;
repeated int32 repeatedInt32Val = 4;
repeated string repeatedStringVal = 5;
}
定义消息:message 关键字后跟上消息名称,之后在其中定义message具有的字段,形式为:
message xxx {
// 字段规则:required -> 字段只能也必须出现 1 次
// 字段规则:optional -> 字段可出现 0 次或1次
// 字段规则:repeated -> 字段可出现任意多次(包括 0)
// 类型:int32、int64、sint32、sint64、string、32-bit ....
// 字段编号:0 ~ 536870911(除去 19000 到 19999 之间的数字)
字段规则 类型 名称 = 字段编号;
}
2.2编译.proto文件生成读写接口
我们在 .proto 文件中定义了数据结构,这些数据结构是面向开发者和业务程序的,并不面向存储和传输。
当需要把这些数据进行存储或传输时,就需要将这些结构数据进行序列化、反序列化以及读写。那么如何实现呢?不用担心, ProtoBuf 将会为我们提供相应的接口代码。如何提供?答案就是通过 protoc 这个编译器。
// $SRC_DIR: .proto 所在的源目录
// --cpp_out: 生成 c++ 代码
// $DST_DIR: 生成代码的目标目录
// xxx.proto: 要针对哪个 proto 文件生成接口代码
protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/xxx.proto
最终生成的代码将提供类似如下的接口:
2.3 调用接口实现序列化、反序列化以及读写
针对第一步中例1定义的 message,我们可以调用第二步中生成的接口,实现测试代码如下:
//
// Created by yue on 18-7-21.
//
#include <iostream>
#include <fstream>
#include <string>
#include "single_length_delimited_all.pb.h"
int main() {
Example1 example1;
example1.set_stringval("hello,world");
example1.set_bytesval("are you ok?");
Example1_EmbeddedMessage *embeddedExample2 = new Example1_EmbeddedMessage();
embeddedExample2->set_int32val(1);
embeddedExample2->set_stringval("embeddedInfo");
example1.set_allocated_embeddedexample1(embeddedExample2);
example1.add_repeatedint32val(2);
example1.add_repeatedint32val(3);
example1.add_repeatedstringval("repeated1");
example1.add_repeatedstringval("repeated2");
std::string filename = "single_length_delimited_all_example1_val_result";
std::fstream output(filename, std::ios::out | std::ios::trunc | std::ios::binary);
if (!example1.SerializeToOstream(&output)) {
std::cerr << "Failed to write example1." << std::endl;
exit(-1);
}
return 0;
}
编译命令:
g++ -o test xxx.cpp xx.pb.cc `pkg-config --cflags --libs protobuf`
3 proto2 语法指南
如下类型的.proto文件:
message SearchRequest {
required string query = 1;
optional int32 page_number = 2;
optional int32 result_per_page = 3;
}
SearchRequest消息格式有3个字段,在消息中承载的数据分别对应于每一个字段。其中每个字段都有一个名字和类型。
- 指定字段类型
在上面的例子中,所有字段都是标量类型:两个整型(page_number和result_per_page),一个string类型(query)。当然,你也可以为字段指定其他的合成类型,包括枚举(enumerations)或其他消息类型。 - 分配标识号
正如上述文件格式,在消息定义中,每个字段都有唯一的一个数字标识符。这些标识符是用来在消息的二进制格式中识别各个字段的,一旦开始使用就不能在改变。注**:[1, 15]之内的标识号在编码的时候会占用一个字节。[16, 2047]之内的标识号则占用2个字节。**所以应该为那些频繁出现的消息元素保留[1, 15]之内的标识号。切记:要为将来有可能添加的、频繁出现的标识号预留一些标识号。
最小的标识号可以从1开始,最大到2^29 - 1(536870911)。不可以使用其中的[19000-19999]的标识号,protobuf协议实现中对这些进行了预留。如果非要在.proto文件中使用这些预留标识号,编译时就会报警。 - 指定字段规则
所指定的消息字段修饰符必须是如下之一:
required: 一个格式良好的消息一定含有1个这种字段。表示该值是必须要设置的;
optional: 消息格式中该字段可以有0个或1个值(不超过1个)。
repeated: 在一个格式良好的消息中,这种字段可以重复任意多次(包括0次)。重复的值的顺序会被保留。表示该值可以重复,相当于java中的list。
由于一些历史原因,基本数值类型的repeated的字段并没有被尽可能地高效编码。在新的代码中,用户应该使用特殊选项[packed=true]来保证更高效的编码。如:
repeated int32 samples = 4 [packed=true];
required是永久性的: 在将一个字段标识为required的时候,应该特别小心。如果在某些情况下不想写入或者发送一个required的字段,将原始该字段修饰符更改为optional可能会遇到问题–旧版本的使用者会认为不含该字段的消息时不完整的,从而可能会无目的的拒绝解析。在这种情况下,你应该考虑编写特别针对应用程序的、自定义的消息校验函数。Google的一些工程师得出了一个结论: 使用required弊多于利;他们更愿意使用optional和repeated而不是required。当然,这个观点并不具有普遍性。 - 添加更多消息类型
在一个.proto文件中可以定义多个消息类型。在定义多个相关的消息的时候,这一点特别有用–例如,如果想定义与SearchRequest消息类型对应的回复消息格式的话,你可以将它添加到相同的.proto文件中。 - 添加注释
向.proto文件添加注释,可以使用C/C++/java风格的双斜杠(//)语法格式。 - 从.proto文件生成了什么?
当用protocolbuffer编译器来运行.proto文件时,编译器将生成所选择语言的代码,这些代码可以操作在.proto文件中定义的消息类型,包括获取、设置字段值,将消息序列化到一个输出流中,以及从一个输入流中解析消息。
对C++来说,编译器会为每个.proto文件生成一个.h文件和一个.cc文件,.proto文件中的每一个消息有一个对应的类。 - 标量数值类型
一个标量消息字段可以含有一个如下的类型–该表格展示了定义于.proto文件中的类型,以及与之对应的、在自动生成的访问类中定义的类型:
- Optional的字段和默认值
如上所述,消息描述中的一个元素可以被标记为“可选的"(optional)。一个格式良好的消息可以包含0个或者一个optional的元素。当解析消息时,如果它不包含optional的元素值,那么解析出来的对象中的对应字段就被值为默认值。默认值可以在消息描述文件中指定。例如,要为SearchRequest消息的result_per_page字段指定默认值10,在定义消息格式时如下所示:
optional int32 result_per_page = 3 [default = 10];
如果没有为optional的元素指定默认值,就会使用与特定类型相关的默认值:
对string来说,默认值是空字符串,对bool来说,默认值是false。
对数值类型来说,默认值是0。对枚举来说,默认值是枚举类型定义中的第一个值。
- 枚举
当需要定义一个消息类型的时候,可能想为一个字段指定某"预定义值序列"中的一个值。例如,假设要为每一个SearchRequest消息添加一个corpus字段,而corpus的值可能是UNIVERSAL, WEB, IMAGES, LOCAL, NEWS,一个enum类型的字段只能用指定的常量集中的一个值作为其值(如果尝试指定不同的值,解析器就会把它当做一个未知的字段来对待)。在下面的例子中,在消息格式中添加了一个叫做Corpus的枚举类型–它含有所有可能的值–以及一个类型为Corpus的字段:
message SearchRequest {
required string query = 1;
optional int32 page_number = 2;
optional int32 result_per_page = 3; [default = 10];
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
optional Corpus corpus = 4 [default = UNIVERSAL];
}
你可以为枚举常量定义别名。需要设置allow_alias option为true,否则protocol编译器会产生错误信息。
enum EnumAllowingAlias {
option allow_alias = true;
UNKNOWN = 0;
STARTED = 1;
RUNNING = 1;
}
enum EnumNotAllowingAlias {
UNKNOWN = 0;
STARTED = 1;
}
枚举常量必须在32位整型值的范围内。因为enum值是使用可变编码方式的,对负数不够高效,因此不推荐在enum中使用负数。如上例所示,可以在一个消息定义的内部或外部定义枚举–这些枚举可以在.proto文件中的任何消息定义里重用。当然也可以在一个消息中声明一个枚举类型,而在另一个不同的消息中使用它–采用MessageType.EnumType的语法格式。
当对一个使用了枚举的.proto文件运行protocol buffer编译器的时候,生成的代码中将有一个对应的enum(对Java或C++来说),或者一个特殊的EnumDescriptor类(对Python来说),它被用来运行时生成的类中创建一系列的整型值符号常量(symbolic constants)。
使用其他消息类型
你可以将其他消息类型用作字段类型。例如,假设在每一个SearchResponse消息中包含Result消息,此时可以在相同的.proto文件中定义一个Result消息类型,然后在SearchResponse消息中指定一个Result类型的字段,如:
message SearchResponse {
repeated Result result = 1;
}
message Result {
required string url = 1;
optional string title = 2;
repeated string snippets = 3;
}
- 导入定义
在上面的例子中,Result消息类型与SearchResponse是定义在同一个文件中的。如果想要使用的消息类型已经在其他.proto文件中已经定义过了呢?
你可以通过导入(importing)其他.proto文件中的定义来使用它们。要导入其他.proto文件的定义,你需要在你的文件中添加一个导入声明,如:
mport "myproject/other_protos.proto";
默认情况下你只能使用直接导入的.proto文件中的定义。然而,有时候你需要移动一个.proto文件到一个新的位置,
可以不直接移动.proto文件,只需要放入一个dummy.proto文件在老的位置,然后使用import转向新的位置:
// new.proto
// All definitions are moved here
// old.proto
// This is the proto that all clients are importing.
import public "new.proto";
import "other.proto"
// client.proto
import "old.proto"
// You use definitions from old.proto and new.proto, but not other.proto
protocol编译器就会在一系列目录中查找需要被导入的文件,这些目录通过protocol编译器的命令行参数-I/-import_path指定。如果不提供参数,编译器就在其调用目录下查找。