mac安装,下载安装包https://github.com/google/protobuf/releases
1. 下载protoc-3.10.0-osx-x86_64.zip 2. 解压 3. 配置环境变量 export PROTOBUF=/Volumes/P/develope/protoc-3.10.0-osx-x86_64/ export PATH=$PATH:$PROTOBUF/bin source ~/.zshrc生成前的目录结构
├── build.gradle └── src ├── main │ ├── java │ ├── proto │ │ └── UserProtobuf.proto │ └── resources │ └── logback.xml └── test ├── java └── resourcesProtobuf 编译器通过描述文件(.proto文件)生成对应于语言的代码,代码中定义了消息类型、获取、设置、编解码序列化等操作。这也是为什么 Protobuf 支持跨语言传输,因为消息所有端共用一个通用的描述文件。UserProtobuf.proto内容如下:
//proto3语法注解:默认是proto2,这必须是文件的第一个非空的非注释行。 syntax = "proto3"; //生成的包名 option java_package = "cn.jannal.protobuf"; //生成的java类名 option java_outer_classname = "User"; message UserProto{ //ID int32 id = 1; //姓名 string name = 2; //年龄 int32 age = 3; //状态 int32 state = 4; }第一种方式:直接通过命令行生成java,使用 protoc 工具可以把编写好的 proto 文件生成不同的语言代码。对Java来说,编译器为每一个消息类型生成了一个.java文件,以及一个特殊的Builder类(该类是用来创建消息类接口的)。对javaNano来说,JavaNano是专门为资源受限系统(如Android)设计的特殊代码生成器和运行时库, 代码量和运行时开销都非常资源友好
-I 后面是 proto 文件所在目录 --java_out 后面是 java 文件存放地址 最后一行是 proto 文件名称 protoc -I=src/main/proto --java_out=src/main/java UserProtobuf.proto第二种方式:通过gradle插件生成,插件地址https://github.com/google/protobuf-gradle-plugin。可以在build之前根据proto文件自动生成java文件
buildscript { repositories { mavenLocal() } dependencies { classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.10' } } subprojects { apply plugin: 'java' apply plugin: 'com.google.protobuf' sourceCompatibility = 1.8 targetCompatibility = 1.8 idea { module { downloadJavadoc = true downloadSources = true } } dependencies { compile group: 'com.google.protobuf', name: 'protobuf-java', version: '3.10.0' } //==============protobuf配置================ sourceSets { main { proto { //默认是 'src/main/proto' srcDir 'src/main/proto' } } test { proto { // 默认 'src/test/proto' srcDir 'src/test/proto' } } } protobuf { protoc { //从仓库下载 artifact = 'com.google.protobuf:protoc:3.10.0' //生成代码的目录 generatedFilesBaseDir = "$projectDir/src" } } //在build之前执行proto代码生成 build.dependsOn(":$project.name:generateProto") //==============protobuf配置================ }生成后的目录结构
. ├── build.gradle └── src ├── main │ ├── java │ │ └── cn │ │ └── jannal │ │ └── protobuf │ │ └── User.java │ ├── proto │ │ └── UserProtobuf.proto │ └── resources │ └── logback.xml └── test ├── java │ └── cn │ └── jannal │ └── protobuf │ └── UserTest.java └── resources测试程序
public class UserTest { private static final Logger logger = LoggerFactory.getLogger(UserTest.class); public static void main(String[] args) throws IOException { User.UserProto.Builder user = User.UserProto.newBuilder(); user.setAge(12); user.setId(1000); user.setName("jannal"); User.UserProto userInfo = user.build(); // 将数据写到输出流 ByteArrayOutputStream output = new ByteArrayOutputStream(); userInfo.writeTo(output); // 将数据序列化后发送 byte[] byteArray = output.toByteArray(); // 接收到流并读取 ByteArrayInputStream input = new ByteArrayInputStream(byteArray); // 反序列化 userInfo = User.UserProto.parseFrom(input); /** * id: 1000 * name: "jannal" * age: 12 */ logger.info("{}", userInfo.toString()); } }一个proto文件可以定义一个或者多个message
// 指定使用proto3,如果不指定的话,编译器会使用proto2去编译 syntax = "proto3"; //[proto2|proto3] message SearchRequests { // 定义SearchRequests的成员变量,需要指定:[变量类型]、[变量名]、[变量Tag] string query = 1; int32 page_number = 2; int32 result_per_page = 3; } message SearchResponse { repeated string result = 1; }嵌套定义
message SearchResponse { message Result { string url = 1; string title = 2; repeated string snippets = 3; } repeated Result results = 1; } message SomeOtherMessage { //定义在message内部的message可以这样使用 SearchResponse.Result result = 1; }注释
使用//表示注释消息由至少一个字段组合而成,类似于C语言中的结构。每个字段都有一定的格式
限定修饰符① | 数据类型② | 字段名称③ | = | 字段编码值④ | [字段默认值⑤]限定修饰符① 包含 required\optional\repeated
Required: 表示是一个必须字段,必须相对于发送方,在发送消息之前必须设置该字段的值,对于接收方,必须能够识别该字段的意思。发送之前没有设置required字段或者无法识别required字段都会引发编解码异常,导致消息被丢弃。Optional:表示是一个可选字段,可选对于发送方,在发送消息时,可以有选择性的设置或者不设置该字段的值。对于接收方,如果能够识别可选字段就进行相应的处理,如果无法识别,则忽略该字段,消息中的其它字段正常处理。因为optional字段的特性,很多接口在升级版本中都把后来添加的字段都统一的设置为optional字段,这样老的版本无需升级程序也可以正常的与新的软件进行通信,只不过新的字段无法识别而已,因为并不是每个节点都需要新的功能,因此可以做到按需升级和平滑过渡。Repeated: 表示该字段可以包含0~N个元素。其特性和optional一样,但是每一次可以包含多个值。可以看作是在传递一个数组的值。变量类型
Proto描述Javadoubledoublefloatfloatint32使用变长编码,对负数编码效率低,如果你的变量可能是负数,可以使用sint32intint64使用变长编码,对负数编码效率低,如果你的变量可能是负数,可以使用sint64longuint32使用变长编码intuint64使用变长编码longsint32使用变长编码,带符号的int类型,对负数编码比int32高效intsint64使用变长编码,带符号的int类型,对负数编码比int64高效longfixed324字节编码, 如果变量经常大于2^28 的话,会比uint32高效intfixed648字节编码, 如果变量经常大于2^56 的话,会比uint64高效longsfixed324字节编码intsfixed648字节编码longboolboolstring必须包含utf-8编码或者7-bit ASCII textStringbytes任意的字节序列StringAny可以让你在 proto 文件中使用未定义的类型,具体里面保存什么数据,是在上层业务代码使用的时候决定的,使用 Any 必须导入 import google/protobuf/any.proto
import "google/protobuf/any.proto"; message ErrorStatus { string message = 1; repeated google.protobuf.Any details = 2; }如果你的消息中有很多可选字段,而同一个时刻最多仅有其中的一个字段被设置的话,你可以使用oneof来强化这个特性并且节约存储空间(设置一个oneof字段会自动清理其他的oneof字段)。
反射API对oneof 字段有效.oneof不支持repeated. name 和 age 都是 LoginReply 的成员,但不能给他们同时设置值 message LoginReply { oneof test_oneof { string name = 3; string age = 4; } required string status = 1; required string token = 2; }Map类型:protobuf 支持定义 map 类型的成员
1. key_type:必须是string或者int 2. value_type:任意类型 map<key_type, value_type> map_field = N; // 举例:map<string, Project> projects = 3;使用 map 要注意:
Map 类型不能使 repeated
Map 是无序的
以文本格式展示时,Map 以 key 来排序
如果有相同的键会导致解析失败
可以用 reserved 关键字,当一个变量不再使用的时候,我们可以把它的变量名或 Tag 用 reserved 标注,这样,当这个 Tag 或者变量名字被重新使用的时候,编译器会报错
message Foo { // 注意,同一个 reserved 语句不能同时包含变量名和 Tag reserved 2, 15, 9 to 11; reserved "foo", "bar"; }定义枚举并使用
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; }枚举定义在一个消息内部或消息外部都是可以的,如果枚举是定义在 message 内部,而其他 message 又想使用,那么可以通过MessageType.EnumType的方式引用。定义枚举的时候,要保证第一个枚举值必须是0,枚举值不能重复,除非使用 option allow_alias = true 选项来开启别名
enum EnumAllowingAlias { option allow_alias = true; UNKNOWN = 0; STARTED = 1; RUNNING = 1; }枚举数常量必须在32位整数的范围内。由于enum值在传输中使用不同的编码,负值效率低下,因此不推荐使用
import
A ->import B B -> import C A不能使用C中的内容,A可以使用B的内容,B可以使用C的内容import public
A ->import B B -> import C A可以使用C中的内容,A可以使用B的内容,B可以使用C的内容为了防止不同消息之间的命名冲突,你可以对特定的.proto文件指定 package 名字。在定义消息的成员的时候,可以指定包的名字:
package foo.bar; message Open { ... } message Foo { ... // 带上包名 foo.bar.Open open = 1; ... }Proto3 支持JSON的编码规范,可实现protobuf与json互相转换。
如果JSON编码的数据丢失或者其本身就是null,这个数据会在解析成protocol buffer的时候被表示成默认值。如果一个字段在协议缓冲区中具有默认值,默认情况下它将在 JSON 编码数据中省略以节省空间映射表
messageobject{“fooBar”: v, “g”: null, …}生成JSON对象。消息字段名映射到lowerCamelCase,成为JSON对象键。如果指定了JSON_name字段选项,则指定的值将被用作密钥。解析器接受lowerCamelCase名称(或JSON_name选项指定的名称)和原始的原域名称。null是所有字段类型的接受值,并被视为相应字段类型的默认值。enumstring“FOO_BAR”使用proto中指定的枚举值的名称。map<K,V>object{“k”: v, …}所有键都转换为字符串。repeated Varray[v, …]null被接受为空列表[]。booltrue, falsetrue, falsestringstring“Hello World!”bytesbase64 string“YWJjMTIzIT8kKiYoKSctPUB+”JSON值使用标准base64编码和paddings编码作为字符串编码的数据。int32, fixed32, uint32number1, -10, 0JSON值将是十进制数。接受数字或字符串。int64, fixed64, uint64string“1”, “-10”JSON值将是十进制数。接受数字或字符串。float, doublenumber1.1, -10.0, 0, “NaN”, “Infinity”JSON值将是一个数字或特殊字符串值"NaN"、“Infinity"和”-Infinity"之一。接受数字或字符串。指数记数法也被接受。Anyobject{"@type": “url”, “f”: v, … }如果Any包含具有特殊JSON映射的值,它将被转换如下: {"@ type": XXX,“value”: yyy }。否则,该值将被转换成JSON对象,并且“@ type”字段将被插入以指示实际的数据类型。Timestampstring“1972-01-01T10:00:20.021Z”使用RFC 3339,其中生成的输出总是Z归一化的,并使用0、3、6或9个小数位数。除“Z”之外的偏移也是可以接受的。Durationstring“1.000340012s”, “1s”根据所需精度,生成的输出总是包含0、3、6或9个小数位数,后跟后缀"s"。接受任何小数位数(也没有),只要它们符合毫微秒精度,并且后缀"s"是必需的。Structobject{ … }任何JSON对象都可以。Wrapper typesvarious types2, “2”, “foo”, true, “true”, null, 0, …包装器在JSON中使用与包装基元类型相同的表示,只是在数据转换和传输期间允许并保留null。FieldMaskstring“f.fooBar,h”请参见字段mask.protoListValuearray[foo, bar, …]ValuevalueNullValuenullJSON nullproto3 的 JSON 实现中提供了以下 4 中 options:
使用默认值发送字段:在默认情况下,默认值的字段在Proto3 JSON 输出中被忽略。一个实现可以提供一个选项来覆盖这个行为,并使用它们的默认值输出字段。忽略未知字段:默认情况下,Proto3 JSON 解析器应拒绝未知字段,但可能提供一个选项来忽略解析中的未知字段。使用 proto 字段名称而不是 lowerCamelCase 名称:默认情况下,proto3 JSON 的 printer 将字段名称转换为 lowerCamelCase 并将其用作 JSON 名称。实现可能会提供一个选项,将原始字段名称用作 JSON 名称。 Proto3 JSON 解析器需要接受转换后的 lowerCamelCase 名称和原始字段名称。发送枚举形式的枚举值而不是字符串:在 JSON 输出中默认使用枚举值的名称。可以提供一个选项来使用枚举值的数值。protocol buffers 替换 JSON,可能是考虑到:
protocol buffers 相同数据,传输的数据量比 JSON 小,gzip 或者 7zip 压缩以后,网络传输消耗较少protocol buffers 不是自我描述的,在缺少 .proto 文件以后,有一定的加密性,数据传输过程中都是二进制流,并不是明文。protocol buffers 提供了一套工具,自动化生成代码也非常方便protocol buffers 具有向后兼容性,改变了数据结构以后,对老的版本没有影响protocol buffers 原生完美兼容 RPC 调用如果很少用到整型数字,浮点型数字,全部都是字符串数据,那么 JSON 和 protocol buffers 性能不会差太多
案例
1. 加入依赖 compile group: 'com.google.protobuf', name: 'protobuf-java-util', version: '3.10.0' 2. json测试 public class UserTest { private static final Logger logger = LoggerFactory.getLogger(UserTest.class); public static void main(String[] args) throws IOException { User.UserProto.Builder user = User.UserProto.newBuilder(); user.setAge(12); user.setId(1000); user.setName("jannal"); User.UserProto userInfo = user.build(); // 将数据写到输出流 ByteArrayOutputStream output = new ByteArrayOutputStream(); userInfo.writeTo(output); // 将数据序列化后发送 byte[] byteArray = output.toByteArray(); // 接收到流并读取 ByteArrayInputStream input = new ByteArrayInputStream(byteArray); // 反序列化 userInfo = User.UserProto.parseFrom(input); /** * id: 1000 * name: "jannal" * age: 12 */ logger.info("{}", userInfo.toString()); //如果不使用protobuf提供的JSON API,而使用fastJson等,直接序列化Msg对象,会报错。 // 如果希望使用第三方的JSON API,可以重新定义一个实体类,抽取需要的字段 //获取Printer对象用于生成JSON字符串 JsonFormat.Printer printer = JsonFormat.printer(); //获取parser对象用于解析JSON字符串 JsonFormat.Parser parser = JsonFormat.parser(); try { //生成JSON字符串 String jsonStr = printer.print(userInfo); System.out.println(jsonStr); //解析JSON字符串 //解析方法接收一个JSON字符串,并把其写入指定的builder User.UserProto.Builder builder = User.UserProto.newBuilder(); parser.merge(jsonStr, builder); User.UserProto userProto = builder.build(); System.out.println(userProto); } catch (InvalidProtocolBufferException e) { e.printStackTrace(); } } }升级更改 proto 需要遵循以下原则
不要修改任何已存在的变量的 Tag
如果新增了变量,新生成的代码依然能解析旧的数据,但新增的变量将会变成默认值。相应的,新代码序列化的数据也能被旧的代码解析,但旧代码会自动忽略新增的变量。
废弃不用的变量用reserved标注
int32、 uint32、 int64、 uint64 和 bool 是相互兼容的,这意味你可以更改这些变量的类型而不会影响兼容性
sint32 和 sint64是兼容的,但跟其他类型不兼容
string 和 bytes 可以兼容,前提是他们都是UTF-8编码的数据
fixed32 和 sfixed32是兼容的
fixed64 和 sfixed64是兼容的
如果要使用 RPC(远程过程调用)系统的消息类型,可以在 .proto 文件中定义 RPC 服务接口,protocol buffer 编译器将使用所选语言生成服务接口代码和 stubs。所以,例如,如果你定义一个 RPC 服务,入参是 SearchRequest 返回值是 SearchResponse,你可以在你的 .proto 文件中定义它,如下所示:
service SearchService { rpc Search (SearchRequest) returns (SearchResponse); }与 protocol buffer 一起使用的最直接的 RPC 系统是gRPC:在谷歌开发的语言和平台中立的开源 RPC 系统。gRPC 在 protocol buffer 中工作得非常好,并且允许你通过使用特殊的 protocol buffer 编译插件,直接从 .proto 文件中生成 RPC 相关的代码。
message 采用驼峰命名法。message 首字母大写开头。字段名采用下划线分隔法命名。
message SongServerRequest { required string song_name = 1; }枚举类型采用驼峰命名法。枚举类型首字母大写开头。每个枚举值全部大写,并且采用下划线分隔法命名。
enum Foo { FIRST_VALUE = 0; SECOND_VALUE = 1; } 每个枚举值用分号结束,不是逗号。服务名和方法名都采用驼峰命名法。并且首字母都大写开头。
service FooService { rpc GetSomething(FooRequest) returns (FooResponse); }默认一个proto文件生成一个java文件,这样如果proto比较长,java文件会比较大。在proto中添加option java_multiple_files = true的配置可以生成多个java文件
syntax = "proto3"; option java_package = "cn.jannal.protobuf"; //生成的java名 option java_outer_classname = "PersonProto"; //生成多个java文件 option java_multiple_files = true; message Person{ string username = 1; string password = 2; } message Student{ string username = 1; string password = 2; }