一、介绍:
gRPC (gRPC Remote Procedure Calls[1]) 是Google发起的一个开源远程过程调用 (Remote procedure call) 系统。该系统基于 HTTP/2 协议传输,使用Protocol Buffers 作为接口描述语言。
支持语言:Java, C++, Python, Objective-C, C#, a lite-runtime (Android Java), Ruby, and JavaScript、Go、Node.js
grpc底层传输层是使用netty实现的。
二、相关资料
- gRpc文档阅读:https://www.grpc.io/docs/guides/
- gRpc核心概念:https://www.grpc.io/docs/guides/concepts/
- gRpc安全认证:https://www.grpc.io/docs/guides/auth/
- gRpc的quickstart:https://www.grpc.io/docs/quickstart/
- gRpc使用手册(gRPC Basics - Java):https://www.grpc.io/docs/tutorials/basic/java/
- gRpc在github地址:https://github.com/grpc针对java语言的源码及手册:https://github.com/grpc/grpc-java
三、特殊性说明
- 虽然可以在使用grpc时使用proto2,grpc也默认使用proto2,但是推荐使用proto3,以在GRPC中可以使用全部特性的支持
- 避免客户端与服务端使用的proto版本不一致造成一些兼容性问题
四、GRPC核心概念
以下内容来自:https://www.grpc.io/docs/guides/concepts/
1、可以使用proto2中message,同时增加了service
2、提供了4种类型的服务方法
- 客户端发送的是普通请求,服务端返回的是普通响应
- 客户端发送的是普通请求,服务端返回的是stream(流:可以理解未一些列对象),客户端会读取这个stream
- 客户端发送的是stream,服务端返回的是普通响应
- 客户端发送的是stream,服务端返回的是stream
什么时候用单向流(服务端向客户端/客户端向服务端)、什么时候用双向流、什么时候用流,什么时候不用流,是取决于业务逻辑的,实际上流只是分批多次将数据进行发送或者接收。将数据一次性发送还是以流式一个个发送,最终效果是相同的,只是看什么场景下需要什么形式而已
第四种:双向流方式,客户端的stream与服务端的stream互不影响,彼此独立。
stream式数据理解:编码层面上可以理解为一个集合或者迭代器
3、在protoc基础上又多出一个plugin
不仅能生成message文件,还能生成客户端与服务端相关的代码
插件的意义:因为grpc是基于protobuf的,而protobuf'提供的protoc工具只能生成特定语言使用的消息代码,而grpc除了此部分代码外,还有包括服务器端、客户端通信代码,所以grpc提供了此插件生成message、客户端、服务端相关代码。
4、grpc提供了3种传输层实现
- 基于netty的传输层:主要的传输层实现,他是基于netty的,包括客户端、服务端
- 基于okhttp的传输层:主要是面向Android开发使用,轻量级传输层实现,只有客户端
- InProcess传输层:主要是面向测试环境,客户端端与服务端在一个进程。
5、grpc规范要求
- rpc方法的参数与返回值类型都是IDL中定义的message类型,而不能是string、int32等变量类型,这一点跟thrift不同,即使只有一个属性,也得定义成message
- grpc生成得即使有返回值得方法,也是void类型的返回值,方法的返回值通过调用rpc方法参数StreamObserver对象的onNext方法实现向下一个处理器流转(发送给对端)
6、grpc即提供了rpc的同步调用也提供了异步调用
- 通过创建不同的Stub体现:
- 同步:newBlockingStub
- 异步:newStub
- newFutureStub??
- 对于客户端请求参数是stream类型,客户端调用rpc时必须使用异步的stub进行调用,同步的blockingstub是无法使用的
五、GRPC的使用
1、运行官网示例1
本示例仅是执行官方可运行的示例,并不包括直接编写及编译IDL文件、编写业务代码等。
以下内容来自:https://www.grpc.io/docs/quickstart/
1)Download the example
git clone -b v1.26.0 https://github.com/grpc/grpc-java
2)通过gradlew编译
cd grpc-java/examples
./gradlew installDist
首次编译会下载gradlew相关依赖
3)启动服务端
./build/install/examples/bin/hello-world-server
输出:
4)启动客户端
./build/install/examples/bin/hello-world-client
2、官网grpc手册-java
以下内容来自:https://www.grpc.io/docs/tutorials/basic/java/
本示例包括如下部分,可以用于系统的学习grpc整个使用流程:
1)定义IDL
2)编译生成源码
3)编写相关业务代码
4)启动服务端、客户端
3、实践:使用grpc
以下内容基本来自:https://github.com/grpc/grpc-java
开发grpc应用(使用java)流程如下:
3.1 添加依赖
maven:
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty-shaded</artifactId>
<version>1.27.1</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
<version>1.27.1</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
<version>1.27.1</version>
</dependency>
gradle:
implementation 'io.grpc:grpc-netty-shaded:1.27.1'
implementation 'io.grpc:grpc-protobuf:1.27.1'
implementation 'io.grpc:grpc-stub:1.27.1'
3.2 配置grpc编译插件
插件的意义:因为grpc是基于protobuf的,而protobuf'提供的protoc工具只能生成特定语言使用的消息代码,而grpc除了此部分代码外,还有包括服务器端、客户端通信代码,所以grpc提供了此插件生成message、客户端、服务端相关代码。
grpc提供了针对maven与gradle的插件集成方式:
maven:如果使用maven作为构建工具,可以使用 protobuf-maven-plugin 插件,如果IDE是eclipse或者netbeans,还应该看看对应的IDE文档:https://github.com/trustin/os-maven-plugin#issues-with-eclipse-m2e-or-other-ides
<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.6.2</version>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.6.1</version>
<configuration>
<protocArtifact>com.google.protobuf:protoc:3.11.0:exe:${os.detected.classifier}</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:1.27.1:exe:${os.detected.classifier}</pluginArtifact>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
其中os-maven-plugin用于识别你的操作系统的版本,提供${os.detected.classifier}参数供后面使用,没有其他的作用
gradle:如果使用的gradle可以使用protobuf-gradle-plugin插件:配置如下:
plugins {
id 'com.google.protobuf' version '0.8.8'
}
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.11.0"
}
plugins {
grpc {
artifact = 'io.grpc:protoc-gen-grpc-java:1.27.1'
}
}
generateProtoTasks {
all()*.plugins {
grpc {}
}
}
}
本文以maven为例,配置好后,会出现下图的一些task:
3.3 编写proto文件
因为grpc也是基于protobuf的,所以需要创建的IDL为.proto
IDL放置在src/main/proto
and src/test/proto
下可以被grpc插件自动识别
syntax = "proto3";
package com.mzj.netty.ssy._08_grpc;
option java_package = "com.mzj.netty.ssy._08_grpc";
option java_outer_classname = "StudentProto";
option java_multiple_files = true;
service StudentService{
//gRpc支持的四种调用形式示例:
rpc GetRealNameByUsername(MyRequest) returns (MyResponse){}//种类1:普通输入参数与返回值
rpc GetStudentsByAge(StudentRequest) returns (stream StudentResponse){}//种类2:服务端rpc方法返回值是stream形式,参数是普通对象
rpc GetStudentsWrapperByAges(stream StudentRequest) returns (StudentResponseList){}//种类3:客户端输入参数是stream形式,返回是一个普通对象
rpc BiTalk(stream StreamRequest) returns (stream StreamResponse){}//种类4:双向的流式的数据传递(客户端发送请求/服务端返回结果都是流式)
//从IDL的定义上,四种调用形式区别体现在rpc定义时方法参数、返回值的message前面是否有stream关键字
//rpc方法的参数与返回值类型都是IDL中定义的message类型,而不能是string、int32等变量类型,这一点跟thrift不同,即使只有一个属性,也得定义成message
}
message MyRequest{
string username = 1;
}
message MyResponse{
string realname = 2;
}
message StudentRequest{
int32 age = 1;
}
message StudentResponse{
string name = 1;
int32 age = 2;
string city = 3;
}
message StudentResponseList{
//protobuf中集合用repeated表示
repeated StudentResponse studentResponse = 1;//repeated表示集合类型,这里表示服务器端向客户端返回的是一个集合类型,集合中元素是StudentResponse
}
message StreamRequest{
string request_info = 1;
}
message StreamResponse{
string response_info = 1;
}
其中包括了4种类型的服务方法。
3.4 编译IDL文件
1、运行插件(protobuf:compile):
生成一堆java文件和一个.exe文件:message相关代码
可以看到生成的exe文件名中有3.11.0,这个是在pom.xml配置protobuf-maven-plugin插件的时候在<configuration></configuration>中配置的。后缀windows-x86_64是当前操作系统版本标识,是由os-maven-plugin插件自动获得的。
2、运行第二个插件(protobuf:compile-custom):
会生成一个exe文件和一个java文件:grpc操作相关代码
3、将这两次生成的java文件拷贝到src对应源码路径下。
3.5 编写服务实现类、服务端、客户端代码
1、服务实现类
1)继承生成的grpc类中public static abstract class类,并Override其中我们定义的、默认实现的业务方法(生成代码中方法的实现为占位实现)
方法参数说明:通常参数包括两部分:
- 客户端发送的请求对象
- 用于向客户端返回结果的对象
package com.mzj.netty.ssy._08_grpc.mycode;
import com.mzj.netty.ssy._08_grpc.*;
import io.grpc.stub.StreamObserver;
import java.util.UUID;
public class StudentServiceImpl extends StudentServiceGrpc.StudentServiceImplBase {
/**
* 方式1:输入参数与返回值都是普通的java对象
*
* grpc生成得即使有返回值得方法,也是void,而方法得返回值通过调用StreamObserver对象的onNext方法实现向下一个处理器流转
*
* rpc业务方法
* @param request:客户端发送的请求对象
* @param responseObserver:用于向客户端返回结果的对象
*/
@Override
public void getRealNameByUsername(MyRequest request, StreamObserver<MyResponse> responseObserver) {
System.out.println("[服务端] 接收到客户端信息:"+request.getUsername());
//返回客户端MyResponse对象
responseObserver.onNext(MyResponse.newBuilder().setRealname("mazhongjia").build());
//标识这次方法调用结束
responseObserver.onCompleted();
}
/**
* 方式2:输入参数是普通的java对象,返回值是流类型
*/
@Override
public void getStudentsByAge(StudentRequest request, StreamObserver<StudentResponse> responseObserver) {
System.out.println("接收到客户端信息:" + request.getAge());
responseObserver.onNext(StudentResponse.newBuilder().setName("马仲佳").setAge(20).setCity("北京").build());
responseObserver.onNext(StudentResponse.newBuilder().setName("呼娜").setAge(21).setCity("北京").build());
responseObserver.onNext(StudentResponse.newBuilder().setName("maxiaotang").setAge(22).setCity("北京").build());
responseObserver.onNext(StudentResponse.newBuilder().setName("mawenge").setAge(23).setCity("北京").build());
responseObserver.onNext(StudentResponse.newBuilder().setName("lvying").setAge(20).setCity("北京").build());
responseObserver.onCompleted();
}
/**
* 方式3:输入参数是流类型,返回值是普通的java对象
*
* 此种方式,rpc方法参数仅有一个StreamObserver用来返回时调用,而请求参数是作为rpc方法的返回值存在(比较怪异)
*
* 实现此rpc方法时,需要返回一个StreamObserver<T>的实现,通过实现其中onNext、onError、onCompleted三个回调方法完成接收请求-->处理请求--->返回响应的三个流程
*/
@Override
public StreamObserver<StudentRequest> getStudentsWrapperByAges(StreamObserver<StudentResponseList> responseObserver){
return new StreamObserver<StudentRequest>() {
/**
* 请求到来时,会调用这里的onNext方法,因为是流式请求参数,所以会调用多次
* @param value
*/
@Override
public void onNext(StudentRequest value) {
//客户端请求到来了一次,会调用onNext方法一次,发送输入参数为StudentRequest类型,整体一次请求为多个StudentRequest对象组成的流
System.out.println("onNext:"+value.getAge());
}
/**
* 出错时调用onError
* @param t
*/
@Override
public void onError(Throwable t) {
System.out.println(t.getMessage());
}
/**
* 客户端流式数据全部发送完毕后调用onCompleted,此方法中需要将请求处理结果(响应)返回给客户端
*/
@Override
public void onCompleted() {
//客户端以流的方式发送完一次请求所有StudentRequest对象后,会调用此方法表示本次请求发送完毕
//服务端被调用此方法后,需要将处理结果返回给客户端,此时返回的是一次普通java对象返回结果
//模拟数据库查询后封装多个StudentResponse
StudentResponse studentResponse = StudentResponse.newBuilder().setName("mazhongjia").setAge(34).setCity("北京").build();
StudentResponse studentResponse2 = StudentResponse.newBuilder().setName("huna").setAge(31).setCity("北京").build();
//本服务接口返回数据类型为StudentResponseList(见IDL定义)
StudentResponseList studentResponseList = StudentResponseList.newBuilder().addStudentResponse(studentResponse).addStudentResponse(studentResponse2).build();
//服务端将结果返回给客户端
responseObserver.onNext(studentResponseList);
responseObserver.onCompleted();
}
};
//mzj分析:
//客户端与服务端分别持有对方的StreamObserver对象:服务端持有调用getStudentsWrapperByAges的方法中参数StreamObserver对象,客户端持有getStudentsWrapperByAges方法返回的new的StreamObserver对象
//建立好这种对象持有关系后,客户端通过调用自己持有StreamObserver的onNext方法将数据发送服务端
//发送完所有数据后,客户端调用自己持有StreamObserver的onConpleted方法通知客户端
//服务端发送给客户端返回结果时,操作的是自己的StreamObserver对象(通过getStudentsWrapperByAges方法参数拿到的)
}
/**
* 方式4:输入参数是流类型,返回值也是流类型
*
* 请求参数泛型StreamRequest与返回结果泛型StreamRequest都是IDL中自定义的类型
*
* 说明:双向的流式数据传递原理:在完全不同的两个流中传递,两个流是互相独立的,一般情况从逻辑上来说,一方流关闭,另一方开着也没啥用,所以逻辑上来说也应该关闭
*
* @param responseObserver
* @return
*/
@Override
public StreamObserver<StreamRequest> biTalk(StreamObserver<StreamResponse> responseObserver) {
return new StreamObserver<StreamRequest>() {
@Override
public void onNext(StreamRequest value) {
//打印客户端发送来的数据
System.out.println("###"+value.getRequestInfo());
//每收到客户端发送一条数据,也向客户端回复一条数据
//这里的实现是在在收到客户端数据时也向客户端返回一条数据
responseObserver.onNext(StreamResponse.newBuilder().setResponseInfo(UUID.randomUUID().toString()).build());
}
@Override
public void onError(Throwable t) {
System.out.println(t.getMessage());
}
@Override
public void onCompleted() {
//这里实现是:客户端发送完毕通知服务端时,服务端也发送完毕通知客户端
//虽然客户端与服务端如果都是流式传递时,双方是在不同流上
//但是从逻辑上来说,一方流关闭,另一方开着也没啥用,所以逻辑上来说也应该关闭
responseObserver.onCompleted();
}
};
}
}
2、服务端代码
package com.mzj.netty.ssy._08_grpc.mycode;
import io.grpc.Server;
import io.grpc.ServerBuilder;
import java.io.IOException;
/**
* @Auther: mazhongjia
* @Description:
*/
public class GrpcServer {
private Server server;
/**
* 服务端启动
* @throws IOException
*/
private void start() throws IOException {
//以下编写方式参考的官方示例
this.server = ServerBuilder.forPort(8080).addService(new StudentServiceImpl()).build().start();
System.out.println("server started....");
//官方示例:grpc server退出的方式:JVM退出之前,关闭grpc server
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("进行关闭");
GrpcServer.this.stop();
}));
System.out.println("server start invork end....");
}
/**
* 服务端退出
*/
private void stop(){
if (null != server){
this.server.shutdown();
}
}
/**
* 阻塞等待服务器关闭没,需要自己调用(与thrift不同,需要自己阻塞等待服务器关闭,而thrift调用serve后会阻塞当前线程)
* @throws InterruptedException
*/
private void awaitTermination() throws InterruptedException {
if (null != server){
this.server.awaitTermination();
}
}
public static void main(String[] args) throws InterruptedException, IOException {
GrpcServer grpcServer = new GrpcServer();
grpcServer.start();
grpcServer.awaitTermination();
}
}
3、客户端代码
package com.mzj.netty.ssy._08_grpc.mycode;
import com.mzj.netty.ssy._08_grpc.*;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.stub.StreamObserver;
import java.time.LocalDateTime;
import java.util.Iterator;
/**
* @Auther: mazhongjia
* @Description:
*/
public class GrpcClient {
/**
* 方式1:输入参数与返回值都是普通的java对象
*/
public static void client1(){
//其中usePlaintext(true):创建的是一个不安全的、不是用ssl证书加密的通道
ManagedChannel managedChannel = ManagedChannelBuilder.forAddress("localhost",8080).usePlaintext().build();
//通过grpc生成的服务stub对象调用rpc请求,这里获取的是阻塞的Stub,也就是同步调用并等待RPC调用返回结果,grpc同样也支持异步调用
StudentServiceGrpc.StudentServiceBlockingStub blockingStub = StudentServiceGrpc.newBlockingStub(managedChannel);
MyResponse myResponse = blockingStub.getRealNameByUsername(MyRequest.newBuilder().setUsername("huna").build());
System.out.println(myResponse.getRealname());
}
/**
* 方式2:输入参数是普通的java对象,返回值是流类型
*/
public static void client2(){
ManagedChannel managedChannel = ManagedChannelBuilder.forAddress("localhost",8080).usePlaintext().build();
StudentServiceGrpc.StudentServiceBlockingStub blockingStub = StudentServiceGrpc.newBlockingStub(managedChannel);
/**
* 流式返回值,API编码时返回的是一个迭代器
*/
Iterator<StudentResponse> iterator = blockingStub.getStudentsByAge(StudentRequest.newBuilder().setAge(31).build());
while(iterator.hasNext()){
StudentResponse studentResponse = iterator.next();
System.out.println(studentResponse.getName()+studentResponse.getAge()+studentResponse.getCity());
}
}
/**
* 方式3:输入参数是流类型,返回值是普通的java对象
*
* 客户端
*/
public static void client3(){
//1.客户端首选创建StreamObserver对象(客户端创建的listener对象,用于传给服务端,监听服务端发送消息的回调listener)
//这里创建的StreamObserver中回调方法是在服务端返回response后,调用,用于客户端处理服务端返回数据(关注回调方法参数类型)
StreamObserver<StudentResponseList> studentResponseListStreamObserver = new StreamObserver<StudentResponseList>() {
@Override
public void onNext(StudentResponseList value) {
value.getStudentResponseList().forEach(studentResponse -> {
System.out.println(studentResponse.getName());
System.out.println(studentResponse.getAge());
System.out.println(studentResponse.getCity());
System.out.println("*********");
});
}
@Override
public void onError(Throwable t) {
System.out.println(t.getMessage());
}
@Override
public void onCompleted() {
System.out.println("completed!");
}
};
//2、创建客户端向服务端发送数据
//****只要客户端是以流式的方式向服务端发送请求,那么交互肯定是异步的****=>典型的异步调用---回调方式的接口设计
//而之前两种方式使用的xxxxServiceBlockingStub是同步使用的,因此需要构造可以用于异步的stub(xxxxServiceStub)
ManagedChannel managedChannel = ManagedChannelBuilder.forAddress("localhost",8080).usePlaintext().build();
StudentServiceGrpc.StudentServiceStub asyncStub = StudentServiceGrpc.newStub(managedChannel);
//通过asyncStub可以调用IDL中声明的所有rpc service接口(异步的方式,mzj:可以对比两种方式,使用API接口--传递参数、返回值有什么不同,体会同步/异步接口的设计感),但是客户端以流行色向服务端发送的rpc请求接口,必须得是通过这种异步的方式
StreamObserver<StudentRequest> studentRequestStreamObserver = asyncStub.getStudentsWrapperByAges(studentResponseListStreamObserver);
//-------------异步接口--------------:
// void asyncStub.getRealNameByUsername(MyRequest request,
// io.grpc.stub.StreamObserver<MyResponse> responseObserver);
//-------------同步接口--------------:
// MyResponse myResponse = blockingStub.getRealNameByUsername(MyRequest request)
studentRequestStreamObserver.onNext(StudentRequest.newBuilder().setAge(20).build());
studentRequestStreamObserver.onNext(StudentRequest.newBuilder().setAge(30).build());
studentRequestStreamObserver.onNext(StudentRequest.newBuilder().setAge(40).build());
studentRequestStreamObserver.onNext(StudentRequest.newBuilder().setAge(50).build());
studentRequestStreamObserver.onCompleted();
//mzj:这里设计的服务端/客户端交互接口,跟我水电GDI设计的异步接口区别在(注意,这里写的区别仅是API接口设计):
//grpc异步接口设计,接口返回值设计成StreamObserver<StudentRequest>,而我设计成public GDIFuture<GDIResponse> method(GDLListener listener)
//区别在于,我是①通过返回值让客户端持有并可以同步获取请求结果②通过参数listener被动通知(回调传递返回结果)
//而grpc是通过返回值让客户端可以调用onNext发送流式request请求
//设计的目的不同,初衷不同。。。。
}
/**
* 方式4:输入参数是流类型,返回值也是流类型
*/
public static void client4(){
//1.客户端首选创建StreamObserver对象(用于传给服务端监听服务端发送消息的listener)
StreamObserver<StreamResponse> responseStreamObserver = new StreamObserver<StreamResponse>() {
@Override
public void onNext(StreamResponse value) {
System.out.println(value.getResponseInfo());
}
@Override
public void onError(Throwable t) {
System.out.println(t.getMessage());
}
@Override
public void onCompleted() {
System.out.println("completed!");
}
};
//2、客户端向服务端发送数据
ManagedChannel managedChannel = ManagedChannelBuilder.forAddress("localhost",8080).usePlaintext().build();
StudentServiceGrpc.StudentServiceStub asyncStub = StudentServiceGrpc.newStub(managedChannel);
StreamObserver<StreamRequest> requestStreamObserver = asyncStub.biTalk(responseStreamObserver);
for(int i=0;i<10;i++){
requestStreamObserver.onNext(StreamRequest.newBuilder().setRequestInfo(LocalDateTime.now().toString()).build());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
// client1();
// client2();
// client3();
client4();
//测试client3、client4时(客户端以流式向服务端发送数据),由于通信是异步的,所以要保证主线程不退出,才能完成调用,测试出效果
Thread.sleep(10000);
}
}
然后依次运行服务端和客户端就可以了。通过main函数4种类型rpc方法测试观察效果
完整工程代码:
https://github.com/mazhongjia/nettyssynetty02/tree/master/src/main/java/com/mzj/netty/ssy/_08_grpc