Java gRPC da zero

Esploriamo come implementare gRPC in Java.

gRPC (Google Remote Procedure Call): gRPC è un’architettura RPC open source sviluppata da Google per consentire comunicazioni ad alta velocità tra microservizi. gRPC consente agli sviluppatori di integrare servizi scritti in linguaggi diversi. gRPC utilizza il formato di messaggistica Protobuf (buffer di protocollo), un formato di messaggistica altamente efficiente e altamente compresso per la serializzazione dei dati strutturati.

Per alcuni casi d’uso, l’API gRPC potrebbe essere più efficiente dell’API REST.

Proviamo a scrivere un server su gRPC. Innanzitutto, dobbiamo scrivere diversi file .proto che descrivono servizi e modelli (DTO). Per un server semplice, useremo ProfileService e ProfileDescriptor.

ProfileService ha questo aspetto:

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 supporta un’ampia gamma di opzioni di comunicazione client-server. Li analizzeremo tutti:

  • Chiamata al server normale: richiesta/risposta.
  • Streaming dal client al server.
  • Streaming dal server al client.
  • E, naturalmente, il flusso bidirezionale.

Il servizio ProfileService utilizza il ProfileDescriptor, specificato nella sezione di importazione:

syntax = "proto3";
package com.deft.grpc;
message ProfileDescriptor {
  int64 profile_id = 1;
  string name = 2;
}
  • int64 è Long per Java. Lascia che l’ID del profilo appartenga.
  • String – proprio come in Java, questa è una variabile stringa.

Puoi usare Gradle o Maven per costruire il progetto. È più conveniente per me usare Maven. E inoltre sarà il codice che utilizza Maven. Questo è abbastanza importante da dire perché per Gradle, la generazione futura di .proto sarà leggermente diversa e il file di build dovrà essere configurato in modo diverso. Per scrivere un semplice server gRPC, abbiamo solo bisogno di una dipendenza:

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

È semplicemente incredibile. Questo antipasto svolge un’enorme quantità di lavoro per noi.

Il progetto che creeremo sarà simile a questo:

Abbiamo bisogno di GrpcServerApplication per avviare l’applicazione Spring Boot. E GrpcProfileService, che implementerà metodi dal servizio .proto. Per utilizzare protoc e generare classi da file .proto scritti, aggiungi protobuf-maven-plugin a pom.xml. La sezione build sarà simile a questa:

<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 – specificando la directory in cui si trovano i file .proto.
  • outputDirectory: selezionare la directory in cui verranno generati i file.
  • clearOutputDirectory – un flag che indica di non cancellare i file generati.

A questo punto, puoi costruire un progetto. Successivamente, devi andare alla cartella che abbiamo specificato nella directory di output. I file generati saranno lì. Ora puoi implementare gradualmente GrpcProfileService.

La dichiarazione di classe sarà simile a questa:

@GRpcService
public class GrpcProfileService extends ProfileServiceGrpc.ProfileServiceImplBase

Annotazione GRpcService: contrassegna la classe come bean grpc-service.

Poiché ereditiamo il nostro servizio da ProfileServiceGrpc, ProfileServiceImplBase, possiamo sovrascrivere i metodi della classe genitore. Il primo metodo che sovrascriveremo è 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();
    }

Per rispondere al client, è necessario chiamare il metodo onNext sullo StreamObserver passato. Dopo aver inviato la risposta, invia un segnale al client che il server ha terminato di funzionareCompletato. Quando si invia una richiesta al server getCurrentProfile, la risposta sarà:

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

Successivamente, diamo un’occhiata al flusso del server. Con questo approccio di messaggistica, il client invia una richiesta al server, il server risponde al client con un flusso di messaggi. Ad esempio, invia cinque richieste in un ciclo. Al termine dell’invio, il server invia un messaggio al client sul corretto completamento del flusso.

Il metodo del flusso del server sovrascritto sarà simile al seguente:

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

Pertanto, il client riceverà cinque messaggi con un ProfileId, uguale al numero di risposta.

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

Il flusso del client è molto simile al flusso del server. Solo ora il client trasmette un flusso di messaggi e il server li elabora. Il server può elaborare i messaggi immediatamente o attendere tutte le richieste dal client e quindi elaborarle.

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

Nel flusso del client, è necessario restituire lo StreamObserver al client, al quale il server riceverà i messaggi. Il metodo onError verrà chiamato se si verifica un errore nel flusso. Ad esempio, è terminato in modo errato.

Per implementare un flusso bidirezionale, è necessario combinare la creazione di un flusso dal server e dal client.

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

In questo esempio, in risposta al messaggio del client, il server restituirà un profilo con un pointCount aumentato.

Conclusione

Abbiamo coperto le opzioni di base per la messaggistica tra un client e un server utilizzando gRPC: flusso del server implementato, flusso client, flusso bidirezionale.

L’articolo è stato scritto da Sergey Golitsyn