Przyjrzyjmy się, 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 programistom integrację usług napisanych w różnych językach. gRPC używa formatu komunikatów Protobuf (bufory protokołów), wysoce wydajnego formatu komunikatów o dużym upakowaniu 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.
Usługa profilu wygląda następująco:
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:
- Zwykłe wywołanie serwera – żądanie/odpowiedź.
- Przesyłanie strumieniowe z klienta na serwer.
- Przesyłanie strumieniowe z serwera do klienta.
- I oczywiście strumień dwukierunkowy.
Usługa ProfileService używa elementu 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ługie dla Javy. Niech identyfikator profilu należy.
- String – podobnie jak w Javie, jest to zmienna łańcuchowa.
Możesz użyć Gradle lub maven do zbudowania projektu. Wygodniej jest mi używać maven. A dalej będzie kod używający 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. Do napisania prostego serwera 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óra zaimplementuje metody z usługi .proto. Aby używać protoc i generować klasy z zapisanych plików .proto, dodaj protobuf-maven-plugin do pom.xml. Sekcja kompilacji będzie wyglądać następująco:
<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 zostaną wygenerowane pliki.
- clearOutputDirectory – flaga informująca o nie kasowaniu wygenerowanych plików.
Na tym etapie możesz zbudować projekt. Następnie musisz przejść do folderu, który określiliśmy w katalogu wyjściowym. Wygenerowane pliki będą tam. Teraz możesz stopniowo wdrażać GrpcProfileService.
Deklaracja klasy będzie wyglądać następująco:
@GRpcService public class GrpcProfileService extends ProfileServiceGrpc.ProfileServiceImplBase
Adnotacja GRpcService — oznacza klasę jako komponent bean grpc-service.
Ponieważ dziedziczymy naszą usługę z ProfileServiceGrpc, ProfileServiceImplBase, możemy zastąpić 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 StreamObserverze. 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 następująca:
{ "profile_id": "1", "name": "test" }
Następnie spójrzmy na strumień serwera. W przypadku tego podejścia do przesyłania wiadomości klient wysyła żądanie do serwera, a serwer odpowiada klientowi strumieniem komunikatów. Na przykład wysyła pięć żądań w pętli. Po zakończeniu wysyłania serwer wysyła do klienta wiadomość o pomyślnym zakończeniu strumienia.
Zastąpiona metoda strumienia serwera będzie wyglądać następująco:
@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ń komunikatów, a serwer je przetwarza. Serwer może przetwarzać wiadomości natychmiast 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ł wiadomości. Metoda onError zostanie wywołana, jeśli w strumieniu wystąpił błąd. Na przykład zakończył się nieprawidłowo.
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ść klienta serwer zwróci profil ze zwiększoną liczbą punktów.
Wniosek
Omówiliśmy podstawowe opcje przesyłania komunikatów między klientem a serwerem za pomocą gRPC: zaimplementowany strumień serwera, strumień klienta, strumień dwukierunkowy.
Artykuł został napisany przez Siergieja Golicyna