当前位置: 首页 > 后端技术 > Java

gRPC入门的一个简单案例

时间:2023-04-01 21:54:37 Java

这篇文章本来是想多年前和朋友交流一下的,但是因为我之前的Mac系统版本是10.13.6,所以这个版本比较旧,今天运行一些新东西有时会有一些bug(比如运行最新版本的Nacos等),运行gRPC的插件也有bug,代码生成也一直有问题,但是因为系统升级是大事,所以等到过年假期在家慢慢折腾Mac升级到现在的13.1版本后,这些问题都没有了,gRPC案例现在可以流畅运行了。那么今天就和小伙伴们简单聊聊gRPC。一、缘起为什么要写gRPC的文章?其实我本来是想和小伙伴们一起梳理一下微服务中调用入城的方式有哪些。在整理的过程中,想到了gRPC。发现还没写文章就和小伙伴聊gRPC。来几篇文章和朋友详细介绍一下gRPC,然后梳理一下微服务中的跨进程解决方案。2、什么是gRPC在了解gRPC之前,我们先了解一下什么是RPC。RPC的全称是RemoteProcedureCall,中文一般翻译为远程过程调用。RPC是一种进程间通信方式,程序分布在不同的地址空间。简单的说就是两个进程相互调用的一种方式。gRPC是由Google发起的开源RPC框架。它是一个高性能的远程过程调用(RPC)框架,可以在任何环境中运行。gRPC通过对负载平衡、跟踪、健康检查和身份验证的可插拔支持,有效地连接数据中心内和数据中心之间的服务。在gRPC中,客户端应用程序可以像调用本地对象一样直接调用部署在不同机器上的服务器应用程序中的方法,从而更容易使用gRPC创建分布式应用程序和服务。和很多RPC系统一样,gRPC是基于定义一个服务的思想,根据参数和返回类型指定一个远程调用的方法。在服务端,服务端实现接口,运行gRPC服务,处理客户端调用。在客户端,客户端有一个存根(Stub,在某些语言中称为客户端),它提供与服务器相同的方法。gRPC客户端和服务器可以在各种环境中运行和相互通信——从谷歌内部的服务器到你自己的桌面——并且可以用gRPC支持的任何语言编写。因此,您可以轻松地在Java中创建gRPC服务器,在Go、Python或Ruby中创建客户端。此外,最新的GoogleAPI将包括接口的gRPC版本,使您可以轻松地将Google功能构建到您的应用程序中。gRPC支持的语言版本:说了这么多,还是要完成两个小案例才能说清楚,废话不多说,直接上案例。3.先来看看我们的项目结构:├──grpc-api│├──pom.xml│├──src├──grpc-client│├──pom.xml│├──src├───grpc-server│├──pom.xml│├──src└──pom.xml各位,这里首先是一个grpc-api,这个模块是用来放我们公共代码的;grpc-server是我们的服务器,grpc-client是我们的客户端,这些都是普通的maven项目。3.1grpc-api在grpc-api中,我们首先引入项目依赖,如下:io.grpcgrpc-netty-shaded1.52.1io.grpcgrpc-protobuf1.52.1io.grpcgrpc-stub1.52.1org.apache.tomcatannotations-api6.0.53provided除了这些常规除了依赖还需要一个插件:kr.motd.mavenos-maven-plugin1.6.2org.xolstice.maven.pluginsprotobuf-maven-plugin0.6.1<配置>com.google.protobuf:protoc:3.21.7:exe:${os.detected.classifier}grpc-javaio.grpc:protoc-gen复制代码-grpc-java:1.51.0:exe:${os.detected.classifier}<执行><执行>compilecompile-custom先说这个插件的作用gRPC默认使用ProtocolBuffers,它是Google提供的一个成熟的开源跨平台序列化数据结构协议。我们编写相应的proto文件。通过上面的插件,我们可以把我们写的proto文件自动转换成对应的Java类。还有一点,不一定要用ProtocolBuffers,也可以用JSON等,不过目前这种场景更常用PortalBuffers。接下来我们在主目录下新建一个proto文件夹,如下:注意这个文件夹位置是默认的。如果我们的proto文件没有放在src/main/proto位置,那么我们需要在配置插件的时候指定proto文件的位置。本文主要是入门,这里使用默认位置。在proto文件夹中,我们创建一个新的product.proto文件,内容如下:product;serviceProductInfo{rpcaddProduct(Product)返回(ProductId);rpcgetProduct(ProductId)返回(Product);}messageProduct{stringid=1;字符串名称=2;字符串描述=3;floatprice=4;}messageProductId{stringvalue=1;}这个配置是比较核心的配置。这里主要解释一下负责进程传输的类和方法长什么样子:syntax="proto3";:这是protocolbuffers的版本。optionjava_multiple_files=true;:这个字段是可选的。如果设置为true,则意味着每个消息文件都会有一个单独的类文件;否则,所有消息都在外部类文件中定义。optionjava_package="org.javaboy.grpc.demo";:该字段可选,用于标识生成的java文件的包。如果未指定,将使用proto中定义的包。如果不指定包,则在根目录下生成。optionjava_outer_classname="ProductProto";:该字段可选,用于指定proto文件生成的java类的outerclass类名。什么是外类?简单的说就是用一个class文件来定义消息对应的所有Java类。这个类是外部类;如果不指定,默认为proto文件的驼峰式大小写;packageproduct;:该属性用于定义消息的包名。包名的含义与平台语言无关。这个包只用在proto文件中,用来区分同名的消息类型。可以理解为消息全名的前缀,与消息名一起唯一标识一个消息类型。当我们在proto文件中导入其他proto文件的消息时,需要加上package前缀。所以包名是用来唯一标识消息的。service:我们定义的跨平台方法都是写在service里面的。在上面的例子中,我们定义了两个方法:addProduct表示添加一个商品,参数是一个Product对象,返回值是刚刚添加成功的商品的ID;getProduct表示通过ID查询商品,参数为商品ID,返回值为查询到的商品对象。这里的定义相当于一个接口,以后我们会用Java代码来实现。message:这有点像我们在Java中定义类。上面我们定义了两个类,分别是Product和ProductId。这两个类在服务中使用。message中的定义有点像我们Java中定义的类,但是Java中的数据类型不能直接使用。毕竟这是Protocolbuffers,与语言无关。以后可以以此为基础生成不同语言的代码。这里我们可以使用类型和我们Java类型的对应关系如下:另外,我们在message中定义属性的时候,会给出一个数字,比如id=1,name=2等,这个数字会在future字段的二进制消息中标识我们,并且一旦使用我们的消息类型就不应更改,这有点像序列化。其实这条消息编译后的字节内容大致如下:这里标签中的内容包括字段索引和字段类型两部分,而字段索引其实就是我们上面定义的数字。定义完成后,接下来我们需要使用插件生成相应的Java代码。之前我们介绍过插件,现在只需要执行即可,如下图所示:注意编译和compile-custom指令都需要执行。其中compile用于编译消息对象,compile-custom依赖消息对象生成接口服务。首先我们点击compile查看生成的代码,如下:然后查看compile-custom生成的代码,如下:OK,那么我们的准备工作就完成了。部分小伙伴生成的代码文件夹颜色不对。这时候有两种解决方法:1、选中目标文件夹,右击,选择MarkDirectoryas->GeneratedSourcesroot;2.选择项目,右击,选择Maven->Reloadproject。推荐第二个选项。3.2grpc-server接下来我们创建grpc-server项目,并使项目依赖grpc-api,然后在grpc-server中提供ProductInfo的具体实现:publicclassProductInfoImplextendsProductInfoGrpc.ProductInfoImplBase{@OverridepublicvoidaddProduct(产品请求,StreamObserverresponseObserver){System.out.println("request.toString()="+request.toString());responseObserver.onNext(ProductId.newBuilder().setValue(request.getId()).build());responseObserver.onCompleted();}@OverridepublicvoidgetProduct(ProductIdrequest,StreamObserverresponseObserver){responseObserver.onNext(Product.newBuilder().setId(request.getValue()).setName("三国之恋").build());responseObserver.onCompleted();}}ProductInfoGrpc.ProductInfoImplBase是根据我们在proto文件中定义的服务自动生成的。我们的ProductInfoImpl继承自该类,并提供了该方法的具体实现。以addProduct方法为例,参数request为client以后调用时传入的Product对象,返回结果通过responseObserver完成。我们方法的逻辑非常简单。我打印出作为参数传入的Product对象,然后构造一个ProductId对象并返回,最后调用responseObserver.onCompleted();表示数据已经返回。其余getProduct方法的逻辑很容易理解,这里不再赘述。最后,让我们再次启动grpc-server项目:publicclassProductInfoServer{Serverserver;publicstaticvoidmain(String[]args)throwsIOException,InterruptedException{ProductInfoServerserver=newProductInfoServer();服务器.start();服务器.blockUntilShutdown();}publicvoidstart()throwsIOException{intport=50051;server=ServerBuilder.forPort(port).addService(newProductInfoImpl()).build().start();运行时.getRuntime()。addShutdownHook(新线程(()->{ProductInfoServer.this.stop();}));}privatevoidstop(){if(server!=null){server.shutdown();}}privatevoidblockUntilShutdown()throwsInterruptedException{if(server!=null){server.awaitTermination();}}}由于我们这里是一个JavaSE项目,为了避免项目启动后停止,我们调用了server.awaitTermination();这里的方法,就是让服务启动成功后不要停止。3.3grpc-client最后来看客户端调用。首先grpc-client项目也需要依赖grpc-api,然后直接进行方法调用,如下:“本地主机”,50051)。usePlaintext().build();ProductInfoGrpc.ProductInfoBlockingStubstub=ProductInfoGrpc.newBlockingStub(通道);Productp=Product.newBuilder().setId("1").setPrice(399.0f).setName("天津项目").setDescription("SpringBoot+Vue3实战视频").build();ProductIdproductId=stub.addProduct(p);System.out.println("productId.getValue()="+productId.getValue());产品product=stub.getProduct(ProductId.newBuilder().setValue("99999").build());System.out.println("product.toString()="+product.toString());}}小伙伴们看这里,首先需要和服务器建立连接,并给出服务器的地址和端口号没错,usePlaintext()方法意味着连接不会使用TLS加密(默认情况下,TLS将用于加密连接)。建议在生产环境中使用加密连接。其余的代码更容易理解。创建一个Product对象,调用addProduct方法添加;创建一个ProductId对象并调用getProduct。Product对象和ProductId对象都是根据我们在proto中定义的消息自动生成的。4.总结一下,举个简单的例子。小伙伴们先上手gRPC吧,后面几篇宋大哥会详细介绍。