本文将记录我初次学习gRPC的一些成果和历程,gRPC是谷歌开发的基于protobuf的一款高性能,开源的通用RPC框架,支持多种语言。可以在任何环境中运行。 它可以有效地连接数据中心内和跨数据中心的服务,并提供可插拔的支持,以实现负载平衡,跟踪,健康检查和身份验证。 它还适用于分布式计算的最后一英里,用于将设备,移动应用程序和浏览器连接到后端服务。
1、工具:IDEA,Gradle
2、下载安装Gradle,参见另外一篇博文:https://blog.csdn.net/ccf199201261/article/details/100058377
3、在IDEA中新建一个Gradle项目,新建项目时使用本地安装的Gradle,我这边的目录结构如下
4、从https://github.com/grpc/grpc-java/blob/master/README.md上将gRPC开发有关的gradle依赖和protobuf生成java代码的gradle插件拷到项目的build.gradle中。
apply plugin: 'java'
apply plugin: 'com.google.protobuf'
group 'com.dxy'
version '1.0-SNAPSHOT'
sourceCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
testCompile group: 'junit', name: 'junit', version: '4.12'
//grpc依赖
implementation 'io.grpc:grpc-netty-shaded:1.23.0'
implementation 'io.grpc:grpc-protobuf:1.23.0'
implementation 'io.grpc:grpc-stub:1.23.0'
}
//protobuf生成java代码的gradle插件
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.8'
}
}
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.9.0"
}
plugins {
grpc {
artifact = 'io.grpc:protoc-gen-grpc-java:1.23.0'
}
}
generateProtoTasks {
all()*.plugins {
grpc {}
}
}
}
此时用gradle clean build命令构建一下项目,便可以生成很多gradle的task,比如像一会会用到的generateProto,用来将proto文件生成java代码,如下图所示,
5、准备工作做得差不多了,接下来去gRPC官网看一看,从官网可知gRPC的接口支持4种形式,如下图
1):最简单的一种也是最常见最好理解的一种形式,既发送一个请求返回一下响应,就像平时的一个方法调用
2):服务器端流式RPC,客户端向服务器发送请求并获取流以读取消息序列。 客户端从返回的流中读取,直到没有更多消息。
3):客户端流式RPC,客户端再次使用提供的流写入一系列消息并将其发送到服务器。 一旦客户端写完消息,它就等待服务器 全部读取它们并返回它的响应。
4):双向流式RPC,双方使用读写流发送一系列消息。 这两个流独立运行,因此客户端和服务器可以按照自己喜欢的顺序进行 读写:例如,服务器可以在写入响应之前等待接收所有客户端消息,或者它可以交替地读取消息然后写入消息, 或者其他 一些读写组合。 保留每个流中的消息顺序。
6、终于进入正题,编写一个proto文件,并放到src/main/proto目录下,这是上面提到的插件决定的默认存放位置,但是可配置的,见https://grpc.io/docs/reference/java/generated-code/
proto文件内容如下:
syntax = "proto3";
package com.dxy.proto;
option java_package = "com.dxy.proto";
option java_outer_classname = "Human";
//生成多个java文件
option java_multiple_files=true;
message MyRequest{
string code = 1;
}
message MyResponse{
string req_code = 1;
string msg = 2;
}
//grpc接口服务,下面4个方法对应gRPC的4中请求方式
service HumanService{
//类似调用常用方法,一个请求一个响应
rpc GetRequestByCode(MyRequest) returns (MyResponse){}
//服务器端流式RPC,一个请求返回一个流式响应
rpc GetStreamInfo(MyRequest) returns (stream MyResponse){}
//客户端流式RPC,一个流式请求,返回一个普通响应
rpc GetInfoByStreamRequest(stream MyRequest) returns (MyResponse){}
//双向流式RPC,一个流式请求,返回一个流式响应
rpc GetStreamInfoByStreamRequest(stream MyRequest) returns (stream MyResponse){}
}
proto文件编写完成后,使用 gradle generateProto 命令自动生成java代码,代码会默认生成到build/generated/source/proto目录下。如果需要让java代码自动生成到项目指定的目录下,比如src/main/java目录,则需要在上面的插件配置处加两个配置项,具体请详细阅读protobuf关于gradle的插件在github上的代码说明,地址:https://github.com/google/protobuf-gradle-plugin ,主要阅读下面截图的内容
这里提到两个参数一个是generatedFilesBaseDir,另一个是outputSubDir,第一个参数是生成代码的外层目录位置,默认是build\generated\source\proto,然后里面还有main/java或是main/grpc,前者是存放message相关类的,后者是存放service相关rpc服务类的。那现在我们需要存放的外层目录是src/main/java,这下就很明显了,只需要将build\generated\source\proto改成src即可,也就是将第一个参数generatedFilesBaseDir设置成src。那第二个参数又是什么意思呢,outputSubDir指的是子目录,它其实具体指的是service也就是rpc服务代码存放的目录的子目录,上面已经说过了service的默认存放目录的子目录是main/grpc,那么outputSubDir的默认值便是grpc,这样就很清楚了,只要将outputSubDir设置成java就好了,这样不管是message还是service的类都会被生成到src/main/java目录下的对应包下了。因此将上面的build.gradle文件的插件相关代码改成如下内容即可:
//protobuf生成java代码的gradle插件
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.8'
}
}
protobuf {
generatedFilesBaseDir = "src"
protoc {
artifact = "com.google.protobuf:protoc:3.9.0"
}
plugins {
grpc {
artifact = 'io.grpc:protoc-gen-grpc-java:1.23.0'
}
}
generateProtoTasks {
all()*.plugins {
grpc {
outputSubDir = 'java'
}
}
}
}
7、编写接口实现类,实现生成的4个方法
package com.dxy.grpc;
import com.dxy.proto.HumanServiceGrpc;
import com.dxy.proto.MyRequest;
import com.dxy.proto.MyResponse;
import io.grpc.stub.StreamObserver;
public class HumanServiceImpl extends HumanServiceGrpc.HumanServiceImplBase {
@Override
public void getRequestByCode(MyRequest request, StreamObserver responseObserver) {
System.out.println(request.getCode());
responseObserver.onNext(MyResponse.newBuilder().setReqCode(request.getCode()).setMsg("Response Message!").build());
responseObserver.onCompleted();
}
@Override
public void getStreamInfo(MyRequest request, StreamObserver responseObserver) {
System.out.println("getStreamInfo:"+request.getCode());
responseObserver.onNext(MyResponse.newBuilder().setReqCode(request.getCode()).setMsg("Stream Response Message1!").build());
responseObserver.onNext(MyResponse.newBuilder().setReqCode(request.getCode()).setMsg("Stream Response Message2!").build());
responseObserver.onNext(MyResponse.newBuilder().setReqCode(request.getCode()).setMsg("Stream Response Message3!").build());
responseObserver.onCompleted();
}
/**
* 由于该接口的返回不是流式,如果在StreamObserver()的onNext()方法去写返回的话会报警告:
* 警告: Cancelling the stream with status Status{code=INTERNAL, description=Too many responses, cause=null}
* 因此这种情况只能在客户端的请求流结束onCompleted()方法中去进行响应返回
* @param responseObserver
* @return
*/
@Override
public StreamObserver getInfoByStreamRequest(StreamObserver responseObserver) {
return new StreamObserver() {
@Override
public void onNext(MyRequest value) {
System.out.println("Received a request:"+value.getCode());
//这样会被警告,并出现错误的结果
// responseObserver.onNext(MyResponse.newBuilder().setReqCode("Multiple Request Code,").setMsg("Multiple Request Code!").build());
}
@Override
public void onError(Throwable t) {
System.out.println(t.getMessage());
}
@Override
public void onCompleted() {
System.out.println("Request Completed!");
responseObserver.onNext(MyResponse.newBuilder().setReqCode("Multiple Request Code,").setMsg("Multiple Request Code!").build());
responseObserver.onCompleted();
}
};
}
/**
* 在这种双向流的情况下,可以实现一边接收请求一边响应结果,并在请求结束时完成响应
* @param responseObserver
* @return
*/
@Override
public StreamObserver getStreamInfoByStreamRequest(StreamObserver responseObserver) {
return new StreamObserver(){
@Override
public void onNext(MyRequest value) {
System.out.println("接收到客户端信息:"+value.getCode());
//接收到客户端请求的同时发送一条消息到服务端
responseObserver.onNext(MyResponse.newBuilder().setReqCode(value.getCode()).setMsg("Response Message").build());
}
@Override
public void onError(Throwable t) {
System.out.println(t.getMessage());
}
@Override
public void onCompleted() {
System.out.println("Request Completed!");
responseObserver.onCompleted();
}
};
}
}
8、编写服务类
package com.dxy.grpc;
import io.grpc.Server;
import io.grpc.ServerBuilder;
public class HumanServer {
private Server server;
private void start() throws Exception{
server = ServerBuilder.forPort(8899).addService(new HumanServiceImpl()).build();
server.start();
Runtime.getRuntime().addShutdownHook(new Thread(()->{
System.out.println("关闭jvm");
HumanServer.this.stop();
}));
}
private void stop(){
server.shutdown();
}
private void awaitTermination() throws Exception{
server.awaitTermination();
}
public static void main(String[] args) throws Exception{
final HumanServer humanServer = new HumanServer();
humanServer.start();
humanServer.awaitTermination();
}
}
9、编写客户端代码
package com.dxy.grpc;
import com.dxy.proto.HumanServiceGrpc;
import com.dxy.proto.MyRequest;
import com.dxy.proto.MyResponse;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.stub.StreamObserver;
import java.util.concurrent.TimeUnit;
public class HumanClient {
public static void main(String[] args) throws Exception{
ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost",8899).usePlaintext().build();
//阻塞式请求
// HumanServiceGrpc.HumanServiceBlockingStub blockingStub = HumanServiceGrpc.newBlockingStub(channel);
// MyResponse response = blockingStub.getRequestByCode(MyRequest.newBuilder().setCode("ABC").build());
// System.out.println(response.getReqCode() + "," + response.getMsg());
/*************************************************************************/
//非阻塞式
HumanServiceGrpc.HumanServiceStub stub = HumanServiceGrpc.newStub(channel);
// stub.getStreamInfo(MyRequest.newBuilder().setCode("BCD").build(),new StreamObserver(){
//
// @Override
// public void onNext(MyResponse value) {
// System.out.println(value.getReqCode()+","+value.getMsg());
// }
//
// @Override
// public void onError(Throwable t) {
// System.out.println(t.getMessage());
//
// }
//
// @Override
// public void onCompleted() {
//
// System.out.println("Completed!");
//
// }
// });
/**********************************************************************/
// StreamObserver streamObserverRequest = stub.getInfoByStreamRequest(new StreamObserver() {
// @Override
// public void onNext(MyResponse value) {
// //获取服务端返回结果
// System.out.println(value.getReqCode()+","+value.getMsg());
// }
//
// @Override
// public void onError(Throwable t) {
// System.out.println(t.getMessage());
// }
//
// @Override
// public void onCompleted() {
// //获取服务端返回结果结束
// System.out.println("Response Completed!");
// }
// });
// for(int i=0;i<10;i++){
// //用异步流的方式向服务端发送10次数据
// streamObserverRequest.onNext(MyRequest.newBuilder().setCode(""+i).build());
// }
// //数据发送完成
// streamObserverRequest.onCompleted();
/*********************************************************************************/
StreamObserver streamObserverRequest2 = stub.getStreamInfoByStreamRequest(new StreamObserver(){
@Override
public void onNext(MyResponse value) {
System.out.println(value.getReqCode()+","+value.getMsg());
}
@Override
public void onError(Throwable t) {
System.out.println(t.getMessage());
}
@Override
public void onCompleted() {
System.out.println("服务端响应结束。");
}
});
for(int i=0; i<10; i++){
//异步发送请求
streamObserverRequest2.onNext(MyRequest.newBuilder().setCode(""+i).build());
}
//请求发送完成
streamObserverRequest2.onCompleted();
//由于客户端异步发送请求,因此需要阻塞线程等待服务端结果
Thread.sleep(8000);
//关闭channel通道
channel.shutdown().awaitTermination(5, TimeUnit.SECONDS);
}
}
最后分别启动服务器端和客户端即可看到效果,客户端代码可以分成4部分逐次运行,查看4中不同请求方式的调用结果。
总结:第一种方式可以用阻塞式的请求,但其他三种涉及到流的都必须使用非阻塞,这个调用关系gRPC已经在代码中强制给我们规范好了,在阻塞式的BlockingStub中将无法请求到流式的方法。特别是第三种方式也即是客户端流式RPC方式,请求一个流,响应一个非流的情况,我们必须在请求的流传入结束之后才能进行结果响应,否则会获得一个警告: Cancelling the stream with status Status{code=INTERNAL, description=Too many responses, cause=null},很明显提示Too many response,因为不是流式响应所以只能有一个Response,最终得到错误的结果。此文乃本人第一次学习gRPC之后整理的点东西,如有不当之处还请指教。
最后需要感谢张龙老师的课程,受益良多。