一、概念介绍
1. RPC调用
开始之前先介绍一下 RPC(Remote procedure call) 的定义,即远程服务调用,即在当前的程序中调用其它程序的服务实现。
假设存在两个程序 Systemc-A 与 Systemc-B,并在 Systemc-A 中定义了接口 getData(),而实现类在 Systemc-B 中。当 Systemc-A 经过一定手段通过 getData() 接口调用 Systemc-B 中的实现类,这个过程即远程服务调用。
2. 实现原理
针对 RPC 服务功能的实现,在 Java 中常可通过 InvocationHandler 动态代理等特性实现。
如通过动态代理的方式创建实例对象时获取目标类信息、目标方法及方法参数等信息,再经由网络传输等手段请求至远端服务将传入的信息利用反射实现服务调用并返回。
二、GRPC集成
1. 依赖引入
GRPC 是由 Google 开发的一款 RPC 服务框架,基于 HTTP2 传输协议实现,针对网络层实现一系列优化从而达到高效传输的目的。
GRPC 提供了多语言的集成方式,包含 Java SDK 依赖,在工程中添加下述依赖即可开箱即用。
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty</artifactId>
<version>1.62.2</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
<version>1.62.2</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
<version>1.62.2</version>
</dependency>
2. 插件依赖
GRPC 的服务接口并不是传统的 .java 文件配置,而是以其相应格式标准定义于 .proto 文件中,因此需要在工程依赖中添加对应的打包插件。
在工程的 pom.xml 文件中添加下述插件配置:
<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.6.1</version>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.6.1</version>
<configuration>
<pluginId>grpc-java</pluginId>
<protocArtifact>
com.google.protobuf:protoc:3.3.0:exe:${os.detected.classifier}
</protocArtifact>
<pluginArtifact>
io.grpc:protoc-gen-grpc-java:1.4.0:exe:${os.detected.classifier}
</pluginArtifact>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
3. 插件安装
除此之外,还需要在 IDEA 中安装 proto 编译插件用于将 proto 文件内容转为 java 文件。
如果 IDEA 为旗舰版则自带 Protocol Buffers 插件,若为社区版则需要手动去插件市场下载 Protobuf 插件。
三、文件编写
1. 基本格式
在 GRPC 中,接口的定义不在处于 java 文件中,而是通过 proto 文件定义。
proto 则是客户端与服务端之间的所约定的接口服务规范,以特定语法规则声明了方法的出入参等信息,双方服务按照文件定义的内容实现服务调用与业务实现。
并且需要注意的一点是 .proto 文件需要放置于 proto 目录下,其与 java 目录处于同一层级。
proto 文件定义并不复杂,其中通过 syntax 指定协议版本。package 用于指定报名,与 java 中的包名作用效果类似用于层级管理;option 用于添加参数配置。
syntax = "proto3";
package xyz.ibudai.practice.tool.grpc;
option java_multiple_files = true;
// 指定生成的 gRPC 服务接口和消息类的包名
option java_package = "xyz.ibudai.practice.tool.grpc.bean";
2. 对象定义
在接口定义中,通过其入参与出参都可能为复杂的 Java 对象,而在 proto 文件中即通过 message 关键字定义对象。
例如存在一个 Java 类定义如下:
public class HelloRequest {
String userName;
}
则 HelloRequest 在 proto 中对应的定义形式如下,其中 userName = 1 的含义代表给 userName 的别名为 1。
众所周知,当一个类以 Json 等形式表达时其格式为 key:value 形式,即上述示例对应的为 "userName": "Alex" 形式,但是在 GRPC 中其为 "1": "Alex",即通过数字别名取代字段名,进一步减少数据的大小降低传输压力。
message HelloRequest {
string userName = 1;
}
3. 接口定义
同理在 proto 中接口类的定义通过 service 与 rpc 关键字定义。
如存在下述示例接口类 TestService 及 getDate() 方法。
public interface TestService {
HelloResponse getDate(HelloRequest req);
}
则上述接口在对应的 proto 文件中的定义格式如下:
service TestService {
rpc getDate (HelloRequest) returns (HelloResponse) {
}
}
下述即为一个完整的 proto 文件定义示例。
syntax = "proto3";
package xyz.ibudai.practice.tool.grpc;
option java_multiple_files = true;
option java_package = "xyz.ibudai.practice.tool.grpc.bean";
// 定义请求体
message HelloRequest {
string userName = 1;
}
// 定义相应内容
message HelloResponse {
string message = 1;
}
// 服务接口,定义请求参数和相应结果
service TestService {
// 定义 RPC 接口方法
rpc getDate (HelloRequest) returns (HelloResponse) {
}
}
4. 文件编译
完成上述内容编写之后需要将其编译为程序可读的 Java 文件。
在 IDEA 右侧 maven 中找到下述依次执行编译。
执行完成后即会在工程的 target 目录下生成对应的文件,其中执行 compile 会生成文件于 target/generated-sources/protobuf/java 下,执行 custom 文件会生成于 target/generated-sources/protobuf/grpc-java 下。
四、服务端应用
1. 接口实现
在服务端项目中,需维护 proto 文件并根据上述中编译生成代码文件,并基于生成后的代码实现相应的接口。
如上述的接口实现编译生成类 TestServiceGrpc.TestServiceImplBase,重写接口并通过 onNext() 与 onCompleted() 将数据提交返回。
public class TestServiceImpl extends TestServiceGrpc.TestServiceImplBase {
@Override
public void getDate(HelloRequest request, StreamObserver<HelloResponse> responseObserver) {
String data = request.getUserName();
System.out.println("Server receive data: " + data);
HelloResponse response = HelloResponse.newBuilder()
.setMessage("Data from server: " + data)
.build();
// 返回数据,等价于 return response;
responseObserver.onNext(response);
responseObserver.onCompleted();
}
}
2. 服务启动
接口定义完成后即可启动服务端,通过 ServerBuilder 构建服务端对象,默认 Host 为当前本地 IP,其中 addService() 为添加服务实现类,
完成后通过 start() 启动服务并由 awaitTermination() 阻塞当前线程等待应用关闭。
import io.grpc.Server;
import io.grpc.ServerBuilder;
public class ServerTest {
private static final int PORT = 9000;
public static void main(String[] args) throws Exception {
Server server = ServerBuilder.forPort(PORT)
.addService(new TestServiceImpl())
.build();
server.start();
server.awaitTermination();
}
}
五、客户端应用
1. 接口请求
在客户端项目中按照定义维护 proto 文件并编译生成相应的代码文件。
在服务请求中客户端实现则更为简单,通过 ManagedChannelBuilder 连接至服务端,并由 newBlockingStub() 创建实例对象。创建完实例对象 stub 即与普通的接口调用并无区别,对应的示例如下:
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
public class ClientTest {
private static final String HOST = "localhost";
private static final int PORT = 9000;
public static void main(String[] args) {
ManagedChannel channel = ManagedChannelBuilder.forAddress(HOST, PORT)
.usePlaintext()
.build();
TestServiceGrpc.TestServiceBlockingStub stub = TestServiceGrpc.newBlockingStub(channel);
HelloRequest request = HelloRequest.newBuilder()
.setUserName("Alex")
.build();
HelloResponse response = stub.getDate(request);
System.out.println(response);
}
}
参考链接