Introduzione a gRPC-Java

1. Introduzione

In questo codelab, utilizzerai gRPC-Java per creare un client e un server che costituiscono la base di un'applicazione di mappatura di percorsi scritta in Java.

Al termine del tutorial, avrai un client che si connette a un server remoto utilizzando gRPC per ottenere il nome o l'indirizzo postale di ciò che si trova in coordinate specifiche su una mappa. Un'applicazione completa potrebbe utilizzare questa progettazione client-server per enumerare o riepilogare i punti di interesse lungo un percorso.

L'API del server è definita in un file Protocol Buffers, che verrà utilizzato per generare codice boilerplate per il client e il server in modo che possano comunicare tra loro, risparmiando tempo e fatica nell'implementazione di questa funzionalità.

Questo codice generato si occupa non solo delle complessità della comunicazione tra il server e il client, ma anche della serializzazione e della deserializzazione dei dati.

Obiettivi didattici

  • Come utilizzare i buffer di protocollo per definire un'API di servizio.
  • Come creare un client e un server basati su gRPC da una definizione di Protocol Buffers utilizzando la generazione automatica del codice.
  • Comprensione della comunicazione client-server con gRPC.

Questo codelab è rivolto agli sviluppatori Java che non hanno mai utilizzato gRPC o che vogliono ripassare le basi di gRPC, nonché a chiunque sia interessato a creare sistemi distribuiti. Non è richiesta alcuna esperienza precedente con gRPC.

2. Prima di iniziare

Prerequisiti

  • JDK versione 8 o successive

Ottieni il codice

Per non dover iniziare da zero, questo codelab fornisce una struttura del codice sorgente dell'applicazione da completare. I passaggi seguenti mostrano come completare l'applicazione, incluso l'utilizzo dei plug-in del compilatore di protocol buffer per generare il codice gRPC boilerplate.

Innanzitutto, crea la directory di lavoro del codelab e accedi tramite cd:

mkdir grpc-java-getting-started && cd grpc-java-getting-started

Scarica ed estrai il codelab:

curl -sL https://github.com/grpc-ecosystem/grpc-codelabs/archive/refs/heads/v1.tar.gz \
  | tar xvz --strip-components=4 \
  grpc-codelabs-1/codelabs/grpc-java-getting-started/start_here

In alternativa, puoi scaricare il file .zip contenente solo la directory del codelab e decomprimerlo manualmente.

Il codice sorgente completato è disponibile su GitHub se vuoi evitare di digitare un'implementazione.

3. Definisci il servizio

Il primo passaggio consiste nel definire il servizio gRPC dell'applicazione, il relativo metodo RPC e i tipi di messaggi di richiesta e risposta utilizzando Protocol Buffers. Il tuo servizio fornirà:

  • Un metodo RPC chiamato GetFeature che il server implementa e il client chiama.
  • I tipi di messaggio Point e Feature, che sono strutture di dati scambiate tra il client e il server quando si utilizza il metodo GetFeature. Il client fornisce le coordinate della mappa come Point nella richiesta GetFeature al server e il server risponde con un Feature corrispondente che descrive ciò che si trova a quelle coordinate.

Questo metodo RPC e i relativi tipi di messaggio verranno definiti nel file src/main/proto/routeguide/route_guide.proto del codice sorgente fornito.

Protocol Buffers sono comunemente noti come protobuf. Per ulteriori informazioni sulla terminologia gRPC, consulta Concetti fondamentali, architettura e ciclo di vita di gRPC.

Poiché in questo esempio generiamo codice Java, abbiamo specificato un'opzione per il file java_package e un nome per la classe Java nel nostro .proto:

option java_package = "io.grpc.examples.routeguide";
option java_outer_classname = "RouteGuideProto";

Tipi di messaggi

Nel file routeguide/route_guide.proto del codice sorgente, definisci innanzitutto il tipo di messaggio Point. Un Point rappresenta una coppia di coordinate di latitudine e longitudine su una mappa. Per questo codelab, utilizza numeri interi per le coordinate:

message Point {
  int32 latitude = 1;
  int32 longitude = 2;
}

I numeri 1 e 2 sono numeri ID univoci per ciascuno dei campi nella struttura message.

Successivamente, definisci il tipo di messaggio Feature. Un Feature utilizza un campo string per il nome o l'indirizzo postale di un elemento in una località specificata da un Point:

message Feature {
  // The name or address of the feature.
  string name = 1;

  // The point where the feature is located.
  Point location = 2;
}

Metodo di servizio

Il file route_guide.proto ha una struttura service denominata RouteGuide che definisce uno o più metodi forniti dal servizio dell'applicazione.

Aggiungi il metodo rpc GetFeature all'interno della definizione di RouteGuide. Come spiegato in precedenza, questo metodo cerca il nome o l'indirizzo di una località da un determinato insieme di coordinate, quindi GetFeature restituisce un Feature per un determinato Point:

