当前位置:   article > 正文

Protobuf介绍

protobuf

1 介绍

1.1 概述

Protocol Buffers ( Protobuf ) 是一种免费的开源 跨平台数据格式,用于序列化结构化数据。它在开发程序以通过网络相互通信或存储数据时很有用。该方法涉及描述某些数据结构的接口描述语言和根据该描述生成源代码以生成或解析表示结构化数据的字节流的程序。
Google开发了供内部使用的 Protocol Buffers,并在开源许可下为多种语言提供了代码生成器。

在这里插入图片描述

1.2 优点 & 缺点

优点

  • 紧凑的数据存储
  • 快速解析
  • 多语言支持
  • 通过自动生成的类优化功能

缺点

  • 超过几兆字节的数据,处理时,由于序列化副本,您可能最终会得到多个数据副本,这可能会导致内存使用量出现惊人的峰值。
  • 消息未压缩。虽然可以像任何其他文件一样对消息进行压缩或 gzip 压缩,但 JPEG 和 PNG 使用的专用压缩算法将为适当类型的数据生成小得多的文件。
  • 对于涉及大型多维浮点数数组的许多科学和工程用途,Protocol buffer 消息在大小和速度方面都达不到最大效率。对于这些应用程序, FITS和类似格式的开销较小。
  • 科学计算中流行的非面向对象语言(如 Fortran 和 IDL)不能很好地支持协议缓冲区。

1.3 示例

A proto definition.

message Person {
  optional string name = 1;
  optional int32 id = 2;
  optional string email = 3;
}
  • 1
  • 2
  • 3
  • 4
  • 5

uses those generated methods

// Java code
Person john = Person.newBuilder()
    .setId(1234)
    .setName("John Doe")
    .setEmail("jdoe@example.com")
    .build();
output = new FileOutputStream(args[0]);
john.writeTo(output);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

Using a generated class to parse persisted data.

// C++ code
Person john;
fstream input(argv[1],
    ios::in | ios::binary);
john.ParseFromIstream(&input);
id = john.id();
name = john.name();
email = john.email();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

2 proto 3 指南

定义消息类型

syntax = "proto3";

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 results_per_page = 3;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

如上指定字段类型和分配字段编号。字段编号范围在 1 和 536,870,911之间。对于最常设置的字段,您应该使用字段编号 1 到 15。较低的字段编号值在有线格式中占用的空间较少。例如,1 到 15 范围内的字段编号需要一个字节进行编码。16 到 2047 范围内的字段编号占用两个字节。

编译.proto文件

运行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 TypeNotesC++ TypeJava/Kotlin Type[1]Python Type[3]Go TypeRuby TypeC# TypePHP TypeDart Type
doubledoubledoublefloatfloat64Floatdoublefloatdouble
floatfloatfloatfloatfloat32Floatfloatfloatdouble
int32Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead.int32intintint32Fixnum or Bignum (as required)intintegerint
int64Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead.int64longint/long[4]int64Bignumlonginteger/string[6]Int64
uint32Uses variable-length encoding.uint32int[2]int/long[4]uint32Fixnum or Bignum (as required)uintintegerint
uint64Uses variable-length encoding.uint64long[2]int/long[4]uint64Bignumulonginteger/string[6]Int64
sint32Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s.int32intintint32Fixnum or Bignum (as required)intintegerint
sint64Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s.int64longint/long[4]int64Bignumlonginteger/string[6]Int64
fixed32Always four bytes. More efficient than uint32 if values are often greater than 228.uint32int[2]int/long[4]uint32Fixnum or Bignum (as required)uintintegerint
fixed64Always eight bytes. More efficient than uint64 if values are often greater than 256.uint64long[2]int/long[4]uint64Bignumulonginteger/string[6]Int64
sfixed32Always four bytes.int32intintint32Fixnum or Bignum (as required)intintegerint
sfixed64Always eight bytes.int64longint/long[4]int64Bignumlonginteger/string[6]Int64
boolboolbooleanboolboolTrueClass/FalseClassboolbooleanbool
stringA string must always contain UTF-8 encoded or 7-bit ASCII text, and cannot be longer than 232.stringStringstr/unicode[5]stringString (UTF-8)stringstringString
bytesMay contain any arbitrary sequence of bytes no longer than 232.stringByteStringstr (Python 2)
bytes (Python 3)[]byteString (ASCII-8BIT)ByteStringstring

