Introduzione a gRPC-Go

1. Introduzione

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

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.

Il servizio è definito 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 Go che non hanno familiarità con gRPC o che vogliono ripassare gRPC, o a chiunque sia interessato a creare sistemi distribuiti. Non è richiesta alcuna esperienza precedente con gRPC.

2. Prima di iniziare

Prerequisiti

Assicurati di aver installato quanto segue:

  • La toolchain Go versione 1.24.5 o successive. Per istruzioni sull'installazione, consulta la sezione Guida introduttiva di Go.
  • Il compilatore del buffer di protocollo, protoc, versione 3.27.1 o successive. Per le istruzioni di installazione, consulta la guida all'installazione del compilatore.
  • I plug-in del compilatore di buffer di protocollo per Go e gRPC. Per installare questi plug-in, esegui i seguenti comandi:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

Aggiorna la variabile PATH in modo che il compilatore del buffer del protocollo possa trovare i plug-in:

export PATH="$PATH:$(go env GOPATH)/bin"

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.

Scarica questo codice sorgente come archivio .ZIP da GitHub ed estraine i contenuti.

In alternativa, 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 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.

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 il codice client e server

A questo punto, genera il codice gRPC boilerplate sia per il client che per il server dal file .proto utilizzando il compilatore del buffer del protocollo. Nella directory routeguide, esegui:

protoc --go_out=. --go_opt=paths=source_relative \
       --go-grpc_out=. --go-grpc_opt=paths=source_relative \
       route_guide.proto

Questo comando genera i seguenti file:

  • route_guide.pb.go, che contiene funzioni per creare i tipi di messaggi dell'applicazione e accedere ai relativi dati.
  • route_guide_grpc.pb.go, che contiene le funzioni utilizzate dal client per chiamare il metodo gRPC remoto del servizio e le funzioni utilizzate dal server per fornire il servizio remoto.

Successivamente, implementeremo il metodo GetFeature sul lato server, in modo che quando il client invia una richiesta, il server possa rispondere.

5. Implementare il servizio

La funzione GetFeature sul lato server è dove viene svolto il lavoro principale: riceve un messaggio Point dal client e restituisce in un messaggio Feature le informazioni sulla posizione corrispondenti da un elenco di luoghi noti. Ecco l'implementazione della funzione in server/server.go:

func (s *routeGuideServer) GetFeature(ctx context.Context, point *pb.Point) (*pb.Feature, error) {
  for _, feature := range s.savedFeatures {
    if proto.Equal(feature.Location, point) {
      return feature, nil
    }
  }
  // No feature was found, return an unnamed feature
  return &pb.Feature{Location: point}, nil
}

Quando questo metodo viene richiamato in seguito a una richiesta di un client remoto, alla funzione viene passato un oggetto Context che descrive la chiamata RPC e un oggetto buffer di protocollo Point dalla richiesta del client. La funzione restituisce un oggetto buffer di protocollo Feature per la posizione cercata e un error, se necessario.

Nel metodo, compila un oggetto Feature con le informazioni appropriate per il Point specificato, quindi return insieme a un errore nil per comunicare a gRPC che hai terminato di gestire la RPC e che l'oggetto Feature può essere restituito al client.

Il metodo GetFeature richiede la creazione e la registrazione di un oggetto routeGuideServer in modo che le richieste dei client per le ricerche di località possano essere indirizzate a questa funzione. Questa operazione viene eseguita in main():

func main() {
  flag.Parse()
  lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", *port))
  if err != nil {
    log.Fatalf("failed to listen: %v", err)
  }

  var opts []grpc.ServerOption
  grpcServer := grpc.NewServer(opts...)

  s := &routeGuideServer{}
  s.loadFeatures()
  pb.RegisterRouteGuideServer(grpcServer, s)
  grpcServer.Serve(lis)
}

Ecco cosa succede in main(), passo dopo passo:

  1. Specifica la porta TCP da utilizzare per l'ascolto delle richieste del client remoto, utilizzando lis, err := net.Listen(...). Per impostazione predefinita, l'applicazione utilizza la porta TCP 50051 come specificato dalla variabile port o passando l'opzione --port nella riga di comando durante l'esecuzione del server. Se non è possibile aprire la porta TCP, l'applicazione termina con un errore irreversibile.
  2. Crea un'istanza del server gRPC utilizzando grpc.NewServer(...), assegnando a questa istanza il nome grpcServer.
  3. Crea un puntatore a routeGuideServer, una struttura che rappresenta il servizio API dell'applicazione, assegnando al puntatore il nome s.
  4. Utilizza s.loadFeatures() per compilare l'array s.savedFeatures con le località che possono essere cercate tramite GetFeature.
  5. Registra il servizio API con il server gRPC in modo che le chiamate RPC a GetFeature vengano indirizzate alla funzione appropriata.
  6. Chiama Serve() sul server con i dettagli della porta per eseguire un'attesa di blocco per le richieste del client; questa operazione continua finché il processo non viene interrotto o viene chiamato Stop().

