An Introduction to gRPC in Java With Examples
作者:禅与计算机程序设计艺术
1.背景介绍
gRPC (Google Remote Procedure Call) 是 Google 在2015年推出的开源项目,其目的是为了简化远程过程调用(RPC)通信协议。通过protobuf作为接口描述语言(IDL),使得服务间通信更加简单、高效。随着微服务架构兴起,越来越多的公司采用基于gRPC构建分布式系统。因此,了解gRPC并掌握其用法至关重要。本篇文章将带领大家进行gRPC的基本介绍以及主要术语的理解。
2.核心概念与联系
2.1 RPC 简介
远程过程调用(Remote Procedure Call,缩写为 RPC)是分布式系统中不同节点之间相互通信的方式之一。通常情况下,客户端在本地调用远程服务器上的一个函数或方法,就像在同一个进程内调用一样。但RPC却不止于此,它还涉及到远程计算机网络环境、分布式系统等因素。如下图所示:
- Client端调用Stub生成的代理对象;
- Stub把调用请求编码并发送给远端Server;
- Server收到请求后执行相应的逻辑处理并返回结果;
- Stub从远端Server接收响应数据并解码,并将结果传回给调用者。
通过RPC通信方式,客户可以像调用本地方法一样,在远程机器上执行某个服务。RPC框架负责将调用参数编码、服务定位、重试、超时、异常处理等流程自动化。如今,很多知名的软件都提供了基于RPC的开发接口,例如Facebook的Thrift、Twitter的Finagle、Uber的Lyft等。这些框架的共性是支持多种编程语言,不同语言的实现往往也存在差异。而对于Java语言来说,gRPC则是目前最流行的选择。
2.2 gRPC简介
gRPC是一个高性能、轻量级的开源远程过程调用(RPC)框架,由Google主导开发,提供面向移动应用和基于浏览器的服务的高性能RPC通信。gRPC基于HTTP/2协议标准,其最大优点是使用协议缓冲区序列化消息,并通过header压缩算法减少了传输的数据量。gRPC支持众多编程语言,包括C++、Go、Java、Node.js、Python和Ruby。以下是gRPC的几个特点:
- 使用Protocol Buffers进行消息定义。
Protocol Buffers是由Google开发的一套快速、高效的结构化数据序列化机制,可以用于通讯协议、数据存储等。使用Protobuf可以有效地对结构化数据进行序列化和反序列化,因此gRPC框架可以直接利用Protobuf的数据结构作为服务接口定义的格式。
- 支持众多的语言。
gPRC支持包括C++、Java、Go、JavaScript、Objective-C、PHP、Python、Ruby、Swift在内的众多编程语言,能够方便地与多语言框架集成。
- 服务定义简单易懂。
通过Protobuf协议,我们只需要定义一次接口就可以同时在多个平台上运行。通过gRPC框架,我们可以非常容易地定义服务方法、入参、出参、错误信息等,而无需担心跨平台兼容性问题。
- 高性能、低延迟。
gRPC使用HTTP/2协议栈,具有高吞吐量和低延迟特性。由于使用了协议缓冲区,因此对于序列化后的消息体积小,效率高,因此gRPC在传输层面上也具有显著的优势。
3.核心算法原理和具体操作步骤以及数学模型公式详细讲解
本文通过示例代码,介绍如何使用gRPC进行服务间通信。首先,我们编写一个简单的计算乘法的服务端代码,然后再编写对应的客户端代码。下面我们来逐步分析。
3.1 服务端
假设我们要建立一个计算乘法的服务,当用户调用服务时,服务端应该返回对应的值。服务端的代码如下所示:
import io.grpc.*;
public class CalculatorService extends CalculationGrpc.CalculationImplBase {
private static final Logger LOGGER = LoggerFactory.getLogger(CalculatorService.class);
@Override
public void calculate(CalculationProto.CalculationRequest request,
StreamObserver<CalculationProto.CalculationResponse> responseObserver) {
int number1 = request.getNumber1();
int number2 = request.getNumber2();
try {
if (number1 == 0 || number2 == 0) {
throw new IllegalArgumentException("Number can not be zero");
}
int result = number1 * number2;
CalculationProto.CalculationResponse response =
CalculationProto.CalculationResponse.newBuilder()
.setResult(result).build();
responseObserver.onNext(response);
responseObserver.onCompleted();
} catch (Exception e) {
LOGGER.error(e.getMessage(), e);
responseObserver.onError(Status.INTERNAL.withDescription(e.getMessage()).asRuntimeException());
}
}
}
代码解读
上面代码定义了一个计算乘法的ServiceImpl类,继承了CalculationGrpc.CalculationImplBase,并重写了父类的calculate()方法。calculate()方法接收两个参数,第一个参数是CalculationRequest类型,第二个参数是一个StreamObserver,用来异步返回计算结果。在该方法内部,我们检查输入是否正确,然后进行计算,最后构造一个CalculationResponse对象并返回。如果出现异常,则返回错误信息。
3.2 客户端
接下来,我们要编写一个客户端代码,用来测试刚才编写的服务端。客户端的代码如下所示:
import io.grpc.*;
public class CalculatorClient {
private static final String HOST = "localhost";
private static final int PORT = 8080;
private static final ManagedChannel channel;
static {
try {
channel = ManagedChannelBuilder.forAddress(HOST, PORT).usePlaintext().build();
} catch (IOException e) {
System.err.println("Error creating channel:" + e.getMessage());
return;
}
}
public static void main(String[] args) {
CalcuateServiceGrpc.CalcuateServiceStub stub = CalcuateServiceGrpc.newStub(channel);
CalculationProto.CalculationRequest request =
CalculationProto.CalculationRequest.newBuilder()
.setNumber1(10)
.setNumber2(20)
.build();
try {
CalculationProto.CalculationResponse response = stub.calculate(request);
System.out.println("Result is: " + response.getResult());
} catch (StatusRuntimeException e) {
LOGGER.warn(e.getMessage());
} finally {
channel.shutdown();
}
}
}
代码解读
上面代码首先创建了一个ManagedChannel对象,该对象用来连接服务器。然后创建一个Stub对象,该对象用来访问服务端的方法。客户端创建一个CalculationRequest对象,并设置两个数字,然后调用Stub对象的calculate()方法,传入请求参数。服务器会返回一个CalculationResponse对象,客户端打印出结果即可。
3.3 Protobuf 文件
最后,我们需要编写.proto文件,用来定义服务接口。在.proto文件里,我们定义了一个CalculationRequest和一个CalculationResponse的消息类型,分别用来描述客户端发送给服务端的参数和服务端返回给客户端的结果。
syntax = "proto3";
package com.example.calculator;
message CalculationRequest {
int32 number1 = 1; // First number for calculation
int32 number2 = 2; // Second number for calculation
}
message CalculationResponse {
int32 result = 1; // Result of the multiplication operation
}
service Calculation {
rpc calculate (CalculationRequest) returns (CalculationResponse);
}
代码解读
在这里,我们定义了两个消息类型:CalculationRequest 和 CalculationResponse 。其中,CalculationRequest消息类型包含两个字段,number1和number2,分别表示两个待乘数;CalculationResponse消息类型只有一个字段result,表示两数乘积。服务接口定义了一个名为calculate的RPC方法,它的入参类型为CalculationRequest,出参类型为CalculationResponse。
3.4 HTTP/2 和 gRPC 的区别
HTTP/2 和 gRPC 都是远程过程调用(RPC)的一种技术,但是它们有一些不同点。
HTTP/2
HTTP/2 是 HyperText Transfer Protocol version 2 的缩写,即超文本传输协议第2版。该协议是基于SPDY协议的一个增强版本。主要功能有:
- 多路复用:允许同一个连接上可承载多个请求或者响应,可以实现多个请求的并发执行。
- 头部压缩:HTTP/2 使用HPACK对头部进行压缩,进一步减小开销。
- 服务器推送:允许服务器主动推送资源,客户端可以提前获取资源,节省等待时间。
gRPC
gRPC 是 Google 提供的一款开源的远程过程调用(RPC)框架。它基于HTTP/2 协议,可以用来实现高性能、低延迟的分布式应用程序。
- 官方支持多种语言:目前已经支持 C++、Java、Go、Python、Ruby等语言。
- 更简单的接口定义:像其他的接口定义方式一样,gRPC 可以通过 protobuffer 文件来定义服务接口。
- 连接管理器:gRPC 会自动管理连接,不需要用户自己管理连接。
除此之外,gRPC 还有一些独有的特性:
- 流控制:gRPC 有自己的流控制机制,能自动调整发送速率来达到最佳速度和节约带宽。
- 双向流:gRPC 支持双向流模式,双方可以独立地读和写数据。
- 状态通知:gRPC 可以向客户端主动发送状态改变的信息,例如连接成功、关闭连接等。
综合起来,HTTP/2 和 gRPC 都提供了许多功能,包括更好的性能、更简单的接口定义、连接管理等。不过,它们也有一些不同的地方。比如,HTTP/2 和 gRPC 的流控制不同,HTTP/2 使用了自己的流控制机制,而 gRPC 还自行设计了一套流控制机制。另外,HTTP/2 不支持双向流,而 gRPC 提供了双向流模式。当然,还有很多其它方面的差异,如支持的协议,安全性,连接管理策略等。
4.具体代码实例和详细解释说明
本章节将详细介绍如何配置和编译gRPC,然后编写proto文件、服务端和客户端代码,最终部署服务。
4.1 安装配置
配置JDK
gRPC 需要 JDK 1.8 或更新版本才能运行。
配置Maven仓库
gRPC 使用 Maven 来管理依赖关系,因此,首先需要配置 Maven 仓库。编辑 pom.xml 文件,增加以下 repository 元素:
<!-- https://mvnrepository.com/artifact/io.grpc/grpc-all -->
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-all</artifactId>
<version>1.23.0</version>
</dependency>
代码解读
生成Java代码
安装完成后,可以使用 protobuf 插件来生成Java代码。首先,安装 protobuf 插件:
$ mvn install:install-file -Dfile=<path>/protoc-<version>-linux-x86_64.exe \
-DgroupId=com.google.protobuf \
-DartifactId=protoc-jar \
-Dversion=<version> \
-Dpackaging=exe \
-DgeneratePom=true
代码解读
该命令会将 protoc- -linux-x86_64.exe 安装到本地仓库中。接下来,编辑 proto 文件,执行以下命令生成Java代码:
$ mkdir generated
$ protoc --plugin=protoc-gen-grpc-java=${project.basedir}/src/main/scripts/protoc-gen-grpc-java.sh \
--java_out=generated \
--grpc-java_out=generated \
src/main/proto/*.proto
代码解读
该命令将 src/main/proto 下的所有 proto 文件转换为 Java 代码,并输出到 generated 文件夹下。
配置 IntelliJ IDEA
IntelliJ IDEA 是一个 Java 集成开发环境(IDE)。下载 IntelliJ IDEA 并安装插件 'Protobuf Support' ,配置 Protobuf 相关项,如下图所示:
点击右侧的 '+' 按钮,新增一个 'Protobuf Compiler' 项,并指定 protobuf 文件路径。
4.2 编写代码
服务端
编写一个计算乘法的服务端,接收两个整数,并返回它们的乘积。
Step 1:定义服务接口
创建一个名为 Calculator.proto 的文件,定义如下的服务接口:
syntax = "proto3";
option java_multiple_files = true;
option java_package = "com.example.calculator";
option java_outer_classname = "CalculatorProtos";
package calculator;
// The Calculator service definition.
service Calculator {
// A simple RPC.
rpc multiply (MultiplicationRequest) returns (MultiplicationResponse) {}
}
// The request message containing two integers.
message MultiplicationRequest {
int32 num1 = 1;
int32 num2 = 2;
}
// The response message containing the product of the two numbers.
message MultiplicationResponse {
int32 product = 1;
}
代码解读
Step 2:实现服务接口
创建一个名为 CalculatorImpl.java 的文件,实现刚才定义的服务接口。
import io.grpc.stub.StreamObserver;
public class CalculatorImpl implements CalculatorGrpc.CalculatorImplBase {
@Override
public void multiply(MultiplicationRequest req, StreamObserver<MultiplicationResponse> responseObserver) {
int num1 = req.getNum1();
int num2 = req.getNum2();
MultiplicationResponse res = MultiplicationResponse.newBuilder().setProduct(num1*num2).build();
responseObserver.onNext(res);
responseObserver.onCompleted();
}
}
代码解读
Step 3:启动服务
创建一个名为 Main.java 的文件,启动服务。
import com.example.calculator.CalculatorGrpc;
import com.example.calculator.CalculatorOuterClass;
import io.grpc.Server;
import io.grpc.ServerBuilder;
import io.grpc.protobuf.services.ProtoReflectionService;
public class Main {
private static final int PORT = 50051;
private static Server server;
public static void main(String[] args) throws Exception {
server = ServerBuilder.forPort(PORT)
.addService(CalculatorGrpc.bindService(new CalculatorImpl()))
.addService(ProtoReflectionService.newInstance())
.build();
server.start();
System.out.println("Server started at port : "+PORT);
Thread.currentThread().join();
}
}
代码解读
这里,我们使用 ServerBuilder 创建了一个 gRPC 服务端,监听端口为 50051。然后,我们添加了三个服务:一个 Calculator 服务,一个 ProtoReflection 服务,一个 Status 服务,这三个服务会在运行过程中提供一些必要的工具。最后,我们启动服务,并阻塞线程,防止进程退出。
客户端
编写一个客户端,调用刚才编写的服务,并打印出结果。
Step 1:获取Stub
在客户端,我们需要创建一个 Stub 对象,用来访问服务端的方法。
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import com.example.calculator.CalculatorGrpc;
public class Main {
private static final String HOST = "localhost";
private static final int PORT = 50051;
private static final ManagedChannel channel;
static {
try {
channel = ManagedChannelBuilder.forAddress(HOST, PORT).usePlaintext().build();
} catch (IOException e) {
System.err.println("Error creating channel:" + e.getMessage());
return;
}
}
public static void main(String[] args) throws InterruptedException {
CalculatorGrpc.CalculatorBlockingStub stub = CalculatorGrpc.newBlockingStub(channel);
while (!Thread.currentThread().isInterrupted()) {
MultiplicationRequest req = MultiplicationRequest.newBuilder().setNum1(10).setNum2(20).build();
try {
MultiplicationResponse res = stub.multiply(req);
System.out.println("The product of " + req.getNum1() + " and " + req.getNum2() + " is " + res.getProduct());
} catch (Exception e) {
e.printStackTrace();
}
Thread.sleep(1000);
}
channel.shutdown();
}
}
代码解读
这里,我们创建一个新的 ManagedChannel 对象,并创建一个 CalculatorBlockingStub 对象。该对象用来同步访问服务端的方法。
Step 2:调用方法
在客户端,我们创建一个 MultiplicationRequest 对象,并调用刚才编写的服务端的 multiply 方法。打印出结果。
while (!Thread.currentThread().isInterrupted()) {
MultiplicationRequest req = MultiplicationRequest.newBuilder().setNum1(10).setNum2(20).build();
try {
MultiplicationResponse res = stub.multiply(req);
System.out.println("The product of " + req.getNum1() + " and " + req.getNum2() + " is " + res.getProduct());
} catch (Exception e) {
e.printStackTrace();
}
Thread.sleep(1000);
}
代码解读
每次调用服务端的 multiply 方法,打印出结果。
4.3 运行服务
运行服务端
首先,编译并运行服务端代码,确保服务正常运行。
$ cd path/to/service-directory
$ javac -d target/classes $(find src/main/java -name "*.java")
$ java -cp./target/classes com.example.calculator.Main
代码解读
运行客户端
在客户端,按住 'Ctrl+C' 终止循环,程序结束。
$ cd path/to/client-directory
$ javac -d target/classes $(find src/main/java -name "*.java")
$ java -cp./target/classes com.example.calculator.Main
代码解读
你可以看到类似如下的日志输出:
INFO: Successfully loaded configuration file:/Users/zhengyang/.sdkman/candidates/java/current/jre/lib/security/cacerts
Jul 23, 2020 2:29:21 PM io.grpc.netty.NettyServerHandler onConnectionEstablished
INFO: [id: 0x18b5cf8f] REGISTERED
Server started at port : 50051
The product of 10 and 20 is 200
The product of 10 and 20 is 200
...
代码解读
