通常,程序运行时产生的一些结构化数据会经历存储或者与网络中的其他设备和程序交换的过程。因此我们需要提前对他们进行序列化和编码。这个过程可以有很多种方式:
有人对这些编码方式做过对比:
详细的实验过程和数据:
N. Gligorić, I. Dejanović and S. Krčo, “Performance evaluation of compact binary XML representation for constrained devices,” 2011 International Conference on Distributed Computing in Sensor Systems and Workshops (DCOSS), Barcelona, Spain, 2011, pp. 1-5, doi: 10.1109/DCOSS.2011.5982183.
syntax
:protobuf版本。一般放在.proto
文件的第一行。默认是proto2message
:包含message名称和消息内字段的定义字段
:标签(可选) + 类型 + 名称 + 值举个例子:
syntax = "proto3";
package testPkg;
import "samePkg/xxx.proto" // 注释1: import当前包内的其他proto
import "otherPkg/yyy.proto" /* 注释2: import其他包内的其他proto */
enum SomeEnum {
someValue1 = 0;
someValue2 = 1;
someValue3 = 2;
}
message SomeMsg {
reserved 10, 13 to 20;
reserved "resvItemName1";
optional uint32 someItem1 = 1;
optional SomeEnum someItem2 = 2;
repeated string someItem3 = 128 [packed = true];
}
字段的编号需要满足3个约束:
optional
:表示消息可包含也可不包含该字段required
:消息必须包含该字段
syntax = "proto3";
message Person {
optional string name = 1;
required int32 age = 2;
}
repeated
:该字段可以重复多次,用于表示数组或列表。每个元素都必须属于指定的数据类型。可以包含零个或多个值extensions
:在不破坏现有消息格式的情况下添加新字段
syntax = "proto3";
message MyMessage {
string message_body = 1;
extensions 100 to 199; // 指定扩展字段的范围
}
extend MyMessage {
int32 custom_field = 100;
}
default
:指定字段的默认值。如果消息中未设置该字段的值,将使用默认值
// .proto
syntax = "proto3";
message Person {
string name = 1;
int32 age = 2 [default = 18];
}
// .cpp
Person person = Person.newBuilder()
.setName("Alice")
.build();
packed
:指示重复字段是否应该使用编码方式进行紧凑打包,以减小序列化后的消息大小。通常用于重复的数值类型字段
// .proto
syntax = "proto3";
message IntList {
repeated int32 numbers = 1 [packed = true];
}
// .cpp
IntList intList = IntList.newBuilder()
.addNumbers(1)
.addNumbers(2)
.addNumbers(3)
.build();
oneof
:定义一组字段,这些字段中只能有一个实际存在
syntax = "proto3";
message Contact {
oneof contact_info {
string email = 1;
string phone = 2;
string address = 3;
}
}
deprecated
:表示该字段已经弃用
syntax = "proto3";
message Person {
string name = 1;
int32 age = 2 [deprecated = true];
}
// 生成的java包:
/**
* int32 age = 2 [deprecated = true];
*/
@java.lang.Deprecated public static final int AGE_FIELD_NUMBER = 2;
private int age_;
在protoBuf中,字段类型可以是一些标量、枚举类型,还可以是复合类型
.proto Type | Notes | C++ Type | Java/Kotlin Type[1] | Python Type[3] | Go Type | Ruby Type | C# Type | PHP Type | Dart Type |
---|---|---|---|---|---|---|---|---|---|
double | double | double | float | float64 | Float | double | float | double | |
float | float | float | float | float32 | Float | float | float | double | |
int32 | 使用可变长度编码。对于编码负数效率低下 - 如果字段可能有负值,改用sint32。 | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer | int |
int64 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead. | int64 | long | int/long[4] | int64 | Bignum | long | integer/string[6] | Int64 |
uint32 | Uses variable-length encoding. | uint32 | int[2] | int/long[4] | uint32 | Fixnum or Bignum (as required) | uint | integer | int |
uint64 | Uses variable-length encoding. | uint64 | long[2] | int/long[4] | uint64 | Bignum | ulong | integer/string[6] | Int64 |
sint32 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s. | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer | int |
sint64 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s. | int64 | long | int/long[4] | int64 | Bignum | long | integer/string[6] | Int64 |
fixed32 | Always four bytes. More efficient than uint32 if values are often greater than 2^28. | uint32 | int[2] | int/long[4] | uint32 | Fixnum or Bignum (as required) | uint | integer | int |
fixed64 | Always eight bytes. More efficient than uint64 if values are often greater than 2^56. | uint64 | long[2] | int/long[4] | uint64 | Bignum | ulong | integer/string[6] | Int64 |
sfixed32 | Always four bytes. | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer | int |
sfixed64 | Always eight bytes. | int64 | long | int/long[4] | int64 | Bignum | long | integer/string[6] | Int64 |
bool | bool | boolean | bool | bool | TrueClass/FalseClass | bool | boolean | bool | |
string | A string must always contain UTF-8 encoded or 7-bit ASCII text, and cannot be longer than 2^32. | string | String | str/unicode[5] | string | String (UTF-8) | string | string | String |
bytes | May contain any arbitrary sequence of bytes no longer than 2^32. | string | ByteString | str (Python 2) bytes (Python 3) |
[]byte | String (ASCII-8BIT) | ByteString | string | List |
[1] Kotlin使用与Java相应的类型,即使对于无符号类型,也要确保在混合Java/Kotlin代码库中的兼容性。
[2] 在Java中,无符号的32位和64位整数是使用它们的有符号对应类型表示的,只是顶部位被存储在符号位中。
[3] 把值设置到字段将执行类型检查来确保它有效。
[4] 解码时,64位或无符号的32位整数始终表示为长整型,但如果在设置字段时提供整数,则可以是整数。同时,值必须适合在设置时表示的类型中。见[2]。
[5] Python字符串在解码时表示为Unicode,但如果提供ASCII字符串(官网说这条规则之后可能会改)则可以是str类型。
[6] 整数用于64位机器,字符串用于32位机器。
枚举类型用于定义一组具有离散取值的常量,这些取值可以用于消息字段,以表示某种状态、选项或标识符,可以是任意整数
enum SomeEnum {
someValue1 = 0;
someValue2 = 1;
someValue3 = 2;
// 可以继续添加更多的枚举值
}
举个例子:
syntax = "proto3";
// 定义一个枚举类型
enum Color {
RED = 0;
GREEN = 1;
BLUE = 2;
}
// 定义一个消息类型,包含一个 Color 类型的字段
message ColoredShape {
Color color = 1;
}
// .cpp
ColoredShape shape = ColoredShape.newBuilder()
.setColor(Color.RED)
.build();
和高级语言中的自定义嵌套类型差不多,语法上这么定义:
message OuterMessage {
// 其他字段的定义
message InnerMessage {
// 内部消息字段的定义
}
enum InnerEnum {
// 内部枚举值的定义
}
// 可以继续定义其他的内部类型
}
嵌套类型的名称是相对于外部消息类型的,相当于可以在不同消息中使用相同名称的嵌套类型,而不会导致冲突
举个例子:
syntax = "proto3";
message AddressBook {
// 外部消息字段的定义
string owner_name = 1;
// 嵌套消息类型
message Contact {
string name = 1;
string email = 2;
}
// 嵌套枚举类型
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
// 更多的外部消息字段的定义
repeated Contact contacts = 2;
}
在protoBuf里,map也对应一个映射,一条map类型的字段这么定义:map
举个例子:
syntax = "proto3";
message AddressBook {
string owner_name = 1;
// 定义一个 map 类型字段,表示联系人列表,键是字符串类型,值是 Contact 消息类型
map contacts = 2;
}
message Contact {
string name = 1;
string email = 2;
}
// .cpp
AddressBook addressBook = AddressBook.newBuilder()
.setOwnerName("John Doe")
.putContacts("Alice", Contact.newBuilder()
.setName("Alice")
.setEmail("[email protected]")
.build())
.putContacts("Bob", Contact.newBuilder()
.setName("Bob")
.setEmail("[email protected]")
.build())
.build();
这其实是一个protobuf3废弃的类型。将多个字段组织在一起,它在proto2版本中引入,并被设计为一种消息组织机制。每个组中的字段都被分配一个唯一的字段编号,并且可以将多个字段组合在一起,以便在解析时更容易处理。类似于之前说的复合类型
举个例子:MyMessage 包含一个名为 group_field 的字段,其类型是 MyGroup。MyGroup 包含了两个字段 field1 和 field2。使用group可以将这两个字段组织在一起,以便更容易地一起处理
syntax = "proto2";
message MyMessage {
message MyGroup {
required int32 field1 = 1;
required string field2 = 2;
}
optional MyGroup group_field = 3;
}
// 在protobuf3中,field1 和 field2 都是独立的字段,不再需要group来组织,直接这么写:
syntax = "proto3";
message MyMessage {
int32 field1 = 1;
string field2 = 2;
}
语法:import "other.proto";
other.proto 是要导入的其他 .proto 文件的名称(相对于当前文件的路径)
举个例子:
假设有两个 .proto 文件,分别是 person.proto 和 address.proto,其中 address.proto 中定义了一个地址信息的消息类型:
// address.proto
syntax = "proto3";
message Address {
string street = 1;
string city = 2;
string state = 3;
string postal_code = 4;
}
// person.proto
syntax = "proto3";
// 导入 address.proto 中的定义
import "address.proto";
message Person {
string name = 1;
int32 age = 2;
Address address = 3; // 使用导入的 Address 消息类型
}
编码有这么几个要点:
举个例子:
// 先定义一个简单的消息类型
syntax = "proto3";
message Person {
string name = 1;
int32 age = 2;
repeated string email = 3;
}
// 创建一个 Person 消息的实例,填上一些字段的值
#include
#include "person.pb.h" // 包含生成的头文件
int encodingFunc() {
// 创建一个 Person 对象
Person person;
// 设置字段的值
person.set_name("Alice");
person.set_age(30);
person.add_email("[email protected]");
person.add_email("[email protected]");
// 将消息对象序列化为二进制数据(可选)
std::string serializedData = person.SerializeAsString();
// 访问消息字段
std::cout << "Name: " << person.name() << std::endl;
std::cout << "Age: " << person.age() << std::endl;
const google::protobuf::RepeatedPtrField& emails = person.email();
std::cout << "Emails:" << std::endl;
for (const std::string& email : emails) {
std::cout << "- " << email << std::endl;
}
return 0;
}
解码的步骤如下:
举个例子:
// 导入消息类型定义
import com.example.PersonProto.Person;
// 创建解码器
Person person = Person.parseFrom(serializedData);
// 访问消息字段
#include
#include
#include "person.pb.h" // 包含生成的头文件
int decodingFunc() {
// 读取二进制数据
std::ifstream input("person.dat", std::ios::in | std::ios::binary);
if (!input) {
std::cerr << "Failed to open input file." << std::endl;
return 1;
}
// 创建 Person 对象并解码数据
Person person;
if (!person.ParseFromIstream(&input)) {
std::cerr << "Failed to parse Person." << std::endl;
return 1;
}
// 访问消息字段
std::cout << "Name: " << person.name() << std::endl;
std::cout << "Age: " << person.age() << std::endl;
const google::protobuf::RepeatedPtrField& emails = person.email();
std::cout << "Emails:" << std::endl;
for (const std::string& email : emails) {
std::cout << "- " << email << std::endl;
}
return 0;
}
高级语言类型 | 生成的文件类型 |
---|---|
C++ | .h 和 .cc 文件 |
Java | .java 文件,以及用于创建消息类实例的特殊 Builder 类 |
Kotlin | .kt 文件,包含用于简化创建消息实例的 DSL |
Python | 生成一个模块,其中包含每个消息类型的静态描述,并在运行时使用元类创建必要的Python数据访问类 |
Go | .pb.go 文件,每个消息类型对应一个类型 |
Ruby | .rb 文件,包含您的消息类型的Ruby模块 |
Objective-C | .pbobjc.h 和 .pbobjc.m 文件,每个消息类型对应一个类 |
C# | .cs 文件,每个消息类型对应一个类 |
Dart | .pb.dart 文件,每个消息类型对应一个类 |