La funzione loadFeatures() ottiene le mappature da coordinate a posizione da server/testdata.go.

6. Crea il client

Ora modifica client/client.go, dove implementerai il codice client.

Per chiamare i metodi del servizio remoto, dobbiamo prima creare un canale gRPC per comunicare con il server. Lo creiamo passando la stringa URI di destinazione del server (che in questo caso è semplicemente l'indirizzo e il numero di porta) a grpc.NewClient() nella funzione main() del client nel seguente modo:

conn, err := grpc.NewClient("dns:///"+*serverAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
        log.Fatalf("fail to dial: %v", err)
}
defer conn.Close()

L'indirizzo del server, definito dalla variabile serverAddr, è localhost:50051 per impostazione predefinita e può essere sostituito dall'opzione --addr nella riga di comando quando esegui il client.

Se il client deve connettersi a un servizio che richiede credenziali di autenticazione, come credenziali TLS o JWT, può passare un oggetto DialOptions come parametro a grpc.NewClient che contiene le credenziali richieste. Il servizio RouteGuide non richiede credenziali.

Una volta configurato il canale gRPC, abbiamo bisogno di uno stub client per eseguire RPC tramite chiamate di funzioni Go. Otteniamo questo stub utilizzando il metodo NewRouteGuideClient fornito dal file route_guide_grpc.pb.go generato dal file .proto dell'applicazione.

import (pb "github.com/grpc-ecosystem/codelabs/getting_started_unary/routeguide")

client := pb.NewRouteGuideClient(conn)

Metodi di servizio di chiamata

In gRPC-Go, le RPC operano in modalità di blocco/sincrona, il che significa che la chiamata RPC attende la risposta del server e restituisce una risposta o un errore.

RPC semplice

Chiamare la semplice RPC GetFeature è quasi semplice come chiamare un metodo locale, in questo caso client.GetFeature:

point := &pb.Point{Latitude: 409146138, Longitude: -746188906}
log.Printf("Getting feature for point (%d, %d)", point.Latitude, point.Longitude)

// Call GetFeature method on the client.
feature, err := client.GetFeature(context.TODO(), point)
if err != nil {
  log.Fatalf("client.GetFeature failed: %v", err)
}

Il client chiama il metodo sullo stub creato in precedenza. Per i parametri del metodo, il client crea e compila un oggetto buffer del protocollo di richiesta Point. Passi anche un oggetto context.Context che ci consente di modificare il comportamento della nostra RPC, se necessario, ad esempio definendo un limite di tempo per la chiamata o annullando una RPC in corso. Se la chiamata non restituisce un errore, il client può leggere le informazioni di risposta dal server dal primo valore restituito:

log.Println(feature)

Nel complesso, la funzione main() del client dovrebbe avere il seguente aspetto:

func main() {
        flag.Parse()

        // Set up a connection to the gRPC server.
        conn, err := grpc.NewClient("dns:///"+*serverAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
        if err != nil {
                log.Fatalf("fail to dial: %v", err)
        }
        defer conn.Close()

        // Create a new RouteGuide stub.
        client := pb.NewRouteGuideClient(conn)

        point := &pb.Point{Latitude: 409146138, Longitude: -746188906}
        log.Printf("Getting feature for point (%d, %d)", point.Latitude, point.Longitude)

        // Call GetFeature method on the client.
        feature, err := client.GetFeature(context.TODO(), point)
        if err != nil {
                log.Fatalf("client.GetFeature failed: %v", err)
        }
        log.Println(feature)
}

7. Prova

Verifica che il server e il client funzionino correttamente tra loro eseguendo i seguenti comandi nella directory di lavoro dell'applicazione:

  1. Esegui il server in un terminale:
cd server
go run .
  1. Esegui il client da un altro terminale:
cd client
go run .

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

Getting feature for point (409146138, -746188906)
name:"Berkshire Valley Management Area Trail, Jefferson, NJ, USA" location:<latitude:409146138 longitude:-746188906 >
Getting feature for point (0, 0)
location:<>

8. Passaggi successivi