1. Introducción
En este codelab, usarás gRPC-Rust para crear un cliente y un servidor que formen la base de una aplicación de asignación de rutas escrita en Rust.
Al final del instructivo, tendrás un cliente que se conecta a un servidor remoto con gRPC para obtener el nombre o la dirección postal de lo que se encuentra en coordenadas específicas en un mapa. Una aplicación completa podría usar este diseño cliente-servidor para enumerar o resumir los puntos de interés a lo largo de una ruta.
El servicio se define en un archivo de Protocol Buffers, que se usará para generar código estándar para el cliente y el servidor, de modo que puedan comunicarse entre sí, lo que te ahorrará tiempo y esfuerzo en la implementación de esa funcionalidad.
Este código generado se encarga no solo de las complejidades de la comunicación entre el servidor y el cliente, sino también de la serialización y deserialización de datos.
Qué aprenderás
- Cómo usar los búferes de protocolo para definir una API de servicio
- Cómo compilar un cliente y un servidor basados en gRPC a partir de una definición de Protocol Buffers con la generación de código automatizada
- Conocimiento de la comunicación cliente-servidor con gRPC
Este codelab está dirigido a desarrolladores de Rust que no conocen gRPC o que desean repasar sus conceptos, o a cualquier otra persona interesada en crear sistemas distribuidos. No se requiere experiencia previa con gRPC.
2. Antes de comenzar
Requisitos previos
Asegúrate de haber instalado lo siguiente:
- GCC. Sigue las instrucciones aquí.
- Rust, versión 1.89.0 Sigue las instrucciones de instalación aquí.
Obtén el código
Para que no tengas que empezar desde cero, este codelab proporciona un andamio del código fuente de la aplicación para que lo completes. En los siguientes pasos, se muestra cómo finalizar la aplicación, incluido el uso de los complementos del compilador de búfer de protocolo para generar el código gRPC estándar.
Primero, crea el directorio de trabajo del codelab y cámbiate a él con el comando cd:
mkdir grpc-rust-getting-started && cd grpc-rust-getting-started
Descarga y extrae el 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
También puedes descargar el archivo .zip que contiene solo el directorio del codelab y descomprimirlo de forma manual.
El código fuente completo está disponible en GitHub si no quieres escribir una implementación.
3. Define el servicio
El primer paso es definir el servicio de gRPC de la aplicación, su método de RPC y sus tipos de mensajes de solicitud y respuesta con búferes de protocolo. Tu servicio proporcionará lo siguiente:
- Un método de RPC llamado
GetFeature
que el servidor implementa y el cliente llama. - Son los tipos de mensajes
Point
yFeature
que son estructuras de datos que se intercambian entre el cliente y el servidor cuando se usa el métodoGetFeature
. El cliente proporciona coordenadas de mapa como unPoint
en su solicitudGetFeature
al servidor, y el servidor responde con unFeature
correspondiente que describe lo que se encuentra en esas coordenadas.
Este método de RPC y sus tipos de mensajes se definirán en el archivo proto/route_guide.proto
del código fuente proporcionado.
Los búferes de protocolo se conocen comúnmente como protobufs. Para obtener más información sobre la terminología de gRPC, consulta los conceptos básicos, la arquitectura y el ciclo de vida de gRPC.
Método de servicio
Primero, definamos nuestros métodos de servicio y, luego, definamos nuestros tipos de mensajes Point
y Feature
. El archivo proto/routeguide.proto
tiene una estructura service
llamada RouteGuide
que define uno o más métodos proporcionados por el servicio de la aplicación.
Agrega el método rpc
GetFeature
dentro de la definición de RouteGuide
. Como se explicó anteriormente, este método buscará el nombre o la dirección de una ubicación a partir de un conjunto de coordenadas determinado, por lo que GetFeature
devolverá un Feature
para un Point
determinado:
service RouteGuide {
// Definition of the service goes here
// Obtains the feature at a given position.
rpc GetFeature(Point) returns (Feature) {}
}
Este es un método de RPC unario: una RPC simple en la que el cliente envía una solicitud al servidor y espera a que vuelva una respuesta, al igual que una llamada a función local.
Tipos de mensajes
En el archivo proto/route_guide.proto
del código fuente, primero define el tipo de mensaje Point
. Un objeto Point
representa un par de coordenadas de latitud y longitud en un mapa. En este codelab, usa números enteros para las coordenadas:
message Point {
int32 latitude = 1;
int32 longitude = 2;
}
Los números 1
y 2
son números de ID únicos para cada uno de los campos de la estructura message
.
A continuación, define el tipo de mensaje Feature
. Un Feature
usa un campo string
para el nombre o la dirección postal de algo en una ubicación especificada por 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 el código del cliente y del servidor
Ya te proporcionamos el código generado del archivo .proto
en el directorio generado.
Como con cualquier proyecto, debemos pensar en las dependencias necesarias para nuestro código. En el caso de los proyectos de Rust, las dependencias estarán en Cargo.toml
. Ya enumeramos las dependencias necesarias en el archivo Cargo.toml
.
Si quieres aprender a generar código a partir del archivo .proto
por tu cuenta, consulta estas instrucciones.
El código generado contiene lo siguiente:
- Son definiciones de struct para los tipos de mensajes
Point
yFeature
. - Un rasgo de servicio que deberemos implementar:
route_guide_server::RouteGuide
. - Un tipo de cliente que usaremos para llamar al servidor:
route_guide_client::RouteGuideClient<T>
.
A continuación, implementaremos el método GetFeature
en el servidor para que, cuando el cliente envíe una solicitud, el servidor pueda responder con una respuesta.
5. Implementa el servicio
En src/server/server.rs
, podemos incluir el código generado en el alcance a través de la macro include_generated_proto!
de gRPC y, luego, importar el rasgo RouteGuide
y Point
.
mod grpc_pb {
grpc::include_generated_proto!("generated", "routeguide");
}
pub use grpc_pb::{
route_guide_server::{RouteGuideServer, RouteGuide},
Point, Feature,
};
Podemos comenzar por definir un struct para representar nuestro servicio. Por ahora, podemos hacerlo en src/server/server.rs
:
#[derive(Debug)]
pub struct RouteGuideService {
features: Vec<Feature>,
}
Ahora, debemos implementar el rasgo route_guide_server::RouteGuide
desde nuestro código generado.
RPC unaria
La clase RouteGuideService
implementa todos nuestros métodos de servicio. La función get_feature
del servidor es donde se realiza el trabajo principal: toma un mensaje Point
del cliente y devuelve en un mensaje Feature
la información de ubicación correspondiente de una lista de lugares conocidos. A continuación, se muestra la implementación de la función en 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()))
}
}
En el método, propaga un objeto Feature
con la información adecuada para el Point
determinado y, luego, devuélvelo.
Una vez que implementamos este método, también debemos iniciar un servidor de gRPC para que los clientes puedan usar nuestro servicio. Reemplaza main()
por lo siguiente.
#[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(())
}
Esto es lo que sucede en main()
, paso a paso:
- Especifica el puerto que queremos usar para escuchar las solicitudes del cliente
- Crea un
RouteGuideService
con las funciones cargadas llamando a la función auxiliarload()
. - Crea una instancia del servidor de gRPC con
RouteGuideServer::new()
usando el servicio que creamos. - Registra nuestra implementación del servicio con el servidor de gRPC.
- Llama a
serve()
en el servidor con los detalles de nuestro puerto para realizar una espera de bloqueo hasta que se detenga el proceso.
6. Crea el cliente
En esta sección, veremos cómo crear un cliente de Rust para nuestro servicio RouteGuide en src/client/client.rs
.
Como hicimos en src/server/server.rs
, podemos poner el código generado en el alcance a través de la macro include_generated_code!
de gRPC y, luego, importar el tipo RouteGuideClient
.
mod grpc_pb {
grpc::include_generated_proto!("generated", "routeguide");
}
use grpc_pb::{
route_guide_client::RouteGuideClient,
Point,
};
Llama a métodos de servicio
En gRPC-Rust, las RPC operan en un modo de bloqueo o síncrono, lo que significa que la llamada a la RPC espera a que responda el servidor y mostrará una respuesta o un error.
Para llamar a los métodos del servicio, primero debemos crear un canal para comunicarnos con el servidor. Para crear esto, primero creamos un extremo, nos conectamos a él y pasamos el canal creado cuando nos conectamos a RouteGuideClient::new()
de la siguiente manera:
#[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(())
}
En esta función, cuando creamos el cliente, encapsulamos el canal genérico creado anteriormente con el código auxiliar generado que implementa los métodos específicos definidos en el servicio .proto.
RPC simple
Llamar al RPC simple GetFeature
es casi tan sencillo como llamar a un método local. Agrega esto en 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(())
Como puedes ver, llamamos al método en el código auxiliar que obtuvimos antes. En los parámetros del método, creamos y propagamos un objeto de búfer de protocolo de solicitud (en nuestro caso, Point
). Si la llamada no devuelve un error, podemos leer la información de respuesta del servidor desde el primer valor de devolución.
println!("Response = Name = \"{}\", Latitude = {}, Longitude = {}",
response.name(),
response.location().latitude(),
response.location().longitude());
En total, la función main()
del cliente debería verse de la siguiente manera:
#[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. Probar
Primero, para ejecutar nuestro cliente y servidor, agreguémoslos como destinos binarios a nuestro crate. Debemos editar nuestro Cargo.toml
según corresponda y agregar lo siguiente:
[[bin]]
name = "routeguide-server"
path = "src/server/server.rs"
[[bin]]
name = "routeguide-client"
path = "src/client/client.rs"
Luego, ejecuta los siguientes comandos desde nuestro directorio de trabajo:
- Ejecuta el servidor en una terminal:
RUSTFLAGS="-Awarnings" cargo run --bin routeguide-server
- Ejecuta el cliente desde otra terminal:
RUSTFLAGS="-Awarnings" cargo run --bin routeguide-client
Verás un resultado como este, con marcas de tiempo omitidas para mayor claridad:
*** SIMPLE RPC *** FEATURE: Name = "Berkshire Valley Management Area Trail, Jefferson, NJ, USA", Lat = 409146138, Lon = -746188906
8. ¿Qué sigue?
- Obtén información sobre cómo funciona gRPC en Introducción a gRPC y Conceptos básicos
- Lee el instructivo sobre conceptos básicos.