Comienza a usar gRPC-Rust

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 y Feature que son estructuras de datos que se intercambian entre el cliente y el servidor cuando se usa el método GetFeature. El cliente proporciona coordenadas de mapa como un Point en su solicitud GetFeature al servidor, y el servidor responde con un Feature 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 y Feature.
  • 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:

  1. Especifica el puerto que queremos usar para escuchar las solicitudes del cliente
  2. Crea un RouteGuideService con las funciones cargadas llamando a la función auxiliar load().
  3. Crea una instancia del servidor de gRPC con RouteGuideServer::new() usando el servicio que creamos.
  4. Registra nuestra implementación del servicio con el servidor de gRPC.
  5. 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:

  1. Ejecuta el servidor en una terminal:
RUSTFLAGS="-Awarnings" cargo run --bin routeguide-server 
  1. 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?