LQH入职第五天--protobuf学习

1、protobuf的介绍和优缺点

protof的描述

首先 protobuf是一个开源项目,是goole内部久经考验的一个东西。主要用于结构化数据串行化的灵活、高效、自动的方法,有如XML,不过他更小,更快,也更简单。你可以定义自己的数据结构,然后使用代码生成器生成的代码来读写这个数据结构。甚至可以在无需重新部署程序的情况下更新数据结构。
protobuf的优点:
(1)性能好/效率高
时间开销: XML格式化(序列化)的开销还好;但是XML解析(反序列化)的开销就不敢恭维了。 但是protobuf在这个方面就进行了优化。可以使序列化和反序列化的时间开销都减短。
空间开销:也减少了很多
(2)有代码生成机制

比如你你写个一下类似结构体的内容

message testA
{
    required int32 m_testA = 1;
}

向写一个这样的机构可以自动生成它的.h 文件和点.cpp文件。 他将对结构体testA的操作封装成一个类。
(3)支持向后兼容和向前兼容

当客户端和服务器同事使用一块协议的时候, 当客户端在协议中增加一个字节,并不会影响客户端的使用

(4)支持多种编程语言
protobuf的缺陷
(1)二进制格式导致可读性差
为了提高性能,protobuf采用了二进制格式进行编码。这直接导致了可读性差。
(2)缺乏自描述
一般来说,XML是自描述的,而protobuf格式则不是。 给你一段二进制格式的协议内容,不配合你写的结构体是看不出来什么作用的。
在这里插入图片描述

2、protobuf语法学习

(1)版本号
文件首个非空、非注释的行必须注明pb的版本
syntax = “proto3”;
(2)Message 相当于结构体
message 定义实体

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}

每一个字段都由类型名称组成,位于等号右边的值不是字段默认值,而是数字标签,可以理解为字段身份的标识符类似于数据库中的主键不可重复,标识符用于在编译后的二进制消息格式中对字段进行识别,一旦你的pb消息投入使用字段的标识就不应该再改变。数字标签的范围是[1, 536870911],其中19000~19999是保留数字。

注释:message可以看作是一个结构体,可以嵌套

一个 proto 文件可以定义多个 message ,比如我们可以在刚才那个 proto 文件中把服务端返回的消息结构也一起定义:

message SearchRequest {
    string query = 1;
    int32 page_number = 2;
    int32 result_per_page = 3;
}

message SearchResponse {
    repeated string result = 1;
}

message 可以嵌套定义,比如 message 可以定义在另一个 message 内部

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

定义在 message 内部的 message 可以这样使用:

message SomeOtherMessage {
    SearchResponse.Result result = 1;
}

(3)类型
每个字段的类型(int32,string)都是scalar的类型,和其他语言类型的对比如下:
在这里插入图片描述
(4)修饰符
singular:0或者1个,但不能多于1个
repeated:任意数量(包括0),可以看成是一个数组
reserved:保留字
当一个变量不再使用的时候,如果他们以后加载旧版本的旧版本,可能会导致严重的问题.proto,包括数据损坏隐私错误等。确保不会发生这种情况的一种方法是,将已删除的条目的数字值(和/或名称,也可能导致JSON序列化的问题)指定为reserved。如果将来有任何用户尝试使用这些标识符,则协议缓冲区编译器会报错

message Foo {
    // 注意,同一个 reserved 语句不能同时包含变量名和 Tag 
    reserved 2, 15, 9 to 11;
    reserved "foo", "bar";
}

(5)枚举
每个枚举值有对应的数值,数值不一定是连续的。第一个枚举值的数值必须是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;
}

一个数值可以对应多个枚举值,必须标明option allow_alias = true;

enum EnumAllowingAlias {
  option allow_alias = true;
  UNKNOWN = 0;
  STARTED = 1;
  RUNNING = 1;
}

(6)如何引用其他 proto 文件
在proto语法中,有两种引用其他 proto 文件的方法: importimport public,这两者有什么区别呢?下面举个例子说明:
在这里插入图片描述
在情景1中, my.proto 不能使用 second.proto 中定义的内容
在情景2中, my.proto 可以使用 second.proto 中定义的内容
情景1和情景2中,my.proto 都可以使用 first.proto
情景1和情景2中,first.proto 都可以使用 second.proto

// my.proto
import "first.proto";
// first.proto
//import "second.proto";
import public "second.proto";
// second.proto
...

(7)升级 proto 文件正确的姿势
升级更改 proto 需要遵循以下原则

