Java gRPC od nuly

Photo of author

By etechblogcz

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