学习目标: 了解grpc的使用方法,在现有的服务中如何集成gRPC,如何通过proto文件定义一个服务,如何创建gRPC服务并启动服务以及客户端如何请求服务。
一、简介
gRPC是最初由 Google 开发的高性能、开源的 RPC 框架,实现服务之的远程调用,屏蔽了底层的通讯、连接、序列化等技术,用户只需要专注于定义服务、实现服务、调用服务。更重要的它是语言无关的RPC框架。
2. 概述
该框架基于远程过程调用的客户端-服务器模型。客户端应用程序可以直接调用服务器应用程序上的方法,就好像它是本地方法对象一样。
本文将使用以下步骤使用 gRPC 创建典型的客户端-服务器应用程序:
- 在.proto文件中定义服务
- 使用protocol buffer compiler工具生成服务器和客户端代码
- 创建服务器应用程序,实现生成的服务接口并创建 gRPC 服务器
- 创建客户端应用程序,使用生成的stub包进行 RPC 调用
让我们定义一个简单的HelloService,它返回问候以换取名字和姓氏。
3. Maven依赖
我们基于之前搭建的【nacos-spring-cloud-provider-example】让我们添加grpc-netty、grpc-protobuf和grpc-stub依赖项:
<dependencies>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty</artifactId>
<version>1.19.0</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
<version>1.19.0</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
<version>1.19.0</version>
</dependency>
</dependencies>
4. 定义服务
我们首先定义一个服务,指定可以远程调用的方法及其参数和返回类型。通过配置在.proto文件中完成的。包括发送、响应消息的结构信息等。
4.1。基本配置
让我们为示例HelloService创建一个proto/HelloService.proto文件。我们首先添加一些基本的配置:
syntax = "proto3";
option java_multiple_files = true;
package org.baeldung.grpc;
第一行告诉编译器在这个文件中使用了什么语法。默认情况下,编译器在单个 Java 文件中生成所有 Java 代码。第二行覆盖此设置,所有内容都将在单个文件类种生成。
最后,我们指定要用于生成的 Java 类的包。
4.2. 定义消息结构
接下来,我们定义消息:
message HelloRequest {
string firstName = 1;
string lastName = 2;
}
这定义了请求的数据结构。在这里,进入消息的每个属性都与其类型一起定义。
需要为每个属性分配一个唯一编号,称为标记。protocol buffer 使用此标记值来表示属性,而不是使用属性名称。
因此,与 JSON 中我们每次都传递属性名称firstName不同,protocol buffer 将使用数字 1 来表示firstName。响应的数据结构类似于请求的结构。
message HelloResponse {
string greeting = 1;
}
4.3. 定义服务
最后,让我们定义服务。对于我们的HelloService,我们定义了一个hello()操作:
service HelloService {
rpc hello(HelloRequest) returns (HelloResponse);
}
hello()操作接受HelloRequest请求并返回HelloResponse响应。gRPC 还通过在请求和响应中添加stream关键字来支持流式传输。
5. 生成代码
现在我们将HelloService.proto文件传递给编译器生成 Java 文件。有多种触发方式。
5.1。使用Protocol Buffer Compiler
首先,我们需要 Protocol Buffer Compiler。我们可以从这里提供的许多预编译二进制文件中进行选择。
此外,我们需要获取gRPC Java Codegen Plugin。
最后,我们可以使用以下命令生成代码:
protoc --plugin=protoc-gen-grpc-java=$PATH_TO_PLUGIN -I=$SRC_DIR
--java_out=$DST_DIR --grpc-java_out=$DST_DIR $SRC_DIR/HelloService.proto
5.2. 使用 Maven 插件
作为开发人员,您希望代码生成与您的构建系统紧密集成。gRPC 为Maven 构建系统提供了一个protobuf-maven-plugin :
<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.6.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.3.0:exe:${os.detected.classifier}
</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>
io.grpc:protoc-gen-grpc-java:1.4.0: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}
6. 创建服务器
无论您使用哪种方法生成代码,都会生成以下的文件:
- HelloRequest.java –包含HelloRequest类型定义
- HelloResponse.java -这包含HelleResponse类型定义
-
HelloServiceGrpc.HelloServiceImplBase –抽象类HelloServiceImplBase,它提供了我们在服务接口中定义的所有操作的实现,后面我们的服务就继承这这个类就可以了。
6.1。服务实现
抽象类HelloServiceImplBase的默认实现是抛出运行时异常 io.grpc.StatusRuntimeException表示该方法未实现。
我们将扩展这个类并覆盖我们服务定义中提到的hello()方法:
public class HelloServiceImpl extends HelloServiceGrpc.HelloServiceImplBase {
@Override
public void hello(
HelloRequest request, StreamObserver<HelloResponse> responseObserver) {
String greeting = new StringBuilder()
.append("Hello, ")
.append(request.getFirstName())
.append(" ")
.append(request.getLastName())
.toString();
HelloResponse response = HelloResponse.newBuilder()
.setGreeting(greeting)
.build();
responseObserver.onNext(response);
responseObserver.onCompleted();
}
}
如果我们将hello()的签名与我们在HellService.proto文件中编写的签名进行比较,我们会注意到它没有返回HelloResponse。相反,它将第二个参数作为StreamObserver<HelloResponse>,它是一个响应观察者,是服务器调用其响应的回调。
这样,客户端可以选择进行阻塞调用或非阻塞调用。
gRPC 使用构建器来创建对象。我们使用HelloResponse.newBuilder()并设置问候文本来构建一个HelloResponse对象。我们将此对象设置给 responseObserver 的onNext()方法以将其发送给客户端。
最后,我们需要调用onCompleted()来指定我们已经完成了对 RPC 的处理,否则连接将被挂起,客户端将等待一致等待。
6.2. 启动 Grpc 服务器
接下来,我们需要启动 gRPC 服务器来监听传入的请求:
public class GrpcServer {
public static void main(String[] args) {
Server server = ServerBuilder
.forPort(5000)
.addService(new HelloServiceImpl()).build();
server.start();
server.awaitTermination();
}
}
在这里,我们再次使用构建器在端口 5000上创建一个 gRPC 服务器,并添加我们定义的HelloServiceImpl服务。start()将启动服务器。在我们的示例中,我们将调用awaitTermination()以保持服务器在前台运行。
7. 创建客户端
gRPC 提供了一种通道,可以抽象出连接、连接池、负载平衡等底层细节。
我们将使用ManagedChannelBuilder创建一个频道。在这里,我们指定服务器地址和端口。
我们将使用没有任何加密的纯文本:
public class GrpcClient {
public static void main(String[] args) {
ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 8080)
.usePlaintext()
.build();
HelloServiceGrpc.HelloServiceBlockingStub stub
= HelloServiceGrpc.newBlockingStub(channel);
HelloResponse helloResponse = stub.hello(HelloRequest.newBuilder()
.setFirstName("奇点架构")
.setLastName("gRPC")
.build());
channel.shutdown();
}
}
接下来,我们需要创建一个Stub,用于对hello()进行实际的远程调用。存根是客户端与服务器交互的主要方式。当使用自动生成的Stub时,存根将具有用于包装通道的构造函数。
在这里,我们使用阻塞/同步Stub,以便 RPC 调用等待服务器响应,并且将返回响应或引发异常。gRPC 提供了另外两种类型的Stub,它们有助于非阻塞/异步调用。
最后,进行hello() RPC 调用。这里我们传递了HelloRequest。我们可以使用自动生成的set方法来设置HelloRequest对象的firstName、lastName属性。
我们取回从服务器返回的HelloResponse对象。
8. 结论
在本节中,我们看到了如何使用 gRPC 通过专注于定义服务并让 gRPC 处理所有样板代码来简化两个服务之间的通信开发。如果读者朋友之前么有接触过gRPC ,本节是一个很好的入门。因为Nacos服务端集群之间的通讯使用的是gRPC 。