a、不要修改任何已存在的变量的 Tag
b、如果你新增了变量,新生成的代码依然能解析旧的数据,但新增的变量将会变成默认值。相应的,新代码序列化的数据也能被旧的代码解析,但旧代码会自动忽略新增的变量。
c、废弃不用的变量用 reserved 标注
d、int32、 uint32、 int64、 uint64 和 bool 是相互兼容的,这意味你可以更改这些变量的类型而不会影响兼容性
e、sint32 和 sint64 是兼容的,但跟其他类型不兼容
f、string 和 bytes 可以兼容,前提是他们都是UTF-8编码的数据
g、fixed32 和 sfixed32 是兼容的, fixed64 和 sfixed64是兼容的
(8)Any
Any可以让你在 proto 文件中使用未定义的类型,具体里面保存什么数据,是在上层业务代码使用的时候决定的,使用 Any 必须导入
import google/protobuf/any.proto

import "google/protobuf/any.proto";

message ErrorStatus {
    string message = 1;
    repeated google.protobuf.Any details = 2;
}

可以通过pack()和unpack()(方法名在不同的语言中可能不同)方法装箱/拆箱,以下是Java的例子:

People people = People.newBuilder().setName("proto").setAge(1).build();
// protoc编译后生成的message类
Response r = Response.newBuilder().setData(Any.pack(people)).build();
// 使用Response包装people

System.out.println(r.getData().getTypeUrl());
// type.googleapis.com/example.protobuf.people.People
System.out.println(r.getData().unpack(People.class).getName());
// proto

Any对包装的类型会生成一个URL,默认是type.googleapis.com/packagename.messagename(在Java中可以通过这个特性进行反射操作)。

https://segmentfault.com/a/1190000020875738?utm_source=tag-newest
(9)Oneof 的使用 相当于 interface
Oneof 类似union,如果你的消息中有很多可选字段,而同一个时刻最多仅有其中的一个字段被设置的话,你可以使用oneof来强化这个特性并且节约存储空间,如

message LoginReply {
    oneof test_oneof {
        string name = 3;
        string age = 4;
    }
    required string status = 1;
    required string token = 2;
}

这样,name 和 age 都是 LoginReply 的成员,但不能给他们同时设置值(设置一个oneof字段会自动清理其他的oneof字段)。
(10)Maps 的使用
protobuf 支持定义 map 类型的成员,如:

map<key_type, value_type> map_field = N;
// 举例:map<string, Project> projects = 3;
key_type:必须是string或者int
value_type:任意类型

使用 map 要注意:
a、Map 类型不能使 repeated
b、Map 是无序的
c、以文本格式展示时,Map 以 key 来排序
d、如果有相同的键会导致解析失败

(11)Packages 的使用
为了防止不同消息之间的命名冲突,你可以对特定的.proto文件提指定 package 名字。在定义消息的成员的时候,可以指定包的名字:

package foo.bar;
message Open { ... }
message Foo {
    ...
    // 带上包名
    foo.bar.Open open = 1;
    ...
}

(12)Options

在定义.proto文件时能够标注一系列的options。Options并不改变整个文件声明的含义,但却能够影响特定环境下处理方式。完整的可用选项可以在google/protobuf/descriptor.proto找到。

一些选项是文件级别的,意味着它可以作用于最外范围,不包含在任何消息内部、enum或服务定义中。

Options 分为 file-level options(只能出现在最顶层,不能在消息、枚举、服务内部使用)、 message-level
options(只能在消息内部使用)、field-level options(只能在变量定义时使用)

java_package (file option):指定生成类的包名,如果没有指定此选项,将由关键字package指定包名。此选项只在生成
java 代码时有效 java_multiple_files (file option):如果为 true, 定义在最外层的 message
、enum、service 将作为单独的类存在 java_outer_classname (file
option):指定最外层class的类名,如果不指定,将会以文件名作为类名 optimize_for (file option):可选有
[SPEED|CODE_SIZE|LITE_RUNTIME]
,分别是效率优先、空间优先,第三个lite是兼顾效率和代码大小,但是运行时需要依赖 libprotobuf-lite

cc_enable_arenas (file option):启动arena allocation,c++代码使用
objc_class_prefix (file option):Objective-C使用
deprecated (field option):提示变量已废弃、不建议使用
option java_package = "com.example.foo";
option java_multiple_files = true;
option java_outer_classname = "Ponycopter";
option optimize_for = CODE_SIZE;
int32 old_field = 6 [deprecated=true];

(13)定义 Services
这个其实和gRPC相关,详细可参考:gRPC, 这里做一个简单的介绍
要定义一个服务,你必须在你的 .proto 文件中指定 service

service RouteGuide {
    ...
}

