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

news2025/1/23 17:45:41

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. 原始解析对比:
    在这里插入图片描述

  2. 网络解析时间对比:
    在这里插入图片描述

  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包:
    /**
    * <code>int32 age = 2 [deprecated = true];</code>
    */
    @java.lang.Deprecated public static final int AGE_FIELD_NUMBER = 2;
    private int age_;
    
    

字段类型

在protoBuf中,字段类型可以是一些标量、枚举类型,还可以是复合类型

标量

.proto TypeNotesC++ TypeJava/Kotlin Type[1]Python Type[3]Go TypeRuby TypeC# TypePHP TypeDart Type
doubledoubledoublefloatfloat64Floatdoublefloatdouble
floatfloatfloatfloatfloat32Floatfloatfloatdouble
int32使用可变长度编码。对于编码负数效率低下 - 如果字段可能有负值,改用sint32。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 2^28.uint32int[2]int/long[4]uint32Fixnum or Bignum (as required)uintintegerint
fixed64Always eight bytes. More efficient than uint64 if values are often greater than 2^56.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 2^32.stringStringstr/unicode[5]stringString (UTF-8)stringstringString
bytesMay contain any arbitrary sequence of bytes no longer than 2^32.stringByteStringstr (Python 2)
bytes (Python 3)
[]byteString (ASCII-8BIT)ByteStringstringList

