gRPC快速入门(一)——Protobuf简介

一、Protobuf简介

1、Protobuf简介

Protobuf即Protocol Buffers,是Google公司开发的一种跨语言和平台的序列化数据结构的方式,是一个灵活的、高效的用于序列化数据的协议。
与XML和JSON格式相比,protobuf更小、更快、更便捷。protobuf是跨语言的,并且自带一个编译器(protoc),只需要用protoc进行编译,就可以编译成Java、Python、C++、C#、Go等多种语言代码,然后可以直接使用,不需要再写其它代码,自带有解析的代码。
只需要将要被序列化的结构化数据定义一次(在.proto文件定义),便可以使用特别生成的源代码(使用protobuf提供的生成工具)轻松的使用不同的数据流完成对结构数据的读写操作。甚至可以更新.proto文件中对数据结构的定义而不会破坏依赖旧格式编译出来的程序。
GitHub地址:https://github.com/protocolbuffers/protobuf
不同语言源码版本下载地址:
https://github.com/protocolbuffers/protobuf/releases/latest

2、Protobuf的优缺点

Protobuf的优点如下:
A、性能号,效率高
序列化后字节占用空间比XML少3-10倍,序列化的时间效率比XML快20-100倍。
B、有代码生成机制
将对结构化数据的操作封装成一个类,便于使用。
C、支持向后和向前兼容
当客户端和服务器同时使用一块协议的时候, 当客户端在协议中增加一个字节,并不会影响客户端的使用
D、支持多种编程语言
Protobuf目前已经支持Java,C++,Python、Go、Ruby等多种语言。

Protobuf的缺点如下:
A、二进制格式导致可读性差
B、缺乏自描述

二、Protobuf编译器安装

1、C++版本Protobuf编译器安装

下载C++版本的Protobuf源码protobuf-cpp-3.6.1.tar.gz
解压Protobuf源码:
tar -zxvf protobuf-cpp-3.6.1.tar.gz
进入protobuf-3.6.1源码目录:
cd protobuf-3.6.1
配置变量:
./configure --prefix=/usr/local/protobuf
编译:
make
检查、测试:
make check
安装:
sudo make install
设置环境变量:

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/protobuf/lib
export LIBRARY_PATH=$LIBRARY_PATH:/usr/local/protobuf/lib
export PATH=$PATH:/usr/local/protobuf/bin

检查版本号:
protoc --version

2、Protobuf编译器使用

Protobuf提供了protoc编译器,用于通过定义好的.proto文件来生成Java,Python,C++,Ruby,Objective-C,C#,Go等语言代码。
protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR --go_out=DST_DIR --ruby_out=DST_DIR --javanano_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto
(1)导入目录设置
IMPORT_PATH声明了一个.proto文件所在的解析import具体目录。如果忽略该值,则使用当前目录。如果有多个目录则可以多次调用--proto_path,会顺序的被访问并执行导入。-I=IMPORT_PATH是--proto_path的简化形式。
(2)生成代码指定

--cpp_out :在目标目录DST_DIR中产生C++代码
--java_out :在目标目录DST_DIR中产生Java代码
--python_out :在目标目录 DST_DIR 中产生Python代码
--go_out :在目标目录 DST_DIR 中产生Go代码
--ruby_out:在目标目录 DST_DIR 中产生Ruby代码
--javanano_out:在目标目录DST_DIR中生成JavaNano
--objc_out:在目标目录DST_DIR中产生Object代码
--csharp_out:在目标目录DST_DIR中产生Object代码
 --php_out:在目标目录DST_DIR中产生Object代码

(3)导入proto消息文件指定
必须指定一个或多个.proto文件作为输入,多个.proto文件可以只指定一次。虽然文件路径是相对于当前目录的,每个文件必须位于其IMPORT_PATH下,以便每个文件可以确定其规范的名称。
(4)生成编程语言相关代码
当用Protobuf编译器来运行.proto文件时,编译器将生成所选择语言的代码,相应语言的代码可以操作在.proto文件中定义的消息类型,包括获取、设置字段值,将消息序列化到一个输出流中以及从一个输入流中解析消息。
对C++语言,编译器会为每个.proto文件生成一个.h文件和一个.cc文件,.proto文件中的每一个消息有一个对应的类。
对Java语言,编译器为每一个消息类型生成了一个.java文件以及一个特殊的Builder类(用来创建消息类接口的)。
对Go语言,编译器会为每个消息类型生成了一个.pb.go文件。
对Ruby语言,编译器会为每个消息类型生成了一个.rb文件。

