Protobuf 数据格式

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Protobuf 数据格式相关的知识,希望对你有一定的参考价值。

参考技术A

简单来说, Protocol Buffers 是一种和语言平台都没关的数据交换格式。

关于 Protobuf 在ios下的使用请看上篇文章 iOS 的 Protocol Buffer 简单使用

Protobuf 序列化后的二进制数据消息非常的紧凑,这得益于 Protobuf 所采用的 Varint

Varint 是一种紧凑的表示数字的方法,它用一个或多个字节来表示一个数字,值越小的数字使用越少的字节数。这能减少用来表示数组的字节数。

比如对于 int32 类型的数字,一般需要4个 byte 来标识。但是采用 Varint,对于很小的 int32 类型的数字,也能用1个 byte 来标识。如果数字很大,也就需要5个 byte 来表示了。但是,一般情况下很少会出现数字都是大数的情况下。

正常情况下,每个 byte 的8个 bit 位都用于存储数据用,而在 Varint 中,每个 byte 的最高位的 bit 有着特殊的含义,如果该位为1,表示后续的 byte 也是该数据的一部分;如果该位为0,则结束。其他的7个 bit 位都用来表示数据。因此小于127的 int32 数字都可以用一个 byte 表示,而大于等于 128 的数字:如128,则会用两个字节表示: 1000 0000 0000 0001 (采用的是小端模式),311则表示: 1011 0111 0000 0010

下图演示了 Protobuf 如果通过2个 byte 解析出 128。Protobuf 字节序采用的是 little-endian(小端模式)

int32 数据类型能表示负数,负数的最高位为1,如果负数也使用这种方式表示会出现一个问题,int32 总是需要5个字节,int64 总是需要10个字节。所以 Protobuf 定义了另外一种类型 sint32, sint64,采用 ZigZag 编码,所有的负数都使用正数表示,计算方式为:

Protobuf 消息是一系列的键值对组成。消息的二进制版本仅使用 field 数字当作 key,不同 field 的属性和类型只能通过消息类型的定义 (即 .proto 文件) 在解码端确定。如果消息中不存在该 field,那么序列化后的 Message Buffer 中也不会有该 field,这些特性都有助于节约消息本身的大小。

Key 用来标识具体的 field,在解包的时候,Protobuf 根据 key 就能知道相应的 Value 对应于消息中的哪一个field,数据类型是哪个类型。

Key 的定义如下:

Key 由两部分组成:第一个部分是 field_number,比如上篇文章定义的消息 FooSimpleMessage 中的 msgId 属性的 field_number 为1;第二部分为 wire_type,表示 Value 的传输类型

表1. Wire Type

在之前的例子中,msgId 采用的数据类型为 int32,因此对应的 wire_type 为0,所以对应的 tag 为

FooSimpleMessage 的 msgContent,field_number 为2,wire_type 为2,所以对应的 tag 为

对应 Length-delimited 的 wire type,后面紧跟着的 Varint 类型表示数据的字节数。所以 msgContent 的 key 后面紧跟着的 0x1a 表示后面的数据长度为10个字节,"A protobuf message content" 的 ASCII 值即为: 0x41 0x20 0x70 0x72 0x6f 0x74 0x6f 0x62 0x75 0x66 0x20 0x6d 0x65 0x73 0x73 0x61 0x67 0x65 0x20 0x63 0x6f 0x6e 0x74 0x65 0x6e 0x74

在 Demo 里面定义的 msg 对象,其序列化后的数据的十六进制表示应该为 0801121a 41207072 6f746f62 7566206d 65737361 67652063 6f6e7465 6e74

运行demo,打印一下结果和猜想的一样:

参考地址: Protocol-buffers Encoding

protobuf 详解

protobuf

protobuf概述

protobuf简介

Protobuf是Protocol Buffers的简称,它是Google公司开发的一种数据描述语言,是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化 。它很适合做数据存储RPC 数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。目前提供了 C++、Java、Python 三种语言的 API。

  1. protobuf是类似与json一样的数据描述语言(数据格式)

  2. protobuf非常适合于RPC数据交换格式

注意:protobuf本身并不是和gRPC绑定的。它也可以被用于非RPC场景,如存储等

protobuf的优劣势

1)优势:

  1. 序列化后体积相比Json和XML很小,适合网络传输

  2. 序列化反序列化速度很快,快于Json的处理速度

  3. 消息格式升级和兼容性还不错

  4. 支持跨平台多语言

2)劣势:

  1. 应用不够广(相比xml和json)

  2. 二进制格式导致可读性差

  3. 缺乏自描述

protoc安装(windows)

