GRPC使用之ProtoBuf

1. 入门指导

1. 基本定义

Protocol Buffers提供一种跨语言的结构化数据的序列化能力,类似于JSON,不过更小、更快,除此以外它还能用用接口定义(IDL interface define language),通protoc编译Protocol Buffer定义文件,生成结构化类,以及服务调用的客户端和服务端。

1. person.proto

我们来看一个极简的例子,好让自己有一个直观的感受,假设我们有一个person.proto文件,内容如下:

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类大概是这样的

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 extends
    com.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类的核心用法

java 复制代码
// 创建对象
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级),包括网络传输、数据存储。

不适用的场景包括

  1. 不支持流式解析,待解析的数据要一次性加载进byte数组,然后解析,不能读取部分内容就交由Protocol Buffers解析
  2. 不支持二进制比较,不同语言/平台的序列化后的二进制可能是不同的,要反序列化后才能比较两个对象是否相同
  3. 不支持非面向对象的语言

2. 数据类型

我们先来回顾一下person.proto的定义,这个定义的核心是Person前面的message

proto 复制代码
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

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参加那类促销

Java 复制代码
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,代码如下:

java 复制代码
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,我们再看一下示例

proto 复制代码
message Person {
  optional string name = 1;
  optional int32 id = 2;
  optional string email = 3;
}
3. 自定义枚举

enum关键字和Java的枚举基本一致,假设我们要定义一个性别(Gender)的枚举,可以用下面的语句定义

proto 复制代码
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,比如这样

proto 复制代码
import "myproject/gender.proto";
3. 字段ID

官方叫做Assigned Field Number,在person.proto中name字段的id就是1,id在同一个类型内部必须唯一,取值范围[1,5亿],一般从1开始递增,当然数字越大,消耗的储空间越大(类似UTF-8编码)。19000~19999是预留给内部使用的。

proto 复制代码
optional string name = 1;

所以这个字段标识不能修改,也不能重复,修改导致之前序列化的数据无法解析,重复导致字段混乱。

4. 预留字段

告诉Protocol Buffer预留字段Id ,2、9、10、11、15,预留字段名称foo、bar

proto 复制代码
message Foo {
  reserved 2, 15, 9 to 11;
  reserved "foo", "bar";
}
5. 对象引用

某些场景下我们可能不确定持有的数据类型,比如Object,proto也提供了这样的支持

proto 复制代码
import "google/protobuf/any.proto";

message Handler {
  string message = 1;
  repeated google.protobuf.Any target = 2;
}

通过生成对象的pack、unpack方法来访问target持有的引用

java 复制代码
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 URL
  public 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种类型的调用

proto 复制代码
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]]。

java 复制代码
rpc sayHello (HelloRequest) returns (HelloReply) {}
2. Client端Streaming

Server端Streaming,指客户端可能提交多个参数,最后响应一个结果

java 复制代码
rpc sayHelloClientStream (stream HelloRequest) returns (HelloReply) {}
3. Server端Streaming

Server端Streaming,指客户端提供一个参数,服务端可能会有多个响应

java 复制代码
rpc sayHelloServerStream (HelloRequest) returns (stream HelloReply) {}
4. 双向Streaming

双向Streaming,指客户端可以提交多个参数,服务端也可以有多个响应

java 复制代码
rpc sayHelloBiStream (stream HelloRequest) returns (stream HelloReply) {}

4. JSON互操作

我们可能需要让ProtoBuff和JSON交互,ProtoBuff也为我们考虑到了这个问题,在Java中,可以使用protobuf-java-util实现这个能力

xml 复制代码
<dependency>
    <groupId>com.google.protobuf</groupId>
    <artifactId>protobuf-java-util</artifactId>
    <version>3.x.x</version> <!-- 使用你的protobuf版本 -->
</dependency>
1. 转JSON
java 复制代码
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
Java 复制代码
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()); // 输出: value1
        System.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. 参考资料

  1. https://protobuf.dev/programming-guides/proto3/
相关推荐
武子康几秒前
大数据-230 离线数仓 - ODS层的构建 Hive处理 UDF 与 SerDe 处理 与 当前总结
java·大数据·数据仓库·hive·hadoop·sql·hdfs
武子康2 分钟前
大数据-231 离线数仓 - DWS 层、ADS 层的创建 Hive 执行脚本
java·大数据·数据仓库·hive·hadoop·mysql
苏-言9 分钟前
Spring IOC实战指南:从零到一的构建过程
java·数据库·spring
界面开发小八哥16 分钟前
更高效的Java 23开发,IntelliJ IDEA助力全面升级
java·开发语言·ide·intellij-idea·开发工具
草莓base29 分钟前
【手写一个spring】spring源码的简单实现--容器启动
java·后端·spring
Allen Bright43 分钟前
maven概述
java·maven
编程重生之路1 小时前
Springboot启动异常 错误: 找不到或无法加载主类 xxx.Application异常
java·spring boot·后端
薯条不要番茄酱1 小时前
数据结构-8.Java. 七大排序算法(中篇)
java·开发语言·数据结构·后端·算法·排序算法·intellij-idea
努力进修1 小时前
“探索Java List的无限可能:从基础到高级应用“
java·开发语言·list
politeboy1 小时前
k8s启动springboot容器的时候,显示找不到application.yml文件
java·spring boot·kubernetes