文章目录
- 1. gRPC简介
- 2. Http2.0协议
- 3. 序列化-Protobuf
- 4. gRPC开发实战环境搭建
- 5. gRPC的四种通信方式(重点)
- 6. gRPC的代理方式
- 7. SprintBoot整合gRPC
1. gRPC简介
gRPC是由google开源的高性能的RPC框架。它是由google的Stubby这样一个内部的RPC框架演化出来,gRPC2015年开源,目前是在云原生时代的一个RPC的标准。
gRPC的核心设计思路:
- 协议:使用Http2协议(传输数据使用二进制数据内容、支持双向流[双工]、连接的多路复用)
- 序列化:基于二进制(protobuf- 谷歌开源的一种序列化方式)
- 代理的创建 :让调用者像调本地方法一样去调用远端的方法
Thrift和gRPC的区别:首先Thrift和gRPC这两个RPC框架有一个共性,就是都支持异构语言的RPC
- 网络通信层面:Thrift自己定义了自己的协议,直接基于TCP协议,而gRPC的协议是HTTP2的协议
- 性能角度:ThriftRPC的效率是高于gRPC
- gRPC是大厂背书(google),云原生时代gRPC与其它组件合作的更加好,所以gRPC应用的更广泛
gRPC的优点:
- 能够高效的进行进程间通信(协议+序列化)
- 支持多种语言,对主流的语言提供了原生的支持(C、GO、Java)
- 支持多平台运行(Linux、Android、IOS、Mac1OS、Windows)
- grpc序列化方式使用prorubuf、效率高
- 使用Http2协议
- google大厂背书
2. Http2.0协议
回顾HTTP1.x协议:
- Http1.0协议:基于请求响应的模式,并且是一个短连接(无状态的协议),传输文本格式的数据,并且是单工的(只有客户端找服务端,而无法实现服务端的主动推送)。
这里思考一个问题:Http底层使用TCP协议,TCP是一个长连接协议,为什么Http1.0是一个短连接协议?
因为HTTP1.0的短连接是自己的设计造成的,一次数据发送完毕就会主动断开,导致了Http1.0是一个短连接协议。这种设计是因为当时服务器性能较差,无法支持维持大量的长连接。
- Http1.1协议:基于请求响应模式,但它支持有限的长连接(保持一段时间,一段时间
Keepalived
字段后自动断开连接),基于此出现了WebSocket技术。
总结:Http1.x协议它们传输数据都是文本格式(Http1.1请求体可以是二进制数据),可读性好但是效率低。Http1.x协议无法实现双工通信。Http1.x资源请求时,需要发送多个请求,建立多个连接(比如客户端从服务端拿一个网页,一次请求拿到了这个页面,但是这个页面如果里面有超链接、和CSS以及js资源,而这写资源又要发送新的请求,所以需要建立多个连接)
Http2.0协议
- Http2.0协议是一个二进制协议,传输效率高于Http1.x协议,但是二进制可读性差
- 可以实现双工通信
一个请求可以请求多次和多个数据(多路复用)
为什么Http2.0可以实现多路复用?
Http2.0抽取了3个重要的概念,分别是数据流(Stream)、消息(Message)和帧(Frame),这三个概念有机的整合在一起就可以实现多路复用。
- Http2.0发送的一个数据时是以一个Stream为宏观单位的,例如一个连接上有3个数据流(Stream),每个数据流代表我们请求的一个功能。假如在Http1.x协议中,我们请求一个页面首先会请求到一个HTML页面,然后就会异步的发送两个请求来请求CSS资源和JS资源,所以需要三个连接。而在Http2.0中它会复用一个连接,创建3个Stream流,一个Stream流负责获取HTML页面,另一个Stream流负责获取CSS资源而最后一个Stream流负责获取JS资源,重要的是这三个Stream流会放在一个连接上(连接复用)。
- 而一个Stream中会有一个Message,这个Message里面会有两个frame,一个frame放置请求头,一个frame放置请求体
- 同时响应也可以有多个Stream
其它概念:
- 数据流的优先级:各个Stream有优先级,通过给不同的Stream设置权重,来限制不同流的传输顺序
- 可以做流控,如果客户端的流的发送速度大于服务端处理数据,导致服务端处理不过来,此时服务端可以通知客户端暂时停止发送流
3. 序列化-Protobuf
Protobuf是一种与编程语言无关(它自己定义的IDL语言),与平台无关(操作系统)。它定义了中间语言,可以方便在客户端和服务端中进行RPC传输。Protobuf有两种协议版本,一个是Protobuf2一个是Protobuf3,目前主流使用的是Protobuf3。在使用Protobuf的过程中我们需要按照Protobuf的编译器,这个编译器的作用就是可以把Protobuf的IDL的语言转换为具体某一种开发语言。
- Protobuf编译器的安装
安装网址:https://github.com/protocolbuffers/protobuf/releases
idea同时安装Protobuf插件,方便开发。
注意2021.2版本后的idea是内置了Protobuf插件的,老版本可以选装ProtoBuf版本,但是二者是不能共存的
- Protobuf基本使用(Protobuf3)
文件格式:Protobuf所有的内容都需要写在一个
.proto
文件中
版本设定:
.proto
文件的第一行,syntax="proto3";
注释:
- 单行注释:
//
- 多行注释:
/**/
与java语言相关的语法:
//后续protobuf生产的java代码是一个源文件还是多个源文件
option java_multiple_files = true/false;
//指定protobuf生产的java源文件放置的位置
option java_package="com.jackiechai";
//指定protobuf生成的java类封装的大类的名称(因为protobuf生成的所有java类都会成为一个大类的内部类)
option java_outer_classname="UserService";
逻辑包:
package xxx;
用于protobuf对于文件内容的管理
导入:在实际的开发中我们可能有多个
.proto
,每个.proto
管理自己的内容,现在可能出现一个问题,其中一个.proto
依赖另一个.proto
的内容,这里就需要使用导入语法:import "xxx/Userservice.proto"
- ProtoBuf基本类型
官网可以查看ProtoBuf所支持的所有基本类型:https://protobuf.dev/programming-guides/proto3/
- 枚举类型
enum SEASON{
SPRING = 0;//0和1代表字段的编号
SUMMER = 1;
}
注意枚举的编号是从0开始的
- 消息Message
message LoginRequest{
string username = 1;//1对应这个内容在消息中的编号
string password = 2;
int32 age = 3;
}
客户端就是通过message来进行信息交流。
编号:编号是1开始到 2 2 9 − 1 2^29-1 229−1结束,注意其中19000-19999不能用,这个区间内的编号是protobuf自己保留的
关键字:消息中可以加入两个关键字,singular
,这个是修饰消息字段的,可写也可以不写,表示这个字段的值只能是0个或者1个(默认关键字)。
message LoginRequest{
singular string username = 1;//1对应这个内容在消息中的编号
singular string password = 2;
singular int32 age = 3;
}
repeated
修饰的字段,表示这个字段的返回值是多个,等价于java中的list:
message LoginRequest{
string content=1;
repeated string stutas=2;
}
上面content是singular类型,stutas是repeated类型。
在protobuf中消息是可以嵌套的:
message searchResponse{
message Result{
string url=1;
string title=2;
}
string xxx=1;
int32 yyy=2;
Result ppp=3;
}
message AAA{
string xxx=1;
searchResponse.Result yyy=2;
}
oneof
关键字,如下我们在使用test_oneof对象时,我们只能代表它的值之一,例如test_oneof可以代表name或者代表age。
message simpleMessage{
oneof test_oneof{
string name=1;
int32 age=2;
}
test_oneof xxxx=1;
}
- 服务的定义
service HelloService{
//LoginRequest和HelloResponse就是前面的Message
rpc hello(LoginRequest) returns(HelloResponse){
//hello就相当于一个服务接口,具体的服务逻辑是服务提供方来实现的
}
}
一个服务,例如上面HelloService,里面是可以定义多个服务方法的,而且根据我们的需要我们是可以定义多个服务的。对于gRPC来说它的服务方式是分成4种情况的,这里先不详细分析。
4. gRPC开发实战环境搭建
- 项目结构
- xxxx-api模块:定义protobuf的idl语言,并且通过命令来创建具体的代码,后序服务端和客户端引入使用(客户端和服务端公共模块),包括定义message和service
- xxxx-server模块:服务提供方模块。实现API模块中定义的接口,需要和具体的业务相结合。然后发布整个gRPC服务(创建服务端程序)
- xxxx-client模块:创建服务端的代理,基于这个代理来进行RPC调用
- API模块开发
- 创建API模块
2. 编写proto文件
syntax = "proto3";
option java_multiple_files = false;
option java_package = "com.jackie";
option java_outer_classname = "HelloProto";
/**
发布RPC服务
*/
//定义请求消息
message HelloRequest{
string name = 1;
}
//定义响应消息
message HelloResponse{
string result = 1;
}
/**
开发service
*/
service HelloService{
rpc hello(HelloRequest) returns (HelloResponse){
}
}
3. 将ProtoBuf的IDL内容转换为具体使用的语言的代码(protoc这个编译器完成的,它可以将这个IDL语言转换为我们使用的目标语言),所以我们要做的就是将这个proto文件进行编译:
protoc --java_out=/***/*** /***/***/***.proto
#--java_out=:表示生成java代码
# /***/***:生成的代码文件放的位置
#/***/***/***.proto:定义好的proto文件
上面是编译命令,但这种方式需要频繁的使用终端所以不适合开发。下面看看我们怎么在开发中怎么使用:
其实我们可以使用Maven插件来进行protobuf IDL文件的编译操作。首先我们需要引入相关的依赖:
<dependencies>
<!-- grpc-netty-shaded说明grpc底层通信使用的是netty框架-->
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty-shaded</artifactId>
<version>1.63.0</version>
<scope>runtime</scope>
</dependency>
<!-- protobuf使用所需的相关依赖-->
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
<version>1.63.0</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
<version>1.63.0</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>
</dependencies>
引入相关的maven插件:
<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.7.1</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.25.1:exe:${os.detected.classifier}</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:1.63.0:exe:${os.detected.classifier}</pluginArtifact>
<outputBaseDirectory>${basedir}/src/main/java</outputBaseDirectory>
<clearOutputDirectory>false</clearOutputDirectory>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
使用插件编译proto文件:
这样就生成java文件了:
我们仔细分析一下上面java文件的组成:
首先第一个类名是HelloProto这个类名就是我们在 proto
文件中定义的外部类的名称:
option java_outer_classname = "HelloProto";
HelloRequest和HelloResponse都是在proto
文件中定义过的:
它们都是HelloProto的内部类。HelloProto主要就是用来处理协议消息的。HelloServiceGrpc主要就是用来处理Grpc了。我们来看以下它的内部类,首先是HelloServiceImplBase
它就代表我们的业务接口,后面服务方暴露业务接口给外界时就需要实现这个接口。以stub
结尾的这些类对应的就是客户端的代理对象,这些stub都是客户端的代理,区别就是网络通信方式不同(阻塞、异步等)。
- Server模块开发
主要包括两个过程,首先需要实现业务接口,添加具体的功能,然后创建服务端(Netty)。
- 实现业务接口并添加具体的功能
创建Service模块:
创建ServiceImpl类
public class HelloServiceImpl extends HelloServiceGrpc.HelloServiceImplBase {
/**
* 1. 接收客户端提交的参数
* 2. 业务处理
* 3. 将处理结果返回给调用者
*
* @param request 客户端请求的message
* @param responseObserver 返回给客户端的结果
*/
public void hello(HelloProto.HelloRequest request, StreamObserver<HelloProto.HelloResponse> responseObserver) {
//接收客户端的请求参数
String name = request.getName();
//业务处理
System.out.println("name parameter" + name);
//封装响应
// 1.创建响应对象的构造者
HelloProto.HelloResponse.Builder builder = HelloProto.HelloResponse.newBuilder();
// 2.填充数据
builder.setResult("hello method invoke");
// 3.封装响应
HelloProto.HelloResponse helloResponse = builder.build();
responseObserver.onNext(helloResponse);
responseObserver.onCompleted();
}
}
- 创建服务端
public class GrpcServer1 {
public static void main(String[] args) throws IOException, InterruptedException {
//绑定端口
ServerBuilder serverBuilder = ServerBuilder.forPort(9000);
//有一个服务就调用一个
serverBuilder.addService(new HelloServiceImpl());
//创建服务对象
Server server = serverBuilder.build();
server.start();
server.awaitTermination();
}
}
服务端本质是用的netty
- Client模块开发
客户端就是通过代理对象完成远端对象的调用。
- 创建客户端模型,并导入依赖
2. 创建客户端
public static void main(String[] args) {
//创建通信的管道(netty)
ManagedChannel managedChannel= ManagedChannelBuilder.forAddress("localhost",9000).usePlaintext().build();
try{
//获得代理对象(阻塞模式)
HelloServiceGrpc.HelloServiceBlockingStub helloServiceBlockingStub= HelloServiceGrpc.newBlockingStub(managedChannel);
//完成rpc调用
//1. 准备参数
HelloProto.HelloRequest.Builder builder=HelloProto.HelloRequest.newBuilder();
builder.setName("jackie");
HelloProto.HelloRequest build = builder.build();
HelloProto.HelloResponse hello = helloServiceBlockingStub.hello(build);
System.out.println(hello);
}catch (Exception e){
throw new RuntimeException(e);
}finally {
managedChannel.shutdown();
}
}
- 测试结果
我们回顾一下前面的服务端代码:
request对象里面就封装了客户端传过来的消息,responseObserver就是需要返回给客户端的消息会封装到这个对象中。调用responseObserver
的onNext
方法就可以把处理后的响应消息通过网络回传给客户端,onCompleted
这个方法的作用就是用于通知客户端响应已经结束了。
5. gRPC的四种通信方式(重点)
- gRPC的四种通信方式如下:
- 简单rpc(我们上面的应用程序就是简单rpc),或者是一元rpc(Unary RPC)
- 服务端流式rpc(Server Stream RPC)
- 客户端流式rpc(Client Stream RPC)
- 双向流RPC(Bi-directional Stream RPC)
- 简单rpc
一元rpc的特点如下:
- 当客户端发起调用后,会提交数据,并且等待服务端的响应
- 开发过程中主要采用的就是一元rpc这种通信方式
一元rpc的语法就是:
service HelloService{
rpc hello(HelloRequest) returns (HelloResponse){
}
}
- 服务端流式rpc:客户端一个请求对象,服务端可以回传多个
服务端流式rpc的特点如下:
服务端流式rpc的使用场景有:
例如股票系统中,客户端发送一个股票的编号到服务端(一个数据),服务端就会返回一系列数据,这些数据就是这个股票在某一时刻的行情。
服务端流式rpc的语法变动如下:
service HelloService{
rpc hello(HelloRequest) returns (stream HelloResponse){
}
}
在返回值前面加stream关键字。然后客户端接收到的消息类型是Iterator类型,是一个迭代器,其它内容没有什么变动。
上面的代码中客户端是阻塞的,所以在接收消息时一直会处于阻塞状态,现在我们重写客户端代码,让其采用异步的方式来实现流式rpc的开发。
public static void main(String[] args) {
//创建通信的管道(netty)
ManagedChannel managedChannel= ManagedChannelBuilder.forAddress("localhost",9000).usePlaintext().build();
try{
//获得代理对象(异步模式)
HelloServiceGrpc.HelloServiceStub helloServiceBlockingStub= HelloServiceGrpc.newStub(managedChannel);
//完成rpc调用
//1. 准备参数
HelloProto.HelloRequest.Builder builder=HelloProto.HelloRequest.newBuilder();
builder.setName("jackie");
HelloProto.HelloRequest build = builder.build();
HelloProto.HelloResponse hello = helloServiceBlockingStub.hello(build, new StreamObserver<HelloProto.HelloResponse>() {
//监听服务端的onNext方法(这个方法处理数据)
public void onNext(HelloProto.HelloResponse helloResponse) {
}
//这个方法处理异常
public void onError(Throwable throwable) {
}
//这个处理服务端响应结束
public void onCompleted() {
}
});
//一定要加这句代码,因为上面代码是没有阻塞的,所以客户端的回调方法还没来得及调,客户端就结束了
managedChannel.awaitTermination(12,TimeUnit.SECONDS);
}catch (Exception e){
throw new RuntimeException(e);
}finally {
managedChannel.shutdown();
}
}
- 客户端流式rpc
所谓的客户端流式rpc,指的是客户端发送多个请求对象,服务端只返回一个结果。
客户端流式rpc的语法变动如下:
service HelloService{
rpc hello(stream HelloRequest) returns (HelloResponse){
}
}
- 双向流式rpc
意思就是客户端可以发送多个请求消息,服务端同时也可以发送多条响应消息(例如聊天室)。
双向流式rpc的语法变动如下:
service HelloService{
rpc hello(stream HelloRequest) returns (stream HelloResponse){
}
}
6. gRPC的代理方式
gRPC的代理方式有下面几种:
- BlockingStub:阻塞通信方式
- Stub:异步通过监听处理
- FutureStub:异步方式(类似于netty中的future),同步异步都支持
前两种都分析了,现在来分析FutureStub,这种代理方式只能用于一元RPC
public static void main(String[] args){
ManagedChannel managedChannel=ManagedChannelBuilder.forAddress("localhost",9000).userPlaintext().build();
try{
TestServiceGrpc.TestServiceFuture testServiceFutureStub=TestServiceGrpc.newFutureStub(managedChannel);
ListenableFuture<TestProto.TestResponse> responseListenableFuture=testServiceFutureStub.testSuns(HelloProto.TestRequest.newBuilder().setName("x").build());
//获取响应内容(同步)
TestProto.TestResponse testResponse=responseListenableFuture.get();
System.out.println(restResponse.getResult());
//异步方式
/**
Futuress.addCallback(responseListenableFuture,new FutureCallback<TestProto.TestResponse>(){
@Override
public void onSuccess(TestProto.TestResponse result){
}
@Override
public void onFaulure(Throwable t){
}
},Executors.newCachedThreadPool());
**/
managedChannel.awaitTermination(12,TimeUnit.SECONDS);
}catch(Exception e){
e.printStackTrace();
}finally{
managedChannel.shutdown();
}
}
7. SprintBoot整合gRPC
- SpringBoot和gRPC整合的思想
gRPC与SpringBoot整合主要是两种思想:首先是服务端如何整合,另外就是客户端如何整合。
- SpringBoot与gRPC整合过程中对于服务端做了什么封装
原生的gprc在创建服务端的时候就做了两件事,首先是实现服务接口,然后创建服务端发布gRPC功能。
public class HelloServiceImpl extends HelloServiceGrpc.HelloServiceImplBase {
}
上面的HelloServiceImplBase
就是通过protoBuf的IDL文件生成的一个服务接口,而SpringBoot无法提供实现服务接口这个工作进行封装的(这部分代码是不固定的)。而服务端的启动代码如下:
public static void main(String[] args) throws IOException, InterruptedException {
//绑定端口
ServerBuilder serverBuilder = ServerBuilder.forPort(9000);
//有一个服务就调用一个
serverBuilder.addService(new HelloServiceImpl());
//创建服务对象
Server server = serverBuilder.build();
server.start();
server.awaitTermination();
}
上面代码就是构建服务端的过程,这个过程SpringBoot是可以封装的(相对固定,变动的地方是服务端端口和服务发布的过程)。
- 下面开始创建服务端:
- 搭建开发环境
- 搭建SpringBoot的开发环境
- 引入与Grpc相关的内容
创建服务端模块:
创建api模块:
syntax = "proto3";
option java_multiple_files = false;
option java_package = "com.jackie";
option java_outer_classname = "HelloProto";
/**
发布RPC服务
*/
//定义请求消息
message HelloRequest{
string name = 1;
}
//定义响应消息
message HelloResponse{
string result = 1;
}
/**
开发service
*/
service HelloService{
rpc hello(HelloRequest) returns (HelloResponse){}
rpc c2ss(HelloRequest) returns(stream HelloResponse){}
rpc cs2s(stream HelloRequest) returns(HelloResponse){}
rpc cs2ss(stream HelloRequest) returns(stream HelloResponse){}
}
在服务端模块中引入api模块的依赖:
<dependency>
<groupId>org.example</groupId>
<artifactId>rpc-grpc-myapi</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
在server模块中引入gRPC依赖:
<dependency>
<groupId>net.devh</groupId>
<artifactId>grpc-server-spring-boot-starter</artifactId>
<version>2.14.0.RELEASE</version>
</dependency>
这个依赖里面就帮我们做了gRPC服务端的封装(不是官方的支持)。
- 创建gRPC服务端
@GrpcService
public class HelloServiceImpl extends HelloServiceGrpc.HelloServiceImplBase {
@Override
public void hello(HelloProto.HelloRequest request, StreamObserver<HelloProto.HelloResponse> responseObserver) {
//拿到请求参数
String name=request.getName();
System.out.println("name is "+name);
responseObserver.onNext(HelloProto.HelloResponse.newBuilder().setResult("this is result").build());
responseObserver.onCompleted();
}
@Override
public void c2ss(HelloProto.HelloRequest request, StreamObserver<HelloProto.HelloResponse> responseObserver) {
super.c2ss(request, responseObserver);
}
@Override
public StreamObserver<HelloProto.HelloRequest> cs2s(StreamObserver<HelloProto.HelloResponse> responseObserver) {
return super.cs2s(responseObserver);
}
@Override
public StreamObserver<HelloProto.HelloRequest> cs2ss(StreamObserver<HelloProto.HelloResponse> responseObserver) {
return super.cs2ss(responseObserver);
}
}
- 配置gRPC服务的端口号
- 启动服务类
我们看控制台的数据,我们可以发现一个问题,就是启动过程中Tomcat也启动了,实际上我们服务端是用的grpc服务端所以我们不需要Tomacat,所以在pom中排除Tomcat。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
到此服务端开发就完毕了。
- 下面开始创建客户端
传统的gPPC的客户端的开发流程为:首先通过管道设置服务端的ip和端口,然后获取stub(远端功能的代理)。
- 环境搭建
流程和server创建的过程一样。
然后对一些重要参数进行配置:
spring:
application:
name: grpc-client
grpc:
client:
grpc-server:
address: 'static://127.0.0.1:9000'
negotiation-type: plaintext
- grpc客户端开发
创建controller
@RestController
public class TestController {
//注入代理对象
@GrpcClient("grpc-server")
private HelloServiceGrpc.HelloServiceBlockingStub stub;
@RequestMapping("/test")
public String test1(String name) {
System.out.println("name=" + name);
HelloProto.HelloResponse helloResponse = stub.hello(HelloProto.HelloRequest.newBuilder().setName(name).build());
return helloResponse.getResult();
}
}
这里就完了了客户端的创建,上面其实就是SpringBoot+gRPC实现了一元RPC。