golang工程——protobuf使用及原理

相关文档
源码:https://github.com/grpc/grpc-go
官方文档:https://www.grpc.io/docs/what-is-grpc/introduction/
protobuf编译器源码:https://github.com/protocolbuffers/protobuf
proto3文档:https://protobuf.dev/programming-guides/proto3/

protobuf使用

protoc下载

用于编译.proto文件,生成对应语言的模板文件

#下载地址
https://github.com/protocolbuffers/protobuf/releases/

windows的话选择对应版本,下载解压后配置对应环境变量
在这里插入图片描述

protoc 插件安装

go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28 go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2

protoc 编译

protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go- grpc_opt=paths=source_relative .\echo\echo.proto
  • –proto_path 或者 -I :指定 import 路径,可以指定多个参数,编译时按顺序查找,不指定时默 认查找
    当前目录。(.proto 文件中也可以引入其他 .proto 文件,这里主要用于指定被引入文件的 位置)
  • –go_out :golang编译支持,指定输出文件路径;
  • –-go_opt :指定参数,比如 --go_opt=paths=source_relative 就是表明生成文件输出使用相对 路径。
  • path/to/file.proto :被编译的 .proto 文件放在最后面

protobuf原理

protobuf字段

定义一个搜索相关的proto 消息,请求有 查询字符串,有分页页数,和每页的数量。例子如下

message SearchRequest {string query = 1;  // 查询字符串optional int32 page_number = 2;  // 第几页optional int32 result_per_page = 3;  // 每页的结果数
}
  • 每个消息应该有类型字段编号
  • optional: message 可以包含该字段零次或一次(不超过一次)。
  • repeated: 该字段可以在消息中重复任意多次(包括零)。其中重复值的顺序会被保留。在开发语言中就是数组和列表
字段类型

字段有很多数据类型,看个例子