字段规则

required:必须初始化字段,如果没有赋值,在数据序列化时会抛出异常
optional:可选字段,可以不必初始化。
repeated:数据可以重复(相当于java 中的Array或List)
字段唯一标识:序列化和反序列化将会使用到。

默认值

  • For strings, the default value is the empty string.
  • For bytes, the default value is empty bytes.
  • For bools, the default value is false.
  • For numeric types, the default value is zero.
  • For enums, the default value is the first defined enum value, which must be 0.
  • For message fields, the field is not set. Its exact value is language-dependent. See the generated code guide for details.

枚举

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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 必须有一个零值,这样我们就可以使用 0 作为数字 默认值。
  • 零值需要是第一个元素,以便与 第一个枚举值始终是默认值的proto2语义兼容。

JSON映射

Proto3 支持 JSON 中的规范编码,从而更容易在系统之间共享数据。

3 编写规范

标准文件格式

  • 将行长度保持在 80 个字符以内。
  • 使用 2 个空格的缩进。
  • 对字符串使用双引号。

文件结构

文件应命名为lower_snake_case.proto.【小蛇式】
所有文件应按以下方式排序:

  • License header (if applicable)
  • File overview
  • Syntax
  • Package
  • Imports (sorted)
  • File options
  • Everything else

消息和字段名称

对消息名称使用 PascalCase(首字母大写)——例如, SongServerRequest. 将 lower_snake_case 用于字段名称(包括其中一个字段和扩展名)——例如,song_name.

message SongServerRequest {
  optional string song_name = 1;
}
  • 1
  • 2
  • 3

对字段名称使用此命名约定可为您提供类似于以下两个代码示例中所示的访问器。
C++:

const string& song_name() { ... }
void set_song_name(const string& x) { ... }
  • 1
  • 2

重复字段

对重复字段使用复数名称。

repeated string keys = 1;
...
repeated MyMessage accounts = 17;
  • 1
  • 2
  • 3

枚举

枚举类型名称使用 PascalCase(首字母大写),值名称使用 CAPITALS_WITH_UNDERSCORES:

enum FooBar {
  FOO_BAR_UNSPECIFIED = 0;
  FOO_BAR_FIRST_VALUE = 1;
  FOO_BAR_SECOND_VALUE = 2;
}
  • 1
  • 2
  • 3
  • 4
  • 5

每个枚举值都应以分号而不是逗号结尾。更喜欢为枚举值添加前缀,而不是将它们包围在封闭的消息中。零值枚举应具有后缀UNSPECIFIED,因为获得意外枚举值的服务器或应用程序将在原型实例中将该字段标记为未设置。然后,字段访问器将返回默认值,对于枚举字段,默认值是第一个枚举值。

服务

如果您.proto定义了一个 RPC 服务,您应该为服务名称和任何 RPC 方法名称使用 PascalCase(首字母大写):

service FooService {
  rpc GetSomething(GetSomethingRequest) returns (GetSomethingResponse);
  rpc ListSomething(ListSomethingRequest) returns (ListSomethingResponse);
}
  • 1
  • 2
  • 3
  • 4

4 编码

消息定义如下:

message Test1 {
  optional int32 a = 1;
}
  • 1
  • 2
  • 3

在应用程序中,您创建一条Test1消息并将其设置a为 150。然后将消息序列化为输出流。如果您能够检查编码的消息,您会看到三个字节:

08 96 01
  • 1

如果使用 Protoscope 工具转储这些字节,会得到类似1: 150.

Base 128 变量

可变宽度整数或varints是有线格式的核心。它们允许使用 1 到 10 个字节之间的任意位置对无符号 64 位整数进行编码,较小的值使用较少的字节。

varint 中的每个字节都有一个连续位,指示它后面的字节是否是 varint 的一部分。这是字节的最高有效位(MSB)(有时也称为符号位)。低 7 位是有效载荷;生成的整数是通过将其组成字节的 7 位有效载荷附加在一起而构建的。

因此,例如,这里是数字 1,编码为01- 它是一个字节,因此未设置 MSB:

0000 0001
^ msb
  • 1
  • 2

这里是 150,编码为9601——这有点复杂:

10010110 00000001
^ msb    ^ msb
  • 1
  • 2

