Java gRPC od Scratch

Opublikowany: 2021-07-16

Zobaczmy, jak zaimplementować gRPC w Javie.

gRPC (Google Remote Procedure Call): gRPC to architektura RPC typu open source opracowana przez Google w celu umożliwienia szybkiej komunikacji między mikrousługami. gRPC umożliwia deweloperom integrowanie usług napisanych w różnych językach. gRPC używa formatu komunikatów Protobuf (buforów protokołów), wysoce wydajnego, wysoce upakowanego formatu komunikatów do serializacji danych strukturalnych.

W niektórych przypadkach użycia interfejs API gRPC może być bardziej wydajny niż interfejs API REST.

Spróbujmy napisać serwer na gRPC. Najpierw musimy napisać kilka plików .proto opisujących usługi i modele (DTO). W przypadku prostego serwera użyjemy ProfileService i ProfileDescriptor.

ProfileService wygląda tak:

 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 obsługuje różne opcje komunikacji klient-serwer. Podzielimy je wszystkie:

  • Normalne połączenie z serwerem – żądanie/odpowiedź.
  • Przesyłanie strumieniowe z klienta na serwer.
  • Przesyłanie strumieniowe z serwera do klienta.
  • I oczywiście strumień dwukierunkowy.

Usługa ProfileService korzysta z ProfileDescriptor, który jest określony w sekcji importu:

 syntax = "proto3"; package com.deft.grpc; message ProfileDescriptor { int64 profile_id = 1; string name = 2; }
  • int64 jest długi dla Javy. Niech identyfikator profilu należy.
  • String – podobnie jak w Javie jest to zmienna typu string.

Do zbudowania projektu możesz użyć Gradle lub mavena. Wygodniej jest mi używać maven. A dalej będzie kod za pomocą maven. Jest to wystarczająco ważne, aby powiedzieć, ponieważ w przypadku Gradle przyszła generacja .proto będzie nieco inna, a plik kompilacji będzie musiał zostać skonfigurowany inaczej. Aby napisać prosty serwer gRPC, potrzebujemy tylko jednej zależności:

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

To po prostu niesamowite. Ten starter wykonuje dla nas ogromną pracę.

Projekt, który stworzymy będzie wyglądał mniej więcej tak:

Potrzebujemy GrpcServerApplication do uruchomienia aplikacji Spring Boot. Oraz GrpcProfileService, który zaimplementuje metody z usługi .proto . Aby używać protokołu protoc i generować klasy z zapisanych plików .proto, dodaj wtyczkę protobuf-maven-plugin do pom.xml. Sekcja budowania będzie wyglądać tak:

 <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 – określenie katalogu, w którym znajdują się pliki .proto.
  • outputDirectory – wybierz katalog, w którym będą generowane pliki.
  • clearOutputDirectory – flaga wskazująca, aby nie usuwać wygenerowanych plików.

Na tym etapie możesz zbudować projekt. Następnie musisz przejść do folderu określonego w katalogu wyjściowym. Wygenerowane pliki będą tam. Teraz możesz stopniowo wdrażać GrpcProfileService .

Deklaracja klasy będzie wyglądać tak:

 @GRpcService public class GrpcProfileService extends ProfileServiceGrpc.ProfileServiceImplBase

Adnotacja GRpcService — oznacza klasę jako ziarna usługi grpc-service.

Ponieważ dziedziczymy naszą usługę z ProfileServiceGrpc , ProfileServiceImplBase , możemy przesłonić metody klasy nadrzędnej. Pierwszą metodą, którą zastąpimy, jest 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(); }

Aby odpowiedzieć klientowi, musisz wywołać metodę onNext na przekazanym StreamObserver. Po wysłaniu odpowiedzi wyślij sygnał do klienta, że ​​serwer zakończył pracę naCompleted . Podczas wysyłania żądania do serwera getCurrentProfile odpowiedź będzie:

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

Następnie przyjrzyjmy się strumieniowi serwera. Dzięki takiemu podejściu do przesyłania wiadomości klient wysyła żądanie do serwera, serwer odpowiada klientowi strumieniem wiadomości. Na przykład wysyła w pętli pięć żądań. Po zakończeniu wysyłania serwer wysyła wiadomość do klienta o pomyślnym zakończeniu transmisji.

Nadpisana metoda strumienia serwera będzie wyglądać tak:

 @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(); }

W ten sposób klient otrzyma pięć wiadomości z ProfileId równym numerowi odpowiedzi.

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

Strumień klienta jest bardzo podobny do strumienia serwera. Dopiero teraz klient przesyła strumień wiadomości, a serwer je przetwarza. Serwer może natychmiast przetwarzać wiadomości lub czekać na wszystkie żądania od klienta, a następnie je przetwarzać.

 @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(); } }; }

W strumieniu Client musisz zwrócić StreamObserver do klienta, do którego serwer będzie otrzymywał komunikaty. Metoda onError zostanie wywołana, jeśli w strumieniu wystąpi błąd. Na przykład zakończył się niepoprawnie.

Aby zaimplementować strumień dwukierunkowy, konieczne jest połączenie tworzenia strumienia z serwera i klienta.

 @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(); } }; }

W tym przykładzie, w odpowiedzi na wiadomość od klienta, serwer zwróci profil ze zwiększonym pointCount .

Wniosek

Omówiliśmy podstawowe opcje przesyłania wiadomości między klientem a serwerem za pomocą gRPC : zaimplementowany strumień serwera, strumień klienta, strumień dwukierunkowy.

Artykuł został napisany przez Siergieja Golicyna