Pojďme prozkoumat, jak implementovat gRPC v Javě.
gRPC (Google Remote Procedure Call): gRPC je open-source RPC architektura vyvinutá společností Google, která umožňuje vysokorychlostní komunikaci mezi mikroslužbami. gRPC umožňuje vývojářům integrovat služby napsané v různých jazycích. gRPC používá formát zpráv Protobuf (Protocol Buffers), vysoce účinný, vysoce zabalený formát zpráv pro serializaci strukturovaných dat.
V některých případech použití může být gRPC API efektivnější než REST API.
Zkusme napsat server na gRPC. Nejprve musíme napsat několik souborů .proto, které popisují služby a modely (DTO). Pro jednoduchý server použijeme ProfileService a ProfileDescriptor.
ProfileService vypadá takto:
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 podporuje různé možnosti komunikace klient-server. Všechny je rozebereme:
- Normální volání serveru – požadavek/odpověď.
- Streamování z klienta na server.
- Streamování ze serveru na klienta.
- A samozřejmě obousměrný proud.
Služba ProfileService používá ProfileDescriptor, který je uveden v sekci importu:
syntax = "proto3"; package com.deft.grpc; message ProfileDescriptor { int64 profile_id = 1; string name = 2; }
- int64 je dlouhý pro Javu. Ať patří ID profilu.
- Řetězec – stejně jako v Javě se jedná o řetězcovou proměnnou.
K sestavení projektu můžete použít Gradle nebo maven. Je pro mě pohodlnější používat maven. A dále bude kód pomocí maven. To je dost důležité na to říci, protože pro Gradle bude budoucí generace .proto mírně odlišná a soubor sestavení bude muset být nakonfigurován jinak. K napsání jednoduchého serveru gRPC potřebujeme pouze jednu závislost:
<dependency> <groupId>io.github.lognet</groupId> <artifactId>grpc-spring-boot-starter</artifactId> <version>4.5.4</version> </dependency>
Je to prostě neuvěřitelné. Tento startér za nás udělá ohromný kus práce.
Projekt, který vytvoříme, bude vypadat nějak takto:
Ke spuštění aplikace Spring Boot potřebujeme GrpcServerApplication. A GrpcProfileService, která bude implementovat metody ze služby .proto. Chcete-li použít protokol a generovat třídy z napsaných souborů .proto, přidejte do pom.xml protobuf-maven-plugin. Sekce sestavení bude vypadat takto:
<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 – určení adresáře, kde jsou umístěny soubory .proto.
- outputDirectory – vyberte adresář, do kterého budou generovány soubory.
- clearOutputDirectory – příznak indikující nevymazání generovaných souborů.
V této fázi můžete vytvořit projekt. Dále musíte přejít do složky, kterou jsme zadali ve výstupním adresáři. Budou tam vygenerované soubory. Nyní můžete postupně implementovat GrpcProfileService.
Deklarace třídy bude vypadat takto:
@GRpcService public class GrpcProfileService extends ProfileServiceGrpc.ProfileServiceImplBase
Anotace GRpcService – Označí třídu jako grpc-service bean.
Protože naši službu zdědíme z ProfileServiceGrpc, ProfileServiceImplBase, můžeme přepsat metody nadřazené třídy. První metodou, kterou přepíšeme, je 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(); }
Chcete-li klientovi odpovědět, musíte zavolat metodu onNext na předaném StreamObserver. Po odeslání odpovědi odešlete klientovi signál, že server dokončil práci onCompleted. Při odesílání požadavku na server getCurrentProfile bude odpověď:
{ "profile_id": "1", "name": "test" }
Dále se podívejme na stream serveru. S tímto přístupem zasílání zpráv klient odešle požadavek na server, server odpoví klientovi proudem zpráv. Například odešle pět požadavků ve smyčce. Po dokončení odesílání server odešle klientovi zprávu o úspěšném dokončení streamu.
Metoda přepsaného streamu serveru bude vypadat takto:
@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(); }
Klient tedy obdrží pět zpráv s ProfileId, které se rovná číslu odpovědi.
{ "profile_id": "0", "name": "" } { "profile_id": "1", "name": "" } … { "profile_id": "4", "name": "" }
Klientský stream je velmi podobný serverovému streamu. Teprve nyní klient přenáší proud zpráv a server je zpracovává. Server může zpracovávat zprávy okamžitě nebo čekat na všechny požadavky od klienta a následně je zpracovat.
@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(); } }; }
Ve streamu Client musíte vrátit StreamObserver klientovi, kterému bude server přijímat zprávy. Pokud ve streamu dojde k chybě, bude zavolána metoda onError. Například byl nesprávně ukončen.
Pro implementaci obousměrného streamu je nutné spojit vytváření streamu ze serveru a 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(); } }; }
V tomto příkladu server v reakci na zprávu klienta vrátí profil se zvýšeným počtem bodů.
Závěr
Pokryli jsme základní možnosti zasílání zpráv mezi klientem a serverem pomocí gRPC: implementovaný serverový stream, klientský stream, obousměrný stream.
Článek napsal Sergey Golitsyn