Java gRPC fra bunnen av

La oss utforske hvordan du implementerer gRPC i Java.

gRPC (Google Remote Procedure Call): gRPC er en åpen kildekode RPC-arkitektur utviklet av Google for å muliggjøre høyhastighetskommunikasjon mellom mikrotjenester. gRPC lar utviklere integrere tjenester skrevet på forskjellige språk. gRPC bruker meldingsformatet Protobuf (Protocol Buffers), et svært effektivt, svært pakket meldingsformat for serialisering av strukturerte data.

For noen brukstilfeller kan gRPC API være mer effektivt enn REST API.

La oss prøve å skrive en server på gRPC. Først må vi skrive flere .proto-filer som beskriver tjenester og modeller (DTO). For en enkel server bruker vi ProfileService og ProfileDescriptor.

ProfileService ser slik ut:

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 støtter en rekke klient-server kommunikasjonsalternativer. Vi deler dem alle ned:

  • Vanlig serveranrop – forespørsel/svar.
  • Streaming fra klient til server.
  • Streaming fra server til klient.
  • Og, selvfølgelig, toveisstrømmen.

ProfileService-tjenesten bruker ProfileDescriptor, som er spesifisert i importdelen:

syntax = "proto3";
package com.deft.grpc;
message ProfileDescriptor {
  int64 profile_id = 1;
  string name = 2;
}
  • int64 er lenge for Java. La profil-ID-en høre til.
  • String – akkurat som i Java, er dette en strengvariabel.
  Få Hangouts for å gjenkjenne og knytte telefonnumre til kontakter

Du kan bruke Gradle eller Maven til å bygge prosjektet. Det er mer praktisk for meg å bruke maven. Og videre vil være koden ved hjelp av maven. Dette er viktig nok å si fordi for Gradle vil den fremtidige generasjonen av .proto være litt annerledes, og byggefilen må konfigureres annerledes. For å skrive en enkel gRPC-server trenger vi bare én avhengighet:

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

Det er bare utrolig. Denne starteren gjør enormt mye arbeid for oss.

Prosjektet vi skal lage vil se omtrent slik ut:

Vi trenger GrpcServerApplication for å starte Spring Boot-applikasjonen. Og GrpcProfileService, som skal implementere metoder fra .proto-tjenesten. For å bruke protoc og generere klasser fra skrevne .proto-filer, legg til protobuf-maven-plugin til pom.xml. Byggedelen vil se slik ut:

<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 – spesifiserer katalogen der .proto-filene er plassert.
  • outputDirectory – velg katalogen der filene skal genereres.
  • clearOutputDirectory – et flagg som indikerer ikke å slette genererte filer.

På dette stadiet kan du bygge et prosjekt. Deretter må du gå til mappen som vi spesifiserte i utdatakatalogen. De genererte filene vil være der. Nå kan du gradvis implementere GrpcProfileService.

  5 Enterprise-klare skysårbarhetsskannere for AWS, GCP, Azure og mer

Klasseerklæringen vil se slik ut:

@GRpcService
public class GrpcProfileService extends ProfileServiceGrpc.ProfileServiceImplBase

GRpcService-kommentar – Merker klassen som en grpc-servicebønne.

Siden vi arver tjenesten vår fra ProfileServiceGrpc, ProfileServiceImplBase, kan vi overstyre metodene til den overordnede klassen. Den første metoden vi overstyrer 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 å svare på klienten må du kalle opp onNext-metoden på den beståtte StreamObserveren. Etter å ha sendt svaret, send et signal til klienten om at serveren har fullført arbeidet med Fullført. Når du sender en forespørsel til getCurrentProfile-serveren, vil svaret være:

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

La oss deretter ta en titt på serverstrømmen. Med denne meldingstilnærmingen sender klienten en forespørsel til serveren, serveren svarer klienten med en strøm av meldinger. For eksempel sender den fem forespørsler i en loop. Når sendingen er fullført, sender serveren en melding til klienten om vellykket fullføring av strømmen.

Den overstyrte serverstrømmetoden vil se slik ut:

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

Dermed vil klienten motta fem meldinger med en ProfileId, lik svarnummeret.

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

Klientstrøm er veldig lik serverstrøm. Først nå sender klienten en strøm av meldinger, og serveren behandler dem. Serveren kan behandle meldinger umiddelbart eller vente på alle forespørsler fra klienten og deretter 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 må du returnere StreamObserver til klienten, som serveren vil motta meldinger til. OnError-metoden vil bli kalt hvis det oppstod en feil i strømmen. For eksempel avsluttet den feil.

  Hvordan beskjære et skjermbilde på Mac

For å implementere en toveis strøm, er det nødvendig å kombinere å lage 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 eksemplet, som svar på klientens melding, vil serveren returnere en profil med økt pointCount.

Konklusjon

Vi har dekket de grunnleggende alternativene for meldinger mellom en klient og en server ved hjelp av gRPC: implementert serverstrøm, klientstrøm, toveisstrøm.

Artikkelen er skrevet av Sergey Golitsyn