学习连接:https://blog.csdn.net/shensky711/article/details/69696392#%E5%A6%82%E4%BD%95%E5%BC%95%E7%94%A8%E5%85%B6%E4%BB%96-proto-%E6%96%87%E4%BB%B6

https://developers.google.cn/protocol-buffers/docs/proto3#scalar

3、proto3 区别于 proto2 的地方

1.在第一行非空白非注释行,必须写:
syntax = "proto3";
2.字段规则移除了 “required”,并把 “optional” 改名为 “singular”13.“repeated”字段默认采用 packed 编码;
在 proto2 中,需要明确使用 [packed=true] 来为字段指定比较紧凑的 packed 编码方式。
4.语言增加 Go、Ruby、JavaNano 支持;
5.移除了 default 选项;
6.枚举类型的第一个字段必须为 07.移除了对分组的支持;
8.旧代码在解析新增字段时,会把不认识的字段丢弃,再序列化后新增的字段就没了;
9.移除了对扩展的支持,新增了 Any 类型;
10.增加了 JSON 映射特性;

https://www.jianshu.com/p/340446136a0a

4、proto 编译

protoc --go_out=plugins=grpc:. test.proto
报错
(1)protoc-gen-go: program not found or is not executable
解决:在生成代码之前要先安装go get -u github.com/golang/protobuf/{proto,protoc-gen-go} 并且把GOPATH/bin目录下的protoc-gen-go放到环境变量PATH里面去
(2)

go env -w GOPROXY=***
warning: go env -w GOPROXY=... does not override conflicting OS environment variable

解决:unset GOPROXY
(3)直接在终端输入echo $GOPATH,没有任何返回值。
解决:

vi ~/.bash_profile
source ~/.bash_profile

命令参数
plugins:我们定义的 proto 文件是涉及了 RPC 服务的,而默认是不会生成 RPC 代码的,因此需要给出 plugins 参数传递给 protoc-gen-go,告诉它,请支持 RPC

–go_out=.:设置 Go 代码输出的目录

–proto_path: 指定了要去哪个目录中搜索import中导入的和要编译为.go的proto文件,可以定义多个

syntax="proto3";
import "test.proto";
package protoDemo2;

enum Book {
    UNKNOWN = 0;
    STUDY = 1;
    STORY = 2;
    TECH = 3;
    LAUGHT = 4;
}

enum Food {
    COOKIE = 0;
    HUMBURGER = 1;
    RICE = 2;
    BREAD = 3;
}

message Test2{
    test.ContactBook book = 1;
}
protoc --proto_path=./src/protobufDemo/demo1/ --proto_path=./src/protobufDemo/demo2 --go_out=./src/protobufDemo/demo2 protoDemo2.proto

我使用了两个proto_path,第一个指定了import "test.proto"中test.proto文件的搜索目录,第二个定义了要编译的文件protoDemo2.proto文件的目录。

https://www.cnblogs.com/hsnblog/p/9615742.html

5、protobuf 学习【续】

(1)包名
option go_package = “gitlab.com/***”;
是指编译的包名,按照之后的路径进行编译。
go_package按照路径编译成go
java_package按照路径编译成java。
(2)package
package是proto包名,用于外部引用
(3)参数校验

option (grpc.gateway.protoc_gen_swagger.options.openapiv2_schema) = {
		json_schema: {
			required: ["doc"]
		};
    };

意思是参数必须加上“doc”参数

https://ask.csdn.net/questions/1026195?sort=comments_count
(4)protobuf 三个关键字required、optional、repeated的理解


> required关键字

顾名思义,就是必须的意思,数据发送方和接收方都必须处理这个字段,不然还怎么通讯呢

> optional关键字

字面意思是可选的意思,具体protobuf里面怎么处理这个字段呢,就是protobuf处理的时候另外加了一个bool的变量,用来标记这个optional字段是否有值,发送方在发送的时候,如果这个字段有值,那么就给bool变量标记为true,否则就标记为false,接收方在收到这个字段的同时,也会收到发送方同时发送的bool变量,拿着bool变量就知道这个字段是否有值了,这就是option的意思。

这也就是他们说的所谓平滑升级,无非就是个兼容的意思。

其实和传输参数的时候,给出数组地址和数组数量是一个道理。

> repeated关键字

字面意思大概是重复的意思,其实protobuf处理这个字段的时候,也是optional字段一样,另外加了一个count计数变量,用于标明这个字段有多少个,这样发送方发送的时候,同时发送了count计数变量和这个字段的起始地址,接收方在接受到数据之后,按照count来解析对应的数据即可。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值