Protocol Buffer
ProtocolBuffer是Google公司的一个开源项目,用于结构化数据串行化的灵活、高效、自动的方法,有如XML,不过它更小、更快、也更简单。你可以定义自己的数据结构,然后使用代码生成器生成的代码来读写这个数据结构。你甚至可以在无需重新部署程序的情况下更新数据结构。
一个例子
比如有个电子商务的系统(假设用C++实现),其中的模块A需要发送大量的订单信息给模块B,通讯的方式使用socket。
假设订单包括如下属性:
--------------------------------
时间:time(用整数表示)
客户id:userid(用整数表示)
交易金额:price(用浮点数表示)
交易的描述:desc(用字符串表示)
--------------------------------
如果使用protobuf实现,首先要写一个proto文件(不妨叫Order.proto),在该文件中添加一个名为"Order"的message结构,用来描述通讯协议中的结构化数据。该文件的内容大致如下:
message Order
{
required int32 time = 1;
required int32 userid = 2;
required float price = 3;
optional string desc = 4;
}
然后,使用protobuf内置的编译器编译该proto。由于本例子的模块是C++,你可以通过protobuf编译器让它生成 C++语言的“订单包装类”(一般来说,一个message结构会生成一个包装类)。
然后你使用类似下面的代码来序列化/解析该订单包装类:
发送方:
Order order; order.set_time(XXXX); order.set_userid(123); order.set_price(100.0f); order.set_desc("a test order"); string sOrder; order.SerailzeToString(&sOrder);
然后调用某种socket的通讯库把序列化之后的字符串sOrder发送出去;
接收方:
string sOrder; // 先通过网络通讯库接收到数据,存放到某字符串sOrder // ...... Order order; if(order.ParseFromString(sOrder)){ // 解析该字符串 cout << "userid:" << order.userid() << endl << "desc:" << order.desc() << endl; } else { cerr << "parse error!" << endl; }
有了这种代码生成机制,开发人员再也不用吭哧吭哧地编写那些协议解析的代码了。万一将来需求发生变更,要求给订单再增加一个“状态”的属性,那只需要在Order.proto文件中增加一行代码。对于发送方,只要增加一行设置状态的代码;对于接收方只要增加一行读取状态的代码。另外,如果通讯双方使用不同的编程语言来实现,使用这种机制可以有效确保两边的模块对于协议的处理是一致的。
从某种意义上讲,可以把proto文件看成是描述通讯协议的规格说明书(或者叫接口规范)。这种伎俩其实老早就有了,搞过微软的COM编程或者接触过CORBA的同学,应该都能从中看到IDL(详细解释看“这里 ”)的影子。它们的思想是相通滴。
ProtoBuf支持向后兼容(backward compatible)和向前兼容(forward compatible):
- 向后兼容,比如说,当接收方升级了之后,它能够正确识别发送方发出的老版本的协议。由于老版本没有“状态”这个属性,在扩充协议时,可以考 虑把“状态”属性设置成非必填 的(optional),或者给“状态”属性设置一个缺省值;
- 向前兼容,比如说,当发送方升级了之后,接收方能够正常识别发送方发出的新版本的协议。这时候,新增加的“状态”属性会被忽略;
向后兼容和向前兼容有啥用捏?俺举个例子:当你维护一个很庞大的分布式系统时,由于你无法同时 升级所有 模块,为了保证在升级过程中,整个系统能够尽可能不受影响,就需要尽量保证通讯协议的向后兼容或向前兼容。
proto文件
如上面的例子,使用protobuf,首先需要在一个 .proto 文件中定义你需要做串行化的数据结构信息。每个ProtocolBuffer信息是一小段逻辑记录,包含一系列的键值对。
例如:
message Person { required string name=1; required int32 id=2; optional string email=3; enum PhoneType { MOBILE=0; HOME=1; WORK=2; } message PhoneNumber { required string number=1; optional PhoneType type=2 [default=HOME]; } repeated PhoneNumber phone=4; }
一旦你定义了自己的报文格式(message),你就可以运行ProtocolBuffer编译器,将你的 .proto 文件编译成特定语言的类。这些类提供了简单的方法访问每个字段(像是 query() 和 set_query() ),像是访问类的方法一样将结构串行化或反串行化。例如你可以选择C++语言,运行编译如上的协议文件生成类叫做 Person 。随后你就可以在应用中使用这个类来串行化的读取报文信息。你可以这么写代码:
Person person; person.set_name("John Doe"); person.set_id(1234); person.set_email("jdoe@example.com"); fstream.output("myfile",ios::out | ios::binary); person.SerializeToOstream(&output);
然后,你可以读取报文中的数据:
fstream input("myfile",ios::in | ios:binary); Person person; person.ParseFromIstream(&input); cout << "Name: " << person.name() << endl; cout << "E-mail: " << person.email() << endl;
你可以在不影响向后兼容的情况下随意给数据结构增加字段,旧有的数据会忽略新的字段。所以如果使用ProtocolBuffer作为通信协议,你可以无须担心破坏现有代码的情况下扩展协议。
protobuf 消息
message由至少一个字段组合而成,类似于C语言中的结构。每个字段都有一定的格式:
限定修饰符 | 数据类型 | 字段名称 | = | 字段编码值 | [字段默认值]
限定修饰符:
- Required:表示是一个必须字段,必须相对于发送方,在发送消息之前必须设置该字段的值;对于接收方,必须能够识别该字段的意思。发送之前没有设置required字段或者无法识别required字段都会引发编解码异常,导致消息被丢弃。
- Optional:表示是一个可选字段,可选对于发送方,在发送消息时,可以有选择性的设置或者不设置该字段的值;对于接收方,如果能够识别可选字段就进行相应的处理,如果无法识别,则忽略该字段,消息中的其它字段正常处理。很多接口在升级版本中都把后来添加的字段都统一的设置为optional字段,这样老的版本无需升级程序也可以正常的与新的软件进行通信,只不过新的字段无法识别而已,因为并不是每个节点都需要新的功能,因此可以做到按需升级和平滑过渡。
- Repeated:表示该字段可以包含0 ~ N个元素,可以看作是在传递数组。
字段名称:
字段名称的命名与C、C++、Java等语言的变量命名方式几乎是相同的。
protobuf建议字段的命名采用以下划线分割的驼峰式,例如 first_name 而不是firstName。
字段编码值:
编码值的取值范围为 1~2^32(4294967296)。
消息中的字段的编码值无需连续,只要是合法的,并且不能在同一个消息中有字段包含相同的编码值。
protobuf 建议把经常要传递的值把其字段编码设置为1-15之间的值。
字段默认值:
发送数据时,对于required数据类型,如果用户没有设置值,则使用默认值传递到对端;
接收数据时,对于optional字段,如果没有接收到optional字段,则设置为默认值。
另外:
- message消息支持嵌套定义,消息可以包含另一个消息作为其字段,也可以在消息内定义一个新的消息;
- proto定义文件支持import导入其它proto定义文件;
- 每个proto文件指定一个package名称,对于java解析为java中的包。对于C++则解析为名称空间。
protobuf 数据类型
.proto类型 | C++类型 | 备注 |
double | double |
|
float | float |
|
int32 | int32 | 变长编码,编码负数时不够高效,负数最好使用sint32 |
int64 | int64 | 变长编码,编码负数时不够高效,负数最好使用sint64 |
uint32 | uint32 | 变长编码 |
uint64 | uint64 | 变长编码 |
sint32 | int32 | 变长编码,有符号的整型值,对负数编码效率高于int32s |
sint64 | int64 | 变长编码,有符号的整型值,对负数编码效率高于int64s |
fixed32 | uint32 | 4字节,如果数值总是比总是比228大的话,这个类型会比uint32高效 |
fixed64 | uint64 | 8字节,如果数值总是比总是比256大的话,这个类型会比uint64高效 |
sfixed32 | int32 | 4字节定长编码 |
sfixed64 | int64 | 8字节定长编码 |
bool | bool | 布尔值 |
string | string | 一个字符串必须是UTF-8编码或者7-bit ASCII编码的文本 |
bytes | string | 可能包含任意顺序的字节数据 |
enum | enum |
分类 | 含义 | 范围 |
0 | Varint | int32, int64, uint32, uint64, sint32, sint64, bool, enum |
1 | 64-bit | fixed64, sfixed64, double |
2 | Length-delimited | string, bytes, embedded messages, packed repeated fields |
3 | Start group | groups (deprecated) |
4 | End group | groups (deprecated) |
5 | 32-bit | fixed32, sfixed32, float |
其中:
varint(type=0),动态类型:
- 每个字节第一位表示有无后续字节,有为1,无为0,(双字节,低字节在前,高字节在后);
- 剩余7位倒序合并。
举例: 300 的二进制为 10 0101100
第一位:1(有后续) + 0101100
第二位:0(无后续) + 0000010
最终结果: 101011000000010