三、Protobuf3语法

1、消息定义

Protobuf中,消息即结构化数据。

message Person {
  string name = 1;
  int32 id = 2;  
  string email = 3;
}

Person消息格式有3个字段,在消息中承载的数据分别对应于每一个字段,其中每个字段都有一个名字和一种类型。
在一个消息文件.proto中可以定义多个消息类型,在定义多个相关的消息的时候较为有用。

// [START declaration]
syntax = "proto3";
package Company.Person;

import "google/protobuf/timestamp.proto";
// [END declaration]

// [START messages]
message Person {
  string name = 1;
  int32 id = 2;  // Unique ID number for this person.
  string email = 3;
  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }
  message PhoneNumber {
    string number = 1;
    PhoneType type = 2;
  }

  repeated PhoneNumber phones = 4;

  google.protobuf.Timestamp last_updated = 5;
}

// Our address book file is just one of these.
message AddressBook {
  repeated Person people = 1;
}
// [END messages]

.proto文件中非注释非空的第一行必须使用Proto版本声明,版本声明如下:
syntax = "proto3";
如果不使用proto3版本声明,Protobuf编译器默认使用proto2版本。
Proto消息文件的命名如下:
packageName.MessageName.proto
packageName为package声明的包名
MessageName为消息名称

2、添加注释

添加注释可以使用C/C++/java风格的双斜杠(//)语法格式。

3、Package

.proto文件中可以新增一个可选的package声明符,用来防止不同的消息类型有命名冲突。包的声明符会根据使用语言的不同影响生成的代码:
A、对于C++语言,产生的类会被包装在C++的命名空间中。
B、对于Java语言,包声明符会变为java的一个包,除非在.proto文件中提供了一个明确有java_package。
C、对于Go语言,包可以被用做Go包名称,除非显式的提供一个option go_package在.proto文件中。
Protobuf语法中类型名称的解析与C++是一致的:首先从最内部开始查找,依次向外进行,每个包会被看作是其父类包的内部类。当然对于Company.Person以“.”分隔的是从最外围开始的。
Protobuf编译器会解析.proto文件中定义的所有类型名。 对于不同语言的代码生成器会知道如何来指向每个具体的类型,即使它们使用了不同的规则。

4、字段类型

字段类型包括标量类型和合成类型。
标量类型包括:
gRPC快速入门(一)——Protobuf简介
合成类型包括枚举(enumerations)或其它消息类型。

5、标识符

在消息定义中,每个字段都有唯一的一个数字标识符。标识符用来在消息的二进制格式中识别各个字段,一旦使用就不能够再改变。
最小的标识符可以从1开始,最大到2^29 - 1(536,870,911),不可以使用其中[19000-19999]( Protobuf协议实现中进行了预留,从FieldDescriptor::kFirstReservedNumber 到 FieldDescriptor::kLastReservedNumber)的标识号。如果非要在.proto文件中使用预留标识符,编译时就会报警。
[1,15]内的标识号在编码的时候会占用一个字节。[16,2047]之内的标识号则占用2个字节。所以应该为频繁出现的消息元素保留[1,15]内的标识号。

6、字段规则

消息的字段修饰符必须是如下之一:
A、singular:一个格式良好的message应该有0个或者1个该字段(但不能超过1个)。
B、repeated:在一个格式良好的消息中,该字段可以重复任意多次(包括0次),重复值的顺序会被保留。
在proto3中,repeated的标量字段默认情况下使用packed。

7、保留标识符

如果通过删除或者注释所有字段,以后的用户在更新消息类型的时候可能重用标识符。如果使用旧版本代码加载相同的.proto文件会导致严重的问题,包括数据损坏、隐私错误等等。为了确保不会发生向前兼容可以为字段tag(reserved name可能会JSON序列化的问题)指定reserved标识符,Protobuf编译器会警告未来尝试使用相应字段标识符的用户。
不要在同一行reserved声明中同时声明字段名字和标识符。

message Foo {
    reserved 2, 15, 9 to 11;
    reserved "foo", "bar";
}

8、默认值

当一个消息被解析的时候,如果编码消息里不包含一个特定的singular元素,被解析的对象所对应的字段被设置为一个默认值,不同类型默认值如下:
对于string,默认是一个空string
对于bytes,默认是一个空的bytes
对于bool,默认是false
对于数值类型,默认是0
对于枚举,默认是第一个定义的枚举值,必须为0
对于消息类型(message),字段没有被设置,确切的消息是根据语言确定的,通常情况下是对应语言中空列表。
对于标量消息字段,一旦消息被解析,就无法判断字段是被设置为默认值还是根本没有被设置,应该在定义消息类型时注意。

9、枚举

当定义一个消息类型时,需要为消息中的某个字段指定预定义值序列中的一个值,此时可以使用枚举定义预定以序列。如为Person消息添加一个PhoneType类型的字段,PhoneType类型的值可能是MOBILE,HOME,WORK。

 message Person {
  string name = 1;
  int32 id = 2;  // Unique ID number for this person.
  string email = 3;
  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }
  message PhoneNumber {
    string number = 1;
    PhoneType type = 2;
  }

  repeated PhoneNumber phones = 4;

  google.protobuf.Timestamp last_updated = 5;
}

