protobuf
复习记录
创建时间 | 一天 | 三天 | 十五 | 三十天 |
---|---|---|---|---|
2021.7.20 | ----- | ----- | ----- |
目录
- 描述
- 使用说明
- 协议具体使用
- 协议实现
描述
- protobuf 是一种数据交换格式。
- 二进制编码,编码/解码速度更优。不可肉读。。。
使用说明
proto 是先编写定义文件, 然后通过编译命令将配置编译成对应语言的的结构定义
环境安装
访问官方仓库 下载最新版本
目前官方库支持 C++, C#, Java, JavaScript, Kotlin, Objective-C, PHP, Python, Ruby
的直接输出.如果是其他语言, 需要自行安装插件.
// 使用命令
protoc --cpp_out=. lm.proto
// 使用安装的 go 插件格式化
protoc --gofast_out=. lm.proto
定义 proto 文件
首先我们需要编写一个 proto 文件,定义我们程序中需要处理的结构化数据,在 protobuf 的术语中,结构化数据被称为 Message。下面代码显示了例子应用中的
proto 文件内容:
syntax = "proto3"; //指定版本,必须要写(proto3、proto2)不写就是2,
package main;
message helloworld
{
int32 id = 1; // 字段规则, 类型, ID, 唯一标识
string str = 2; // str
int32 opt = 3; //optional field
}
一个比较好的习惯是认真对待 proto 文件的文件名。比如将命名规则定于如下:
packageName.MessageName.proto
在上例中,package 名字叫做 lm,定义了一个消息 helloworld,该消息有三个成员,类型为 int32 的 id,另一个为类型为 string 的成员 str。opt 是一个可选的成员,即消息中可以不包含该成员。
编译 proto 文件
写好 proto 文件之后就可以用 Protobuf 编译器将该文件编译成目标语言了。可以根据不同的语言来选择不同的编译方式
这里将 proto 文件编译成 go 格式
1. [go编译插件官方](https://github.com/golang/protobuf)
2. [protoc-gen-gofast生成的文件更复杂,性能也更高(快5-7倍)](https://github.com/gogo/protobuf)
// 官方
protoc --go_out=. *.proto
// gogo protoc-gen-gofast
protoc --gofast_out=. *.proto
3 go 中使用
类型与 json 等编码的使用方法
协议说明
协议定于 proto 文件中。
消息类型
- message 关键字定义结构
- 字段:包含 字段名, 数据类型,字段规则,唯一标识,默认值
- 字段类型: 可以是标量类型,或者其他合成的消息类型, 枚举等
- 标识号: 每一个字段都有一个唯一的标识符,一旦开始就不能在改变。从1开始, 先用小的。
- 字段规则:
- required:一个格式良好的消息一定要含有1个这种字段。表示该值是必须要设置的;(3 已经被弃用)
- optional:消息格式中该字段可以有0个或1个值(不超过1个)。(3 中使用 singular 替换)
- repeated:在一个格式良好的消息中,这种字段可以重复任意多次(包括0次)。重复的值的顺序会被保留。表示该值可以重复,相当于java中的List。
- 注释: 双斜杠(//)语法格式
- 默认值的定义:
optional int32 result_per_page = 3 [default = 10];
- 标量类型
.proto类型 | 说明 | C++ 类型 | Java 类型 | Python类型 | Go类型 | Ruby类型 | C#类型 | PHP类型 |
---|---|---|---|---|---|---|---|---|
double | double | double | float | float64 | Float | double | float | |
float | float | float | float | float32 | Float | float | float | |
int32 | 使用可变长度编码。编码负数效率低下——如果你的字段可能有负值,请改用sint32。 | int32 | int | int | int32 | Fixnum或Bignum(根据需要) | int | integer |
int64 | 使用可变长度编码。编码负数效率低下——如果你的字段可能有负值,请改用sint64。 | int64 | long | int/long[3] | int64 | Bignum | long | integer/string[5] |
uint32 | 使用可变长度编码。 | uint32 | int[1] | int/long[3] | uint32 | Fixnum或Bignum (根据需要) | uint | integer |
uint64 | 使用可变长度编码。 | uint64 | long[1] | int/long[3] | uint64 | Bignum | ulong | integer/string[5] |
sint32 | 使用可变长度编码。符号整型值。这些比常规int32s编码负数更有效。 | int32 | int | int | int32 | Fixnum或Bignum (根据需要) | int | integer |
sint64 | 使用可变长度编码。符号整型值。这些比常规int64s编码负数更有效。 | int64 | long | int/long[3] | int64 | Bignum | long | integer/string[5] |
fixed32 | 总是四字节。如果值通常大于228,则比uint32更有效 | uint32 | int[1] | int/long[3] | uint32 | Fixnum或Bignum (根据需要) | uint | integer |
fixed64 | 总是八字节。如果值通常大于256,则比uint64更有效 | uint64 | long[1] | int/long[3] | uint64 | Bignum | ulong | integer/string[5] |
sfixed32 | 总是四字节。 | int32 | int | int | int32 | Fixnum或Bignum (根据需要) | int | integer |
sfixed64 | 总是八字节。 | int64 | long | int/long[3] | int64 | Bignum | long | integer/string[5] |
bool | bool | boolean | bool | bool | TrueClass/FalseClass | bool | boolean | |
string | 字符串必须始终包含UTF - 8编码或7位ASCII文本 | string | String | str/unicode[4] | string | String (UTF-8) string | string | |
bytes | 可以包含任意字节序列 | string | ByteString | str | []byte | String (ASCII-8BIT) | ByteString | string |
枚举类型
一个enum类型的字段只能用指定的常量集中的一个值作为其值(如果尝试指定不同的值,解析器就会把它当作一个未知的字段来对待)。
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
- 枚举常量必须在32位整型值的范围内。因为enum值是使用可变编码方式的,对负数不够高效,因此不推荐在enum中使用负数。
- 可以在一个消息定义的内部或外部定义枚举——这些枚举可以在.proto文件中的任何消息定义里重用。当然也可以在一个消息中声明一个枚举类型,而在另一个不同的消息中使用它——采用MessageType.EnumType的语法格式。
使用其他消息类型
可以将其他消息类型用作字段类型。例如,假设在每一个SearchResponse消息中包含Result消息,此时可以在相同的.proto文件中定义一个Result消息类型,然后在SearchResponse消息中指定一个Result类型的字段,如:
message SearchResponse {
repeated Result result = 1;
}
message Result {
required string url = 1;
optional string title = 2;
repeated string snippets = 3;
}
导入定义
在上面的例子中,Result消息类型与SearchResponse是定义在同一文件中的。如果想要使用的消息类型已经在其他.proto文件中已经定义过了呢?
你可以通过导入(importing)其他.proto文件中的定义来使用它们。要导入其他.proto文件的定义,你需要在你的文件中添加一个导入声明,如:
import "myproject/other_protos.proto";
protocol编译器就会在一系列目录中查找需要被导入的文件,这些目录通过protocol编译器的命令行参数-I/–import_path指定。如果不提供参数,编译器就在其调用目录下查找。
嵌套类型
你可以在其他消息类型中定义、使用消息类型,在下面的例子中,Result消息就定义在SearchResponse消息内,如:
message SearchResponse {
message Result {
required string url = 1;
optional string title = 2;
repeated string snippets = 3;
}
repeated Result result = 1;
}
如果你想在它的父消息类型的外部重用这个消息类型,你需要以Parent.Type的形式使用它,如:
message SomeOtherMessage {
optional SearchResponse.Result result = 1;
}
更新一个消息类型
如果一个已有的消息格式已无法满足新的需求——如,要在消息中添加一个额外的字段——但是同时旧版本写的代码仍然可用。不用担心!更新消息而不破坏已有代码是非常简单的。在更新时只要记住以下的规则即可。
- 不要更改任何已有的字段的数值标识。
- 所添加的任何字段都必须是optional或repeated的。这就意味着任何使用“旧”的消息格式的代码序列化的消息可以被新的代码所解析,因为它们不会丢掉任何required的元素。应该为这些元素设置合理的默认值,这样新的代码就能够正确地与老代码生成的消息交互了。类似地,新的代码创建的消息也能被老的代码解析:老的二进制程序在解析的时候只是简单地将新字段忽略。然而,未知的字段是没有被抛弃的。此后,如果消息被序列化,未知的字段会随之一起被序列化——所以,如果消息传到了新代码那里,则新的字段仍然可用。注意:对Python来说,对未知字段的保留策略是无效的。
- 非required的字段可以移除——只要它们的标识号在新的消息类型中不再使用(更好的做法可能是重命名那个字段,例如在字段前添加“OBSOLETE_”前缀,那样的话,使用的.proto文件的用户将来就不会无意中重新使用了那些不该使用的标识号)。
- 一个非required的字段可以转换为一个扩展,反之亦然——只要它的类型和标识号保持不变。
- int32,uint32,int64,uint64,和bool是全部兼容的,这意味着可以将这些类型中的一个转换为另外一个,而不会破坏向前、向后的兼容性。如果解析出来的数字与对应的类型不相符,那么结果就像在C++中对它进行了强制类型转换一样(例如,如果把一个64位数字当作int32来读取,那么它就会被截断为32位的数字)。
- sint32和sint64是互相兼容的,但是它们与其他整数类型不兼容。
- string和bytes是兼容的——只要bytes是有效的UTF-8编码。
- 嵌套消息与bytes是兼容的——只要bytes包含该消息的一个编码过的版本。
- fixed32与sfixed32是兼容的,fixed64与sfixed64是兼容的。
扩展
通过扩展,可以将一个范围内的字段标识号声明为可被第三方扩展所用。然后,其他人就可以在他们自己的.proto文件中为该消息类型声明新的字段,而不必去编辑原始文件了。看个具体例子:
message Foo {
// …
extensions 100 to 199; // extensions 1000 to max;
}
这个例子表明:在消息Foo中,范围[100,199]之内的字段标识号被保留为扩展用。现在,其他人就可以在他们自己的.proto文件中添加新字段到Foo里了,但是添加的字段标识号要在指定的范围内——例如:
extend Foo {
optional int32 bar = 126;
}
这个例子表明:消息Foo现在有一个名为bar的optionalint32字段。
当用户的Foo消息被编码的时候,数据的传输格式与用户在Foo里定义新字段的效果是完全一样的。
然而,要在程序代码中访问扩展字段的方法与访问普通的字段稍有不同——生成的数据访问代码为扩展准备了特殊的访问函数来访问它。
包(Package)
当然可以为.proto文件新增一个可选的package声明符,用来防止不同的消息类型有命名冲突。如:
package foo.bar;
message Open { ... }
在其他的消息格式定义中可以使用包名+消息名的方式来定义域的类型,如:
message Foo {
required foo.bar.Open open = 1;
}
包的声明符会根据使用语言的不同影响生成的代码。对于C++,产生的类会被包装在C++的命名空间中,如上例中的Open会被封装在foo::bar空间中;对于Java,包声明符会变为java的一个包,除非在.proto文件中提供了一个明确有java_package;对于Python,这个包声明符是被忽略的,因为Python模块是按照其在文件系统中的位置进行组织的。
定义服务(Service)
如果想要将消息类型用在RPC(远程方法调用)系统中,可以在.proto文件中定义一个RPC服务接口,protocol buffer编译器将会根据所选择的不同语言生成服务接口代码及存根。如,想要定义一个RPC服务并具有一个方法,该方法能够接收SearchRequest并返回一个SearchResponse,此时可以在.proto文件中进行如下定义:
service SearchService {
rpc Search (SearchRequest) returns (SearchResponse);
}
protocol编译器将产生一个抽象接口SearchService以及一个相应的存根实现。存根将所有的调用指向RpcChannel,它是一个抽象接口,必须在RPC系统中对该接口进行实现。如,可以实现RpcChannel以完成序列化消息并通过HTTP方式来发送到一个服务器。换句话说,产生的存根提供了一个类型安全的接口用来完成基于protocolbuffer的RPC调用,而不是将你限定在一个特定的RPC的实现中
选项(Options)
在定义.proto文件时能够标注一系列的options。Options并不改变整个文件声明的含义,但却能够影响特定环境下处理方式。完整的可用选项可以在google/protobuf/descriptor.proto找到。
一些选项是文件级别的,意味着它可以作用于最外范围,不包含在任何消息内部、enum或服务定义中。一些选项是消息级别的,意味着它可以用在消息定义的内部。当然有些选项可以作用在域、enum类型、enum值、服务类型及服务方法中。到目前为止,并没有一种有效的选项能作用于所有的类型。
核心思想
基于 128 bites 数值存储方式
- 每块数据由接连的若干个字节表示(小的数据用1个字节就可以表示)。
- 每个字节最高位标识本块数据是否结束(1:未结束,0:结束),低7位表示数据内容。
- 字节序是小端字节序
如:
数字1的表示方法为:0000 0001, 1 个字节足够标识
数字300的表示方法为:1010 1100 0000 0010, 300 大于 2^7(128), 需要2 个字节
小端 -> 大端
1010 1100 0000 0010 -> 0000 0010 1010 1100
-》 000 0010 010 1100 -> 256 + 32 + 8 + 4 = 300
基于序号的协议字段映射(类似key-value结构)
所以字段可以乱序,可缺段(记optional)
message person{
required string name = 1;
required string country = 2;
optional int32 这里我是把以前笔记又重新整理发布一下,.age = 3;
}
效果相当于json数据:person = [{1: "john"}, {2: "USA"}, {3: 30}],其中{3: 30} 还可以不传,person还可以传成 [{2: "USA"}, {1: "john"}],对端仍旧可以正常解析。
基于无符号数的带符号数表示(ZigZag 编码)
原始的带符号数 | ZigZag编码后的表示 |
---|---|
0 | 0 |
-1 | 1 |
1 | 2 |
-2 | 3 |
2147483647 | 4294967294 |
-2147483648 | 4294967295 |
使用 zigzag 编码,充分利用基于128bits的数值存储(Base 128 Varints)的技术,只需要加多1个位来表示符号。当绝对值小的数字非常有利,这种方式可以有效减少协议内容长度。
更多查看这里
协议数据结构
protobuf怎么在一长串二进制中表示若干个数据?
做法就是每块数据前加一个数据头,表示数据类型及协议字段序号。
msg1_head + msg1 + msg2_head + msg2 + ...
数据头也是基于128bits的数值存储方式,一般1个字节就可以表示:
message Test1 {
required int32 a = 1;
}
如上创建了 Test1 的结构并且把 a 设为 2,序列化好的二进制数据为:
0000 1000 0000 0010
以上数据转成十六进制也就是 08 02,其中 8 是怎么得到的?
000 1000 => 0(最高位只有1个字节) 001(序号) 000(数据类型)
低3位表示数据类型:0,其他表示协议字段序号:1,加上最高位0, 结果就是8
数据类型的表示如下:
类型 | 含义 | 用于哪些数据类型 |
---|---|---|
0 | Varint | int32, int64, uint32, uint64, sint32, sint64, bool, enum |
1 | 64-bit | fixed64, sfixed64, double |
2 | Length-delimited | string, bytes, embedded messages, packed repeated fields |
3 | Start group | groups (deprecated) |
4 | End group | groups (deprecated) |
5 | 32-bit | fixed32, sfixed32, float |