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 informazioni sulle funzionalità di un percorso del client, creare un riepilogo di un percorso del client e scambiare informazioni sul percorso, come gli aggiornamenti sul traffico, con il server e altri client.
Il servizio è definito in un file Protocol Buffers, che verrà utilizzato per generare il 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 di streaming 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.
Innanzitutto, crea la directory di lavoro del codelab e cd
al suo interno:
mkdir streaming-grpc-go-getting-started && cd streaming-grpc-go-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-go-streaming/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 messaggi e servizi
Il primo passaggio consiste nel definire il servizio gRPC dell'applicazione, i relativi metodi RPC e i tipi di messaggi di richiesta e risposta utilizzando Protocol Buffers. Il tuo servizio fornirà:
- Metodi RPC chiamati
ListFeatures
,RecordRoute
eRouteChat
che il server implementa e il client chiama. - I tipi di messaggi
Point
,Feature
,Rectangle
,RouteNote
eRouteSummary
, che sono strutture di dati scambiate tra il client e il server quando vengono chiamati i metodi precedenti.
Questi metodi RPC e i relativi tipi di messaggio verranno tutti 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.
Definisci i 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;
}
Poi un messaggio Rectangle
che rappresenta un rettangolo di latitudine e longitudine, rappresentato da due punti diagonalmente opposti "lo" e "hi".
message Rectangle {
// One corner of the rectangle.
Point lo = 1;
// The other corner of the rectangle.
Point hi = 2;
}
Inoltre, un messaggio RouteNote
che rappresenta un messaggio inviato in un determinato momento.
message RouteNote {
// The location from which the message is sent.
Point location = 1;
// The message to be sent.
string message = 2;
}
Richiederemo anche un messaggio su RouteSummary
. Questo messaggio viene ricevuto in risposta a una RPC RecordRoute
, che viene spiegata nella sezione successiva. Contiene il numero di punti individuali ricevuti, il numero di caratteristiche rilevate e la distanza totale percorsa come somma cumulativa della distanza tra ogni punto.
message RouteSummary {
// The number of points received.
int32 point_count = 1;
// The number of known features passed while traversing the route.
int32 feature_count = 2;
// The distance covered in metres.
int32 distance = 3;
// The duration of the traversal in seconds.
int32 elapsed_time = 4;
}
Definisci i metodi di servizio
Per definire un servizio, specifica un servizio denominato nel file .proto. Il file route_guide.proto
ha una struttura service
denominata RouteGuide
che definisce uno o più metodi forniti dal servizio dell'applicazione.
Definisci i metodi RPC
all'interno della definizione del servizio, specificando i tipi di richiesta e risposta. In questa sezione del codelab, definiamo:
ListFeatures
Recupera gli Feature
disponibili all'interno del Rectangle
specificato. I risultati vengono trasmessi in streaming anziché restituiti contemporaneamente (ad es. in un messaggio di risposta con un campo ripetuto), poiché il rettangolo potrebbe coprire un'area di grandi dimensioni e contenere un numero elevato di funzionalità.
Un tipo appropriato per questa RPC è una RPC di streaming lato server: il client invia una richiesta al server e riceve un flusso per leggere una sequenza di messaggi. Il client legge dallo stream restituito finché non ci sono più messaggi. Come puoi vedere nel nostro esempio, devi specificare un metodo di streaming lato server inserendo la parola chiave stream prima del tipo di risposta.
rpc ListFeatures(Rectangle) returns (stream Feature) {}
RecordRoute
Accetta un flusso di Point
su un percorso attraversato, restituendo un RouteSummary
al termine dell'attraversamento.
In questo caso, una RPC streaming lato client sembra appropriata: il client scrive una sequenza di messaggi e li invia al server, sempre utilizzando un flusso fornito. Una volta che il client ha finito di scrivere i messaggi, attende che il server li legga tutti e restituisca la risposta. Specifichi un metodo di streaming lato client inserendo la parola chiave stream prima del tipo di richiesta.
rpc RecordRoute(stream Point) returns (RouteSummary) {}
RouteChat
Accetta un flusso di RouteNote
inviati durante il percorso di un itinerario, mentre riceve altri RouteNote
(ad es. da altri utenti).
Questo è esattamente il tipo di caso d'uso per lo streaming bidirezionale. Una RPC di streaming bidirezionale fa sì che entrambe le parti inviino una sequenza di messaggi utilizzando uno stream di lettura/scrittura. I due flussi operano in modo indipendente, quindi client e server possono leggere e scrivere nell'ordine che preferiscono.
Ad esempio, il server potrebbe attendere di ricevere tutti i messaggi del client prima di scrivere le risposte oppure potrebbe leggere un messaggio e poi scriverne un altro o una combinazione di letture e scritture.
L'ordine dei messaggi in ogni stream viene mantenuto. Specifichi questo tipo di metodo inserendo la parola chiave stream prima della richiesta e della risposta.
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
4. Genera 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 e alla definizione dei tipi che rappresentano i messaggi.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 i metodi lato server, in modo che quando il client invia una richiesta, il server possa rispondere.
5. Implementare il servizio
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: esecuzione del "lavoro" effettivo del nostro servizio.
- Esecuzione di un server gRPC per ascoltare le richieste dei client e inviarle all'implementazione del servizio corretta.
Implementiamo RouteGuide in server/server.go
.
Implementa RouteGuide
Dobbiamo implementare l'interfaccia RouteGuideService
generata. Ecco come apparirebbe l'implementazione.
type routeGuideServer struct {
...
}
...
func (s *routeGuideServer) ListFeatures(rect *pb.Rectangle, stream pb.RouteGuide_ListFeaturesServer) error {
...
}
...
func (s *routeGuideServer) RecordRoute(stream pb.RouteGuide_RecordRouteServer) error {
...
}
...
func (s *routeGuideServer) RouteChat(stream pb.RouteGuide_RouteChatServer) error {
...
}
Esaminiamo in dettaglio ogni implementazione RPC.
RPC di streaming lato server
Inizia con una delle nostre RPC di streaming. ListFeatures
è una RPC di streaming lato server, quindi dobbiamo inviare più Feature
al nostro client.
func (s *routeGuideServer) ListFeatures(rect *pb.Rectangle, stream pb.RouteGuide_ListFeaturesServer) error {
for _, feature := range s.savedFeatures {
if inRange(feature.Location, rect) {
if err := stream.Send(feature); err != nil {
return err
}
}
}
return nil
}
Come puoi vedere, anziché ottenere semplici oggetti di richiesta e risposta nei parametri del nostro metodo, questa volta otteniamo un oggetto di richiesta (il Rectangle
in cui il nostro cliente vuole trovare Features
) e uno speciale oggetto RouteGuide_ListFeaturesServer
per scrivere le nostre risposte. Nel metodo, inseriamo tutti gli oggetti Feature
necessari per la restituzione, scrivendoli in RouteGuide_ListFeaturesServer
utilizzando il metodo Send()
. Infine, come nella nostra semplice RPC, restituiamo un errore nil
per comunicare a gRPC che abbiamo terminato di scrivere le risposte. Se si verifica un errore in questa chiamata, restituiamo un errore non nullo; il livello gRPC lo traduce in uno stato RPC appropriato da inviare via cavo.
RPC di streaming lato client
Ora esaminiamo qualcosa di un po' più complicato: il metodo di streaming lato client RecordRoute
, in cui riceviamo un flusso di Points
dal client e restituiamo un singolo RouteSummary
con informazioni sul viaggio. Come puoi vedere, questa volta il metodo non ha alcun parametro di richiesta. Riceve invece un flusso RouteGuide_RecordRouteServer
, che il server può utilizzare per leggere e scrivere messaggi. Può ricevere messaggi client utilizzando il metodo Recv()
e restituire la singola risposta utilizzando il metodo SendAndClose()
.
func (s *routeGuideServer) RecordRoute(stream pb.RouteGuide_RecordRouteServer) error {
var pointCount, featureCount, distance int32
var lastPoint *pb.Point
startTime := time.Now()
for {
point, err := stream.Recv()
if err == io.EOF {
endTime := time.Now()
return stream.SendAndClose(&pb.RouteSummary{
PointCount: pointCount,
FeatureCount: featureCount,
Distance: distance,
ElapsedTime: int32(endTime.Sub(startTime).Seconds()),
})
}
if err != nil {
return err
}
pointCount++
for _, feature := range s.savedFeatures {
if proto.Equal(feature.Location, point) {
featureCount++
}
}
if lastPoint != nil {
distance += calcDistance(lastPoint, point)
}
lastPoint = point
}
}
Nel corpo del metodo utilizziamo il metodo Recv()
di RouteGuide_RecordRouteServer
per leggere ripetutamente le richieste del nostro client in un oggetto richiesta (in questo caso un Point
) finché non ci sono più messaggi: il server deve controllare l'errore restituito da Recv()
dopo ogni chiamata. Se è nil
, lo stream è ancora valido e può continuare a leggere; se è io.EOF
, lo stream di messaggi è terminato e il server può restituire il relativo RouteSummary
. Se ha un altro valore, restituiamo l'errore "così com'è" in modo che venga tradotto in uno stato RPC dal livello gRPC.
RPC di streaming bidirezionale
Infine, diamo un'occhiata alla nostra RPC di streaming bidirezionale RouteChat()
.
func (s *routeGuideServer) RouteChat(stream pb.RouteGuide_RouteChatServer) error {
for {
in, err := stream.Recv()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
key := serialize(in.Location)
s.mu.Lock()
s.routeNotes[key] = append(s.routeNotes[key], in)
// Note: this copy prevents blocking other clients while serving this one.
// We don't need to do a deep copy, because elements in the slice are
// insert-only and never modified.
rn := make([]*pb.RouteNote, len(s.routeNotes[key]))
copy(rn, s.routeNotes[key])
s.mu.Unlock()
for _, note := range rn {
if err := stream.Send(note); err != nil {
return err
}
}
}
}
Questa volta otteniamo uno stream RouteGuide_RouteChatServer
che, come nell'esempio di streaming lato client, può essere utilizzato per leggere e scrivere messaggi. Tuttavia, questa volta restituiamo i valori tramite lo stream del nostro metodo mentre il client sta ancora scrivendo messaggi nel suo stream di messaggi. La sintassi per la lettura e la scrittura qui è molto simile al nostro metodo di streaming lato client, tranne per il fatto che il server utilizza il metodo send()
dello stream anziché SendAndClose()
perché scrive più risposte. Sebbene ogni parte riceva sempre i messaggi dell'altra nell'ordine in cui sono stati scritti, sia il client che il server possono leggere e scrivere in qualsiasi ordine. I flussi operano in modo completamente indipendente.
Avvia il server
Una volta implementati tutti i nostri metodi, dobbiamo anche avviare un server gRPC in modo che i client possano effettivamente utilizzare il nostro servizio. Il seguente snippet mostra come lo facciamo per il nostro servizio RouteGuide
:
lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", *port))
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
grpcServer := grpc.NewServer()
s := &routeGuideServer{routeNotes: make(map[string][]*pb.RouteNote)}
s.loadFeatures()
pb.RegisterRouteGuideServer(grpcServer, s)
grpcServer.Serve(lis)
Ecco cosa succede in main()
, passo dopo passo:
- 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 TCP50051
come specificato dalla variabileport
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. - Crea un'istanza del server gRPC utilizzando
grpc.NewServer(...)
, assegnando a questa istanza il nomegrpcServer
. - Crea un puntatore a
routeGuideServer
, una struttura che rappresenta il servizio API dell'applicazione, assegnando al puntatore il nomes.
- Utilizza
s.loadFeatures()
per compilare l'arrays.savedFeatures
. - Registra l'implementazione del servizio con il server gRPC.
- 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 chiamatoStop()
.
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:
// 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()
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_streaming/routeguide")
client := pb.NewRouteGuideClient(conn)
Metodi di servizio di chiamata
Ora vediamo come chiamiamo i metodi del servizio. 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 di streaming lato server
Qui chiamiamo il metodo di streaming lato server ListFeatures
, che restituisce un flusso di oggetti geografici Feature
.
rect := &pb.Rectangle{ ... } // initialize a pb.Rectangle
log.Printf("Looking for features within %v", rect)
stream, err := client.ListFeatures(context.Background(), rect)
if err != nil {
log.Fatalf("client.ListFeatures failed: %v", err)
}
for {
// For server-to-client streaming RPCs, you call stream.Recv() until it
// returns io.EOF.
feature, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
log.Fatalf("client.ListFeatures failed: %v", err)
}
log.Printf("Feature: name: %q, point:(%v, %v)", feature.GetName(),
feature.GetLocation().GetLatitude(), feature.GetLocation().GetLongitude())
}
Come nella semplice RPC, passiamo al metodo un contesto e una richiesta. Tuttavia, anziché ricevere un oggetto di risposta, riceviamo un'istanza di RouteGuide_ListFeaturesClient
. Il client può utilizzare lo stream RouteGuide_ListFeaturesClient
per leggere le risposte del server. Utilizziamo il metodo Recv()
di RouteGuide_ListFeaturesClient
per leggere ripetutamente le risposte del server a un oggetto buffer di protocollo di risposta (in questo caso un Feature
) finché non ci sono più messaggi: il client deve controllare l'errore err restituito da Recv()
dopo ogni chiamata. Se nil
, lo stream è ancora valido e può continuare a leggere; se è io.EOF
, lo stream di messaggi è terminato; in caso contrario, deve esserci un errore RPC, che viene passato tramite err
.
RPC di streaming lato client
Il metodo di streaming lato client RecordRoute
è simile al metodo lato server, tranne per il fatto che passiamo al metodo solo un contesto e riceviamo un flusso RouteGuide_RecordRouteClient
, che possiamo utilizzare sia per scrivere che per leggere i messaggi.
// Create a random number of random points
r := rand.New(rand.NewSource(time.Now().UnixNano()))
pointCount := int(r.Int31n(100)) + 2 // Traverse at least two points
var points []*pb.Point
for i := 0; i < pointCount; i++ {
points = append(points, randomPoint(r))
}
log.Printf("Traversing %d points.", len(points))
c2sStream, err := client.RecordRoute(context.TODO())
if err != nil {
log.Fatalf("client.RecordRoute failed: %v", err)
}
// Stream each point to the server.
for _, point := range points {
if err := c2sStream.Send(point); err != nil {
log.Fatalf("client.RecordRoute: stream.Send(%v) failed: %v", point, err)
}
}
// Close the stream and receive the RouteSummary from the server.
reply, err := c2sStream.CloseAndRecv()
if err != nil {
log.Fatalf("client.RecordRoute failed: %v", err)
}
log.Printf("Route summary: %v", reply)
RouteGuide_RecordRouteClient
ha un metodo Send()
che possiamo utilizzare per inviare richieste al server. Una volta terminata la scrittura delle richieste del cliente nel flusso utilizzando Send()
, dobbiamo chiamare CloseAndRecv()
nel flusso per comunicare a gRPC che abbiamo terminato la scrittura e che ci aspettiamo di ricevere una risposta. Otteniamo lo stato RPC dall'errore restituito da CloseAndRecv()
. Se lo stato è nil
, il primo valore restituito da CloseAndRecv()
sarà una risposta valida del server.
RPC di streaming bidirezionale
Infine, diamo un'occhiata alla nostra RPC di streaming bidirezionale RouteChat()
. Come nel caso di RecordRoute
, passiamo al metodo solo un oggetto contesto e riceviamo un flusso che possiamo utilizzare per scrivere e leggere i messaggi. Tuttavia, questa volta restituiamo i valori tramite lo stream del nostro metodo mentre il server sta ancora scrivendo messaggi nel suo stream di messaggi.
biDiStream, err := client.RouteChat(context.Background())
if err != nil {
log.Fatalf("client.RouteChat failed: %v", err)
}
// this channel is used to wait for the receive goroutine to finish.
recvDoneCh := make(chan struct{})
// receive goroutine.
go func() {
for {
in, err := biDiStream.Recv()
if err == io.EOF {
// read done.
close(recvDoneCh)
return
}
if err != nil {
log.Fatalf("client.RouteChat failed: %v", err)
}
log.Printf("Got message %s at point(%d, %d)", in.Message, in.Location.Latitude, in.Location.Longitude)
}
}()
// send messages simultaneously.
for _, note := range notes {
if err := biDiStream.Send(note); err != nil {
log.Fatalf("client.RouteChat: stream.Send(%v) failed: %v", note, err)
}
}
biDiStream.CloseSend()
// wait for the receive goroutine to finish.
<-recvDoneCh
La sintassi per la lettura e la scrittura qui è molto simile al nostro metodo di streaming lato client, tranne per il fatto che utilizziamo il metodo CloseSend()
dello stream una volta terminata la chiamata. Sebbene ogni parte riceva sempre i messaggi dell'altra nell'ordine in cui sono stati scritti, sia il client che il server possono leggere e scrivere in qualsiasi ordine. I flussi operano in modo completamente indipendente.
7. Prova
Verifica che il server e il client funzionino correttamente tra loro eseguendo i seguenti comandi nella directory di lavoro dell'applicazione:
- Esegui il server in un terminale:
cd server go run .
- Esegui il client da un altro terminale:
cd client go run .
Vedrai un output simile a questo, con i timestamp omessi per chiarezza:
Looking for features within lo:<latitude:400000000 longitude:-750000000 > hi:<latitude:420000000 longitude:-730000000 > name:"Patriots Path, Mendham, NJ 07945, USA" location:<latitude:407838351 longitude:-746143763 > ... name:"3 Hasta Way, Newton, NJ 07860, USA" location:<latitude:410248224 longitude:-747127767 > Traversing 56 points. Route summary: point_count:56 distance:497013163 Got message First message at point(0, 1) Got message Second message at point(0, 2) Got message Third message at point(0, 3) Got message First message at point(0, 1) Got message Fourth message at point(0, 1) Got message Second message at point(0, 2) Got message Fifth message at point(0, 2) Got message Third message at point(0, 3) Got message Sixth message at point(0, 3)
8. Passaggi successivi
- Scopri come funziona gRPC in Introduzione a gRPC e Concetti di base.
- Completa il tutorial sulle nozioni di base.
- Esplora il riferimento API.