一、概念介绍
1. RPC调用
开始之前先介绍一下 RPC(Remote procedure call)
的定义,即远程服务调用,即在当前的程序中调用其它程序的服务实现。
假设存在两个程序 Systemc-A
与 Systemc-B
,并在 Systemc-A
中定义了接口 getData()
,而实现类在 Systemc-B
中。当 Systemc-A
经过一定手段通过 getData()
接口调用 Systemc-B
中的实现类,这个过程即远程服务调用。
2. 实现原理
针对 RPC
服务功能的实现,通常可通过动态代理等特性实现。
如通过动态代理的方式创建实例对象时获取目标类信息、目标方法及方法参数等信息,再经由网络传输等手段请求至远端服务将传入的信息利用反射实现服务调用并返回。
二、工程配置
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
目录下,其与 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. 接口实现
完成上述的接口定义之后即可编写接口的实现类,实现编译生成类 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()
阻塞当前线程等待应用关闭。
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. 接口请求
相对于服务端而言客户端实现实现更为简单,通过 ManagedChannelBuilder
连接至服务端,并由 newBlockingStub()
创建实例对象。
当创建完实例对象 stub
即与普通的接口调用并无区别,对应的示例如下:
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);
}
}
参考链接