1. 入门指导
1. 基本定义
Protocol Buffers提供一种跨语言的结构化数据的序列化能力,类似于JSON,不过更小、更快,除此以外它还能用用接口定义(IDL interface define language),通protoc编译Protocol Buffer定义文件,生成结构化类,以及服务调用的客户端和服务端。
1. person.proto
我们来看一个极简的例子,好让自己有一个直观的感受,假设我们有一个person.proto
文件,内容如下:
syntax = "proto3";option java_multiple_files = true;
option java_package = "org.keyniu.grpc.proto";message Person {optional string name = 1;optional int32 id = 2;optional string email = 3;
}
2. Person.java
通过protoc生成的Person.java类大概是这样的
// Generated by the protocol buffer compiler. DO NOT EDIT!
// source: person.proto// Protobuf Java Version: 3.25.1
package org.keyniu.grpc.proto;/*** Protobuf type {@code Person}*/
public final class Person extendscom.google.protobuf.GeneratedMessageV3 implements// @@protoc_insertion_point(message_implements:Person)PersonOrBuilder {
private static final long serialVersionUID = 0L;// Use Person.newBuilder() to construct.private Person(com.google.protobuf.GeneratedMessageV3.Builder<?> builder) {super(builder);}private Person() {name_ = "";email_ = "";}
... // 后续的省略
3. 核心用例
我们看一下Person类的核心用法
// 创建对象
Person p = Person.newBuilder().setName("randy").setId(1).setEmail("randy@gmail.com").build();
// 序列化
byte[] serialized = p.toByteArray();
// 反序列化
Person p2 = Person.parseFrom(serialized);
System.out.println(p2);
2. 适用场景
Protocol Buffers提供了结构化数据的序列化/反序列化能力,对领域对象的修改能够兼容历史版本,官方推荐适用于小规模数据(MB级),包括网络传输、数据存储。
不适用的场景包括
- 不支持流式解析,待解析的数据要一次性加载进byte数组,然后解析,不能读取部分内容就交由Protocol Buffers解析
- 不支持二进制比较,不同语言/平台的序列化后的二进制可能是不同的,要反序列化后才能比较两个对象是否相同
- 不支持非面向对象的语言
2. 数据类型
我们先来回顾一下person.proto的定义,这个定义的核心是Person前面的message
message Person {optional string name = 1; // label(optional)、字段类型(string)、字段名(name)、字段Id(1)optional int32 id = 2;optional string email = 3;
}
1. label
label | 说明 | 举例 |
---|---|---|
optional | 字段是否可选,允许不设置值,proto3中字段默认optional,对应proto2中的required | optional string name = 1; |
repeated | 可以有0或多个值,保留写入顺序 | repeated string name = 1; |
map | 对应Java里的Map | map<int32, string> idToName = 2; |
空 | 字段是否存在,被称为implicit field presence,如果字段未设置值,序列化 | |
oneof | 一组关联字段,只保留一个值,设置两个字段时,会把第一个清空 |
来看一个oneof的实例,一个Product对象,它可以参加一种促销(抵用券或打折),但不能同时参加,可以这样定义product.proto
syntax = "proto3";
option java_multiple_files = true;
option java_package = "org.keyniu.grpc.proto";message Product {optional string name = 1;oneof promotion {string coupon = 2;string discount = 3;}
}
我们来看看生成的Product类,Product类自动生成了一个Product.PromotionCase类,我们可以通过它判断当前Product参加那类促销
Product prod = Product.newBuilder().setName("Mate60Pro").setCoupon("满10减3").build();
System.out.print(prod.toString());
switch (prod.getPromotionCase()) {case DISCOUNT:System.out.println(prod.getDiscount());break;case COUPON:System.out.println(prod.getCoupon());break;
}
输出如下
如果我们给Product同时设置Coupon和Discount,代码如下:
prod = Product.newBuilder().setName("Mate60Pro").setCoupon("满100减1").setDiscount("7折").build();
System.out.println(prod.toString());
输出如下
2. 字段类型
类型分为内置基本类型和自己通过message(enum)定义的类型,我们先来看看基本类型。 proto3的内置基本类型,包括整数、浮点数、布尔型、字符串以及字节数组
1. 基本类型
Proto类型 | 对应Java类型 | 说明 | 默认值 |
---|---|---|---|
double | double | 0 | |
float | float | 0 | |
int32 | int | 变长编码,对负数的编码效率较低,如果有负数建议使用sint32 | 0 |
int64 | long | 变长编码,对负数的编码效率较低,如果有负数建议使用sint64 | 0 |
uint32 | int | 变长编码,相当于unsigned int32 | 0 |
uint64 | long | 变长编码,相当于unsigned int64 | 0 |
sint32 | int | 变长编码,对负数的编码效率较高 | 0 |
sint64 | long | 变长编码,对负数的编码效率较高 | 0 |
fixed32 | int | 定长编码,总是使用4 Byte,占空间多,但编码效率高,相当于unsigned int32 | 0 |
sfixed32 | int | 定长编码,总是使用4 Byte,相当于signed int32 | 0 |
fixed64 | long | 定长编码,总是使用8 Byte,占空间多,但编码效率高,相当于unsigned int64 | 0 |
sfixed64 | long | 定长编码,总是使用8 Byte,相当于signed int64 | 0 |
bool | boolean | 布尔值 | false |
string | String | 字符串 | 空字符串 |
bytes | ByteString | 字节序列,适用于存储任何数据,比如图片的byte数字 | 空字节数组 |
2. 自定义类型
除此以外,proto允许用户自己通过message、enum定义自己的类型,比如之前提到的Person,我们再看一下示例
message Person {optional string name = 1;optional int32 id = 2;optional string email = 3;
}
3. 自定义枚举
enum关键字和Java的枚举基本一致,假设我们要定义一个性别(Gender)的枚举,可以用下面的语句定义
enum Gender {MALE = 0;FEMALE = 1;
}
要特别注意的是枚举字段的定义后面的字段id要从0开始。此外enum还有一个Java没有有的特性,枚举类型可以指定别名,比如将MAN作为MALE的别名可以这么写
enum Gender {option allow_alias = true;MALE = 0;MAN = 0;FEMALE = 1;WOMEN = 1;
}
4. 跨文件引用
如果通过message、enum定义的类型都在同一个文件中,可以直接相互引用,如果是在两个proto文件中,需要手动import,比如这样
import "myproject/gender.proto";
3. 字段ID
官方叫做Assigned Field Number,在person.proto中name字段的id就是1,id在同一个类型内部必须唯一,取值范围[1,5亿]
,一般从1开始递增,当然数字越大,消耗的储空间越大(类似UTF-8编码)。19000~19999是预留给内部使用的。
optional string name = 1;
所以这个字段标识不能修改,也不能重复,修改导致之前序列化的数据无法解析,重复导致字段混乱。
4. 预留字段
告诉Protocol Buffer预留字段Id ,2、9、10、11、15,预留字段名称foo、bar
message Foo {reserved 2, 15, 9 to 11;reserved "foo", "bar";
}
5. 对象引用
某些场景下我们可能不确定持有的数据类型,比如Object,proto也提供了这样的支持
import "google/protobuf/any.proto";message Handler {string message = 1;repeated google.protobuf.Any target = 2;
}
通过生成对象的pack、unpack方法来访问target持有的引用
class Any {// Packs the given message into an Any using the default type URL// prefix “type.googleapis.com”.public static Any pack(Message message);// Packs the given message into an Any using the given type URLpublic static Any pack(Message message, String typeUrlPrefix);// Checks whether this Any message’s payload is the given type.public <T extends Message> boolean is(class<T> clazz);// Unpacks Any into the given message type. Throws exception if// the type doesn’t match or parsing the payload has failed.public <T extends Message> T unpack(class<T> clazz) throws InvalidProtocolBufferException;
}
3. 服务定义
proto3支持4中类型的服务定义,通过service关键字类定义服务的接口,比如下面示例中的Greeter服务,定义了4个方法,分别对应4种类型的调用
syntax = "proto3";option java_multiple_files = true;
option java_package = "org.keyniu.grpc.generate";service Greeter {rpc sayHello (HelloRequest) returns (HelloReply) {}rpc sayHelloClientStream (stream HelloRequest) returns (HelloReply) {}rpc sayHelloServerStream (HelloRequest) returns (stream HelloReply) {}rpc sayHelloBiStream (stream HelloRequest) returns (stream HelloReply) {}
}
1. 基本调用
基本调用,处理入参HelloRequest,生成响应HelloReply,在Java中怎么实现可以参考[[Helloworld#2. 实现Server]]。
rpc sayHello (HelloRequest) returns (HelloReply) {}
2. Client端Streaming
Server端Streaming,指客户端可能提交多个参数,最后响应一个结果
rpc sayHelloClientStream (stream HelloRequest) returns (HelloReply) {}
3. Server端Streaming
Server端Streaming,指客户端提供一个参数,服务端可能会有多个响应
rpc sayHelloServerStream (HelloRequest) returns (stream HelloReply) {}
4. 双向Streaming
双向Streaming,指客户端可以提交多个参数,服务端也可以有多个响应
rpc sayHelloBiStream (stream HelloRequest) returns (stream HelloReply) {}
4. JSON互操作
我们可能需要让ProtoBuff和JSON交互,ProtoBuff也为我们考虑到了这个问题,在Java中,可以使用protobuf-java-util实现这个能力
<dependency><groupId>com.google.protobuf</groupId><artifactId>protobuf-java-util</artifactId><version>3.x.x</version> <!-- 使用你的protobuf版本 -->
</dependency>
1. 转JSON
import com.google.protobuf.util.JsonFormat;
import your.package.YourProtoMessage; // 替换为你的protobuf消息类型public class Main {public static void main(String[] args) throws Exception {YourProtoMessage message = YourProtoMessage.newBuilder() // 构建你的protobuf消息.setField1("value1") // 设置字段.setField2(123) // 设置字段.build();JsonFormat.Printer printer = JsonFormat.printer();String jsonString = printer.print(message);System.out.println(jsonString);}
}
2. 解析JSON
import com.google.protobuf.util.JsonFormat;
import your.package.YourProtoMessage; // 替换为你的protobuf消息类型public class Main {public static void main(String[] args) throws Exception {String jsonString = "{\"field1\":\"value1\",\"field2\":123}";JsonFormat.Parser parser = JsonFormat.parser();YourProtoMessage message = parser.merge(jsonString, YourProtoMessage.newBuilder()).build();System.out.println(message.getField1()); // 输出: value1System.out.println(message.getField2()); // 输出: 123}
}
5. 配置选项
选项 | 示例 | 说明 |
---|---|---|
option java_package | option java_package = “com.example.foo” | 生成Java类的包名 |
java_outer_classname | option java_outer_classname = “Person”; | 生成Java类外部包装类名称 |
java_multiple_files | option java_multiple_files = true; | 一个proto文件message、service生成多.java文件 |
optimize_for | option optimize_for = CODE_SIZE; | 可选值SPEED、CODE_SIZE、LITE_RUNTIME SPEED: 追求执行速度,生成序列化/反序列化等代码 CODE_SIZE: 追求代码体积,通过反射实现序列化 LITE_RUNTIME: 类似SPEED,但省略descriptor和reflection代码,依赖libprotobuf-lite |
A. 参考资料
- https://protobuf.dev/programming-guides/proto3/