每个枚举类型必须将其第一个类型映射为0。
可以通过allow_alias选项为true,将不同的枚举常量指定为相同的值,否则编译器会在别名的地方产生一个错误信息。

enum EnumAllowingAlias {
    option allow_alias = true;
    UNKNOWN = 0;
    STARTED = 1;
    RUNNING = 1;
}
enum EnumNotAllowingAlias {
    UNKNOWN = 0;   //EnumNotAllowingAlias中没有设置allow_alias
    STARTED = 1;
    // RUNNING = 1;//error 
}

枚举常量必须在32位整型值的范围内。因为enum值是使用可变编码方式的,对负数不够高效,因此不推荐在enum中使用负数。
可以在一个消息定义的内部或外部定义枚举,枚举可以在.proto文件中的任何消息定义里重用。可以在一个消息中声明一个枚举类型,而在另一个不同的消息中使用枚举(采用MessageType.EnumType的语法格式)。
当对一个使用了枚举的.proto文件运行Protobuf编译器的时候,生成的代码中将有一个对应的enum(Java或C++),被用来在运行时生成的类中创建一系列的整型值符号常量(symbolic constants)。
在反序列化的过程中,无法识别的枚举值会被保存在消息中。对支持开放枚举类型超出指定范围外的语言(例如C++和Go),未识别的值会被表示成所支持的整型;对封闭枚举类型的语言中(Java),使用枚举中的一个类型来表示未识别的值,并且可以使用所支持整型来访问;在其它情况下,如果解析的消息被序列号,未识别的值将保持原样。

10、引用其它消息类型

可以将其它消息类型用作字段类型。对于同一个消息文件内部定义的消息,可以在其它消息内部直接引用消息类型;对于在其它消息文件定义的消息类型,可以通过导入其他消息文件中的定义来使用相应的消息类型。如使用google.protobuf.Timestamp消息类型需要导入相应消息文件:
import "google/protobuf/timestamp.proto";
如果要在父消息类型的外部重用消息类型,需要以Parent.Type的形式使用。

11、Any类型

Any类型消息允许在没有指定.proto定义的情况下使用消息作为一个嵌套类型。一个Any类型包括一个可以被序列化bytes类型的任意消息以及一个URL作为一个全局标识符和解析消息类型。
为了使用Any类型,需要导入import google/protobuf/any.proto。

import "google/protobuf/any.proto";
message ErrorStatus {
    string message = 1;
    repeated google.protobuf.Any details = 2;
}

