protoco

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. 字段类型: 可以是标量类型,或者其他合成的消息类型, 枚举等
  2. 标识号: 每一个字段都有一个唯一的标识符,一旦开始就不能在改变。从1开始, 先用小的。
  3. 字段规则:
    1. required:一个格式良好的消息一定要含有1个这种字段。表示该值是必须要设置的;(3 已经被弃用)
    2. optional:消息格式中该字段可以有0个或1个值(不超过1个)。(3 中使用 singular 替换)
    3. repeated:在一个格式良好的消息中,这种字段可以重复任意多次(包括0次)。重复的值的顺序会被保留。表示该值可以重复,相当于java中的List。
  4. 注释: 双斜杠(//)语法格式
  5. 默认值的定义: 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;
}
  1. 枚举常量必须在32位整型值的范围内。因为enum值是使用可变编码方式的,对负数不够高效,因此不推荐在enum中使用负数。
  2. 可以在一个消息定义的内部或外部定义枚举——这些枚举可以在.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个字节就可以表示)。
  2. 每个字节最高位标识本块数据是否结束(1:未结束,0:结束),低7位表示数据内容。
  3. 字节序是小端字节序

如:

数字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

你可能感兴趣的:(protoco)