protoc就是protobuf的编译器,它把proto文件编译成不同的语言

下载安装protoc编译器(protoc)

下载protobuf:https://github.com/protocolbuffers/protobuf/releases/download/v3.20.1/protoc-3.20.1-win64.zip

解压后,将目录中的 bin 目录的路径添加到系统环境变量,然后打开cmd输入protoc查看输出信息,此时则安装成功

安装protocbuf的go插件(protoc-gen-go)

由于protobuf并没直接支持go语言需要我们手动安装相关插件

protocol buffer编译器需要一个插件来根据提供的proto文件生成 Go 代码,Go1.16+要使用下面的命令安装插件:

 go install google.golang.org/protobuf/cmd/protoc-gen-go@latest  // 目前最新版是v1.3.0

安装grpc(grpc)

 go get -u -v google.golang.org/grpc@latest    // 目前最新版是v1.53.0

安装grpc的go插件(protoc-gen-go-grpc)

说明:在google.golang.org/protobuf中,protoc-gen-go纯粹用来生成pb序列化相关的文件,不再承载gRPC代码生成功能,所以如果要生成grpc相关的代码需要安装grpc-go相关的插件:protoc-gen-go-grpc

 go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest  // 目前最新版是v1.3.0

protobuf语法

 

 

protobuf语法

  • 类型:类型不仅可以是标量类型(intstring等),也可以是复合类型(enum等),也可以是其他message

  • 字段名:字段名比较推荐的是使用下划线/分隔名称

  • 字段编号:一个message内每一个字段编号都必须唯一的,在编码后其实传递的是这个编号而不是字段名

  • 字段规则:消息字段可以是以下字段之一

    • singular:格式正确的消息可以有零个或一个字段(但不能超过一个)。使用 proto3 语法时,如果未为给定字段指定其他字段规则,则这是默认字段规则

    • optional:与 singular 相同,不过可以检查该值是否明确设置

    • repeated:在格式正确的消息中,此字段类型可以重复零次或多次。系统会保留重复值的顺序

    • map:这是一个成对的键值对字段

  • 保留字段:为了避免再次使用到已移除的字段可以设定保留字段。如果任何未来用户尝试使用这些字段标识符,编译器就会报错

简单语法

proto文件基本语法

 syntax = "proto3";              // 指定版本信息,不指定会报错
 package pb; // 后期生成go文件的包名
 // message为关键字,作用为定义一种消息类型
 message Person
     string name = 1;   // 名字
     int32  age = 2 ;   // 年龄
 
 
 enum test
  int32 age = 0;
 

protobuf消息的定义(或者称为描述)通常都写在一个以 .proto 结尾的文件中:

  1. 第一行指定正在使用proto3语法:如果不这样做,协议缓冲区编译器将假定正在使用proto2(这也必须是文件的第一个非空的非注释行)

  2. 第二行package指明当前是pb包(生成go文件之后和Go的包名保持一致)

  3. message关键字定义一个Person消息体,类似于go语言中的结构体,是包含一系列类型数据的集合。

    • 许多标准的简单数据类型都可以作为字段类型,包括boolint32floatdouble,和string

    • 也可以使用其他message类型作为字段类型。

在message中有一个字符串类型的value成员,该成员编码时用1代替名字。在json中是通过成员的名字来绑定对应的数据,但是Protobuf编码却是通过成员的唯一编号来绑定对应的数据,因此Protobuf编码后数据的体积会比较小,能够快速传输,缺点是不利于阅读。

message常见的数据类型与go中类型对比

.proto类型Go类型介绍
double float64 64位浮点数
float float32 32位浮点数
int32 int32 使用可变长度编码。编码负数效率低下——如果你的字段可能有负值,请改用sint32。
int64 int64 使用可变长度编码。编码负数效率低下——如果你的字段可能有负值,请改用sint64。
uint32 uint32 使用可变长度编码。
uint64 uint64 使用可变长度编码。
sint32 int32 使用可变长度编码。符号整型值。这些比常规int32s编码负数更有效。
sint64 int64 使用可变长度编码。符号整型值。这些比常规int64s编码负数更有效。
fixed32 uint32 总是四字节。如果值通常大于228,则比uint 32更有效
fixed64 uint64 总是八字节。如果值通常大于256,则比uint64更有效
sfixed32 int32 总是四字节。
sfixed64 int64 总是八字节。
bool bool 布尔类型
string string 字符串必须始终包含UTF - 8编码或7位ASCII文本
bytes []byte 可以包含任意字节序列

protobuff语法进阶

message嵌套

