介绍
protobuf是一种将结构数据进行序列化的方法。protobuf序列化是将数据序列化为二进制,比其他序列化方式(如JSON)更加高效。
protobuf序列化规则非常简单,它序列化只存储字段编号、数据类型、字段值。
(字段编号 + 数据类型) + 字段值
protobuf使用
protobuf的使用流程分为3步:
1,编写.proto文件
.proto是协议文件,用来定义要传输的信息字段。如下编写contacts.proto
syntax = "proto3"; // 版本,使用proto3版本
package contacts; // 包名(C++命名空间)
message People { // message消息的定义string name = 1; // 字段1(1是字段编)
int32 age = 2; // 字段2(2是字段编号)
}
// 字段编号用于唯一识别字段,传输数据时根据编号识别字段
以上是基本的proto语法,具体的其他语法后面会详细介绍。
2,用protoc编译生成C++代码
当proto文件编写好后,下面需要用protobuf提供的编译器编译proto文件,使其生成C++代码。编译格式如下:
protoc --proto_path=IMPORT_PATH --cpp_out=OUT_DIR path/to/file.proto
protoc // 是protobuf提供的命令行编译工具
--proto_path // 指定被编译的.proto问件所在木录,可多次指定。可简写成 -I。若不指定改参数,则在当前目录进行搜索
--cpp_out= // 指编译后的文件为 C++ 文件
OUT_DIR // 编译后生成文件的目标路径
path/to/file.proto // 要编译的.proto文件
编译 contacts.proto 文件命令如下:
protoc --cpp_out=. contacts.proto
3,引入编译的C++文件
编译 contacts.proto 后,会生成所选择语言的代码,我们选择的是C++,所以编译后生成了两个文件:contacts.pb.h 和 contacts.pb.cc。其中,每个 message,都会生成一个对应的消息类。在消息类中,编译器为每个字段提供了获取和设置方法,以及能够操作字段的方法。其中,序列化和反序列化方法也包含在其中。序列化与反序列化相关的接口如下:
class MessageLite {
public:
// 序列化
// 将序列化后数据写入文件流
bool SerializeToOstream(ostream* output) const;
// 把消息序列化成二进制,写入指定的内存缓冲区(数组)
bool SerializeToArray(void *data, int size) const;
// 把消息序列化成二进制,存入 C++ 字符串string
bool SerializeToString(string* output) const;
// 反序列化,与上面序列化接口对应
bool ParseFromIstream(istream* input); // 从流中读取数据,再反序列化
bool ParseFromArray(const void* data, int size);
bool ParseFromString(const string& data);
};
编译C++代码时,需要链接protubuf的库,且至少是C++11语法。例如:
g++ main.cc contacts.pb.cc -o TestProtoBuf -std=c++11 -lprotobuf
proto语法详解
proto语法这里主要围绕proto3展开。
1,字段规则
message消息的字段可以用 singular 和 repeated 两个字段规则修饰。singular表示消息中可以包含该字段零次或⼀次(不超过⼀次)。proto3语法中,字段默认使用该规则。repeated表示消息中可以包含该字段任意多次,可以将其看作一个数组。
syntax = "proto3";
package contacts;
message People {
string name = 1;
int32 age = 2;
repeated string phone_numbers = 3; // phone_numbers可以看作一个数组
}
2,消息类型的定义与使用
在单个.proto文件中可以定义多个消息体,且支持定义嵌套类型的消息(任意多层)。每个消息体中的字段编号可以重复。除此外,还可以使用 import 导入其他的proto文件
// 嵌套写法
syntax = "proto3";
package contacts;
message Peoplefo {
string name = 1;
int32 age = 2;
message Phone {
string number = 1;
}
}
// 非嵌套写法
syntax = "proto3";
package contacts;
message Phone {
string number = 1;
}
message Peoplefo {
string name = 1;
int32 age = 2;
}
嵌套信息在信息的作用域内,访问时需要使用域的解引用操作符("::"),非嵌套信息在全局中,可直接访问。后面样例会说明。
contacts.proto文件:
syntax = "proto3";package contacts;
message Peoplefo {
string name = 1;
int32 age = 2;
message Phone {
string number = 1;
}
repeated Phone phone = 3;
}
phone.proto文件:
syntax = "proto3";package phone;
message Phone {
string number = 1;
}
contacts.proto中的Peoplefo 使用 Phone 消息:
syntax = "proto3";package contacts;
import "phone.proto";
message Peoplefo {
string name = 1;
int32 age = 2;
// 引入的文件声明了package,使用消息时,需要用 "命名空间.消息类型" 格式
repeated phone.Phone phone = 3;
}
3,enum类型
enum表示枚举类型,且枚举必须定义在 message内部或文件顶层,枚举值与编号默认情况下是相同的,格式如下:
enum 枚举名 {
枚举项 = 编号;
...
}
proto3语法有强制要求,第一个枚举项的编号必须 = 0,枚举编号必须是整数、非负、唯一,同级作用域下的枚举,不能包含相同枚举值名称。
enum PhoneType {
MP = 0;
TEL = 1;
}
enum PhoneTypeCopy {
MP = 0; // 编译后报错:MP 已经定义
}
4,Any类型
Any是万能容器,可以包裹任意一个其他消息对象,实现动态类型嵌套,类似 C++ 里的 "基类指针或泛型容器"。
Any类型是在安装protobuf时,就已经定义好的,在include目录下的goole里的protobuf中可以看到。
syntax = "proto3";
import "google/protobuf/any.proto"; // 引入 any.proto 文件
// 使用Any存储任意消息
message AnyMessage {
google.protobuf.Any data = 1;
}
5,oneof类型
oneof是protobuf中定义互斥字段的关键字,核心作用是在一组字段里,最多只能同时设置一个字段,设置其中一个会自动清空同组内的其他所有字段。oneof定义的规则如下:
oneof后面跟组名(用于代码访问)- 大括号内定义多个同级别互斥字段
- 字段编号不能重复(和普通 message 规则一致)
- 同一时间只能赋值一个字段,给 oneof 组内任意一个字段赋值,会自动清空组内其他字段。
- oneof 内部不能定义 repeated 字段。
- 同一个 oneof 组里的所有字段,共享同一块内存空间。
syntax = "proto3";
message UserInfo {
string name = 1;
// 定义 oneof 组:contact 是组名(自定义)
oneof contact {
string phone = 2; // 字段1
string email = 3; // 字段2
int32 wechat = 4; // 字段3
}
}
oneof内的字段可看成信息作用域中的字段,只不过该字段被oneof进行了修饰限制,使用时跟上面name字段的基本运用方法还是一样的。
6,保留字段reserved
reserved保留字段是为了防止误用老的字段、防止协议兼容出问题的一种方式。来看以下情况
message User {
int32 id = 1;
string name = 2;
string phone = 3; // 后来不用了,想删掉
}
这里若直接删掉 phone = 3,未来别人可能会复用 3 这个编号给新字段:string email = 3; 这里就会出现严重问题,旧数据里的 3 是 phone,新代码里的 3 是 email,导致数据错乱。
reserved把废弃的字段名 + 字段编号 保护起来,不让别人用:
message User {
int32 id = 1;
string name = 2;
reserved 3,4; // 保留 3 这个编号,不许用
reserved "phone"; // 保留 "phone" 这个名字,不许用
}
这样再写 编号3、4 或者 名称phone,编译器编译时会直接报错。
7,选项option
option选项是 protobuf 的核心配置机制,用来定义 protobuf 协议行为规则,简单说,它不定义数据结构本身,只控制数据结构如何被处理和使用。该选项分为文件级、消息级、字段级等,文件级是在整个 proto 文件中设置,消息级是在message中进行设置,字段级是在字段中进行设置。
文件级:
syntax = "proto3";option cc_generic_services = true;
.....
消息级:
message TestMsg {
option no_standard_descriptor = true;
int32 id = 1;
}
字段级:
string old_name = 1 deprecated = true;
对于C++而言,常用的有 optimize_for 和 allow_alias 两个选项。
optimize_for:该选项为文件选项,可以设置 protoc 编译器的优化级别,分别为 SPEED、CODE_SIZE、LITE_RUNTIME。受该选项影响,设置不同的优化级别,编译 .proto 文件后生成的代码内容不同。
SPEED:protoc编译器将生成的代码是高度优化的,代码运行效率高,序列化/反序列化速度最快,但是由此生成的代码,编译后会占用更多的空间,代码体积大,功能全面。SPEED是默认选项。
CODE_SIZE:proto编译器将生成最少的类,与 SPEED 相反,它的代码运行效率较低,但会占用更少的空间,精简代码。这种方式适合资源受限的设备。
LITE_RUNTIME:生成的代码执行效率高,同时生成代码编译后的所占用的空间也是非常少。这是以牺牲 protobuf 提供的反射功能为代价的, 其次编译连接时必须链接protobuf-lite,不能链接完整 protobuf。
allow_alias:该选项为枚举选项,用来定义别名,它允许同一个枚举值,对应多个不同的名字,也就是取别名。别名值相同,名字不同,与 C++ 里别名(引用)是完全等价的。
enum Test {
A = 1;
B = 1; // 报错:值重复
}
enum Test {
option allow_alias = true; // 开启别名
A = 1;
B = 1; // 合法,A 和 B 是别名
}
protobuf实战------实现一个通讯录:protobuf通讯录