1. Introduction
Dans cet atelier de programmation, vous allez utiliser gRPC-Rust pour créer un client et un serveur qui constituent la base d'une application de cartographie d'itinéraire écrite en Rust.
À la fin de ce tutoriel, vous disposerez d'un client qui se connecte à un serveur distant à l'aide de gRPC pour obtenir le nom ou l'adresse postale d'un lieu situé à des coordonnées spécifiques sur une carte. Une application complète peut utiliser cette conception client-serveur pour énumérer ou résumer les points d'intérêt le long d'un itinéraire.
Le service est défini dans un fichier Protocol Buffers, qui sera utilisé pour générer du code passe-partout pour le client et le serveur afin qu'ils puissent communiquer entre eux. Vous gagnerez ainsi du temps et des efforts pour implémenter cette fonctionnalité.
Ce code généré gère non seulement les complexités de la communication entre le serveur et le client, mais aussi la sérialisation et la désérialisation des données.
Points abordés
- Utiliser Protocol Buffers pour définir une API de service.
- Comment créer un client et un serveur basés sur gRPC à partir d'une définition Protocol Buffers à l'aide de la génération de code automatisée.
- Comprendre la communication client-serveur avec gRPC.
Cet atelier de programmation s'adresse aux développeurs Rust qui découvrent gRPC ou qui souhaitent se rafraîchir la mémoire sur gRPC, ou à toute personne intéressée par la création de systèmes distribués. Aucune expérience préalable avec gRPC n'est requise.
2. Avant de commencer
Prérequis
Assurez-vous d'avoir installé les éléments suivants :
Obtenir le code
Pour que vous n'ayez pas à partir de zéro, cet atelier de programmation fournit un échafaudage du code source de l'application que vous devez compléter. Les étapes suivantes vous montreront comment finaliser l'application, y compris en utilisant les plug-ins du compilateur de tampon de protocole pour générer le code gRPC standard.
Commencez par créer le répertoire de travail de l'atelier de programmation et accédez-y :
mkdir grpc-rust-getting-started && cd grpc-rust-getting-started
Téléchargez et extrayez l'atelier de programmation :
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
Vous pouvez également télécharger le fichier .zip contenant uniquement le répertoire de l'atelier de programmation et le décompresser manuellement.
Le code source complet est disponible sur GitHub si vous ne souhaitez pas saisir d'implémentation.
3. Définir le service
La première étape consiste à définir le service gRPC de l'application, sa méthode RPC, ainsi que ses types de messages de requête et de réponse à l'aide de Protocol Buffers. Votre service fournira :
- Méthode RPC appelée
GetFeature
que le serveur implémente et que le client appelle. - Les types de messages
Point
etFeature
sont des structures de données échangées entre le client et le serveur lors de l'utilisation de la méthodeGetFeature
. Le client fournit des coordonnées cartographiques sous la forme d'unPoint
dans sa requêteGetFeature
au serveur, et le serveur répond avec unFeature
correspondant qui décrit ce qui se trouve à ces coordonnées.
Cette méthode RPC et ses types de messages seront tous définis dans le fichier proto/route_guide.proto
du code source fourni.
Les Protocol Buffers sont communément appelés "protobufs". Pour en savoir plus sur la terminologie gRPC, consultez Concepts fondamentaux, architecture et cycle de vie de gRPC.
Méthode de service
Définissons d'abord nos méthodes de service, puis nos types de messages Point
et Feature
. Le fichier proto/routeguide.proto
possède une structure service
nommée RouteGuide
qui définit une ou plusieurs méthodes fournies par le service de l'application.
Ajoutez la méthode rpc
GetFeature
dans la définition RouteGuide
. Comme expliqué précédemment, cette méthode recherche le nom ou l'adresse d'un lieu à partir d'un ensemble de coordonnées donné. Par conséquent, faites en sorte que GetFeature
renvoie un Feature
pour un Point
donné :
service RouteGuide {
// Definition of the service goes here
// Obtains the feature at a given position.
rpc GetFeature(Point) returns (Feature) {}
}
Il s'agit d'une méthode RPC unaire : un RPC simple où le client envoie une requête au serveur et attend une réponse, comme pour un appel de fonction local.
Types de messages
Dans le fichier proto/route_guide.proto
du code source, définissez d'abord le type de message Point
. Un Point
représente une paire de coordonnées (latitude et longitude) sur une carte. Pour cet atelier de programmation, utilisez des nombres entiers pour les coordonnées :
message Point {
int32 latitude = 1;
int32 longitude = 2;
}
Les nombres 1
et 2
sont des ID uniques pour chacun des champs de la structure message
.
Ensuite, définissez le type de message Feature
. Un Feature
utilise un champ string
pour le nom ou l'adresse postale d'un élément à un emplacement spécifié par 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. Générer le code client et serveur
Nous vous avons déjà fourni le code généré à partir du fichier .proto
dans le répertoire généré.
Comme pour tout projet, nous devons réfléchir aux dépendances nécessaires à notre code. Pour les projets Rust, les dépendances se trouveront dans Cargo.toml
. Nous avons déjà listé les dépendances nécessaires dans le fichier Cargo.toml
.
Si vous souhaitez apprendre à générer du code à partir du fichier .proto
, consultez ces instructions.
Le code généré contient les éléments suivants :
- Définitions de structure pour les types de messages
Point
etFeature
. - Un trait de service que nous devrons implémenter :
route_guide_server::RouteGuide
. - Type de client que nous utiliserons pour appeler le serveur :
route_guide_client::RouteGuideClient<T>
.
Ensuite, nous allons implémenter la méthode GetFeature
côté serveur, afin que lorsque le client envoie une requête, le serveur puisse y répondre.
5. Implémenter le service
Dans src/server/server.rs
, nous pouvons mettre le code généré dans le champ d'application à l'aide de la macro include_generated_proto!
de gRPC et importer le trait RouteGuide
et Point
.
mod grpc_pb {
grpc::include_generated_proto!("generated", "routeguide");
}
pub use grpc_pb::{
route_guide_server::{RouteGuideServer, RouteGuide},
Point, Feature,
};
Nous pouvons commencer par définir une struct pour représenter notre service. Pour l'instant, nous pouvons le faire sur src/server/server.rs
:
#[derive(Debug)]
pub struct RouteGuideService {
features: Vec<Feature>,
}
Nous devons maintenant implémenter le trait route_guide_server::RouteGuide
à partir du code généré.
RPC unaire
RouteGuideService
implémente toutes nos méthodes de service. La fonction get_feature
côté serveur est l'endroit où le gros du travail est effectué : elle prend un message Point
du client et renvoie dans un message Feature
les informations de localisation correspondantes à partir d'une liste de lieux connus. Voici l'implémentation de la fonction dans 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()))
}
}
Dans la méthode, remplissez un objet Feature
avec les informations appropriées pour le Point
donné, puis renvoyez-le.
Une fois cette méthode implémentée, nous devons également démarrer un serveur gRPC pour que les clients puissent réellement utiliser notre service. Remplacez main()
par le code suivant :
#[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(())
}
Voici ce qui se passe dans main()
, étape par étape :
- Spécifiez le port que nous souhaitons utiliser pour écouter les requêtes du client.
- Créez un
RouteGuideService
avec des fonctionnalités chargées en appelant la fonction d'assistanceload()
. - Créez une instance du serveur gRPC à l'aide de
RouteGuideServer::new()
et du service que nous avons créé. - Enregistrez l'implémentation de notre service auprès du serveur gRPC.
- Appelez
serve()
sur le serveur avec les détails de notre port pour effectuer une attente bloquante jusqu'à ce que le processus soit arrêté.
6. Créer le client
Dans cette section, nous allons voir comment créer un client Rust pour notre service RouteGuide dans src/client/client.rs
.
Comme dans src/server/server.rs
, nous pouvons mettre le code généré dans le champ d'application à l'aide de la macro include_generated_code!
de gRPC et importer le type RouteGuideClient
.
mod grpc_pb {
grpc::include_generated_proto!("generated", "routeguide");
}
use grpc_pb::{
route_guide_client::RouteGuideClient,
Point,
};
Appeler les méthodes de service
Dans gRPC-Rust, les RPC fonctionnent en mode bloquant/synchrone, ce qui signifie que l'appel RPC attend la réponse du serveur et renvoie une réponse ou une erreur.
Pour appeler des méthodes de service, nous devons d'abord créer un canal pour communiquer avec le serveur. Pour ce faire, nous créons d'abord un point de terminaison, nous nous y connectons et nous transmettons le canal créé lors de la connexion à RouteGuideClient::new()
comme suit :
#[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(())
}
Dans cette fonction, lorsque nous créons le client, nous encapsulons le canal générique créé ci-dessus avec le stub de code généré qui implémente les méthodes spécifiques définies dans le service .proto.
RPC simple
L'appel du RPC simple GetFeature
est presque aussi simple que l'appel d'une méthode locale. Ajoutez-le dans 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(())
Comme vous pouvez le voir, nous appelons la méthode sur le stub que nous avons obtenu précédemment. Dans les paramètres de notre méthode, nous créons et remplissons un objet de tampon de protocole de requête (dans notre cas, Point
). Si l'appel ne renvoie pas d'erreur, nous pouvons lire les informations de réponse du serveur à partir de la première valeur renvoyée.
println!("Response = Name = \"{}\", Latitude = {}, Longitude = {}",
response.name(),
response.location().latitude(),
response.location().longitude());
Au total, la fonction main()
du client devrait se présenter comme suit :
#[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. Essayer
Tout d'abord, pour exécuter notre client et notre serveur, ajoutons-les en tant que cibles binaires à notre crate. Nous devons modifier notre Cargo.toml
en conséquence et ajouter les éléments suivants :
[[bin]]
name = "routeguide-server"
path = "src/server/server.rs"
[[bin]]
name = "routeguide-client"
path = "src/client/client.rs"
Exécutez ensuite les commandes suivantes à partir de notre répertoire de travail :
- Exécutez le serveur dans un terminal :
RUSTFLAGS="-Awarnings" cargo run --bin routeguide-server
- Exécutez le client à partir d'un autre terminal :
RUSTFLAGS="-Awarnings" cargo run --bin routeguide-client
Un résultat semblable à celui-ci s'affiche (les codes temporels sont omis pour plus de clarté) :
*** SIMPLE RPC *** FEATURE: Name = "Berkshire Valley Management Area Trail, Jefferson, NJ, USA", Lat = 409146138, Lon = -746188906
8. Étape suivante
- Découvrez comment fonctionne gRPC dans Présentation de gRPC et Concepts de base.
- Suivez le tutoriel sur les principes de base.