service RouteGuide {
  // Definition of the service goes here

  // Obtains the feature at a given position.
  rpc GetFeature(Point) returns (Feature) {}
}

Si tratta di un metodo RPC unario: una RPC semplice in cui il client invia una richiesta al server e attende una risposta, proprio come una chiamata di funzione locale.

4. Genera codice client e server

Successivamente, dobbiamo generare le interfacce client e server gRPC dalla definizione del servizio .proto. A questo scopo, utilizziamo il compilatore di protocol buffer protoc con un plug-in Java gRPC speciale. Per generare servizi gRPC, devi utilizzare il compilatore proto3 (che supporta sia la sintassi proto2 sia quella proto3).

Quando utilizzi Gradle o Maven, il plug-in di compilazione protoc può generare il codice necessario come parte della compilazione. Per informazioni su come generare codice dai tuoi file .proto, consulta il file README di grpc-java.

Nel codice sorgente del codelab abbiamo fornito un ambiente e una configurazione Gradle per creare questo progetto.

All'interno della directory grpc-java-getting-started, esegui questo comando:

$ chmod +x gradlew
$ ./gradlew generateProto

Le seguenti classi vengono generate dalla nostra definizione di servizio:

  • Feature.java, Point.java e altri che contengono tutto il codice del buffer del protocollo per popolare, serializzare e recuperare i nostri tipi di messaggi di richiesta e risposta.
  • RouteGuideGrpc.java che contiene (insieme ad altro codice utile) una classe base per l'implementazione dei server RouteGuide, RouteGuideGrpc.RouteGuideImplBase, con tutti i metodi definiti nel servizio RouteGuide e classi stub da utilizzare per i client.

5. Implementare il server

Per prima cosa, vediamo come creare un server RouteGuide. Il funzionamento del nostro servizio RouteGuide si basa su due elementi:

  • Implementazione dell'interfaccia di servizio generata dalla nostra definizione di servizio, che svolge il "lavoro" effettivo del nostro servizio.
  • Esecuzione di un server gRPC per ascoltare le richieste dei client e inviarle all'implementazione del servizio corretta.

Implementa RouteGuide

Come puoi vedere, il nostro server ha una classe RouteGuideService che estende la classe astratta RouteGuideGrpc.RouteGuideImplBase generata:

private static class RouteGuideService extends RouteGuideGrpc.RouteGuideImplBase {
...
}

Abbiamo fornito i seguenti due file per inizializzare il server con le funzionalità:

./src/main/java/io/grpc/examples/routeguide/RouteGuideUtil.java

./src/main/resources/io/grpc/examples/routeguide/route_guide_db.json

Esaminiamo in dettaglio una semplice implementazione RPC.

RPC unario

RouteGuideService implementa tutti i nostri metodi di servizio. In questo caso, è solo GetFeature(), prende un messaggio Point dal client e restituisce in un messaggio Feature le informazioni sulla posizione corrispondenti da un elenco di luoghi noti.

@Override
public void getFeature(Point request, StreamObserver<Feature> responseObserver) {
  responseObserver.onNext(checkFeature(request));
  responseObserver.onCompleted();
}

Il metodo getFeature() accetta due parametri:

  • Point: la richiesta.
  • StreamObserver<Feature>: un osservatore di risposta, ovvero un'interfaccia speciale che il server chiama con la sua risposta.

Per restituire la nostra risposta al cliente e completare la chiamata:

  1. Costruiamo e popoliamo un oggetto di risposta Feature da restituire al client, come specificato nella definizione del servizio. In questo esempio, lo facciamo in un metodo privato checkFeature() separato.
  2. Utilizziamo il metodo onNext() dell'observer della risposta per restituire Feature.
  3. Utilizziamo il metodo onCompleted() dell'observer di risposta per specificare che abbiamo terminato di gestire la RPC.

Avviare il server

Una volta implementati tutti i nostri metodi di servizio, dobbiamo avviare un server gRPC in modo che i client possano effettivamente utilizzare il nostro servizio. Nel nostro boilerplate includiamo la creazione dell'oggetto ServerBuilder:

ServerBuilder.forPort(port), port, RouteGuideUtil.parseFeatures(featureFile)

