Java gRPC fra bunden

Lad os undersøge, hvordan man implementerer gRPC i Java.

gRPC (Google Remote Procedure Call): gRPC er en open source RPC-arkitektur udviklet af Google for at muliggøre højhastighedskommunikation mellem mikrotjenester. gRPC giver udviklere mulighed for at integrere tjenester skrevet på forskellige sprog. gRPC bruger Protobuf-meddelelsesformatet (Protocol Buffers), et yderst effektivt, meget pakket meddelelsesformat til serialisering af strukturerede data.

I nogle tilfælde kan gRPC API være mere effektiv end REST API.

Lad os prøve at skrive en server på gRPC. Først skal vi skrive flere .proto-filer, der beskriver tjenester og modeller (DTO). For en simpel server bruger vi ProfileService og ProfileDescriptor.

Profilservice ser sådan ud:

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 understøtter en række klient-server kommunikationsmuligheder. Vi opdeler dem alle sammen:

  • Normalt serverkald – anmodning/svar.
  • Streaming fra klient til server.
  • Streaming fra server til klient.
  • Og selvfølgelig den tovejsstrøm.

ProfileService-tjenesten bruger ProfileDescriptor, som er specificeret i importafsnittet:

syntax = "proto3";
package com.deft.grpc;
message ProfileDescriptor {
  int64 profile_id = 1;
  string name = 2;
}
  • int64 er længe for Java. Lad profil-id’et høre til.
  • String – ligesom i Java er dette en strengvariabel.

Du kan bruge Gradle eller Maven til at bygge projektet. Det er mere bekvemt for mig at bruge maven. Og yderligere vil være koden ved hjælp af maven. Dette er vigtigt nok at sige, fordi for Gradle vil den fremtidige generation af .proto være lidt anderledes, og build-filen skal konfigureres anderledes. For at skrive en simpel gRPC-server behøver vi kun én afhængighed:

<dependency>
    <groupId>io.github.lognet</groupId>
    <artifactId>grpc-spring-boot-starter</artifactId>
    <version>4.5.4</version>
</dependency>

Det er bare utroligt. Denne starter gør et enormt arbejde for os.

  Sådan gentager du sange i Spotify

Projektet, som vi vil skabe, vil se nogenlunde således ud:

Vi har brug for GrpcServerApplication for at starte Spring Boot-applikationen. Og GrpcProfileService, som skal implementere metoder fra .proto-tjenesten. For at bruge protoc og generere klasser fra skrevne .proto-filer, skal du tilføje protobuf-maven-plugin til pom.xml. Byggeafsnittet vil se sådan ud:

<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 – angiver den mappe, hvor .proto-filerne er placeret.
  • outputDirectory – vælg den mappe, hvor filerne skal genereres.
  • clearOutputDirectory – et flag, der angiver, at du ikke må rydde genererede filer.

På dette stadium kan du bygge et projekt. Dernæst skal du gå til den mappe, som vi har angivet i outputmappen. De genererede filer vil være der. Nu kan du gradvist implementere GrpcProfileService.

  Sådan downloader du Google Meet

Klasseerklæringen ser således ud:

@GRpcService
public class GrpcProfileService extends ProfileServiceGrpc.ProfileServiceImplBase

GRpcService annotation – Markerer klassen som en grpc-service bønne.

Da vi arver vores service fra ProfileServiceGrpc, ProfileServiceImplBase, kan vi tilsidesætte metoderne for den overordnede klasse. Den første metode, vi tilsidesætter, er 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();
    }

For at svare på klienten skal du kalde onNext-metoden på den beståede StreamObserver. Når du har sendt svaret, skal du sende et signal til klienten om, at serveren er færdig med at arbejde på Completed. Når du sender en anmodning til getCurrentProfile-serveren, vil svaret være:

{
  "profile_id": "1",
  "name": "test"
}

Lad os derefter tage et kig på serverstrømmen. Med denne meddelelsestilgang sender klienten en anmodning til serveren, serveren svarer klienten med en strøm af meddelelser. For eksempel sender den fem anmodninger i en løkke. Når afsendelsen er fuldført, sender serveren en besked til klienten om den vellykkede afslutning af streamen.

Den tilsidesatte serverstream-metode vil se sådan ud:

@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();
    }

Klienten vil således modtage fem beskeder med et ProfileId svarende til svarnummeret.

{
  "profile_id": "0",
  "name": ""
}
{
  "profile_id": "1",
  "name": ""
}
…
{
  "profile_id": "4",
  "name": ""
}

Klientstrøm minder meget om serverstrøm. Først nu sender klienten en strøm af meddelelser, og serveren behandler dem. Serveren kan behandle beskeder med det samme eller vente på alle anmodninger fra klienten og derefter behandle dem.

    @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();
            }
        };
    }

I klientstrømmen skal du returnere StreamObserveren til klienten, hvortil serveren vil modtage beskeder. OnError-metoden vil blive kaldt, hvis der opstod en fejl i streamen. For eksempel afsluttede det forkert.

  Skjul/Vis Spotlight-søgning fra menulinjen i OS X

For at implementere en tovejsstrøm er det nødvendigt at kombinere oprettelse af en strøm fra serveren og klienten.

@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();
            }
        };
    } 

I dette eksempel vil serveren som svar på klientens besked returnere en profil med et øget pointCount.

Konklusion

Vi har dækket de grundlæggende muligheder for meddelelser mellem en klient og en server ved hjælp af gRPC: implementeret serverstrøm, klientstrøm, tovejsstrøm.

Artiklen er skrevet af Sergey Golitsyn