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:
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
eFeature
, che sono strutture di dati scambiate tra il client e il server quando si utilizza il metodoGetFeature
. Il client fornisce le coordinate della mappa comePoint
nella richiestaGetFeature
al server e il server risponde con unFeature
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
eFeature
. - 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:
- Specifica la porta che vogliamo utilizzare per ascoltare le richieste dei client
- Crea un
RouteGuideService
con le funzionalità caricate chiamando la funzione helperload()
- Crea un'istanza del server gRPC utilizzando
RouteGuideServer::new()
utilizzando il servizio che abbiamo creato. - Registra l'implementazione del servizio con il server gRPC.
- 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:
- Esegui il server in un terminale:
RUSTFLAGS="-Awarnings" cargo run --bin routeguide-server
- 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
- Scopri come funziona gRPC in Introduzione a gRPC e Concetti di base.
- Completa il tutorial sulle nozioni di base.