Costruiamo il servizio nel costruttore:

  1. Specifica la porta che vogliamo utilizzare per ascoltare le richieste dei client utilizzando il metodo forPort() del builder (utilizzerà l'indirizzo jolly).
  2. Crea un'istanza della nostra classe di implementazione del servizio RouteGuideService e passala al metodo addService() del builder.
  3. Chiama build() sul builder per creare un server RPC per il nostro servizio.

Il seguente snippet mostra come creare un oggetto ServerBuilder.

/** Create a RouteGuide server listening on {@code port} using {@code featureFile} database. */
public RouteGuideServer(int port, URL featureFile) throws IOException {
    this(Grpc.newServerBuilderForPort(port, InsecureServerCredentials.create()),
        port, RouteGuideUtil.parseFeatures(featureFile));
  }

Il seguente snippet mostra come creare un oggetto server per il nostro servizio RouteGuide.

/** Create a RouteGuide server using serverBuilder as a base and features as data. */
public RouteGuideServer(ServerBuilder<?> serverBuilder, int port, Collection<Feature> features) {
  this.port = port;
  server = serverBuilder.addService(new RouteGuideService(features))
      .build();
}

Implementa un metodo di avvio che chiama start sul server che abbiamo creato sopra.

public void start() throws IOException {
  server.start();
  logger.info("Server started, listening on " + port);
}

Implementa un metodo per attendere il completamento del server in modo che non venga chiuso immediatamente.

/** Await termination on the main thread since the grpc library uses daemon threads. */
private void blockUntilShutdown() throws InterruptedException {
  if (server != null) {
    server.awaitTermination();
  }
}

Come puoi vedere, creiamo e avviamo il nostro server utilizzando un ServerBuilder.

Nel metodo principale:

  1. Crea un'istanza RouteGuideServer.
  2. Chiama start() per attivare un server RPC per il nostro servizio.
  3. Attendi l'interruzione del servizio chiamando il numero blockUntilShutdown().
 public static void main(String[] args) throws Exception {
    RouteGuideServer server = new RouteGuideServer(8980);
    server.start();
    server.blockUntilShutdown();
  }

6. Crea il client

In questa sezione vedremo come creare un client per il nostro servizio RouteGuide.

Istanziare uno stub

Per chiamare i metodi di servizio, dobbiamo prima creare uno stub. Esistono due tipi di stub, ma per questo codelab dobbiamo utilizzare solo quello di blocco. I due tipi sono:

  • uno stub bloccante/sincrono che effettua una chiamata RPC e attende la risposta del server, quindi restituisce una risposta o genera un'eccezione.
  • uno stub non bloccante/asincrono che effettua chiamate non bloccanti al server, dove la risposta viene restituita in modo asincrono. Puoi effettuare determinati tipi di chiamate in streaming solo utilizzando lo stub asincrono.

Per prima cosa, dobbiamo creare un canale gRPC e poi utilizzarlo per creare lo stub.

Avremmo potuto utilizzare un ManagedChannelBuilder direttamente per creare il canale.

ManagedChannelBuilder.forAddress(host, port).usePlaintext().build

Utilizziamo invece un metodo di utilità che accetta una stringa con hostname:port.

Grpc.newChannelBuilder(target, InsecureChannelCredentials.create()).build();

Ora possiamo utilizzare il canale per creare lo stub di blocco. Per questo codelab, abbiamo solo RPC bloccanti, quindi utilizziamo il metodo newBlockingStub fornito nella classe RouteGuideGrpc che abbiamo generato dal nostro .proto.

blockingStub = RouteGuideGrpc.newBlockingStub(channel);

Metodi di servizio di chiamata

Ora vediamo come chiamiamo i metodi del servizio.

RPC semplice

Chiamare l'RPC semplice GetFeature è quasi semplice come chiamare un metodo locale.

Creiamo e compiliamo un oggetto buffer di protocollo di richiesta (nel nostro caso Point), lo passiamo al metodo getFeature() sullo stub di blocco e riceviamo un Feature.

Se si verifica un errore, viene codificato come Status, che possiamo ottenere da StatusRuntimeException.

Point request = Point.newBuilder().setLatitude(lat).setLongitude(lon).build();

Feature feature;
try {
  feature = blockingStub.getFeature(request);
} catch (StatusRuntimeException e) {
  logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus());
  return;
}

Il boilerplate registra un messaggio contenente i contenuti in base alla presenza o meno di una funzionalità nel punto specificato.

7. Prova

  1. All'interno della directory start_here, esegui questo comando:
$ ./gradlew installDist

In questo modo, il codice verrà compilato, inserito in un file JAR e verranno creati gli script che eseguono l'esempio. Verranno creati nella directory build/install/start_here/bin/. I copioni sono: route-guide-server e route-guide-client.

Il server deve essere in esecuzione prima di avviare il client.

  1. Esegui il server:
$ ./build/install/start_here/bin/route-guide-server
  1. Esegui il client:
$ ./build/install/start_here/bin/route-guide-client

Vedrai un output simile a questo, con i timestamp omessi per chiarezza:

INFO: *** GetFeature: lat=409,146,138 lon=-746,188,906
INFO: Found feature called "Berkshire Valley Management Area Trail, Jefferson, NJ, USA" at 40.915, -74.619
INFO: *** GetFeature: lat=0 lon=0
INFO: Found no feature at 0, 0

8. Passaggi successivi