对于给定的消息类型的默认类型URL是type.googleapis.com/packagename.messagename
不同语言的实现会支持动态库以线程安全的方式去帮助封装或者解封装Any值。例如在java中,Any类型会有特殊的pack()和unpack()访问器,在C++中会有PackFrom()和UnpackTo()方法。

12、Oneof

Oneof定义用来代表在实现的时候,该组属性中有且只能有一个被定义,不能出现多个。

message SampleMessage {
  oneof test_oneof {
    string name = 4;
    SubMessage sub_message = 9;
  }
}

上述定义中只能出现name或者sub_message的出现,不能同时出现,同时Oneof不能出现repeated域。重复传递值到Oneof多个域仅仅最后的会生效,其它的将被忽略掉。

13、Map

如果要创建一个关联映射,Protobuf提供了一种快捷的语法:

map<key_type, value_type> map_field = N;

其中key_type可以是任意Integer或者string类型(除了floating和bytes的任意标量类型都可以),value_type可以是任意类型,但不能是map类型。
例如,创建一个Project的映射,每个Projecct使用一个string作为key:

map<string, Project> projects = 3;

Map的字段可以是repeated。
序列化后的顺序和map迭代器的顺序是不确定的,所以不要期望以固定顺序处理Map
当为.proto文件产生生成文本格式的时候,map会按照key 的顺序排序,数值化的key会按照数值排序。
从序列化中解析或者融合时,如果有重复的key则后一个key不会被使用,当从文本格式中解析map时,如果存在重复的key。
向后兼容性问题
map语法序列化后等同于如下内容,因此即使是不支持map语法的Protobuf实现也可以处理数据:

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

14、定义服务

如果想要将消息类型用在RPC(远程方法调用)系统中,可以在.proto文件中定义一个RPC服务接口,Protobuf编译器将会根据所选择的不同语言生成服务接口代码及stub。如要定义一个RPC服务并具有一个方法Search,Search方法能够接收SearchRequest并返回一个SearchResponse,可以在.proto文件中进行如下定义:

service SearchService {
    rpc Search (SearchRequest) returns (SearchResponse);
}

最直观的使用Protobuf的RPC系统是gRPC,由谷歌开发的语言和平台中的开源的PRC系统,gRPC在使用Protobuf时非常有效,如果使用特殊的Protobuf插件可以直接从.proto文件中产生相关的RPC代码。
如果不想使用gRPC,可以使用Protobuf用于自己的RPC实现。

15、JSON映射

Proto3支持JSON的编码规范,便于在不同系统之间共享数据。
如果JSON编码的数据丢失或者其本身是null,数据会在解析成Protobuf的时候被表示成默认值。如果一个字段在Protobuf中表示为默认值,会在转化成JSON编码的时候忽略掉以节省空间。
gRPC快速入门(一)——Protobuf简介

16、更新消息类型

如果一个已有的消息格式已无法满足新的需求,需要在要息中添加一个额外的字段,但同时旧版本写的代码仍然可用。可以使用更新消息解决,更新消息而不破坏已有代码是非常简单的。更新消息时规则如下:
A、不要更改任何已有字段的标识符。
B、如果增加新的字段,使用旧格式的字段仍然可以被新产生的代码所解析。应该记住元素的默认值,新代码就可以以适当的方式和旧代码产生的数据交互。通过新代码产生的消息也可以被旧代码解析,但新增加的字段会被忽视掉。未被识别的字段会在反序列化的过程中丢弃掉,如果消息再被传递给新的代码,新的字段依然是不可用的。
C、非required的字段可以移除。只要标识符在新的消息类型中不再使用(推荐重命名字段,例如在字段前添加“OBSOLETE_”前缀)。
D、int32, uint32, int64, uint64,和bool是全部兼容的,可以相互转换,而不会破坏向前、 向后的兼容性。
E、sint32和sint64是互相兼容的,但与其它整数类型不兼容。
F、string和bytes是兼容的——只要bytes是有效的UTF-8编码。
G、嵌套消息与bytes是兼容的——只要bytes包含该消息的一个编码过的版本。
H、fixed32与sfixed32是兼容的,fixed64与sfixed64是兼容的。
I、枚举类型与int32,uint32,int64和uint64相兼容(注意如果值不相兼容则会被截断),然而在客户端反序列化后可能会有不同的处理方式,例如,未识别的proto3枚举类型会被保留在消息中,但表示方式会依照语言而定。int类型的字段总会保留他们的
J、可以添加新的optional或repeated的字段, 但必须使用新的标识符(消息中从未使用过的标识符,不能使用已经被删除过的标识符)。

