Inizia a utilizzare gRPC-Rust

1. Introduzione

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

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 Rust 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:

  • GCC. Segui le istruzioni qui.
  • Rust, versione 1.89.0. Segui le istruzioni di installazione qui.

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-rust-getting-started && cd grpc-rust-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-rust-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 proto/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.

Metodo di servizio

Definiamo prima i metodi di servizio e poi i tipi di messaggio Point e Feature. Il file proto/routeguide.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.

Tipi di messaggi

Nel file proto/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;
}

4. Genera il codice client e server

Ti abbiamo già fornito il codice generato dal file .proto nella directory generata.

Come per qualsiasi progetto, dobbiamo pensare alle dipendenze necessarie per il nostro codice. Per i progetti Rust, le dipendenze si troveranno in Cargo.toml. Abbiamo già elencato le dipendenze necessarie nel file Cargo.toml.

Se vuoi scoprire come generare il codice dal file .proto, consulta queste istruzioni.

Il codice generato contiene:

  • Definizioni di struct per i tipi di messaggi Point e Feature.
  • Una caratteristica del servizio che dovremo implementare: route_guide_server::RouteGuide.
  • Un tipo di client che utilizzeremo per chiamare il server: route_guide_client::RouteGuideClient<T>.

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

In src/server/server.rs, possiamo portare il codice generato nell'ambito tramite la macro include_generated_proto! di gRPC e importare il tratto RouteGuide e Point.

mod grpc_pb {
    grpc::include_generated_proto!("generated", "routeguide");
}

pub use grpc_pb::{
    route_guide_server::{RouteGuideServer, RouteGuide},
    Point, Feature,
};

Possiamo iniziare definendo una struct per rappresentare il nostro servizio. Per ora possiamo farlo su src/server/server.rs:

#[derive(Debug)]
pub struct RouteGuideService {
    features: Vec<Feature>,
}

Ora dobbiamo implementare il tratto route_guide_server::RouteGuide dal codice generato.

RPC unario

RouteGuideService implementa tutti i nostri metodi di servizio. La funzione get_feature 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 src/server/server.rs:

#[tonic::async_trait]
impl RouteGuide for RouteGuideService {
    async fn get_feature(&self, request: Request<Point>) -> Result<Response<Feature>, Status> {
        println!("GetFeature = {:?}", request);
        let requested_point = request.get_ref();
        for feature in self.features.iter() {
            if feature.location().latitude() == requested_point.latitude() {
                if feature.location().longitude() == requested_point.longitude(){
                    return Ok(Response::new(feature.clone()))
                };
            };    
        }
        Ok(Response::new(Feature::default()))
    }
}

Nel metodo, compila un oggetto Feature con le informazioni appropriate per il Point specificato, quindi restituiscilo.

Una volta implementato questo metodo, dobbiamo anche avviare un server gRPC in modo che i client possano effettivamente utilizzare il nostro servizio. Sostituisci main() con questo.

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let addr = "[::1]:10000".parse().unwrap();
    println!("RouteGuideServer listening on: {addr}");
    let route_guide = RouteGuideService {
        features: load(),
    };
    let svc = RouteGuideServer::new(route_guide);
    Server::builder().add_service(svc).serve(addr).await?;
    Ok(())
}

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

  1. Specifica la porta che vogliamo utilizzare per ascoltare le richieste dei client
  2. Crea un RouteGuideService con le funzionalità caricate chiamando la funzione helper load()
  3. Crea un'istanza del server gRPC utilizzando RouteGuideServer::new() utilizzando il servizio che abbiamo creato.
  4. Registra l'implementazione del servizio con il server gRPC.
  5. Chiama serve() sul server con i dettagli della porta per eseguire un'attesa di blocco fino all'interruzione della procedura.

6. Crea il client

In questa sezione, vedremo come creare un client Rust per il nostro servizio RouteGuide in src/client/client.rs.

Come abbiamo fatto in src/server/server.rs, possiamo portare il codice generato nell'ambito tramite la macro include_generated_code! di gRPC e importare il tipo RouteGuideClient.

mod grpc_pb {
    grpc::include_generated_proto!("generated", "routeguide");
}

use grpc_pb::{
    route_guide_client::RouteGuideClient,
    Point,
};

Metodi di servizio di chiamata

In gRPC-Rust, gli 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.

Per chiamare i metodi di servizio, dobbiamo prima creare un canale per comunicare con il server. Per farlo, creiamo prima un endpoint, ci connettiamo a questo endpoint e passiamo il canale creato quando ci connettiamo a RouteGuideClient::new() nel seguente modo:

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create endpoint to connect to
    let endpoint = Endpoint::new("http://[::1]:10000")?; 
    let channel = endpoint.connect().await?;             

    // Create a new client
    let mut client = RouteGuideClient::new(channel);
    Ok(())
}

In questa funzione, quando creiamo il client, racchiudiamo il canale generico creato in precedenza con lo stub di codice generato che implementa i metodi specifici definiti nel servizio .proto.

RPC semplice

Chiamare l'RPC semplice GetFeature è quasi semplice come chiamare un metodo locale. Aggiungi questo nel seguente paese: main().

println!("*** SIMPLE RPC ***");
let point = proto!(Point{
    latitude: 409_146_138,
    longitude: -746_188_906
});
let response = client
    .get_feature(Request::new(point))
    .await?.into_inner();
Ok(())

Come puoi vedere, chiamiamo il metodo sullo stub che abbiamo ottenuto in precedenza. Nei parametri del metodo creiamo e compiliamo un oggetto buffer di protocollo di richiesta (nel nostro caso Point). Se la chiamata non restituisce un errore, possiamo leggere le informazioni di risposta dal server dal primo valore restituito.

println!("Response = Name = \"{}\", Latitude = {}, Longitude = {}",
    response.name(),
    response.location().latitude(),
    response.location().longitude());

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

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    //Create endpoint to connect to
    let endpoint = Endpoint::new("http://[::1]:10000")?; 
    let channel = endpoint.connect().await?;             

    // Create a new client
    let mut client = RouteGuideClient::new(channel); 

    println!("*** SIMPLE RPC ***");
    let point = proto!(Point{
        latitude: 409_146_138,
        longitude: -746_188_906
    });
    let response = client
        .get_feature(Request::new(point))
        .await?.into_inner();

    println!("Response = Name = \"{}\", Latitude = {}, Longitude = {}",
        response.name(),
        response.location().latitude(),
        response.location().longitude());
    Ok(())
}

7. Prova

Innanzitutto, per eseguire il client e il server, aggiungiamoli come target binari al nostro crate. Dobbiamo modificare il nostro Cargo.toml di conseguenza e aggiungere quanto segue:

[[bin]]
name = "routeguide-server"
path = "src/server/server.rs"

[[bin]]
name = "routeguide-client"
path = "src/client/client.rs"

Quindi, esegui i seguenti comandi dalla nostra directory di lavoro:

  1. Esegui il server in un terminale:
RUSTFLAGS="-Awarnings" cargo run --bin routeguide-server 
  1. Esegui il client da un altro terminale:
RUSTFLAGS="-Awarnings" cargo run --bin routeguide-client

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

*** SIMPLE RPC ***

FEATURE: Name = "Berkshire Valley Management Area Trail, Jefferson, NJ, USA", Lat = 409146138, Lon = -746188906

8. Passaggi successivi