【全网最全protobuf中文版教程】

protocal buffer Notes

文章目录

  • protocal buffer Notes
  • 为什么是ProtoBuf
  • protoBuf数据要素
    • 字段编号
    • 标签
    • 字段类型
      • 标量
      • 枚举
      • 复合类型
      • map
      • group
  • 导入其他消息类型
  • 编码解码
    • 编码
    • 解码
  • proto生成文件

为什么是ProtoBuf

通常,程序运行时产生的一些结构化数据会经历存储或者与网络中的其他设备和程序交换的过程。因此我们需要提前对他们进行序列化和编码。这个过程可以有很多种方式:

  1. 直接采用二进制数据。
    • 虽然比较简单,但是它要求程序的运行环境永远保持一致的内存布局和字节序。同时,这种格式难以扩展。
  2. 采用简单的自定义手段进行编码。譬如二叉树的序列化存储可以直接保存按照某些顺序遍历的结果。
    • 这种方式编解码容易,需要的算力低,适用于数据结构简单的数据。
  3. 采用一些标准化的格式。譬如ASN.1,XML,json,protobuf等
    • ASN.1解码需要消耗大量算力,XML是空间密集型,同时XML索引采用XMLDOM树,也更为复杂。

有人对这些编码方式做过对比:

  1. 原始解析对比:
    【全网最全protobuf中文版教程】_第1张图片

  2. 网络解析时间对比:
    【全网最全protobuf中文版教程】_第2张图片

  3. 网络解析内存占比:
    【全网最全protobuf中文版教程】_第3张图片

详细的实验过程和数据:
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.

protoBuf数据要素

  1. syntax:protobuf版本。一般放在.proto文件的第一行。默认是proto2
  2. message:包含message名称和消息内字段的定义
  3. 字段:标签(可选) + 类型 + 名称 + 值

举个例子:

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个约束:

  1. 每个字段的编号在所属的message内是唯一的
  2. 编号值在1~536870911( 2 29 − 1 2^{29}-1 2291)之内(尽量小)
  3. 不能使用先前保留的编号
    • 19000~19999被protobuf保留作实现用

标签

  1. optional:表示消息可包含也可不包含该字段
  2. required:消息必须包含该字段
    • 举个例子:
    syntax = "proto3";
    
    message Person {
        optional string name = 1;
        required int32 age = 2;
    }
    
  3. repeated:该字段可以重复多次,用于表示数组或列表。每个元素都必须属于指定的数据类型。可以包含零个或多个值
  4. extensions:在不破坏现有消息格式的情况下添加新字段
    • 举个例子:
    syntax = "proto3";
    
    message MyMessage {
        string message_body = 1;
        extensions 100 to 199; // 指定扩展字段的范围
    }
    
    extend MyMessage {
        int32 custom_field = 100;
    }
    
  5. default:指定字段的默认值。如果消息中未设置该字段的值,将使用默认值
    • 举个例子:
    // .proto
    syntax = "proto3";
    
    message Person {
        string name = 1;
        int32 age = 2 [default = 18];
    }
    
    // .cpp
    Person person = Person.newBuilder()
                          .setName("Alice")
                          .build();
    
  6. packed:指示重复字段是否应该使用编码方式进行紧凑打包,以减小序列化后的消息大小。通常用于重复的数值类型字段
    • 举个例子:序列化下面这个消息时,整数数组将以紧凑的方式编码,而不会包含额外的字段编号,从而减小了消息的大小,减小数据带宽和传输成本。
    // .proto
    syntax = "proto3";
    
    message IntList {
        repeated int32 numbers = 1 [packed = true];
    }
    
    // .cpp
    IntList intList = IntList.newBuilder()
                             .addNumbers(1)
                             .addNumbers(2)
                             .addNumbers(3)
                             .build();
    
  7. oneof:定义一组字段,这些字段中只能有一个实际存在
    • 举个例子:Contact 消息类型有三个字段:email、phone 和 address,但只能设置其中一个字段的值
    syntax = "proto3";
    
    message Contact {
        oneof contact_info {
            string email = 1;
            string phone = 2;
            string address = 3;
        }
    }
    
  8. 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;
}

map

在protoBuf里,map也对应一个映射,一条map类型的字段这么定义:map field_name = field_number;

  • key_type 是键的数据类型,可以是整数类型、字符串类型等。
  • value_type 是值的数据类型,可以是任何支持的数据类型,包括消息类型。
  • field_name 是字段的名称。
  • field_number 是字段的编号,用于在二进制编码中标识该字段。

举个例子:

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();

group

这其实是一个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 消息类型
}

编码解码

编码

编码有这么几个要点:

  1. 字段值:字段的值是消息中的实际数据。不同的数据类型使用不同的编码方式来表示字段的值。
  2. Varint 编码:对于整数类型(例如 int32、int64、uint32、uint64、sint32 和 sint64),protobuf 使用Base 128 Varint编码来表示字段的值,可以根据值的大小选择使用不同的字节数来表示整数。
  3. 长度-值编码:对于字符串、字节数组和消息类型字段,protobuf 使用长度-值编码方式。首先,编码字段的长度,然后编码字段的值。可以在不解码字段的情况下跳过不感兴趣的字段。
  4. 重复字段编码:对于重复字段(repeated),多个值按顺序编码,形成一个列表。在编码中,首先编码列表的长度,然后编码每个元素的值。

举个例子:

// 先定义一个简单的消息类型
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;
}

解码

解码的步骤如下:

  1. 导入消息类型定义:在解码之前,需要导入用于定义消息类型的 .proto 文件或已编译的消息类型定义。确保解码器能够理解二进制数据的结构。
  2. 创建消息解码器:需要创建一个用于解码的消息对象,该对象对应于待解码的消息类型。
  3. 使用解码器解码数据:将二进制数据传递给消息解码器,以便将其还原为消息对象。解码器将根据字段的规则和类型来解析二进制数据。

举个例子:

// 导入消息类型定义
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;
}

proto生成文件

高级语言类型 生成的文件类型
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 文件,每个消息类型对应一个类

你可能感兴趣的:(结构化数据,protobuf)