17、选项

在定义.proto文件时能够标注一系列的options。options并不改变整个文件声明的含义,但却能够影响特定环境下处理方式。完整的可用选项可以在google/protobuf/descriptor.proto找到。
一些选项是文件级别的,意味着它可以作用于最外范围,不包含在任何消息内部、enum或服务定义中。一些选项是消息级别的,意味着它可以用在消息定义的内部。当然有些选项可以作用在域、enum类型、enum值、服务类型及服务方法中。到目前为止,并没有一种有效的选项能作用于所有的类型
optimize_for(文件选项): 可以被设置为LITE_RUNTIME,SPEED,CODE_SIZE。这些值将通过如下的方式影响C++及Java代码的生成: 
SPEED (default): Protobuf编译器将通过在消息类型上执行序列化、语法分析及其它通用的操作,生成的代码最优。
CODE_SIZE:Protobuf编译器将会产生最少量的类,通过共享或基于反射的代码来实现序列化、语法分析及各种其它操作。采用CODE_SIZE方式产生的代码将比SPEED要少得多,但操作要相对慢些。CODE_SIZE方式生成代码中实现的类及其对外的API与SPEED模式都是一样的,常用在一些包含大量的.proto文件而且并不盲目追求速度的应用中。
LITE_RUNTIME:Protobuf编译器依赖于运行时核心类库来生成代码(即采用libprotobuf-lite替代libprotobuf)。libprotobuf-lite核心类库由于忽略了一些描述符及反射,要比全类库小得多。这种模式经常在移动手机平台应用多一些。编译器采用LITE_RUNTIME模式产生的方法实现与SPEED模式不相上下,产生的类通过实现MessageLite接口,但仅仅是Messager接口的一个子集。
option optimize_for = CODE_SIZE;
cc_enable_arenas(文件选项):对于C++产生的代码启用arena allocation。
objc_class_prefix(文件选项):设置Objective-C类的前缀,添加到所有Objective-C从此.proto文件产生的类和枚举类型。没有默认值,所使用的前缀应该是×××荐的3-5个大写字符,注意2个字节的前缀是苹果所保留的。
deprecated(字段选项):如果设置为true则表示该字段已经被废弃,并且不应该在新的代码中使用。在大多数语言中没有实际的意义。
int32 old_field = 6 [deprecated=true];
java_package (file option):指定生成java类所在的包,如果在.proto文件中没有明确的声明java_package,会使用默认包名。不需要生成java代码时不起作用。
java_outer_classname (file option):指定生成Java类的名称,如果在.proto文件中没有明确声明java_outer_classname,生成的class名称将会根据.proto文件的名称采用驼峰式的命名方式进行生成。如(foo_bar.proto生成的java类名为FooBar.java),不需要生成java代码时不起任何作用
objc_class_prefix (file option): 指定Objective-C类前缀,会前置在所有类和枚举类型名之前。没有默认值,应该使用3-5个大写字母。注意所有2个字母的前缀是Apple保留的。

四、proto文件编码规范

Proto文件编码规范如下:
A、描述文件以.proto做为文件后缀。
B、除结构定义外的语句以分号结尾,结构定义包括:message、service、enum;rpc方法定义结尾的分号可有可无。
C、Message命名采用驼峰命名方式,字段命名采用小写字母加下划线分隔方式。
D、Enums类型名采用驼峰命名方式,字段命名采用大写字母加下划线分隔方式。
E、Service与rpc方法名统一采用驼峰式命名。