messsage除了能放简单数据类型外,还能存放另外的message类型:

 syntax = "proto3";          // 指定版本信息,不指定会报错
 package pb; // 后期生成go文件的包名
 // message为关键字,作用为定义一种消息类型
 message Person
     string name = 1;  // 名字
     int32  age = 2 ;  // 年龄
     // 定义一个message
     message PhoneNumber
    string number = 1;
    int64 type = 2;
 
  PhoneNumber phone = 3;
 

message成员编号,可以不从1开始,但是不能重复,不能使用19000 - 19999

repeated关键字

repeadted关键字类似与go中的切片,编译之后对应的也是go的切片,用法如下:

 syntax = "proto3";              // 指定版本信息,不指定会报错
 package pb; // 后期生成go文件的包名
 // message为关键字,作用为定义一种消息类型
 message Person
     string name = 1;   // 名字
     int32  age = 2 ;   // 年龄
     // 定义一个message
     message PhoneNumber
    string number = 1;
    int64 type = 2;
 
  repeated PhoneNumber phone = 3;
 

默认值

解析数据时,如果编码的消息不包含特定的单数元素,则解析对象对象中的相应字段将设置为该字段的默认值

不同类型的默认值不同,具体如下:

  • 对于字符串,默认值为空字符串

  • 对于字节,默认值为空字节

  • 对于bools,默认值为false

  • 对于数字类型,默认值为零

  • 对于枚举,默认值是第一个定义的枚举值,该值必须为0

  • repeated字段默认值是空列表

  • message字段的默认值为空对象

enum关键字

在定义消息类型时,可能会希望其中一个字段有一个预定义的值列表

比如说,电话号码字段有个类型,这个类型可以是,home,work,mobile

我们可以通过enum在消息定义中添加每个可能值的常量来非常简单的执行此操作。示例如下:

 syntax = "proto3";              // 指定版本信息,不指定会报错
 package pb; // 后期生成go文件的包名
 // message为关键字,作用为定义一种消息类型
 message Person
     string name = 1;   // 名字
     int32  age = 2 ;   // 年龄
     // 定义一个message
     message PhoneNumber
    string number = 1;
    PhoneType type = 2;
 
 
  repeated PhoneNumber phone = 3;
 
 
 // enum为关键字,作用为定义一种枚举类型
 enum PhoneType
  MOBILE = 0;
     HOME = 1;
     WORK = 2;
 

如上,enum的第一个常量映射为0,每个枚举定义必须包含一个映射到零的常量作为其第一个元素。这是因为:

  • 必须有一个零值,以便我们可以使用0作为数字默认值。

  • 零值必须是第一个元素,以便与proto2语义兼容,其中第一个枚举值始终是默认值。

enum还可以为不同的枚举常量指定相同的值来定义别名。如果想要使用这个功能必须将allow_alias选项设置为true,负责编译器将报错。示例如下:

 syntax = "proto3";              // 指定版本信息,不指定会报错
 package pb; // 后期生成go文件的包名
 // message为关键字,作用为定义一种消息类型
 message Person
     string name = 1;   // 名字
     int32  age = 2 ;   // 年龄
     // 定义一个message
     message PhoneNumber
         string number = 1;
         PhoneType type = 2;
   
     repeated PhoneNumber phone = 3;
 
 
 // enum为关键字,作用为定义一种枚举类型
 enum PhoneType
  // 如果不设置将报错
     option allow_alias = true;
     MOBILE = 0;
     HOME = 1;
     WORK = 2;
     Personal = 2;
 

oneof关键字

如果有一个包含许多字段的消息,并且最多只能同时设置其中的一个字段,则可以使用oneof功能,示例如下:

 message Person
     string name = 1; // 名字
     int32  age = 2 ; // 年龄
     //定义一个message
     message PhoneNumber
         string number = 1;
         PhoneType type = 2;
   
 
     repeated PhoneNumber phone = 3;
     oneof data
         string school = 5;
         int32 score = 6;
   
 

定义RPC服务

如果需要将message与RPC一起使用,则可以在.proto文件中定义RPC服务接口,protobuf编译器将根据你选择的语言生成RPC接口代码。示例如下:

 //定义RPC服务
 service HelloService
     rpc Hello (Person)returns (Person);
 

注意:默认protobuf编译期间,不编译服务,如果要想让其编译,需要使用gRPC

protobuf编译

编译器调用

protobuf 编译是通过编译器 protoc 进行的,通过这个编译器,我们可以把 .proto 文件生成 go,Java,Python,C++, Ruby或者C# 代码

