Java gRPC od nuly

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.
  Jak nahrávat zvuk na iPhone a iPad

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.

  Jak nastavit váš Mac, aby se každý den automaticky zapínal

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.

  Systemd změní, jak váš domovský adresář Linuxu funguje

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