Java gRPC od Scratch
Opublikowany: 2021-07-16Zobaczmy, 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.ProfileServiceImplBaseAdnotacja 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
