Java gRPC od nuly
Zkoumání implementace gRPC v Javě
Podívejme se, jak lze v Javě efektivně implementovat gRPC.
gRPC (Google Remote Procedure Call): Jedná se o open-source RPC framework od společnosti Google, navržený pro rychlou komunikaci mezi mikroservisami. Umožňuje vývojářům propojovat služby napsané v různých programovacích jazycích. gRPC používá pro serializaci strukturovaných dat formát zpráv Protobuf (Protocol Buffers), který je vysoce efektivní a kompaktní.
V mnoha situacích může být gRPC API výkonnější než tradiční REST API.
Zkusme si vytvořit gRPC server. Nejprve potřebujeme definovat .proto soubory, které popisují služby a datové objekty (DTO). Pro jednoduchý server použijeme `ProfileService` a `ProfileDescriptor`.
Definice `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é způsoby komunikace klient-server. Pojďme se na ně podívat:
- Běžné volání serveru: požadavek/odpověď.
- Streamování z klienta na server.
- Streamování ze serveru na klienta.
- A také obousměrný stream.
Služba `ProfileService` využívá `ProfileDescriptor`, jehož definici najdeme v sekci importu:
syntax = "proto3";
package com.deft.grpc;
message ProfileDescriptor {
int64 profile_id = 1;
string name = 2;
}
- `int64` je v Javě ekvivalent `long`. Zde reprezentuje ID profilu.
- `string` je řetězcová proměnná, stejně jako v Javě.
Pro sestavení projektu můžeme použít Gradle nebo Maven. Preferuji Maven, a proto bude dále použit kód s Mavenem. Toto je důležité zmínit, protože konfigurace generování .proto souborů bude v Gradlu odlišná, a tedy i sestavení projektu.
Pro vytvoření jednoduchého gRPC serveru potřebujeme jedinou závislost:
<dependency>
<groupId>io.github.lognet</groupId>
<artifactId>grpc-spring-boot-starter</artifactId>
<version>4.5.4</version>
</dependency>
Tento starter nám velmi usnadní práci. Vyřeší za nás spoustu komplikací.
Struktura našeho projektu bude vypadat následovně:
K běhu Spring Boot aplikace potřebujeme `GrpcServerApplication`. Dále `GrpcProfileService`, která implementuje metody definované v .proto souboru. Pro použití protokolu a generování tříd z .proto souborů je nutné přidat do `pom.xml` plugin `protobuf-maven-plugin`. Sekce sestavení (`build`) by tedy měla 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čuje adresář, kde se nacházejí .proto soubory.
- `outputDirectory` - specifikuje cílový adresář pro generované soubory.
- `clearOutputDirectory` - příznak, který určuje, zda se mají generované soubory mazat.
V tomto bodě můžeme projekt vytvořit. Následně je nutné přejít do složky, kterou jsme nastavili jako `outputDirectory`. Tam najdeme vygenerované soubory. Nyní můžeme postupně implementovat `GrpcProfileService`.
Deklarace třídy bude vypadat následovně:
@GRpcService public class GrpcProfileService extends ProfileServiceGrpc.ProfileServiceImplBase
Anotace `@GRpcService` označuje třídu jako gRPC service bean.
Protože naše služba dědí z `ProfileServiceGrpc.ProfileServiceImplBase`, můžeme přepisovat 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();
}
Pro zaslání odpovědi klientovi musíme zavolat metodu `onNext` předaného `StreamObserver`. Po odeslání odpovědi, pošleme klientovi signál, že server dokončil práci voláním `onCompleted`. Při odeslání požadavku na server `getCurrentProfile` bude odpověď:
{
"profile_id": "1",
"name": "test"
}
Nyní se podívejme na stream serveru. V tomto případě klient odešle požadavek na server, který odpoví klientovi proudem zpráv. Například odešle pět požadavků v cyklu. Po odeslání všech zpráv, server pošle klientovi zprávu o úspěšném dokončení streamu.
Metoda serverového streamu 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 iterace.
{
"profile_id": "0",
"name": ""
}
{
"profile_id": "1",
"name": ""
}
…
{
"profile_id": "4",
"name": ""
}
Klientský stream funguje podobně jako serverový. Rozdíl je v tom, že tentokrát klient odesílá proud zpráv a server je zpracovává. Server může zprávy zpracovávat okamžitě nebo vyčkat, až klient odešle všechny požadavky a pak 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();
}
};
}
V klientském streamu musíte klientovi vrátit `StreamObserver`, skrze který server přijímá zprávy. Pokud ve streamu dojde k chybě, bude zavolána metoda `onError`, například, když je spojení nesprávně ukončeno.
Pro implementaci obousměrného streamu je nutné propojit vytváření streamu ze serveru 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();
}
};
}
V tomto příkladu, server v reakci na zprávu od klienta, vrátí profil se zvýšeným počtem bodů.
Závěr
Probrali jsme základní možnosti komunikace mezi klientem a serverem pomocí gRPC: implementovali jsme serverový stream, klientský stream i obousměrný stream.
Článek napsal Sergey Golitsyn