可以使用以下命令来通过 .proto 文件生成go代码(以及grpc代码)

 // 将当前目录中的所有 .proto文件进行编译生成go代码
 protoc --go_out=./ --go_opt=paths=source_relative *.proto

protobuf 编译器会把 .proto 文件编译成 .pd.go 文件

--go_out 参数

作用:指定go代码生成的基本路径

  1. protocol buffer编译器会将生成的Go代码输出到命令行参数go_out指定的位置

  2. go_out标志的参数是你希望编译器编写 Go 输出的目录

  3. 编译器会为每个.proto 文件输入创建一个源文件

  4. 输出文件的名称是通过将.proto 扩展名替换为.pb.go 而创建的

--go_opt 参数

protoc-gen-go提供了--go_opt参数来为其指定参数,可以设置多个:

  1. paths=import:生成的文件会按go_package路径来生成,当然是在--go_out目录

    • 例如,go_out/$go_package/pb_filename.pb.go

    • 如果未指定路径标志,这就是默认输出模式

  2. paths=source_relative:输出文件与输入文件放在相同的目录中

    • 例如,一个protos/buzz.proto输入文件会产生一个位于protos/buzz.pb.go的输出文件。

  3. module=$PREFIX:输出文件放在以 Go 包的导入路径命名的目录中,但是从输出文件名中删除了指定的目录前缀。

    • 例如,输入文件 pros/buzz.proto,其导入路径为 example.com/project/protos/fizz 并指定example.com/projectmodule前缀,结果会产生一个名为 pros/fizz/buzz.pb.go 的输出文件。

    • 在module路径之外生成任何 Go 包都会导致错误,此模式对于将生成的文件直接输出到 Go 模块非常有用。

--proto_path 参数

--proto_path=IMPORT_PATH

  • IMPORT_PATH是 .proto 文件所在的路径,如果忽略则默认当前目录。

  • 如果有多个目录则可以多次调用--proto_path,它们将会顺序的被访问并执行导入。

使用示例:

 protoc --proto_path=src --go_out=out --go_opt=paths=source_relative foo.proto bar/baz.proto
 // 编译器将从 `src` 目录中读取输入文件 `foo.proto` 和 `bar/baz.proto`,并将输出文件 `foo.pb.go` 和 `bar/baz.pb.go` 写入 `out` 目录。如果需要,编译器会自动创建嵌套的输出子目录,但不会创建输出目录本身

使用grpc的go插件

安装proto-gen-go-grpc

google.golang.org/protobuf中,protoc-gen-go纯粹用来生成pb序列化相关的文件,不再承载gRPC代码生成功能。生成gRPC相关代码需要安装grpc-go相关的插件protoc-gen-go-grpc

 // 安装protoc-gen-go-grpc
 go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest  // 目前最新版是v1.3.0

生成grpc的go代码:

 // 主要是--go_grpc_out参数会生成go代码
 protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative  *.proto

--go-grpc_out 参数

作用:指定grpc go代码生成的基本路径

命令会产生的go文件:

  1. protoc-gen-go:包含所有类型的序列化和反序列化的go代码

  2. protoc-gen-go-grpc:包含service中的用来给client调用的接口定义以及service中的用来给服务端实现的接口定义

--go-grpc_opt 参数

protoc-gen-go类似,protoc-gen-go-grpc提供 --go-grpc_opt 来指定参数,并可以设置多个

github.com/golang/protobufgoogle.golang.org/protobuf

github.com/golang/protobuf

  1. github.com/golang/protobuf 现在已经废弃

  2. 它可以同时生成pb和gRPC相关代码的

用法:

 // 它在--go_out加了plugin关键字,paths参数有两个选项,分别是 import 和 source_relative
 --go_out=plugins=grpc,paths=import:.  *.proto

google.golang.org/protobuf

  1. github.com/golang/protobuf的升级版本,v1.4.0之后github.com/golang/protobuf仅是google.golang.org/protobuf的包装

  2. 它纯粹用来生成pb序列化相关的文件,不再承载gRPC代码生成功能,生成gRPC相关代码需要安装grpc-go相关的插件protoc-gen-go-grpc

用法:

 // 它额外添加了参数--go-grpc_out以调用protoc-gen-go-grpc插件生成grpc代码
 protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative  *.proto
 

以上是关于Protobuf 数据格式的主要内容,如果未能解决你的问题,请参考以下文章

im即时通讯开发:Protobuf数据传输格式

protobuf 版本之间的数据格式兼容性

连信的protobuf数据格式

flink消费kafka protobuf格式数据

im即时通讯开发:Protobuf数据传输格式

用于解析包含数组格式的 protobuf 数据的二进制文件的 Python API