gRPC框架学习笔记
一.什么是RPC
1.简介:
RPC:Remote Procedure Call,远程过程调用。简单来说就是两个进程之间的数据交互。
正常服务端的接口服务是提供给用户端(在Web开发中就是浏览器)或者自身调用的,也就是本地过程调用。
和本地过程调用相对的就是:假如两个服务端不在一个进程内怎么进行数据交互?使用RPC。
尤其是现在微服务的大量实践,服务与服务之间的调用不可避免,RPC更显得尤为重要。
2.原理:
计算机的世界中不管使用哪种技术,核心都是对数据的操作。RPC不过是将数据的操作垮了一个维度而已。
解决的问题本质上只是数据在不同进程间的传输。我们所说的RPC一般是指在传输层使用TCP协议进行的数据交互,
也有很多基于HTTP的成熟框架。
上图描述了一个RPC的完整调用流程:
1:client向client stub发起方法调用请求。
2:client stub接收到请求后,将方法名,请求参数等信息进行编码序列化。
3:client stub通过配置的ip和端口使用socket通过网络向远程服务器server发起请求。
4:远程服务器server接收到请求,解码反序列化请求信息。
5:server将请求信息交给server stub,server stub找到对应的本地真实方法实现。
6:本地方法处理调用请求并将返回的数据交给server stub。
7:server stub 将数据编码序列化交给操作系统内核,使用socket将数据返回。
8:client端socket接收到远程服务器的返回信息。
9:client stub将信息进行解码反序列化。
10:client收到远程服务器返回的信息。
上图中有一个stub(存根)的概念。
stub负责接收本地方法调用,并将它们委托给各自的具体实现对象。
server端stub又被称为skeleton(骨架)。可以理解为代理类。而实际上基于Java的RPC框架stub基本上也都是使用动态代理。
我们所说的client端和server端在RPC中一般也都是相对的概念。
而所谓的RPC框架也就是封装了上述流程中2-9的过程,让开发者调用远程方法就像调用本地方法一样。
二.常用的RPC框架选型
Dubbo
- 阿里开源的基于TCP的RPC框架,基本上是国内生产环境应用最广的开发框架了。使用zookeeper做服务的注册与发现,使用Netty做网络通信。遗憾的是不能跨语言,目前只支持Java。
Thrift
- Facebook开源的跨语言的RPC框架,通过IDL来定义RPC的接口和数据类型,使用thrift编译器生成不同语言的实现。据说是目前性能最好的RPC框架,只是暂没使用过。
gRPC
- 这个是我们今天要聊的重点。gRPC是Google的开源产品,是跨语言的通用型RPC框架,使用Go语言编写。 Java语言的应用同样使用了Netty做网络通信,Go采用了Goroutine做网络通信。序列化方式采用了Google自己开源的Protobuf。请求的调用和返回使用HTTP2的Stream。
SpringCloud
- SpringCloud并不能算一个RPC框架,它是Spring家族中一个微服务治理的解决方案,是一系列框架的集合。但在这个方案中,微服务之间的通信使用基于HTTP的Restful API,使用Eureka或Consul做服务注册与发现,使用声明式客户端Feign做服务的远程调用。这一系列的功能整合起来构成了一套完整的远程服务调用。
如何选择
如果公司项目使用Java并不牵扯到跨语言,且规模并没有大到难以治理,我推荐Dubbo。如果项目规模大,服务调用错综复杂,我推荐SpringCloud。
如果牵扯到跨语言,我推荐gRPC,这也是目前我司的选择。即使Thrift性能是gRPC的2倍,但没办法,它有个好爹,现在我们的开发环境考虑最多的还是生态,不得不向Google爸爸臣服。
三.gRPC原理
一个RPC框架必须有两个基础的组成部分:数据的序列化和进程数据通信的交互方式。
对于序列化gRPC采用了自家公司开源的Protobuf。什么是Protobuf?先看一句网络上 大部分的解释:
Google Protocol Buffer(简称 Protobuf)是一种轻便高效的结构化数据存储格式,平台无关、语言无关、可扩展,可用于通讯协议和数据存储等领域。
上句有几个关键点:它是一种数据存储格式,跨语言,跨平台,用于通讯协议和数据存储。
这么看和我们熟悉的JSON类似,但其实着重点有些本质的区别:
JSON主要是用于数据的传输,因为它轻量级,可读性好,解析简单。
Protobuf主要是用于跨语言的IDL,它除了和JSON、XML一样能定义结构体之外,还可以使用自描述格式定于出接口的特性,
并可以使用针对不同语言的protocol编译器产生不同语言的stub类。所以天然的适用于跨语言的RPC框架中。
而关于进程间的通讯,无疑是Socket。Java方面gRPC同样使用了成熟的开源框架Netty。使用Netty Channel作为数据通道。传输协议使用了HTTP2。
通过以上的分析,我们可以将一个完整的gRPC流程总结为以下几步:
通过.proto文件定义传输的接口和消息体。
通过protocol编译器生成server端和client端的stub程序。
将请求封装成HTTP2的Stream。
通过Channel作为数据通信通道使用Socket进行数据传输。
四.gRPC传输格式protobuf
protobuf (Google Protocol Buffers) 是Google提供- -个具有高效的协议
数据交换格式工具库(类似Json),Protobuf有 更高的转化效率,时间效率
和空间效率都是JSON的3-5倍。
五、实例备战
news服务端
1、依赖
<dependencies> <!-- https://mvnrepository.com/artifact/io.grpc/grpc-netty-shaded --> <dependency> <groupId>io.grpc</groupId> <artifactId>grpc-netty-shaded</artifactId> <version>1.42.0</version> </dependency> <!-- https://mvnrepository.com/artifact/io.grpc/grpc-protobuf --> <dependency> <groupId>io.grpc</groupId> <artifactId>grpc-protobuf</artifactId> <version>1.42.0</version> </dependency> <!-- https://mvnrepository.com/artifact/io.grpc/grpc-stub --> <dependency> <groupId>io.grpc</groupId> <artifactId>grpc-stub</artifactId> <version>1.42.0</version> </dependency> <!-- https://mvnrepository.com/artifact/org.apache.tomcat/annotations-api --> <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>annotations-api</artifactId> <version>6.0.53</version> <scope>provided</scope> </dependency> </dependencies> <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.21.4:exe:${os.detected.classifier}</protocArtifact> <pluginId>grpc-java</pluginId> <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.42.0:exe:${os.detected.classifier}</pluginArtifact> </configuration> <executions> <execution> <goals> <goal>compile</goal> <goal>compile-custom</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
2、创建proto(服务定义脚本)
3、利用protobuf:compile 生成存根类protobuf:compile-custom 生成grpc通讯类
4、服务端核心业务service
public class NewsService extends NewsServiceGrpc.NewsServiceImplBase {
@Override
public void list(NewsProto.NewsRequest request, StreamObserver<NewsProto.NewsResponse> responseObserver) {
String date =request.getDate();
NewsProto.NewsResponse newList=null;
try {
NewsProto.NewsResponse.Builder newListBuilder=NewsProto.NewsResponse.newBuilder();
for (int i = 0; i <=100; i++) {
NewsProto.News news = NewsProto.News.newBuilder().setId(i)
.setContent(i)
.setTitle("新闻标题"+i)
.setCreateTime(System.currentTimeMillis())
.build();
newListBuilder.addNews(news);
}
newList=newListBuilder.build();
}catch (Exception e){
responseObserver.onError(e);
}finally {
responseObserver.onNext(newList);
}
responseObserver.onCompleted();
}
}
5、开发启动器
public class GrpcServer {
private static final int port =9999;
public static void main(String[] args) throws IOException, InterruptedException {
Server server = ServerBuilder.forPort(port).addService(new NewsService()).build().start();
System.out.println(String.format("GRPC服务端启动成功,端口号:%d",port));
server.awaitTermination();
}
}
news客户端
1、2、3同上
4、客户端类发起远程调用
public class NewsClient {
private static final String host ="localhost";
private static final int serverPort =9999;
public static void main(String[] args) {
ManagedChannel channel = ManagedChannelBuilder.forAddress(host, serverPort).usePlaintext()
//无需加密或认证
.build();
try {
NewsServiceGrpc.NewsServiceBlockingStub blockingStub = NewsServiceGrpc.newBlockingStub(channel);
NewsProto.NewsRequest request = NewsProto.NewsRequest.newBuilder().setDate("20220801").build();
NewsProto.NewsResponse newsResponse = blockingStub.list(request);
List<NewsProto.News> newsList = newsResponse.getNewsList();
for (NewsProto.News news:newsList
) {
System.out.println(news);
}
} finally {
//关闭
channel.shutdown();
}
}
}
六、ProtoBuf语法
// 指定的当前proto语法的版本,有2和3
syntax = "proto3";
// 生成多个类
option java_multiple_files = false;
//生成java类所在的包
option java_package = "com.lim.news.proto";
//生成外层类包
option java_outer_classname = "NewsProto";
//包名.proto
package news;
//定义RPC服务RouteGuide
service NewsService{
// list 方法名,NewsRequest表示传入的参数,
rpc list (NewsRequest) returns (NewsResponse) {}
}
// 消息是gRPC描述信息的基本单位,类似Java的"实体类"
// 消息的名字,对应于生成代码后的类名
// 每一个消息都对应生成一个类, 根据java_ multiple_ files 设置不同文件数量也不同
// java_ multiple_ files=true, protobuf为每一个 消息自动生成一个java文件
// java_ multiple_ files=false, protobuf会生成一个大类, 消息作为子类集中在一个java 文件
message NewsRequest{
// 宇段:类型名称=索引值(id)
// 每个字段都要定义唯- -的索引值, 这些数字是用来在消息的二进制格式中识别各个字段的。
// 一旦 开始使用就不能够再改变,//最 小的标识号可以从1开始,最大到2^29 - 1, or 536,870, 911。
// 不可以使用其中的[19000 - 19999]的标识号,Protobuf 协议实现中对这些进行J预留。
// 切记:要为将来有可能添加的、频繁出现的标识号预留一些标识号。
string date = 1;
}
message NewsResponse{
//repeated说明是一个集合(数组),数组的每一个元素都是News对象
repeated News news = 1;// List<News> getNewsList();
}
//News新闻实体
message News{
int32 id = 1; //int id = 0
string title = 2;//String title =""
string content = 3 ;
int64 createTime =4;//long createTime= 0
}
类型对照表
存根类解析
七、通信模式介绍
1、一元RPC通信模式
概念:客户端向服务器发送单个请求并获得响应
实例(news-demo)
2、服务端流式RPC通信模式
概念:从窖户端发起1次请求,会产生从服务器的多次响应
服务端流式RPC proto文件
// 指定的当前proto语法的版本,有2和3
syntax = "proto3";
// 生成多个类
option java_multiple_files = false;
//生成java类所在的包
option java_package = "com.lim.sms.proto";
//生成外层类包
option java_outer_classname = "SmsProto";
//包名.proto
package sms;
service SmsService{
// stream 流式请求响应
rpc sendSms(SmsRequest) returns(stream SmsResponse);
}
message SmsRequest{
repeated string phoneNumber=1;
string content=2;
}
message SmsResponse{
string result=1;
}
服务端流式 客户端调用类
public class SmsClient {
private static final String host ="localhost";
private static final int serverPort =9999;
public static void main(String[] args) {
//建立网络通道
ManagedChannel channel = ManagedChannelBuilder.forAddress(host, serverPort).usePlaintext()
//无需加密或认证
.build();
//BlockingStub支持流式通信
try {
SmsServiceGrpc.SmsServiceBlockingStub blockingStub = SmsServiceGrpc.newBlockingStub(channel);
Iterator<SmsProto.SmsResponse> sendSms = blockingStub.sendSms(SmsProto.SmsRequest.newBuilder()
.setContent("下午三点开会")
.addPhoneNumber("130248622581")
.addPhoneNumber("130248682581")
.addPhoneNumber("130248681581")
.addPhoneNumber("130248686581")
.build()
);
while (sendSms.hasNext()){
SmsProto.SmsResponse next = sendSms.next();
System.out.println(next.getResult());
}
} finally {
channel.shutdown();
}
}
}
3、 客户端流式RPC通信模式
概念:从客户端发起不定次请求,产生从服务器的1次响应
客户端流式RPC proto文件
service SmsService{
// stream 流式请求响应
rpc createPhone(stream PhoneNumberRequest) returns (PhoneNumberResponse);
}
服务端逻辑层
@Override
public StreamObserver<SmsProto.PhoneNumberRequest> createPhone(StreamObserver<SmsProto.PhoneNumberResponse> responseObserver) {
// 异步通信基于responseObserver进行回调
return new StreamObserver<SmsProto.PhoneNumberRequest>(){
int i=0;
@Override
//接收到客户端发送过来的新的电话号码时,触发onNext
public void onNext(SmsProto.PhoneNumberRequest request) {
System.out.println(request.getPhoneNumber()+"手机号已注册");
i+=1;
}
@Override
public void onError(Throwable t) {
t.printStackTrace();
}
//客户端传输完毕,完成消息统计
@Override
public void onCompleted() {
responseObserver.onNext(SmsProto
.PhoneNumberResponse.newBuilder()
.setResult("本次导入"+i+"个员工号码").build());
responseObserver.onCompleted();
}
};
客户端调用
public class SmsClientAsync {
private SmsServiceGrpc.SmsServiceStub asyncStub=null;
private static final String host ="localhost";
private static final int serverPort =9999;
public static void main(String[] args) {
SmsClientAsync smsClientAsync = new SmsClientAsync();
//建立网络通道
ManagedChannel channel = ManagedChannelBuilder.forAddress(host, serverPort).usePlaintext()
//无需加密或认证
.build();
smsClientAsync.asyncStub=SmsServiceGrpc.newStub(channel);
try {
smsClientAsync.createPhone();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void createPhone() throws InterruptedException {
//实例化RequestObject,发起请求
StreamObserver<SmsProto.PhoneNumberRequest> requestObserver= asyncStub
.createPhone(responseObserver);
for (int i = 0; i <=10; i++) {
SmsProto.PhoneNumberRequest request = SmsProto
.PhoneNumberRequest
.newBuilder()
.setPhoneNumber(String.valueOf(12345678 + i))
.build();
requestObserver.onNext(request);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
requestObserver.onCompleted();
Thread.sleep(1000);
}
//监听服务器响应
StreamObserver<SmsProto.PhoneNumberResponse>responseObserver=new StreamObserver<SmsProto.PhoneNumberResponse>(){
//
@Override
public void onNext(SmsProto.PhoneNumberResponse response) {
System.out.println(response.getResult());
}
@Override
public void onError(Throwable t) {
t.printStackTrace();
}
@Override
public void onCompleted() {
System.out.println("处理完毕");
}
};
}
4 、双向流式RPC通信模式
概念:从客户端发起不定次请求,产生从服务器的不定次响应
双向流式RPC通信模式 proto文件
service SmsService{
// stream 流式请求响应
rpc createAndSendSms(stream PhoneNumberRequest) returns (stream PhoneNumberResponse);
}
服务端逻辑层
@Override
public StreamObserver<SmsProto.PhoneNumberRequest> createAndSendSms(StreamObserver<SmsProto.PhoneNumberResponse> responseObserver) {
// 异步通信基于responseObserver进行回调
return new StreamObserver<SmsProto.PhoneNumberRequest>(){
@Override
//接收到客户端发送过来的新的电话号码时,触发onNext,创建多个响应
public void onNext(SmsProto.PhoneNumberRequest request) {
//模拟写入操作
System.out.println(request.getPhoneNumber()+"已完成注册");
responseObserver.onNext(SmsProto.PhoneNumberResponse.newBuilder().setResult(request.getPhoneNumber()+"手机号已经完成注册,已发送给王经理").build());
responseObserver.onNext(SmsProto.PhoneNumberResponse.newBuilder().setResult(request.getPhoneNumber()+"手机号已经完成注册,已发送给林总").build());
responseObserver.onNext(SmsProto.PhoneNumberResponse.newBuilder().setResult(request.getPhoneNumber()+"手机号已经完成注册,已发送给叶总监").build());
}
@Override
public void onError(Throwable t) {
t.printStackTrace();
}
//客户传输完毕
@Override
public void onCompleted() {
responseObserver.onCompleted();
}
};
}
客户端调用( 与客户端流式相同)
八、Spring Boot集成 grpc
服务端
服务端pom
<dependency>
<groupId>net.devh</groupId>
<artifactId>grpc-server-spring-boot-starter</artifactId>
<version>2.13.0.RELEASE</version>
</dependency>
<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.17.3:exe:${os.detected.classifier</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:1.42.0:exe:${os.detected.classifier}</pluginArtifact>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
服务端逻辑层
@GrpcService
public class NewsService extends NewsServiceGrpc.NewsServiceImplBase {
@Override
public void hello(NewsProto.StringRequest request, StreamObserver<NewsProto.StringResponse> responseObserver) {
String name = request.getName();
NewsProto.StringResponse response=NewsProto.StringResponse.newBuilder().setResult("你好,gRPC"+name).build();
//调用onNext进行回传response
responseObserver.onNext(response);
//结束服务器端处理
responseObserver.onCompleted();
}
}
服务端配置文件
grpc:
server:
port: 9090
客户端
客户端pom
<dependency>
<groupId>net.devh</groupId>
<artifactId>grpc-client-spring-boot-starter</artifactId>
<version>2.13.0.RELEASE</version>
</dependency>
<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.17.3:exe:${os.detected.classifier}</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:1.42.0:exe:${os.detected.classifier}</pluginArtifact>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
客户端配置文件
grpc:
client:
grpc-server:
address: 'static://127.0.0.1:9090'
negotiationType: plaintext
客户端控制层
@RestController
public class NewsController {
@GrpcClient("grpc-server")
private NewsServiceGrpc.NewsServiceBlockingStub blockingStub;
@GetMapping ("sayhello/{name}")
public String sayHello( @PathVariable String name){
NewsProto.StringResponse response = blockingStub.hello(NewsProto.StringRequest.newBuilder().setName(name).build());
return response.getResult();
}
}
```
客户端配置文件
grpc:
client:
grpc-server:
address: 'static://127.0.0.1:9090'
negotiationType: plaintext
客户端控制层
@RestController
public class NewsController {
@GrpcClient("grpc-server")
private NewsServiceGrpc.NewsServiceBlockingStub blockingStub;
@GetMapping ("sayhello/{name}")
public String sayHello( @PathVariable String name){
NewsProto.StringResponse response = blockingStub.hello(NewsProto.StringRequest.newBuilder().setName(name).build());
return response.getResult();
}
}