syntax = "proto3";
option go_package = "protos/pbs";
enum Status{Status1=0;Status2=1;Status3=2;
}message Source{//金条int64 Gold  =1;//血条int64 Blood=2;
}
message Role {//idint64  Id =1; //有符号整型//姓名string Name=2;//字符串类型//属性map<int64,int64> Attr=3; //map 类型//状态Status typ =4;   //枚举类型//是不是vipbool IsVip =5; //bool 类型//资源Source source=6;     //复合类型
}
字段编号
  • 每个字段有唯一的编号,proto编码的时候是不管字段名的,仅根据编号来确定是哪个字段
  • 消息被使用了,字段就不能改了,改了会造成数据错乱(常见坑),服务器和客户端很多bug,是proto buffer 文件更改,未使用更改后的协议导致。
  • 1 到 15 范围内的字段编号需要一个字节进行编码,编码结果将同时包含编号和类型
  • 16 到 2047 范围内的字段编号占用两个字节。因此,非常频繁出现的 message 元素保留字段编号 1 到 15。
  • 字段最小数字为1,最大字段数为2^29 - 1。(原因在编码原理那章讲解过,字段数字会作为key,key最后三位是类型
  • 19000 through 19999 (FieldDescriptor::kFirstReservedNumber through FieldDescriptor::kLastReservedNumber这些数字不能用,这些是保留字段,如果使用会编译器会报错
syntax = "proto3";
option go_package = "protos/pbs";message Role {int64  Id =19527;
}

编译会报下面的错

protoc --go_out=. ./*.proto
intro.proto:5:14: Field numbers 19000 through 19999 are reserved for the protocol buffer library implementation.
  • 保留字段指 reserved 关键字指定的字段

protobuf 数据类型

变量类型
protoYypenotesGO type
double*float64
float*float32
int32可变长编码,负数编码效率低,要经过zigzag*int32
int64可变长编码,负数编码效率低,要经过zigzag*int64
uint32可变长编码*uint32
uint64可变长编码*uint64
sint32可变长编码,比int32效率高*int32
sint64可变长编码,比int64效率高*int64
fixed32总是4字节,如果经常比228大,那比uint32效率更高*uint32
fixed64总是4字节,如果经常比256大,那比uint32效率更高*uint64
sfixed32总是4字节*int32
sfixed64总是8字节*int64
bool*bool
stringutf8或7-bit ASCII文本编码*string
bytes任意序列字节[]byte
  • java中,无符号32 位和64位使用其有符号类型表示。最高位是符号位
  • 所有场景中,给字段赋值都会给类型检查确保它是有效的
  • 64 位或无符号 32 位整数在解码时始终表示为 long,但如果在设置字段时给出 int,则可以为int
默认值

解析消息的时候,编码的消息字段没有赋值,将会设置默认值

  • 字符串类型默认值是" "
  • bytes 默认值是空字节
  • bool 默认值是false
  • 数字类型默认是0
  • 枚举值默认是0,详情看下面枚举类型
  • 空列表在合适的语言会转换成合适的数据类型空列表
枚举

当我们想定义一个消息类型,只使用定义好的一系列值的一个,我们就可以使用枚举

message SearchRequest {string query = 1;int32 page_number = 2;int32 result_per_page = 3;enum Corpus { //定义枚举UNIVERSAL = 0;WEB = 1;IMAGES = 2;LOCAL = 3;NEWS = 4;PRODUCTS = 5;VIDEO = 6;}Corpus corpus = 4; //使用枚举
}

注意

枚举第一个字段必须是0,像上面UNIVERSAL = 0,而且不能省略,原因有两点:

  • 当该枚举类型字段没有赋值的时候,我们使用0这个定义作为默认值
  • 兼容proto2 第一个字段总是默认值

如何给枚举定义别名? 当我们希望两个枚举值一样,但是变量名不一样的时候,我们可以添加allow_alias option,并设置值为true,类似于下面这个样式,要不然编译器会报错。

message MyMessage1 {enum EnumAllowingAlias {option allow_alias = true;UNKNOWN = 0;STARTED = 1;RUNNING = 1;}
}
message MyMessage2 {enum EnumNotAllowingAlias {UNKNOWN = 0;STARTED = 1; //直接使用会报错,因为这两个值一样了// RUNNING = 1;  // Uncommenting this line will cause a compile error inside Google and a warning message outside.}
}

枚举值范围是32位范围内的整数,这是因为是是varint encoding 编码的。对于没有定义的枚举值,在go 和c++中会识别成数字

枚举保留字段

枚举更新的安全性,官方新增了个枚举保留字段,使用方法如下

enum Foo {reserved 2, 15, 9 to 11, 40 to max;reserved "FOO", "BAR";
}

如果删除枚举定义或者注释来更新枚举类型,将来用户可能不注意去重用该类型的值,如果以后proto buffer 版本更新了,再加载到旧版本,那么可能导致严重问题,包括数据损坏、隐私漏洞等。

官方提供了保留字段,可以保留枚举字段名和枚举字段值,如下,使用保留的字段会报错,超过max 也会报错。

例如下面这个例子

enum TestType {Hello1=0;Hello2=1;Hello3=2;Hello4=3;FOO=4; # 使用保留字段BAR=5;foo=6;Bar=7;Foo1=8;Hello6=39;Hello6=40;Hello5=99; # 超过最大值reserved 2,3, 15, 9 to 11, 40 to max;reserved "FOO", "BAR";
}
消息嵌套和导入其他proto
消息引用

当使用其他消息的时候,如果在本文件,直接使用就可以了。Result代表自定义消息类型

message SearchResponse {repeated Result results = 1;
}message Result {string url = 1;string title = 2;repeated string snippets = 3;
}
消息嵌套

如何在消息里面再定义消息了?例子如下,在SearchResponse定义了个内部消息Result,然后直接引用就可以了。

message SearchResponse {message Result {string url = 1;string title = 2;repeated string snippets = 3;}repeated Result results = 1;
}

如果其他消息引用消息内部的消息呢?语法为_Parent_.Type

message SomeOtherMessage {SearchResponse.Result result = 1;
}
导入其他文件proto

在项目开发中,我们有这种需要,将相同的结构放在一个公共文件夹,将请求响应的业务消息放一个文件夹,然后请求响应的proto 会引用通文件夹。我们来写一个例子,文件结构如下。

  • bussiness 代表业务文件夹,里面存放业务逻辑
  • share 存放公共结构文件夹

user_business.proto

syntax = "proto3";
option go_package = "protos/pbs";
import "share/user.proto";
//获取角色信息请求
message GetUserRequest {}
//获取角色信息响应
message GetUserResponse {User user=1;
}

user.proto

syntax = "proto3";
option go_package = "protos/pbs";//用户定义
message User {string Id=1;string Name=2;string Age=3;
}
protoc --go_out=. ./business/*.proto ./share/*.proto
Any

官方说作用是集成proto 没有定义的类型,其实可以理解为go 语言接口类型,可以存任何类型的值,但是跨语言只能通过字节流代表任意类型,所以any 内部实现包含字节流,和标识字节流唯一url

用这个关键字,官方说要导入官方proto,类似下面,相信如果直接编译肯定会有坑,编译不过,因为没有any.proto这个文件

import "google/protobuf/any.proto";message ErrorStatus {string message = 1;repeated google.protobuf.Any details = 2;
}
  • 去官方下载这个文件
  • 安装protobuf的时候有个proto目录,拷贝过来

最后序列化出来的any 结构包含下面两个字段:

TypeUrl string `protobuf:"bytes,1,opt,name=type_url,json=typeUrl,proto3" json:"type_url,omitempty"`
// Must be a valid serialized protocol buffer of the above specified type.
Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
  • 一个是序列化成bytes 的属性value
  • 一个是标识这个属性全局唯一的标识TypeUrl
Oneof

如果在平时在一个消息有许多字段,但是最多设置一个字段,我们可以使用oneof 来执行并节省内存。

Oneof 字段类似于常规字段,除了Oneof共享内存的所有字段之外,最多可以同时设置一个字段。设置Oneof 的任何成员都会自动清除所有其他成员。您可以使用case()或WhichOneof()方法检查Oneof 中的哪个值被设置(如果有的话),具体取决于选择的语言。

syntax = "proto3";
option go_package = "protos/pbs";message SubMessage {int32 Id=1;string Age2=2;}
message SampleMessage {oneof test_oneof {string name = 4;SubMessage sub_message = 9;}
}

oneof 可以添加任何字段,除了repeated字段

oneof功能
  • oneof 设置一个字段会清除其他字段,如果设置了几个字段,自会保留最后一个设置的字段,可以看到在go中是通过一个接口类型来做到oneof的,只能给这个字段赋值为定义的字段结构体
package mainimport ("fmt""grpcdemo/protobuf/any/protos/pbs"
)func main()  {p:=&pbs.SampleMessage{TestOneof: &pbs.SampleMessage_Name{Name: "hello"},}fmt.Println(p)fmt.Println(p.GetTestOneof())p.TestOneof=&pbs.SampleMessage_SubMessage{SubMessage: &pbs.SubMessage{Id: 1}}fmt.Println(p)fmt.Println(p.GetTestOneof())
}
  • oneof 不能被repeated
  • 反射作用于oneof的字段

兼容性问题

添加或删除其中一个字段时要小心。如果检查 oneof 的值返回 None/NOT_SET,则可能意味着 oneof 尚未设置或已设置为 oneof 的另一个字段。这种情况是无法区分的,因为无法知道未知字段是否是 oneof 成员。

标签重用问题

  • 将 optional 可选字段移入或移出 oneof:在序列化和解析 message 后,你可能会丢失一些信息(某些字段将被清除)。但是,你可以安全地将单个字段移动到新的 oneof 中,并且如果已知只有一个字段被设置,则可以移动多个字段。
  • 删除 oneof 字段并将其重新添加回去:在序列化和解析 message 后,这可能会清除当前设置的 oneof 字段。
  • 拆分或合并 oneof:这与移动常规的 optional 字段有类似的问题。
maps

在数据定义创建map,语法格式为

map<key_type, value_type> map_field = N;

例如,创建一个项目,key 是string,value 是Project

map<string, Project> projects = 3;

PS:

  • map 类型不能加repeated,简单来说map 是不支持map 数组的
  • map是无序的,不能依赖map 的特定顺序

总的来说,map 语法等价于下面的语法,所以protocol buffers 的实现在不支持map 的语言上也能处理数据

message MapFieldEntry {key_type key = 1;value_type value = 2;
}repeated MapFieldEntry map_field = N;
packages

package提供命名空间,防冲突

package foo.bar;
message Open { ... }

在其他地方引用

message Foo {...foo.bar.Open open = 1;...
}
options
  • 可用的选项列表在google/protobuf/descriptor.proto
  • 其它选项官方有,是其它语言相关的,这里就不细讲了,看官方文档Options。
  • deprecated选项设为true 代表字段被废弃,在新代码不应该被使用,在大多数语言都没有实际的效果,在java 变成@Deprecated注解。 在未来,可能产生废弃的注解在方法字段的入口。并且将会引起警告当编译这个字段的时候。如果这个字段没人使用,可以将字段的声明改为保留字段,上面已经讲解
int32 old_field = 6 [deprecated = true];
custom options

proto buffer 提供大多人都不会使用的高级功能-自定义选项。

由于选项是由 google/protobuf/descriptor.proto(如 FileOptions 或 FieldOptions)中定义的消息定义的,因此定义你自己的选项只需要扩展这些消息

import "google/protobuf/descriptor.proto";extend google.protobuf.MessageOptions {optional string my_option = 51234;
}message MyMessage {option (my_option) = "Hello world!";
}

获取选项

package mainimport ("fmt""grpcdemo/protobuf/any/protos/pbs"
)func main()  {p:=&pbs.MyMessage{}fmt.Println(p.ProtoReflect().Descriptor().Options())//[my_option]:"Hello world!"
}

Protocol Buffers可以为每种类型提供选项

import "google/protobuf/descriptor.proto";extend google.protobuf.FileOptions {optional string my_file_option = 50000;
}
extend google.protobuf.MessageOptions {optional int32 my_message_option = 50001;
}
extend google.protobuf.FieldOptions {optional float my_field_option = 50002;
}
extend google.protobuf.OneofOptions {optional int64 my_oneof_option = 50003;
}
extend google.protobuf.EnumOptions {optional bool my_enum_option = 50004;
}
extend google.protobuf.EnumValueOptions {optional uint32 my_enum_value_option = 50005;
}
extend google.protobuf.ServiceOptions {optional MyEnum my_service_option = 50006;
}
extend google.protobuf.MethodOptions {optional MyMessage my_method_option = 50007;
}option (my_file_option) = "Hello world!";message MyMessage {option (my_message_option) = 1234;optional int32 foo = 1 [(my_field_option) = 4.5];optional string bar = 2;oneof qux {option (my_oneof_option) = 42;string quux = 3;}
}enum MyEnum {option (my_enum_option) = true;FOO = 1 [(my_enum_value_option) = 321];BAR = 2;
}message RequestType {}
message ResponseType {}service MyService {option (my_service_option) = FOO;rpc MyMethod(RequestType) returns(ResponseType) {// Note:  my_method_option has type MyMessage.  We can set each field//   within it using a separate "option" line.option (my_method_option).foo = 567;option (my_method_option).bar = "Some string";}
}

引用其他包的选项需要加上包名

// foo.proto
import "google/protobuf/descriptor.proto";
package foo;
extend google.protobuf.MessageOptions {optional string my_option = 51234;
}
// bar.proto
import "foo.proto";
package bar;
message MyMessage {option (foo.my_option) = "Hello world!";
}
  • 自定义选项是扩展名,必须分配字段号,像上面的例子一样。在上面的示例中,使用了 50000-99999 范围内的字段编号。这个字段范围供个人组织使用,所以可以内部用。
  • 在公共应用使用的话,要保持全球唯一数字,需要申请,申请地址为: protobuf global extension registry
  • 通常只需要一个扩展号,可以多个选项放在子消息中来实现一个扩展号声明多个选项
message FooOptions {optional int32 opt1 = 1;optional string opt2 = 2;
}extend google.protobuf.FieldOptions {optional FooOptions foo_options = 1234;
}// usage:
message Bar {optional int32 a = 1 [(foo_options).opt1 = 123, (foo_options).opt2 = "baz"];// alternative aggregate syntax (uses TextFormat):optional int32 b = 2 [(foo_options) = { opt1: 123 opt2: "baz" }];
}

每种选项类型(文件级别,消息级别,字段级别等)都有自己的数字空间,例如:可以使用相同的数字声明 FieldOptions 和 MessageOptions 的扩展名。

编解码原理

Base 128 Varints

可变字节长度编码,用一个字节或者多个字节表示整数类型,更小的数占用更小的字节。

编码的理念是:越小的数字花费越少的字节

来看看下面的例子:

00000000 00000000 00000000 00000001 //int32 
  • 假设值为1 ,类型为int32 在网络传输,其实有效位就一个,其他的位都是无效的,Base 128 Varints的出现就是为了解决这个问题

Base 128 Varints 原理

Base128 Varints 采用的是小端序, 即数字的低位存放在高地址。

比如数字 666, 其以标准的整型存储, 其二进制表示为

在这里插入图片描述

而采用 Varints 编码, 其二进制形式为

在这里插入图片描述

可以尝试来复原一下上面这个 Base128 Varints 编码的二进制串, 首先看最高有效位

在这里插入图片描述

接下来我们移除标识位, 由于 Base128 Varints 采用小端字节序, 因此数字的高位存放于低地址上

在这里插入图片描述

移除标志位并交换字节序, 便得到原本的数值 1010011010, 即数字 666

在这里插入图片描述

可变长整型编码对于不同大小的数字, 其所占用的存储空间是不同的, 编码思想与 CPU 的间接寻址原理相似, 都是用一比特来标识是否走到末尾, 但采用这种方式存储数字, 也有一个相对不好的点便是, 无法对一个序列的数值进行随机查找, 因为每个数字所占用的存储空间不是等长的, 因此若要获得序列中的第 N 个数字, 无法像等长存储那样在查找之前直接计算出 Offset, 只能从头开始顺序查找

zigzag 编码

Varints 编码的实质在于去掉数字开头的 0, 因此可缩短数字所占的存储字节数, 在上面的例子中, 我们只举例说明了正数的 Varints 编码, 但如果数字为负数, 则采用 Varints 编码会恒定占用 10 个字节, 原因在于负数的符号位为 1, 对于负数其从符号位开始的高位均为 1, 在 Protobuf 的具体实现中, 会将此视为一个很大的无符号数, 以 Go 语言的实现为例, 对于 int32 类型的 pb 字段, 对于如下定义的 proto

syntax = "proto3";
package pbTest;message Request {int32 a = 1;
}

Request 中包含类型为 int32 类型的字段, 当 a 为负数时, 其序列化之后将恒定占用 10 个字节, 我们可以使用如下的测试代码

func main() {a := pbTest.Request{A: -5,}bytes, err := proto.Marshal(&a)if err != nil {fmt.Println(err)return}fmt.Println(fmt.Sprintf("%08b", bytes))
}

对于 int32 类型的数字 -5, 其序列化之后的二进制为

在这里插入图片描述

究其原因在于 Protobuf 的内部将 int32 类型的负数转换为 uint64 来处理, 转换后的 uint64 数值的高位全为 1, 相当于是一个 8 字节的很大的无符号数, 因此采用 Base128 Varints 编码后将恒定占用 10 个字节的空间, 可见 Varints 编码对于表示负数毫无优势, 甚至比普通的固定 32 位存储还要多占 4 个字节。Varints 编码的实质在于设法移除数字开头的 0 比特, 而对于负数, 由于其数字高位都是 1, 因此 Varints 编码在此场景下失效

Zigzag 编码便是为了解决这个问题, Zigzag 编码的大致思想是首先对负数做一次变换, 将其映射为一个正数, 变换以后便可以使用 Varints 编码进行压缩, 这里关键的一点在于变换的算法, 首先算法必须是可逆的, 即可以根据变换后的值计算出原始值, 否则就无法解码, 同时要求变换算法要尽可能简单, 以避免影响 Protobuf 编码、解码的速度, 我们假设 n 是一个 32 位类型的数字, 则 Zigzag 编码的计算方式为

(n << 1) ^ (n >> 31)

在这里插入图片描述

首先对其进行一次逻辑左移, 移位后空出的比特位由 0 填充

在这里插入图片描述

然后对原数字进行 15 次算术右移, 得到 16 位全为原符号位(即 1)的数字

在这里插入图片描述

然后对逻辑移位和算术移位的结果按位异或, 便得到最终的 Zigzag 编码

在这里插入图片描述

可以看到, 对负数使用 Zigzag 编码以后, 其高位的 1 全部变成了 0, 这样以来我们便可以使用 Varints 编码进行进一步地压缩, 再来看正数的情形, 对于 16 位的正数 5。后面就可以用varints编码了

可以看到, 对负数使用 Zigzag 编码以后, 其高位的 1 全部变成了 0, 这样以来我们便可以使用 Varints 编码进行进一步地压缩, 再来看正数的情形, 对于 16 位的正数 5, 其在内存中的存储形式为

在这里插入图片描述

我们按照与负数相同的处理方法, 可以得到其 Zigzag 编码为

在这里插入图片描述

从上面的结果来看, 无论是正数还是负数, 经过 Zigzag 编码以后, 数字高位都是 0, 这样以来, 便可以进一步使用 Varints 编码进行数据压缩, 即 Zigzag 编码在 Protobuf 中并不单独使用, 而是配合 Varints 编码共同来进行数据压缩

消息编码

protocol buffer消息是由一些key-value 组成的,其中key 代表字段后面的数字,变量名和变量类型仅仅决定编码的最终截止位置。

消息编码的时候,key 和value 都会被编码进字节流。当解码器解码时,需要跳过不能识别的字段,因为新添加字段不会对原来造成影响。每个key 由两部分组成,1个是定义在proto消息字段后面的数字,后面跟的是wire type (消息类型)。通过消息类型能够找到后面值的长度。

可用的wire type

在这里插入图片描述

每个key 在消息流里面都是这样的结构,(field_number << 3) | wire_type,最后三位存储wire_type,直白来说,wire_type类似语言中的数据类型,标识存储数据的长度

解码例子

假设有下面这种消息类型Test1

message Test1 {optional int32 a = 1;
}

当我们定义上面的消息,并赋值a=150,我们将得到下面序列化结构,总共三个字节

08 96 01

解码步骤

1、数据流第一个数字是 varint key,这里是08,二进制数据为000 1000

  • 最后三位000是wire_type(0),右移三位得到000 1(1),所以知道了字段1和后面的值是varint 类型
  • 将96 01 通过上面的Base 128 Varints解码方法得到数字150
96 01 = 1001 0110  0000 0001000 0001  ++  001 0110 (drop the msb and reverse the groups of 7 bits)10010110128 + 16 + 4 + 2 = 150
Non-varint 数字
  • double and fixed64 用的wire type 是1,编译器解析时会认为是64位的块数据。直接取64位解析,没有varint 编解码过程。
  • float and fixed32使用wire type 5,告诉编译器是32位的数据
  • 该数字都被排成小端字节序了
字符串编码

字符串的wire_type 是2,代表值是可变的,长度会被编码进字节流里面。

如下例子:

message Test2 {optional string b = 2;
}

将b 赋值为"testing" ,得到下面的结果

12 07 [74 65 73 74 69 6e 67]
  • key 是0x12,最后三位代表wire_type 结果为2(length-delimited),key 为2
  • []里面的内容是UTF8 的 “testing”
0x120001 0010  (binary representation)00010 010  (regroup bits)
→ field_number = 2, wire_type = 2
  • 长度是07,代表后面的7个字节为字符串内容
复合结构消息
message Test1 {optional int32 a = 1;
}
message Test3 {optional Test1 c = 3;
}

Test1’s a 字段依然是150:

 1a 03 08 96 01
  • 后面08 96 01就不说了,前面解析过了
  • 1a 二进制为00011010,后三位代表wire_type 为2,前面代表key 为数字3。所以Test1结果被当作字符串对待了
  • 03 为长度,代表Test3里面内容长度为3 个字节
optional and repoeated
  • 在proto2 里面,消息字段定义为repeated没有在后面加选项packed=true,编码的消息可能有零个或者多个key-value 键值对,这些键值对也不是连续的,可能中间插入了其他字段,意思是和其他字段交替出现。
  • 任何不是repeated字段在proto3 里面或者optional 字段在proto2,编码消息可能有也可能没有那个字段形成的key value键值对
  • 通常编码消息对于不是repeated字段永远不可能出现超过1个的键值对,解析器期望去处理这种情况。对于数字类型和字符串类型,如果同一个字段出现多次,解析器会使用最后看见的一个值。对于复合类型字段,解析器合并多个实例到同一个字段,就像Message::MergeFrom方法一样。同个嵌套类型,如果出现了多个键值对,解析器会采取合并策略。
MyMessage message;
message.ParseFromString(str1 + str2);

和下面的结果是一样的

MyMessage message, message2;
message.ParseFromString(str1);
message2.ParseFromString(str2);
message.MergeFrom(message2);
packed repoeated fields
  • proto3默认使用packed编码repeated数字字段
  • 这些函数类似于重复字段,但是编码方式不同,包含零元素的压缩重复字段不会出现在编码消息中,要不然,该字段的所有元素会打包到wire_type 为2 的键值对中。每个元素的编码方式于正常情况相同,只是前面没有键

例如下面的类型

message Test4 {repeated int32 d = 4 [packed=true];
}

Test4 的repeated 有三个值,3、270、86942 。编码结果将会如下面所示

22        // key (field number 4, wire type 2)
06        // payload size (6 bytes)
03        // first element (varint 3)
8E 02     // second element (varint 270)
9E A7 05  // third element (varint 86942)
  • 只有varint, 32-bit, or 64-bit wire types可以使用packed
  • 虽然通常情况下没有必要为编码repeated字段使用多个键值对,但是解析器也必须做这样的编码,每对包含完整的信息
  • Protocol buffer必须能解析编译为packed的字段跟没有使用packed一样。在兼容性上就可以向前向后兼容使用[packed=true]
filed oder

字段数字顺序可以任何顺序出现在proto里面。顺序对消息序列化没有任何影响。

当消息被序列化时,是无法保证已知字段和未知字段被写入,序列化是一个实现细节,任何特定实现的细节在将来都会被改变,因此protocol buffer 必须能够解析字段在任何顺序。

未知字段
  • 未知字段是protocol buffer无法识别的字段,通常发送在旧二进制文件去解析新二进制发送的数据时,这些新字段就是未知字段
  • 最初,proto3消息在解析期间总是丢弃未知字段,但在3.5版本中,将未知字段保存以匹配proto2行为。 在版本3.5及更高版本中,未知字段在解析期间保留并包含在序列化输出中。

【侵权删】

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

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

相关文章

加入PreAuthorize注解鉴权之后NullPointerException报错

记录一次很坑的bug&#xff0c;加入PreAuthorize注解鉴权之后NullPointerException报错&#xff0c;按理来说没有权限应该403报错&#xff0c;但是这个是500报错&#xff0c;原因是因为controller层的service注入失败&#xff0c;然而我去掉注解后service注入成功&#xff0c;并…

使用VSCODE 调试ros2具体设置

vscode 调试 ROS2 张得帅&#xff01; 于 2023-09-09 15:39:39 发布 456 收藏 1 文章标签&#xff1a; vscode ros2 版权 1、在下列目录同层级找到.vscode文件夹 . ├── build ├── install ├── log └── src 2、 安装ros插件 3、创建tasks.json文件&#xff0c;添…

二十七、[进阶]MySQL默认存储引擎InnoDB的简单介绍

1、MySQL体系结构 MySQL大致可以分为连接层、服务层、引擎层、存储层四个层&#xff0c;这里需要注意&#xff0c;索引的结构操作是在存储引擎层完成的&#xff0c;所以不同的存储引擎&#xff0c;索引的结构是不一样的。 &#xff08;1&#xff09;体系结构示意图 &#xff0…

国庆10.01

TCPselect 代码 服务器 #include<myhead.h> #include<sqlite3.h> #define PORT 6666 //端口号 #define IP "192.168.0.104" //IP地址//键盘事件 int jp(fd_set tempfds,int maxfd) {char buf[128] ""; //用来接收数据char buf1[128] …

【算法|贪心算法系列No.2】leetcode2208. 将数组和减半的最少操作次数

个人主页&#xff1a;兜里有颗棉花糖 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 兜里有颗棉花糖 原创 收录于专栏【手撕算法系列专栏】【LeetCode】 &#x1f354;本专栏旨在提高自己算法能力的同时&#xff0c;记录一下自己的学习过程&#xff0c;希望…

Spring注册Bean系列--方法1:@Component

原文网址&#xff1a;Spring注册Bean系列--方法1&#xff1a;Component_IT利刃出鞘的博客-CSDN博客 简介 本文介绍Spring注册Bean的方法&#xff1a;Component。 注册Bean的方法我写了一个系列&#xff0c;见&#xff1a;Spring注册Bean(提供Bean)系列--方法大全_IT利刃出鞘…

开绕组电机零序Bakc EMF-based无感控制以及正交锁相环inverse Park-based

前言 最近看论文遇到了基于反Park变换的锁相环&#xff0c;用于从开绕组永磁同步电机零序电压信号中提取转子速度与位置信息&#xff0c;实现无感控制。在此记录 基于零序Back EMF的转子估算 开绕组电机的零序反电动势 e 0 − 3 ω e ψ 0 s i n 3 θ e e_0-3\omega_e\psi_…

​68条萝卜刀《乡村振兴战略下传统村落文化旅游设计》许少辉八一新书

​68条萝卜刀《乡村振兴战略下传统村落文化旅游设计》许少辉八一新书 ​68条萝卜刀《乡村振兴战略下传统村落文化旅游设计》许少辉八一新书

借助 ControlNet 生成艺术二维码 – 基于 Stable Diffusion 的 AI 绘画方案

背景介绍 在过去的数月中&#xff0c;亚马逊云科技已经推出了多篇博文&#xff0c;来介绍如何在亚马逊云科技上部署 Stable Diffusion&#xff0c;或是如何结合 Amazon SageMaker 与 Stable Diffusion 进行模型训练和推理任务。 为了帮助客户快速、安全地在亚马逊云科技上构建、…

【QT开发(6)】0926-QT 中加入 fastDDS 通信库的程序使用说明

在智能驾驶中&#xff0c;DDS有可能被广泛使用&#xff0c;因此推出这篇说明教程。 1、基于【QT开发&#xff08;5&#xff09;】教程的项目文档进行开发 2、安装DDS 查看《【eProsima Fast DDS&#xff08;1&#xff09;】安装eProsima Fast DDS》 至少安装: foonathan_m…

LeetCode每日一题:2136. 全部开花的最早一天(2023.9.30 C++)

目录 2136. 全部开花的最早一天 题目描述&#xff1a; 实现代码与解析&#xff1a; 贪心 原理思路&#xff1a; 2136. 全部开花的最早一天 题目描述&#xff1a; 你有 n 枚花的种子。每枚种子必须先种下&#xff0c;才能开始生长、开花。播种需要时间&#xff0c;种子的生…

Java八股文

JAVA八股文 这里写目录标题 **JAVA八股文**面向对象三大特征接口与抽象类的区别重载与重写与equals异常处理机制HashMap原理红黑树乐观锁和悲观锁HashTable与HashMap的区别ArrayList和LinkedList的区别如何保证ArrayList的线程安全什么是线程上下文切换sleep()和wait()的区别yi…

Nginx简介与Docker Compose部署指南

Nginx是一款高性能的开源Web服务器和反向代理服务器&#xff0c;以其卓越的性能、可伸缩性和灵活性而闻名。它在全球范围内广泛用于托管Web应用程序、负载均衡、反向代理和更多场景中。在本文中&#xff0c;我们将首先介绍Nginx的基本概念&#xff0c;然后演示如何使用Docker C…

stm32 - GPIO

stm32 - GPIO GPIO结构图GPIO原理图输入上拉/下拉/浮空施密特触发器片上外设 输出推挽/开漏/关闭输出方式 GPIO88种模式复用输出 GPIO寄存器端口配置寄存器_CRL端口输入数据寄存器_IDR端口输出数据寄存器_ODR端口位设置/清除寄存器_BSRR端口位清除寄存器_BRR端口配置锁定寄存器…

Window 安装多个版本的 java 并按需切换

1、按需下载对应版本的 java 官网链接&#xff1a;Java Downloads | Oracle 2、执行安装程序&#xff0c;根据安装向导一步一步走就行&#xff0c;每个版本安装在不同的目录下。 3、配置环境变量 a&#xff09;为每个版本 java 新建不同名称的 JAVA_HOME 系统变量&#xff0…

【Java 进阶篇】JDBC插入数据详解

在Java应用程序中&#xff0c;与数据库交互是一项常见的任务。其中&#xff0c;插入数据操作是一种基本的数据库操作之一。本文将详细介绍如何使用Java JDBC&#xff08;Java Database Connectivity&#xff09;来执行插入数据操作。无论您是初学者还是有一定经验的开发人员&am…

数仓精品理论-做大数据还有没有前途?

数仓精品理论-做大数据还有没有前途&#xff1f; 做大数据还有没有前途&#xff1f;大数据三要三不要我来讲讲大数据前景 做大数据还有没有前途&#xff1f; 先说&#xff0c;答案是肯定的&#xff0c;但一定要记住三要三不要。 datapulse官网&#xff1a; github:https://data…

LeetCode 热题 HOT 100:回溯专题

LeetCode 热题 HOT 100&#xff1a;https://leetcode.cn/problem-list/2cktkvj/ 文章目录 17. 电话号码的字母组合22. 括号生成39. 组合总和46. 全排列补充&#xff1a;47. 全排列 II &#xff08;待优化)78. 子集79. 单词搜索124. 二叉树中的最大路径和200. 岛屿数量437. 路径…

手机电脑数码小程序商城的作用是什么

手机几乎是每个成年人人手一个以上&#xff0c;市场非常大&#xff0c;加之产品更新迭代速度快&#xff0c;每年都会推出多个型号、造型等&#xff0c;因此对高收入群体或爱机人群来说&#xff0c;新手机往往一年或二年时间就会换&#xff0c;或者直接购买当备用机等。 每个城…

关于解决 unable to start ssh-agent service, error :1058

前言 操作系统&#xff1a;win11 命令终端&#xff1a;Powershell 当我在终端输入命令 启动 ssh-agent 代理的时候 ssh-agent -s 很不幸出现了 unable to start ssh-agent service, error :1058以下错误 问题的解决 查看我们ssh-agent 服务是否运行&#xff0c;执行如下命令…