Protocol Buffers ( Protobuf ) 是一种免费的开源 跨平台数据格式,用于序列化结构化数据。它在开发程序以通过网络相互通信或存储数据时很有用。该方法涉及描述某些数据结构的接口描述语言和根据该描述生成源代码以生成或解析表示结构化数据的字节流的程序。
Google开发了供内部使用的 Protocol Buffers,并在开源许可下为多种语言提供了代码生成器。
message Person {
optional string name = 1;
optional int32 id = 2;
optional string email = 3;
}
// Java code
Person john = Person.newBuilder()
.setId(1234)
.setName("John Doe")
.setEmail("[email protected]")
.build();
output = new FileOutputStream(args[0]);
john.writeTo(output);
// C++ code
Person john;
fstream input(argv[1],
ios::in | ios::binary);
john.ParseFromIstream(&input);
id = john.id();
name = john.name();
email = john.email();
syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 results_per_page = 3;
}
如上指定字段类型和分配字段编号。字段编号范围在 1 和 536,870,911之间。对于最常设置的字段,您应该使用字段编号 1 到 15。较低的字段编号值在有线格式中占用的空间较少。例如,1 到 15 范围内的字段编号需要一个字节进行编码。16 到 2047 范围内的字段编号占用两个字节。
运行protocol buffer 编译器编译 .proto 时,编译器会以按设定的语言生成代码,您需要使用您在文件中描述的消息类型,包括获取和设置字段值、将消息序列化为输出流,并从输入流中解析您的消息。
For C++, the compiler generates a .h and .cc file from each .proto, with a class for each message type described in your file.
.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 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead. | 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 228. | 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 256. | 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 232. | string | String | str/unicode[5] | string | String (UTF-8) | string | string | String |
bytes | May contain any arbitrary sequence of bytes no longer than 232. | string | ByteString | str (Python 2) | |||||
bytes (Python 3) | []byte | String (ASCII-8BIT) | ByteString | string |
required:必须初始化字段,如果没有赋值,在数据序列化时会抛出异常
optional:可选字段,可以不必初始化。
repeated:数据可以重复(相当于java 中的Array或List)
字段唯一标识:序列化和反序列化将会使用到。
enum Corpus {
CORPUS_UNSPECIFIED = 0;
CORPUS_UNIVERSAL = 1;
CORPUS_WEB = 2;
CORPUS_IMAGES = 3;
CORPUS_LOCAL = 4;
CORPUS_NEWS = 5;
CORPUS_PRODUCTS = 6;
CORPUS_VIDEO = 7;
}
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 results_per_page = 3;
Corpus corpus = 4;
}
Proto3 支持 JSON 中的规范编码,从而更容易在系统之间共享数据。
文件应命名为lower_snake_case.proto.【小蛇式】
所有文件应按以下方式排序:
对消息名称使用 PascalCase(首字母大写)——例如, SongServerRequest. 将 lower_snake_case 用于字段名称(包括其中一个字段和扩展名)——例如,song_name.
message SongServerRequest {
optional string song_name = 1;
}
对字段名称使用此命名约定可为您提供类似于以下两个代码示例中所示的访问器。
C++:
const string& song_name() { ... }
void set_song_name(const string& x) { ... }
对重复字段使用复数名称。
repeated string keys = 1;
...
repeated MyMessage accounts = 17;
枚举类型名称使用 PascalCase(首字母大写),值名称使用 CAPITALS_WITH_UNDERSCORES:
enum FooBar {
FOO_BAR_UNSPECIFIED = 0;
FOO_BAR_FIRST_VALUE = 1;
FOO_BAR_SECOND_VALUE = 2;
}
每个枚举值都应以分号而不是逗号结尾。更喜欢为枚举值添加前缀,而不是将它们包围在封闭的消息中。零值枚举应具有后缀UNSPECIFIED,因为获得意外枚举值的服务器或应用程序将在原型实例中将该字段标记为未设置。然后,字段访问器将返回默认值,对于枚举字段,默认值是第一个枚举值。
如果您.proto定义了一个 RPC 服务,您应该为服务名称和任何 RPC 方法名称使用 PascalCase(首字母大写):
service FooService {
rpc GetSomething(GetSomethingRequest) returns (GetSomethingResponse);
rpc ListSomething(ListSomethingRequest) returns (ListSomethingResponse);
}
消息定义如下:
message Test1 {
optional int32 a = 1;
}
在应用程序中,您创建一条Test1消息并将其设置a为 150。然后将消息序列化为输出流。如果您能够检查编码的消息,您会看到三个字节:
08 96 01
如果使用 Protoscope 工具转储这些字节,会得到类似1: 150.
可变宽度整数或varints是有线格式的核心。它们允许使用 1 到 10 个字节之间的任意位置对无符号 64 位整数进行编码,较小的值使用较少的字节。
varint 中的每个字节都有一个连续位,指示它后面的字节是否是 varint 的一部分。这是字节的最高有效位(MSB)(有时也称为符号位)。低 7 位是有效载荷;生成的整数是通过将其组成字节的 7 位有效载荷附加在一起而构建的。
因此,例如,这里是数字 1,编码为01
- 它是一个字节,因此未设置 MSB:
0000 0001
^ msb
这里是 150,编码为9601
——这有点复杂:
10010110 00000001
^ msb ^ msb
你怎么知道这是150?首先你从每个字节中删除 MSB,因为它只是告诉我们是否已经到达数字的末尾(如你所见,它被设置在第一个字节中,因为 varint 中有多个字节) . 然后我们连接 7 位有效载荷,并将其解释为小端、64 位无符号整数:
10010110 00000001 // Original inputs.
0010110 0000001 // Drop continuation bits.
0000001 0010110 // Put into little-endian order.
10010110 // Concatenate.
128 + 16 + 4 + 2 = 150 // Interpret as integer.
因为 varint 对协议缓冲区至关重要,所以在 protoscope 语法中,我们将它们称为普通整数。150与 相同9601
。
协议缓冲区消息是一系列键值对。消息的二进制版本仅使用字段的编号作为键——每个字段的名称和声明类型只能在解码端通过引用消息类型的定义(即文件)来确定.proto。Protoscope 无法访问此信息,因此它只能提供字段编号。
当对消息进行编码时,每个键值对都会变成一个记录 ,其中包含字段编号、线路类型和有效负载。线路类型告诉解析器它之后的有效负载有多大。这允许旧解析器跳过它们不理解的新字段。这种类型的方案有时称为 Tag-Length-Value或 TLV。
有六种线型:VARINT、I64、LEN、SGROUP、EGROUP和I32
ID | Name | Used For |
---|---|---|
0 | VARINT | int32, int64, uint32, uint64, sint32, sint64, bool, enum |
1 | I64 | fixed64, sfixed64, double |
2 | LEN | string, bytes, embedded messages, packed repeated fields |
3 | SGROUP | group start (deprecated) |
4 | EGROUP | group end (deprecated) |
5 | I32 | fixed32, sfixed32, float |
记录的“标签”被编码为一个 varint,由字段编号和数值类型通过公式 组成(field_number << 3) | wire_type。换句话说,在对表示字段的 varint 进行解码后,低 3 位告诉我们数值类型,其余整数告诉我们字段编号。
现在让我们再看看我们的简单例子。您现在知道流中的第一个数字始终是一个 varint 键,这里是08
, 或(删除 MSB):
000 1000
取最后三位以获得数值类型 (0),然后右移三位以获得字段编号 (1)。Protoscope 将标签表示为整数后跟冒号和线类型,因此我们可以将上述字节写为 1:VARINT.
因为数值类型是 0,或者VARINT,我们知道我们需要解码一个 varint 来获取有效负载。正如我们在上面看到的,字节9601
varint-decode 为 150,为我们提供了记录。我们可以在 Protoscope 中将其写为1:VARINT 150.
长度前缀是数值格式中的另一个主要概念。电线LEN类型有一个动态长度,由紧跟在标签后面的 varint 指定,后面跟往常一样是有效负载。
考虑这个消息模式:
message Test2 {
optional string b = 2;
}
该字段的记录b是一个字符串,并且字符串是LEN编码的。如果我们设置 b为"testing",我们编码为LEN包含 ASCII 字符串的字段编号 2 的记录"testing"。结果是120774657374696e67
。分解字节,
12 07 [74 65 73 74 69 6e 67]
我们看到标签 ,12
是00010 010, 或2:LEN。后面的字节是带符号的 int32 varint 7,接下来的七个字节是 的 UTF-8 编码"testing"。int32 varint 表示字符串的最大长度为 2GB。
在 Protoscope 中,这被写为2:LEN 7 “testing”. 但是,重复字符串的长度可能会很不方便(在 Protoscope 文本中,字符串已经用引号分隔)。将 Protoscope 内容包裹在大括号中会为其生成一个长度前缀:{“testing”}是7 “testing”. {}总是由字段推断为LEN记录,因此我们可以将此记录简单地写为2: {“testing”}.
bytes字段以相同的方式编码。
映射字段只是一种特殊的重复字段的简写。如果我们有
message Test6 {
map<string, int32> g = 7;
}
这实际上是一样的
message Test6 {
message g_Entry {
optional string key = 1;
optional int32 value = 2;
}
repeated g_Entry g = 7;
}
https://protobuf.dev/getting-started/cpptutorial/
from 陈硕
form google protobuf的原理和思路提炼
1、wiki-Protocol Buffers
2、官网–protobuf
3、github–protobuf
4、Protobuf的简单介绍、使用和分析
5、google protobuf的原理和思路提炼
6、Protobuf3语言指南
7、如何阅读protobuf源码?