從頭開始的 Java gRPC

已發表: 2021-07-16

讓我們探索如何在 Java 中實現 gRPC。

gRPC(Google Remote Procedure Call):gRPC 是 Google 開發的一種開源 RPC 架構,用於實現微服務之間的高速通信。 gRPC 允許開發人員集成用不同語言編寫的服務。 gRPC 使用 Protobuf 消息格式(Protocol Buffers),這是一種高效、高度打包的消息格式,用於序列化結構化數據。

對於某些用例,gRPC API 可能比 REST API 更高效。

讓我們嘗試在 gRPC 上編寫一個服務器。 首先,我們需要編寫幾個描述服務和模型 (DTO) 的.proto文件。 對於簡單的服務器,我們將使用 ProfileService 和 ProfileDescriptor。

ProfileService 看起來像這樣:

 syntax = "proto3"; package com.deft.grpc; import "google/protobuf/empty.proto"; import "profile_descriptor.proto"; service ProfileService { rpc GetCurrentProfile (google.protobuf.Empty) returns (ProfileDescriptor) {} rpc clientStream (stream ProfileDescriptor) returns (google.protobuf.Empty) {} rpc serverStream (google.protobuf.Empty) returns (stream ProfileDescriptor) {} rpc biDirectionalStream (stream ProfileDescriptor) returns (stream ProfileDescriptor) {} }

gRPC 支持各種客戶端-服務器通信選項。 我們將把它們全部分解:

  • 正常的服務器調用——請求/響應。
  • 從客戶端流式傳輸到服務器。
  • 從服務器流式傳輸到客戶端。
  • 當然,還有雙向流。

ProfileService 服務使用在導入部分中指定的 ProfileDescriptor:

 syntax = "proto3"; package com.deft.grpc; message ProfileDescriptor { int64 profile_id = 1; string name = 2; }
  • int64是 Java 的 Long。 讓配置文件 id 屬於。
  • 字符串——就像在 Java 中一樣,這是一個字符串變量。

您可以使用 Gradle 或 maven 來構建項目。 我用maven比較方便。 進一步將是使用 maven 的代碼。 這一點很重要,因為對於 Gradle 來說,未來的 .proto 版本會略有不同,並且構建文件需要進行不同的配置。 要編寫一個簡單的 gRPC 服務器,我們只需要一個依賴項:

 <dependency> <groupId>io.github.lognet</groupId> <artifactId>grpc-spring-boot-starter</artifactId> <version>4.5.4</version> </dependency>

簡直不可思議。 這個啟動器為我們做了大量的工作。

我們將創建的項目如下所示:

我們需要 GrpcServerApplication 來啟動 Spring Boot 應用程序。 還有 GrpcProfileService,它將實現.proto服務中的方法。 要使用 protoc 並從編寫的 .proto 文件生成類,請將 protobuf-maven-plugin 添加到 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> <protoSourceRoot>${project.basedir}/src/main/proto</protoSourceRoot> <outputDirectory>${basedir}/target/generated-sources/grpc-java</outputDirectory> <protocArtifact>com.google.protobuf:protoc:3.12.0:exe:${os.detected.classifier}</protocArtifact> <pluginId>grpc-java</pluginId> <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.38.0:exe:${os.detected.classifier}</pluginArtifact> <clearOutputDirectory>false</clearOutputDirectory> </configuration> <executions> <execution> <goals> <goal>compile</goal> <goal>compile-custom</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
  • protoSourceRoot – 指定 .proto 文件所在的目錄。
  • outputDirectory – 選擇將生成文件的目錄。
  • clearOutputDirectory – 指示不清除生成的文件的標誌。

在這個階段,您可以構建一個項目。 接下來,您需要轉到我們在輸出目錄中指定的文件夾。 生成的文件將在那裡。 現在您可以逐步實現GrpcProfileService

