什么是.pb.h 和 .pb.cc 文件?
protobuf的核心是一个.proto
文件,我们自定义一个.proto来创建我们的协议数据,然后使用protocol buffer 编译器工具编译生成两个"文件名.pb.cc"
和"文件名.pb.h"
的文件。
Protocol Buffers,是Google公司开发的一种数据格式,类似于XML和json,是一种用于数据传输时将数据序列化和反序列化的一个跨平台(支持目前主流的各种语言)工具库,可用于数据存储、通信协议等方面。它不依赖于语言和平台并且可扩展性极强。
作用:
- Protobuf支持多种编程语言,如Java、C++、Python等。你可以使用protobuf编译器将
.proto
文件编译成对应语言的源代码,从而实现跨平台、跨语言的数据交换。 - 生成的代码提供了序列化和反序列化的功能,可以很容易地将数据结构转换为字节流,或将字节流转换为数据结构。
- 使用protobuf编译器从
.proto
文件生成代码,可以省去手动编写序列化和反序列化代码的繁琐工作,减少出错的可能性。 - 生成的代码通常包含数据结构的访问器(accessor)和修改器(mutator),使得操作数据结构更加方便。
总结:使用protobuf并通过.proto
文件定义数据协议,然后编译生成对应的源代码文件,可以带来高效性、灵活性、跨平台性、跨语言支持、自动生成代码、兼容性、可扩展性以及减少网络带宽和存储空间消耗等好处。这些优点使得protobuf在分布式系统、网络通信、数据存储等领域得到了广泛的应用。并且Protobuf支持向后兼容性和向前兼容性。向后兼容性意味着新版本的程序可以读取旧版本的数据;向前兼容性意味着旧版本的程序可以选择性地忽略新版本数据中的新增字段。
通俗来说:想象一下,你正在开发一个应用,这个应用需要在不同的设备或不同的软件之间传输数据。这些数据可能包含用户的信息、订单详情、图片描述等等。但是,每台设备或每种软件都有它自己的方式来理解和存储这些数据,这就好像大家都说不同的语言,无法直接沟通。现在,你希望有一种方式,能让所有的设备或软件都理解并使用同一种数据格式。这就是protobuf的作用了。
.proto
文件就是protobuf的“蓝图”或“模板”。你可以在这个文件里,用简单明了的语法定义你的数据结构,比如一个用户信息可能包括姓名、年龄、地址等字段。然后,你可以使用protobuf的编译器(就像是一个翻译官)将这个.proto
文件“翻译”成各种编程语言都可以理解的代码(比如C++的.pb.cc
和.pb.h
文件)。
这些生成的代码就像是一个“说明书”,它告诉每种语言的程序,如何理解这个数据结构,如何把这个数据结构转换成可以传输的数据(这个过程叫做序列化),以及如何把接收到的数据转换回原来的数据结构(这个过程叫做反序列化)。
使用protobuf的好处有很多:
- 大家都懂:所有的设备或软件都可以使用同一种数据格式,就像大家都说同一种语言,沟通起来就方便了。
- 节省资源:protobuf使用了一种紧凑的二进制格式,所以传输的数据会更小,节省网络带宽,也节省存储空间。
- 容易修改:如果以后需要添加或修改数据字段,你只需要修改
.proto
文件,然后重新“翻译”一下就可以了,不需要改动已经写好的代码。 - 安全可靠:protobuf在序列化和反序列化的过程中,会对数据进行校验,确保数据的完整性和安全性。
所以,.proto
文件和protobuf就是帮助我们实现不同设备或软件之间高效、安全、方便的数据交换的工具。
模板
编写 Protocol Buffers(protobuf)的 .proto 文件时,通常遵循以下步骤和最佳实践:
1.指定语法版本:
首先,指定你正在使用的 protobuf 语法版本。目前主要有 proto2 和 proto3 两种版本。
syntax = "proto3"; // 或者 "proto2"
2.定义包名:
为你的 .proto 文件定义一个包名,这有助于防止不同项目中的消息类型名称冲突。
package your.package.name;
3.导入其他 .proto 文件:
如果你的 .proto 文件需要使用在其他 .proto 文件中定义的消息类型,你需要使用 import 语句导入它们。
import "other_protos/other.proto";
4.定义消息类型:
在 .proto 文件中定义你的消息类型。消息类型由字段组成,每个字段都有名称、类型和唯一的标识符(标签)。
message Person {
string name = 1;
int32 id = 2;
string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
string number = 1;
PhoneType type = 2;
}
repeated PhoneNumber phones = 4;
}
5.定义字段规则:
在 proto3 中,字段规则只有 required(已被移除)、optional(也被移除,所有字段默认为可选)和 repeated(表示字段可以重复,即数组)。但在 proto3 中,通常不需要显式指定 optional,因为所有字段默认就是可选的。
在 proto2 中,你需要明确指定字段是 required、optional 还是 repeated。但请注意,required 字段在 proto3 中已被移除。
6.定义服务(可选):
如果你的 .proto 文件用于定义 gRPC 服务,你可以在其中定义服务及其方法。
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
7.使用保留字段:
如果你删除了某个字段,并且之后可能会重用该字段的标识符,你应该使用 reserved 关键字来保留该标识符,以确保不会在未来发生字段标识符冲突。
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
int32 baz = 1;
}
8.注释:
使用 // 添加单行注释,或使用 /* ... */ 添加多行注释。
9.编译 .proto 文件:
使用 Protocol Buffers 编译器 protoc 将 .proto 文件编译成你需要的编程语言的源代码文件。
//bash
protoc --python_out=. your_file.proto
上面的命令将 your_file.proto 编译成 Python 代码。根据你的需要,你可以使用不同的插件(如 --cpp_out=、--java_out= 等)来生成不同编程语言的代码。
测试和验证:
在将生成的代码集成到你的项目中之前,确保通过编写单元测试和集成测试来验证 .proto 文件的定义和生成的代码的正确性。
例子:
syntax = "proto2";package InterVariable;
//这定义了一个名为 InterVariable 的包。在生成的 C++ 代码中,所有消息类都将被放置在这个命名空间内。message VariableList{
required string name = 1;
required int32 row = 2;
required int32 col = 3;
required int32 type = 4;
repeated string value = 5;//把变量的值都用string类型存储起来,使用stringstream将string类型转换成需要的类型
}
message VariableResquest{
required int32 option = 1;//0 -> exit 1 -> modify 2-> select 3 -> modify + select
repeated VariableList variable_resquest_modify = 2;
repeated VariableList variable_resquest_select = 3;
}message VariableResponse{
optional string res = 1;//如果是修改的请求,用res返回“Successful!”或者“Failed!"给客户端
repeated VariableList variable_response = 2;//如果是查询请求,返回的select的信息即查询的所有变量组成的链表给客户端
}
消息类型:VariableResquest
option
:一个必需的32位整数字段,用于表示请求的选项。注释中说明了该字段的可能值及其含义(0 表示退出,1 表示修改,2 表示选择,3 表示修改和选择)。variable_resquest_modify
:一个可重复的VariableList
字段,用于包含需要修改的变量列表。variable_resquest_select
:一个可重复的VariableList
字段,用于包含需要选择的变量列表。
消息类型:VariableResponse
res
:一个可选的字符串字段,用于在修改请求时返回成功或失败的消息。variable_response
:一个可重复的VariableList
字段,用于在查询请求时返回查询到的变量列表。
总结:
定义了一个用于处理变量请求和响应的消息结构。VariableList
消息用于表示一个变量,包括其名称、位置(行和列)、类型以及值。VariableResquest
消息用于表示一个请求,包括请求的类型(修改、选择等)以及相关的变量列表。VariableResponse
消息用于表示对请求的响应,包括响应消息和(在查询请求时)返回的变量列表。
相关解释和说明
在 Protocol Buffers(protobuf)中,字段的修饰符决定了字段的特性。在 proto3
语法中,required
已经被移除,而 optional
字段也没有明确的修饰符(因为所有字段默认都是可选的)。但在 proto2
语法中,这三个修饰符都存在,并具有以下含义:
-
required(仅在
proto2
中):required
字段表示该字段在序列化消息时必须存在,并且在解析消息时也必须存在。如果消息缺少required
字段,解析将失败并抛出异常。由于这个严格的要求,required
字段在proto3
语法中被移除了,因为proto3
的设计更偏向于简单性和向后兼容性。 -
optional(在
proto2
中,但在proto3
中默认):optional
字段表示该字段在序列化消息时可以存在,也可以不存在。在解析消息时,如果该字段不存在,它的值将被设置为默认值(对于基本类型,默认值通常是零或空字符串)。在proto3
语法中,所有字段默认都是optional
的,即使没有显式指定。 -
repeated:
repeated
字段表示该字段可以包含任意数量的元素(包括零个)。在序列化消息时,这些元素会被重复地写入。在解析消息时,你可以通过迭代这个字段来获取所有的元素。repeated
字段常用于表示数组或列表。
三种修饰符的使用场景示例:
1.required
使用场景:当某个字段对于消息来说是必不可少的,如果缺少这个字段,消息就不完整或无效。
例子:假设你正在设计一个表示“人”的消息格式,其中“名字”是一个必须存在的字段。
message Person { required string name = 1; // 名字是必须的 int32 age = 2; // 年龄是可选的 }
在这个例子中,如果序列化一个 Person 消息时没有包含 name 字段,那么序列化将失败。同样,如果尝试解析一个不包含 name 字段的 Person 消息,解析也会失败。
2.optional
使用场景:当某个字段对于消息来说是可选的,即使缺少这个字段,消息仍然是完整和有效的。
例子:在上面的 Person
消息中,age
字段是可选的。这意味着你可以序列化一个只有 name
字段的 Person
消息,而不包含 age
字段。
在 proto3
中,由于所有字段默认都是 optional
的,所以你不需要显式地使用 optional
修饰符。
3.repeated
使用场景:当某个字段可以包含多个值时,例如一个人的多个电话号码或一个项目的多个任务。
例子:假设你正在设计一个表示“联系人”的消息格式,其中一个人可以有多个电话号码。
message PhoneNumber { string number = 1; enum Type { HOME = 0; WORK = 1; MOBILE = 2; } Type type = 2;
} message Person { required string name = 1; repeated PhoneNumber phones = 2; // 一个人可以有多个电话号码
}