[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<key_type, value_type> field_name = field_number;

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

举个例子:

syntax = "proto3";

message AddressBook {
  string owner_name = 1;

  // 定义一个 map 类型字段,表示联系人列表,键是字符串类型,值是 Contact 消息类型
  map<string, Contact> 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("alice@example.com")
        .build())
    .putContacts("Bob", Contact.newBuilder()
        .setName("Bob")
        .setEmail("bob@example.com")
        .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 <iostream>
#include "person.pb.h"  // 包含生成的头文件

int encodingFunc() {
    // 创建一个 Person 对象
    Person person;

    // 设置字段的值
    person.set_name("Alice");
    person.set_age(30);
    person.add_email("alice@example.com");
    person.add_email("alice@gmail.com");

    // 将消息对象序列化为二进制数据(可选)
    std::string serializedData = person.SerializeAsString();

    // 访问消息字段
    std::cout << "Name: " << person.name() << std::endl;
    std::cout << "Age: " << person.age() << std::endl;

    const google::protobuf::RepeatedPtrField<std::string>& 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 <iostream>
#include <fstream>
#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<std::string>& 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 文件,每个消息类型对应一个类

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/996258.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于Java+SpringBoot+Vue校园求职招聘系统的设计与实现 前后端分离【Java毕业设计·文档报告·代码讲解·安装调试】

&#x1f34a;作者&#xff1a;计算机编程-吉哥 &#x1f34a;简介&#xff1a;专业从事JavaWeb程序开发&#xff0c;微信小程序开发&#xff0c;定制化项目、 源码、代码讲解、文档撰写、ppt制作。做自己喜欢的事&#xff0c;生活就是快乐的。 &#x1f34a;心愿&#xff1a;点…

docker系列(2) - 常用命令篇

文章目录 2. docker常用命令2.1 参数说明(tomcat案例)2.2 基本命令2.3 高级命令2.4 其他 2. docker常用命令 2.1 参数说明(tomcat案例) 注意如果分成多行&#xff0c;\后面不能有空格 # 拉取运行 docker run \ -d \ -p 8080:8080 \ --privilegedtrue \ --restartalways \ -m…

docker系列(5) - docker仓库

文章目录 5 docker仓库5.1 创建命名空间5.2 创建镜像仓库5.3 设置访问凭证5.3 镜像仓库命令信息5.4 登录阿里云上传镜像5.5 拉取镜像运行5.6 私有仓库(docker Registry)5.6.1 安装docker registry5.6.2 准备镜像5.6.2 本地私服仓库5.6.3 推送到私服仓库5.6.4 拉取私服镜像 5 do…

信息系统项目管理师(第四版)教材精读思维导图-第十三章项目资源管理

请参阅我的另一篇文章&#xff0c;综合介绍软考高项&#xff1a; 信息系统项目管理师&#xff08;软考高项&#xff09;备考总结_计算机技术与软件专业技术_铭记北宸的博客-CSDN博客 本章思维导图源文件 13.1 管理基础 13.2 管理过程 13.3 规划资源管理 13.4 估算活动资源 13.5…

2023/9/10周报

目录 摘要 文献阅读1 1、标题和提出问题 2、小波变换 3、Encoder和Decoder 4、模型结构 5、实验 文献阅读2 1、标题和提出问题 2、高斯扩散模型和烛台图 3、研究方法 4、实验准备 5、实验结果 总结 摘要 本周阅读了两篇论文&#xff0c;其一为一种基于空气质量时…

数字图像滤波的本质

一、说明 在数字时代&#xff0c;图像是我们交流和表达不可或缺的一部分。从社交媒体到医学成像&#xff0c;图像的质量和内容非常重要。这就是图像过滤和卷积领域介入的地方&#xff0c;为我们提供了一个转换和完善这些视觉叙事的工具包。 图像过滤不仅仅是让照片看起来更好;这…

浙大MBA二战上岸:笔试备考辛酸路

作为一个即将进入三十而立年纪的考生&#xff0c;我深切感受到了压力的存在。在决定是否要将这么多时间和精力投入到考研中时&#xff0c;我开始思考这决定是否值得。毕竟参加二战考试需要付出大量的时间成本和努力。最终经过反复权衡和深思熟虑&#xff0c;我认为这是值得的。…

总结 - 组件通用封装思路(组件封装)

组件封装&#xff1a; 1. 不变&#xff1b; 2. 变&#xff1a;①prop ②slot插槽 详细总结在文末。 --------------------------------------------------------------------------------------------------- 问题&#xff1a;结构相似&#xff0c;内容不同 --》 可以用组…

注意力机制讲解与代码解析

一、SEBlock(通道注意力机制) 先在H*W维度进行压缩&#xff0c;全局平均池化将每个通道平均为一个值。 &#xff08;B, C, H, W&#xff09;---- (B, C, 1, 1) 利用各channel维度的相关性计算权重 (B, C, 1, 1) --- (B, C//K, 1, 1) --- (B, C, 1, 1) --- sigmoid 与原特征相…

【Linux】粗谈权限

背景知识 我们日常口头讲的Linux操作系统&#xff0c;狭义来说只是指它的内核&#xff0c;广义上来说指它的内核加外壳&#xff08;shell&#xff0c;配套的程序&#xff09;。 那么为什么一定要有外壳呢&#xff1f;shell外壳主要有以下两个功能 1.命令行解释器 将用户输入的…

趣解设计模式之《办理入职这么难吗?》

〇、小故事 小王大学毕业&#xff0c;经过大学期间的刻苦学习&#xff0c;终于成功的面试上了一家公司。按照约定的时间&#xff0c;小王兴高采烈的来到公司前台去办理入职手续。 “您好&#xff0c;我是来办理入职手续的”小王对前台妹妹说&#xff0c;前台妹妹看都没看他一…

管理类联考——数学——汇总篇——知识点突破——路程

⛲️ 路程问题是根据速度、时间、路程之间的关系&#xff0c;研究物体相向、相背和同向运动的问题&#xff0c;解决路程问题常用方法&#xff1a; &#xff08;1&#xff09;分解。将综合性的题目先分解成若干个基本题&#xff0c;再按其所属类型&#xff0c;直接利用基本数量…

SpringBoot结合MyBatis实现多数据源配置

SpringBoot结合MyBatis实现多数据源配置 一、前提条件 1.1、环境准备 SpringBoot框架实现多数据源操作&#xff0c;首先需要搭建Mybatis的运行环境。 由于是多数据源&#xff0c;也就是要有多个数据库&#xff0c;所以&#xff0c;我们创建两个测试数据库&#xff0c;分别是…

Alibaba(获得店铺的所有商品) API接口

为了进行电商平台 的API开发&#xff0c;首先我们需要做下面几件事情。 1&#xff09;开发者注册一个账号 2&#xff09;然后为每个alibaba应用注册一个应用程序键&#xff08;App Key) 。 3&#xff09;下载alibaba API的SDK并掌握基本的API基础知识和调用 4&#xff09;利…

持安-大连万达集团零信任项目入选中国信通院2023零信任优秀案例

2023年8月25日&#xff0c;以“链接云端&#xff0c;可信而安”为主题的“2023首届SecGo云和软件安全大会”在京隆重召开。会上&#xff0c;中国信息通信研究院重磅揭晓了“安全守卫者计划”优秀案例评选结果。 零信任办公安全技术创新企业持安科技&#xff0c;与用户大连万达…

SpringBoot整合MQTT(MqttClient)

一、SpringBoot整合MQTT 创建项目&#xff0c;引入 MQTT依赖&#xff1a; <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><version>2.3.12.RELEASE</version></dep…

java读取jar包中的程序版本号

java程序打包后的jar如下图所示&#xff1a; 可以看到META-INF目录下的三处均可以读取到程序的版本号&#xff1a; MANIFEST.MFbuild-info.propertiesmaven/xxx/xx/pom.properties 或 pom.xml 一、MANIFEST.MF 程序版本号字段 即 Implementation-Version Manifest-Version:…

【Java】之继承

Java中使用extends关键字&#xff0c;让我们在类和类之间建立联系。 使用继承的好处&#xff1a; 可以将多个类中重复的代码&#xff0c;抽象至父类中供子类使用。 子类可以在父类上的基础上继续增强&#xff0c;使子类可以有自己独特的性质和功能。 特点&#xff1a; 在J…

LASSO回归

LASSO回归 LASSO(Least Absolute Shrinkage and Selection Operator&#xff0c;最小绝对值收敛和选择算子算法)是一种回归分析技术&#xff0c;用于变量选择和正则化。它由Robert Tibshirani于1996年提出&#xff0c;作为传统最小二乘回归方法的替代品。 损失函数 1.线性回…

Alibaba(按关键字搜索商品) API接口

为了进行电商平台 的API开发&#xff0c;首先我们需要做下面几件事情。 1&#xff09;开发者注册一个账号 2&#xff09;然后为每个alibaba应用注册一个应用程序键&#xff08;App Key) 。 3&#xff09;下载alibaba API的SDK并掌握基本的API基础知识和调用 4&#xff09;利…