1.protobuf简介
protobuf是google提供的一个开源序列化框架,类似于XML,JSON这样的数据表示语言,**其最大的特点是基于二进制**,因此比传统的XML表示高效短小得多。虽然是二进制数据格式,但并没有因此变得复杂,**开发人员通过按照一定地语法定义结构化的消息格式,然后送给命令行工具,工具将自动生成相关的类**,可以支持php、java、c++、python等语言环境。通过将这些类包含在项目中,可以很轻松的调用相关方法来完成业务消息的序列化与反序列化工作。
protobuf在google中是一个比较核心的基础库,作为分布式运算涉及到大量的不同业务消息的传递,如何高效简介的表示、操作这些业务消息在google这样的大规模应用中是至关重要的。而protobuf这样的库正好是在效率、数据大小、易用性之间取得了很好的平衡。
2. protobuf工作流程
第一步:首先建立一个.proto文件,在里面定义你需要做的串行化的数据结构信息。每个protobuffer信息是一小段逻辑记录,包含一系 列的键值对。
例:
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;
option PhoneType type=2[default=HOME];
}
repeated PhoneNumber phone=4;
}
每个消息类型拥有一个或多个特定的数字字段,每个字段拥有一个名字和一个值类型。值类型可以使数字(整数或浮点数)、布尔型、字符串、原始字节或者其他protobuffer类型,还允许数据结构的分级。你可以指定可选字段,必选字段和重复字段。
第二步:一旦你定义了自己的报文格式(message),你就可以运行potocolbuffer编译器,将你的.proto文件编译成特定语言的类。这些类提供了简单的方法访问每个字段(像是query()和set_query()),像是访问类的方法一样将结构串行化和反串行化。如果你选择了C++语言运行编译如上的协议文件生成类叫做Person。随后你就可以在应用中使用这个类来串行化的读取报文信息。
例
Person person;
person.set_name("rookiezhao");
person.set_id(9527);
person.set_email("rookiezhao.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作为通信协议,你可以无需担心破坏现有代码的情况下扩展协议。
数据的序列化和反序列化
序列化(serializetion):将数据结构或对象转换成二进制串的过程。
反序列化(Deserialization):将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程。
3.protobuf消息定义
消息由至少一个字段组合而成,类似于C语言中的结构。每个字段都有一定的格式。
字段格式:限定修饰符①|数据类型②|字段名称③|=|字段编码值④|[字段默认值⑤]
1、限定修饰符包含required\optional\repeated
required:表示是一个必须字段,不许相对于发送方,在发送消息之前必须设置该字段的值,对于接收方,必须能够识别该字段的意思。发送之前没有设置required字段或者无法识别required字段都会引发编码解码异常,导致消息被丢弃。
optional:表示是一个可选字段,可选对于发送方,在发送消息时,可以有选择性的设置或者不设置该字段的值。对于接收方,如果能够识别可选字段就进行相应的处理,如果无法识别则忽略该字段,消息中的其他字段正常处理。——因为optional字段的特性,很多借口在升级版本中都把后来添加的字段都统一的设置为optional字段,这样老的版本无需升级程序也可以正常的与新的软件进行通信,只不过新的字段无法识别而已,因为并不是每个节点都需要新的功能,因此可以做到按需升级和平滑过渡。
repeated:表示该字段可以包含0~N个元素。其特性和optional一样,但是每一次可以包含多个值。可以看做实在传递一个数组的值。
2、数据类型
数据类型 | 描述 | 大小 | C++对应类型 |
---|---|---|---|
bool | 布尔类型 | 1 | bool |
double | 64位浮点数 | N | double |
float | 32位浮点数 | N | float |
int32 | 32位整数 | N | int |
uint32 | 无符号32位整数 | N | unsigned int |
int64 | 64位整数 | N | _int64 |
uint64 | 64位无符号整数 | N | unsigned _int64 |
sint32 | 32位整数,处理负数效率更高 | N | int32 |
sint64 | 64位整数,处理负数效率更高 | N | _int64 |
fixed32 | 32位无符号整数 | 4 | unsigned int32 |
fixed64 | 64位无符号整数 | 8 | unsigned _int64 |
sfixed32 | 32位整数、能以更高的效率处理负数 | 4 | unsigned int32 |
sfixed64 | 64位整数 | 8 | unsigned _int64 |
string | 只能处理ASCII字符 | N | std::string |
bytes | 只能处理多字节的语言字符,如中文 | N | std::string |
enum | 可以包含一个用户自定义的枚举类型uint32 | N(uint32) | enum |
message | 可以包含一个用户自定义的消息类型 | N | object of class |
N表示打包的字节并不是固定。而是根据数据的大小或者长度。
例如int32,如果数值比较小,在0~127时,使用一个字节打包。
关于枚举的打包方式和uint32相同。
关于message,类似于C计中的结构包含另外一个数据结构作为数据成员一样。
关于fixed32和int32的区别。fixed32的打包效率比int32的效率高,但是使用空间一般比int32多,因此一个属于时间效率高,一个属于空间效率高。根据项目的实际情况,一般选择fixed32,如果遇到对传输数据量要求补缴苛刻的环境,可以选择int32。
3、字段名称
形同C、C++。
4、字段编码值
有了该值,通信双方才能互相识别对方的字段,当然相同的编码值,其限定修饰符和数据类型必须相同。
编码值的取值范围为1-4294967296。
其中1~15的编码时间和空间效率都是最高的编码值越大,其编码时间和空间效率就越低(相对于1-15),当然一般情况下相邻的2个值编码是相同的,除非两个值恰好是在4字节,8字节,20字节等的临界区,比如15和16。
1900~1000编码值为Google protobuf系统内部保留值,建议不要在自己的项目中使用。
消息中的字段的编码值无需连续,只要是合法的,并且不能在同一个消息中有字段包含相同的编码值。
建议:项目投入运营以后涉及到版本升级的新增消息字段全部使用potional或者repeated,尽量不适用required、如果使用了required,需要全网统一升级,如果使用optionall或者repeated可以平滑升级。
**5、默认值。**当在传递数据时,对于required数据类型,如果用户么有设置值,则使用默认值传递到对端。当接收数据时,对于optional字段,如果没有接收到optional字段,则设置为默认值。
关于import
protobuf接口文件可以像C语言的h文件一样,分离为多个,在需要的时候通过import导入需要对文件。其行为和C语言的#include或者java的import的行为大致相同。
关于package
.proto文件以一个package声明开始。避免名称冲突,可以给每个文件制定一个package名称,对于java解析为java中的包。对于C++则解析为名称空间。
关于message
支持嵌套消息,消息可以包含另一个消息作为其字段。也可以在消息内定义一个新的消息。
关于enum
枚举的定义和C++相同,但是有一些限制。枚举值必须大于等于0的整数。
使用分号“;”分隔枚举变量而不是C++语言中的逗号“,”;
例
enum LadyGagaProtocol
{
H323 = 1;
SIP = 2;
MGCP = 3;
H248=4;
}
4. protocol buffer API
例
package tutorial
message Student{
required uint64 id = 1;
required string name = 2;
optional string email = 3;
enum PhoneType{
MOBILE = 0;
HOME = 1;
}
message PhoneNumber{
required string number = 1;
optional PhoneType type = 2[default = HOME];
}
repeated PhoneNumber phone = 4;
}
查看proto未见为我们生成的.pb.h文件,就会发现你得到了一个类,他对应于student.proto而文件中写的每一个消息(message)。更深入一步看看 Student类:编译器为每一个字段生成了读写函数。例如,对name,id,email以及phone字段,分别有如下函数:
// required uint64 id = 1;
inline bool has_id() const;
inline void clear_id();
static const int kIdFieldNumber = 1;
inline ::google::protobuf::uint64 id() const;
inline void set_id(::google::protobuf::uint64 value);
// required string name = 2;
inline bool has_name() const;
inline void clear_name();
static const int kNameFieldNumber = 2;
inline const ::std::string& name() const;
inline void set_name(const ::std::string& value);
inline void set_name(const char* value);
inline void set_name(const char* value, size_t size);
inline ::std::string* mutable_name();
inline ::std::string* release_name();
inline void set_allocated_name(::std::string* name);
// optional string email = 3;
inline bool has_email() const;
inline void clear_email();
static const int kEmailFieldNumber = 3;
inline const ::std::string& email() const;
inline void set_email(const ::std::string& value);
inline void set_email(const char* value);
inline void set_email(const char* value, size_t size);
inline ::std::string* mutable_email();
inline ::std::string* release_email();
inline void set_allocated_email(::std::string* email);
// repeated .tutorial.Student.PhoneNumber phone = 4;
inline int phone_size() const;
inline void clear_phone();
static const int kPhoneFieldNumber = 4;
inline const ::tutorial::Student_PhoneNumber& phone(int index) const;
inline ::tutorial::Student_PhoneNumber* mutable_phone(int index);
inline ::tutorial::Student_PhoneNumber* add_phone();
inline const ::google::protobuf::RepeatedPtrField< ::tutorial::Student_PhoneNumber >& phone() const;
inline ::google::protobuf::RepeatedPtrField< ::tutorial::Student_PhoneNumber >* mutable_phone();
关于标准消息函数
每一个消息(message)还包含了其他一系列函数,用来检查或管理整个消息,包括:
bool IsInitialized() const;//检查是否全部的required字段都被(set)了值。
string DebugString() const;//返回一个易读的消息表达形式,对调试特别有用。
void CopyFrom(const Person& from);//用于外部消息的值,复写调用者消息内部的值。
void Clear();//将所有项复位到空状态。
关于解析&序列化的几个函数
bool SerializeToString(string *output)const;//将消息序列化存储在指定的string中。注意里面的内容是二进制的,而不是文本;我们只是使用string作为一个很方便的容器。
bool ParseFromString(const string &data);//从给定的string解析消息。
bool SerializeToOstream(ostream *output) const;//将消息写入到给定的C++ ostream中
bool ParseFromIstream(istream *input);//从给定的C++ istream解析消息。