類聲明將如下所示:

 @GRpcService public class GrpcProfileService extends ProfileServiceGrpc.ProfileServiceImplBase

GRpcService註釋——將類標記為 grpc-service bean。

由於我們從ProfileServiceGrpcProfileServiceImplBase繼承了我們的服務,因此我們可以覆蓋父類的方法。 我們將覆蓋的第一個方法是getCurrentProfile

 @Override public void getCurrentProfile(Empty request, StreamObserver<ProfileDescriptorOuterClass.ProfileDescriptor> responseObserver) { System.out.println("getCurrentProfile"); responseObserver.onNext(ProfileDescriptorOuterClass.ProfileDescriptor .newBuilder() .setProfileId(1) .setName("test") .build()); responseObserver.onCompleted(); }

要響應客戶端,需要調用傳入的 StreamObserver 上的onNext方法。 發送響應後,向客戶端發送服務器已完成onCompleted工作的信號。 當向 getCurrentProfile 服務器發送請求時,響應將是:

 { "profile_id": "1", "name": "test" }

接下來,我們來看看服務器流。 使用這種消息傳遞方法,客戶端向服務器發送請求,服務器用消息流響應客戶端。 例如,它在一個循環中發送五個請求。 發送完成後,服務器向客戶端發送有關流成功完成的消息。

覆蓋的服務器流方法將如下所示:

 @Override public void serverStream(Empty request, StreamObserver<ProfileDescriptorOuterClass.ProfileDescriptor> responseObserver) { for (int i = 0; i < 5; i++) { responseObserver.onNext(ProfileDescriptorOuterClass.ProfileDescriptor .newBuilder() .setProfileId(i) .build()); } responseObserver.onCompleted(); }

因此,客戶端將收到五個帶有 ProfileId 的消息,等於響應號。

 { "profile_id": "0", "name": "" } { "profile_id": "1", "name": "" } … { "profile_id": "4", "name": "" }

客戶端流與服務器流非常相似。 只有現在客戶端傳輸消息流,服務器處理它們。 服務器可以立即處理消息或等待來自客戶端的所有請求,然後再處理它們。

 @Override public StreamObserver<ProfileDescriptorOuterClass.ProfileDescriptor> clientStream(StreamObserver<Empty> responseObserver) { return new StreamObserver<>() { @Override public void onNext(ProfileDescriptorOuterClass.ProfileDescriptor profileDescriptor) { log.info("ProfileDescriptor from client. Profile id: {}", profileDescriptor.getProfileId()); } @Override public void onError(Throwable throwable) { } @Override public void onCompleted() { responseObserver.onCompleted(); } }; }

在客戶端流中,您需要將StreamObserver返回給客戶端,服務器將向客戶端接收消息。 如果流中發生錯誤,將調用 onError 方法。 例如,它錯誤地終止。

要實現雙向流,需要結合從服務器和客戶端創建流。

 @Override public StreamObserver<ProfileDescriptorOuterClass.ProfileDescriptor> biDirectionalStream( StreamObserver<ProfileDescriptorOuterClass.ProfileDescriptor> responseObserver) { return new StreamObserver<>() { int pointCount = 0; @Override public void onNext(ProfileDescriptorOuterClass.ProfileDescriptor profileDescriptor) { log.info("biDirectionalStream, pointCount {}", pointCount); responseObserver.onNext(ProfileDescriptorOuterClass.ProfileDescriptor .newBuilder() .setProfileId(pointCount++) .build()); } @Override public void onError(Throwable throwable) { } @Override public void onCompleted() { responseObserver.onCompleted(); } }; }

在這個例子中,為了響應客戶端的消息,服務器將返回一個增加了pointCount的配置文件。

結論

我們已經介紹了使用gRPC在客戶端和服務器之間進行消息傳遞的基本選項:實現的服務器流、客戶端流、雙向流。

這篇文章是由謝爾蓋 Golitsyn 撰寫的