实践常见的RPC框架:gRPC
文章目录
一、gRPC介绍
gRPC是现代开源能运行在任意环境上的高性能的远程程序调用框架(RPC)。它能有效的连接数据中心内外的服务,并且插件式的支持负载均衡、跟踪、健康检查和身份校验。它也能应用在分布式计算的最后一英里,将设备、手机应用程序、浏览器连接到后台服务。
-
简单的服务定义:通过Protocol Buffers定义服务。Protocol Bufffers是一个强大的二进制序列化工具集和语言。
-
启动迅速并且可扩展:使用一行代码就能安装在运行时环境和开发环境,也能扩展到每秒数百万个RPC框架。
-
能够跨语言和跨平台的工作:为你在各种语言和平台下的服务自动生成公用的客户端和服务端存根代码。
-
双向流并且融合了用户检验:双向的流,并且完全融合了基于HTTP/2-based通信的插件式的用户认证。
二、第一步:gRPC服务的定义(四种RPC服务方法)
第一步是用Protocol Buffers定义gRPC服务和方法上使用的request和response类型。
gRPC允许我们定义四种服务方法:
(1)简单RPC服务
// 一个简单的RPC:一个客户端使用存根向服务端发送一个请求,然后等待响应,就想正常的方法调用。
rpc GetFeature(Point) returns (Feature) {}
(2)服务端流形式的RPC
// 一个服务端流形式的RPC: 客户端发送一个请求给服务端,然后获取到一个用户读取一系列返回消息的流。
// 客户端读取返回消息的流知道没有任何数据。
// 通过放置关键词 stream 在返回值类型
rpc ListFeatures(Rectangle) returns (stream Feature) {}
(3)客户端流形式的RPC
// 一个客户端流形式的RPC:客户端通过流的形式写入一系列消息给服务端。一旦消息写入完成,就会等待服务端读取完它们然后返回。
// 通过放置关键词 stream 在请求参数类型前面
rpc RecordRoute(stream Point) returns (RouteSummary) {}
(4)双向流的PRC
// 一个双向流的PRC:客户端和服务端都通过读写流发送一系列消息
// 两个流操作是独立的,因此客户端和服务端可以按照它们喜欢的任何方式进行读写操作。
// 例如:服务端能够等到接受完所有的消息之前写入消息,也可以轮流的读取一个消息然后写入一个消息。
// 消息的顺序被保存在每个流中
// 通过在请求参数类型和返回参数类型前放置stream关键词:
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
示例:src/main/proto/route_guite.proto
// 使用proto3语言
syntax = "proto3";
// 指定生成Java代码的包文件
option java_package = "com.hef.demo.grpc.api";
// 指定生成多个Java文件
option java_multiple_files = true;
// 指定生成Java文件的类名
option java_outer_classname = "RouteGuideProto";
// 定义服务
service RouteGuide {
/*
1. 在服务的内部定义rpc方法,并指定这些方法的请求参数和响应参数类型。
2. gRPC允许我们定义四种服务方法,这四种服务方法都能应用在 RouteGuide中。
*/
// 一个简单的RPC:一个客户端使用存根向服务端发送一个请求,然后等待响应,就想正常的方法调用。
rpc GetFeature(Point) returns (Feature) {}
// 一个服务端流形式的RPC: 客户端发送一个请求给服务端,然后获取到一个用户读取一系列返回消息的流。
// 客户端读取返回消息的流知道没有任何数据。
// 通过放置关键词 stream 在返回值类型
rpc ListFeatures(Rectangle) returns (stream Feature) {}
// 一个客户端流形式的RPC:客户端通过流的形式写入一系列消息给服务端。一旦消息写入完成,就会等待服务端读取完它们然后返回。
// 通过放置关键词 stream 在请求参数类型前面
rpc RecordRoute(stream Point) returns (RouteSummary) {}
// 一个双向流的PRC:客户端和服务端都通过读写流发送一系列消息
// 两个流操作是独立的,因此客户端和服务端可以按照它们喜欢的任何方式进行读写操作。
// 例如:服务端能够等到接受完所有的消息之前写入消息,也可以轮流的读取一个消息然后写入一个消息。
// 消息的顺序被保存在每个流中
// 通过在请求参数类型和返回参数类型前放置stream关键词:
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
}
message Point {
int32 latitude = 1;
int32 longitude = 2;
}
message Feature {
string name = 1;
Point location = 2;
}
message Rectangle {
Point lo = 1;
Point hi = 2;
}
message RouteSummary {
int32 point_count = 1;
int32 feature_count = 2;
int32 distance = 3;
int32 elapsed_time = 4;
}
message RouteNote {
Point location = 1;
string message = 2;
}
// 在RPC中未使用。相反,这个是序列化到磁盘的格式
// 本案例中:读取带有Feature列表的JSON文件数据,创建出Feature数据列表
message FeatureDatabase {
repeated Feature feature = 1;
}
三、第二步:生成客户端和服务端代码
接下来我们要根据.proto
文件中对服务的定义生成gRPC的客户端和服务端代码。我们使用带有特定gRPC Java插件protocol buffer编译器做这件事情。我们使用proto3编译器(proto3同时支持proto2和proto3语法)生成服务代码。
当使用Gradle或Maven的时候,protoc构建插件能作为构建的一部分来产生需要的代码,可以参考grpc-java README。
为项目添加maven依赖:
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty-shaded</artifactId>
<version>1.44.1</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
<version>1.44.1</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
<version>1.44.1</version>
</dependency>
<dependency> <!-- necessary for Java 9+ -->
<groupId>org.apache.tomcat</groupId>
<artifactId>annotations-api</artifactId>
<version>6.0.53</version>
<scope>provided</scope>
</dependency>
项目的初始状态:
3.1 方式一:手动执行protoc命令生成服务代码
protoc -I=/Users/lifei/Documents/workspace/githubRepositoies/JAVARebuild/projects/action-rpcfx-demo/demo-grpc-api/src/main/proto --java_out=/Users/lifei/Documents/workspace/githubRepositoies/JAVARebuild/projects/action-rpcfx-demo/demo-grpc-api/src/main/java route_guide.proto
执行上面的命令后生成的代码结构为:
3.2 方式二:通过protobuf-base的代码生成器,直接生成编译后的class文件,不生成源代码
这种方式需要将.proto
文件放在src/main/proto
和src/test/proto
目录下,并在pom.xml
中引入插件:
<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.19.2:exe:${os.detected.classifier}</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:1.44.1:exe:${os.detected.classifier}</pluginArtifact>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
然后执行mvn compile,发现在target/classes
下生成了编译后的class文件,而src/main/java
下并没有源代码:
3.3 从服务定义文件生成的classes
Feature.java
、Point.java
、Rectangle.java
和其他的,它们都包含了填充、序列化和接受请求和响应消息类型的代码。RouteGuideGrpc.java
包含的内容有:- 一个基于
RouteGuid
服务的实现,RouteGuideGrpc.RouteGuideImplBase
中包含了所有RouteGuid
服务中定义的方法; - stub类是让客户端和服务端进行通信;
- 一个基于
3.4 API的稳定性
带有@Internal
注解的APIs,它们是被gRPC库内部使用的,不应该被gRPC的用户使用。带有@ExperimentalApi
注解的APIs它们主要是为了改变将来发布的版本,其他项目依赖的库代码不应该使用这些APIs。
推荐使用grpc-java-api-checker(一个Erro Prone插件)在任何依赖gRPC的库代码中检查@ExperimentalApi
和@Internal
注解的使用。它也能用于在非库代码中检查@Internal
的使用或者意外使用了@ExperimentalApi
。
3.5 高级组件
对库代码的高层有三个明显的层级:存根(Stub)、管道(Channel)、传输(Transport)。
(1)存根(Stub)层
存根层(Stub)是暴露给广大开发者,提供类型安全的你正在适配的数据绑定(datamodel)、接口定义(IDL)、接口。gRPC带有一个protocol-buffers编译器插件,能通过.proto
文件生成存根接口,并且绑定其他的数据模型和接口定义也很容易并且是鼓励的。
(2)管道(Channel)层
管道层是传输(Transport)处理之上的抽象,它用于拦截、装饰,并且它暴漏出比存根层更多的行为给应用。它故意让应用框架的使用者容易使用这一层去解决横切问题,例如日志、监控、身份验证等等。
(3)传输(Transport)层
传输层(Transport)用于完成从线路上放置和提取字节的工作。那个接口刚好足够插入不同的实现,注意传输层的API是被考虑用于gRPC内部,其保证比核心包io.grpc
下面的接口代码更弱。
gRPC提供了三种传输的实现:
- 基于Netty的传输,这种方式是基于Neety的主要传输实现。既可以用于客户端,也可以用于服务端;
- 基于OkHttp的传输,这种方式是基于OkHttp的轻量级传输实现。它主要用于安卓并且仅仅用于客户端;
- 内部进程传输,当服务端是和客户端在同一个进程的时候使用。它用于测试,同时也可以安全的用于生产;
四、第三步:创建服务
为了创建我们自己的RouteGuide
服务,有两部分工作要做:
- 覆盖从我们服务定义产生的基础服务类:在我们的类中去做真正的工作;
- 运行一个gRPC服务去监听来自客户端的请求,并返回服务响应;
下面是自己定义的服务案例:src/main/java/com/hef/demo/grpc/server/RouteGuideService.java。
(1)实现RouteGuideService
RouteGuideService
继承RouteGuideGrpc.RouteGuideImplBase
,并实现了我们所有的服务方法。
/**
* 一个简单的服务实现
* @Date 2022/2/26
* @Author lifei
*/
public class RouteGuideService extends RouteGuideGrpc.RouteGuideImplBase{
private final Collection<Feature> features;
public RouteGuideService(Collection<Feature> features) {
this.features = features;
}
// ......
}
(2)简单的RPC
/**
* 从客户端获取Point,并从Feature数据库返回相应的Feature信息
* @param request 请求参数
* @param responseObserver 一个响应的观察者,它是一个特定的接口为了让服务端去调用传递它的响应
*/
@Override
public void getFeature(Point request, StreamObserver<Feature> responseObserver) {
responseObserver.onNext(checkFeature(request));
responseObserver.onCompleted();
}
private Feature checkFeature(Point location) {
for (Feature feature : features) {
if (feature.getLocation().getLatitude() == location.getLatitude()
&& feature.getLocation().getLongitude()==location.getLongitude())