Google数据交换格式:ProtoBuf

本文转自:https://www.cnblogs.com/chenny7/p/5157335.html

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):

  1. 向后兼容,比如说,当接收方升级了之后,它能够正确识别发送方发出的老版本的协议。由于老版本没有“状态”这个属性,在扩充协议时,可以考 虑把“状态”属性设置成非必填 的(optional),或者给“状态”属性设置一个缺省值;
  2. 向前兼容,比如说,当发送方升级了之后,接收方能够正常识别发送方发出的新版本的协议。这时候,新增加的“状态”属性会被忽略;

向后兼容和向前兼容有啥用捏?俺举个例子:当你维护一个很庞大的分布式系统时,由于你无法同时 升级所有 模块,为了保证在升级过程中,整个系统能够尽可能不受影响,就需要尽量保证通讯协议的向后兼容或向前兼容。

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("[email protected]");
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. 每个字节第一位表示有无后续字节,有为1,无为0,(双字节,低字节在前,高字节在后);
  2. 剩余7位倒序合并。

复制代码

举例: 300 的二进制为 10 0101100

第一位:1(有后续) + 0101100

第二位:0(无后续) + 0000010

最终结果: 101011000000010

复制代码

======专注高性能web服务器架构和开发=====

猜你喜欢

转载自blog.csdn.net/piaopiaopiaopiaopiao/article/details/84371857