你怎么知道这是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.
  • 1
  • 2
  • 3
  • 4
  • 5

因为 varint 对协议缓冲区至关重要,所以在 protoscope 语法中,我们将它们称为普通整数。150与 相同9601

消息结构

协议缓冲区消息是一系列键值对。消息的二进制版本仅使用字段的编号作为键——每个字段的名称和声明类型只能在解码端通过引用消息类型的定义(即文件)来确定.proto。Protoscope 无法访问此信息,因此它只能提供字段编号。

当对消息进行编码时,每个键值对都会变成一个记录 ,其中包含字段编号、线路类型和有效负载。线路类型告诉解析器它之后的有效负载有多大。这允许旧解析器跳过它们不理解的新字段。这种类型的方案有时称为 Tag-Length-Value或 TLV。

有六种线型:VARINT、I64、LEN、SGROUP、EGROUP和I32

IDNameUsed For
0VARINTint32, int64, uint32, uint64, sint32, sint64, bool, enum
1I64fixed64, sfixed64, double
2LENstring, bytes, embedded messages, packed repeated fields
3SGROUPgroup start (deprecated)
4EGROUPgroup end (deprecated)
5I32fixed32, sfixed32, float

记录的“标签”被编码为一个 varint,由字段编号和数值类型通过公式 组成(field_number << 3) | wire_type。换句话说,在对表示字段的 varint 进行解码后,低 3 位告诉我们数值类型,其余整数告诉我们字段编号。

现在让我们再看看我们的简单例子。您现在知道流中的第一个数字始终是一个 varint 键,这里是08, 或(删除 MSB):

000 1000
  • 1

取最后三位以获得数值类型 (0),然后右移三位以获得字段编号 (1)。Protoscope 将标签表示为整数后跟冒号和线类型,因此我们可以将上述字节写为 1:VARINT.

因为数值类型是 0,或者VARINT,我们知道我们需要解码一个 varint 来获取有效负载。正如我们在上面看到的,字节9601varint-decode 为 150,为我们提供了记录。我们可以在 Protoscope 中将其写为1:VARINT 150.

长度分隔的记录

长度前缀是数值格式中的另一个主要概念。电线LEN类型有一个动态长度,由紧跟在标签后面的 varint 指定,后面跟往常一样是有效负载。
考虑这个消息模式:

message Test2 {
  optional string b = 2;
}
  • 1
  • 2
  • 3

该字段的记录b是一个字符串,并且字符串是LEN编码的。如果我们设置 b为"testing",我们编码为LEN包含 ASCII 字符串的字段编号 2 的记录"testing"。结果是120774657374696e67。分解字节,

12 07 [74 65 73 74 69 6e 67]
  • 1

我们看到标签 ,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字段以相同的方式编码。

map

映射字段只是一种特殊的重复字段的简写。如果我们有

message Test6 {
  map<string, int32> g = 7;
}
  • 1
  • 2
  • 3

这实际上是一样的

message Test6 {
  message g_Entry {
    optional string key = 1;
    optional int32 value = 2;
  }
  repeated g_Entry g = 7;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

5 使用

C++

https://protobuf.dev/getting-started/cpptutorial/

6 源码理解

from 陈硕

  • protobuf 大概分成两部分:compiler 和 runtime 。
  • compiler 的前端是手写的递归下降 parser ,如果你学过编译原理,很容易读懂。这个编译器的后端是各个目标语言的代码生成器,可以选你熟悉的来读。前后端通过 descriptor 联系起来,非常清晰,也便于扩展。
  • runtime 主要功能是序列化和反序列化。每个目标语言各有一套,可以根据需要来读,一般要结合生成的代码一起读。

form google protobuf的原理和思路提炼

  • protobuf为了降低编码结果的大小,牺牲了数据的自解析性
  • 为了弥补不可自解析,进行了很多硬编码,牺牲了数据的存储空间
  • 因为硬编码,不需要反射了,因此加快了数据的编码、解码速度

参考

1、wiki-Protocol Buffers
2、官网–protobuf
3、github–protobuf
4、Protobuf的简单介绍、使用和分析
5、google protobuf的原理和思路提炼
6、Protobuf3语言指南
7、如何阅读protobuf源码?

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/IT小白/article/detail/901786
推荐阅读
相关标签
  

闽ICP备14008679号