Cómo usar gRPC con Rust Tonic y Postgres con ejemplos

SteadylearnerPublished 2 years ago

En este post aprendermos a usar Rust, Tonic y la crate gRPC, y implementaremos un CRUD con Postgresql database.

Puedes contactarme por Telegram si necesitas contratar a un desarrollador Blockchain Full Stack.

También puedes unirte a mi grupo de telegram, en el cual puedes encontrar otros desarrolladores blockchain, así como reclutadores, jefes de proyecto, así como hacer preguntas y hacer conexiones.

El propósito de este post es poder ayudarte trabajar con Rust Tonic y que puedas empezar a programar tu propio proyecto.


Prerequisítos

  1. ¿Qué es gRPC y buffers de protocolo?
  2. Rust, Postgresql (u otras bases de datos)
  3. Guía oficial de Tonic
  4. Cliente gRPC

Si no tienes instalado Rust en tu eqipo, lee Como instalar Rust. Usaremos el crate Rust Postgresql para este post. Deberías instalar Postgresql en caso de que no lo hayas hecho aún.

Asumiré que ya estas familiarizado con gRPC, Rust y la base de datos Postgresql. De lo contrario por favor lee la documentación antes de empezar.

Puede que no necesites el cliente gRPC. Pero lo dejaré aquí antes del resto del contenido, porque es muy útil y ademas toma mucho tiempo para instalarse. Para ahorrarte tiempo te dejaré las instrucciones.


Tabla de Contenido.

  1. Configuración del proyecto
  2. Definir el servicio gRPC para el CRUD
  3. Preparar nuestro Cargo.toml para instalar dependencias
  4. Hacer el server gRPC con Tonic
  5. Implementar el servicio gRPC de el CRUD con Rust Postgresql
  6. Usar el cliente gRPC para probarlo
  7. Conclusión

Reusé algunas parte de la guía oficial de Tonic para hacer funcionar el flujo de forma similar. Esto va a ayudar a que entiendas el post mejor.

Puedes encontrar el código fuente aquí


1. Configuración del Proyecto

Lo primero que haremos será configurar los datos para la base de datos. Espero que ya tengas instalado alguna base de datos SQL en tu equipo. Puedes consultar estos comandos SQL.

Puedes llamar a tu base de datos como desees.

CREATE DATABASE grpc OWNER you; \c grpc;

Luego escribe, $psql users < users.sql o pegalos manualmente en l consola psql después de autenticarte.

-- users.sql CREATE TABLE users( id VARCHAR(255) PRIMARY KEY, first_name VARCHAR(255) NOT NULL, last_name VARCHAR(255) NOT NULL, date_of_birth Date NOT NULL ); INSERT INTO users VALUES ('steadylearner', 'steady', 'learner', 'yours'); INSERT INTO users VALUES ('mybirthdayisblackfriday', 'mybirthdayis', 'blackfriday', '2019-11-25'); INSERT INTO users VALUES ('mybirthdayisnotblackfriday', 'mybirthdayis', 'notblackfriday', '2019-11-26');

Luego puedes guardar esos datos desde Postgresql con $pg_dump users > users.sql.

La Configuración de la base de datos esta lista. Ahora crea un nuevo proyecto de Rust para usarla y aprender a usar Tonic.

$cargo new user $cd user

Crea un archivo .env en la carpeta para proteger tu base de datos y la información de login.Guiate con este comando.

$echo DATABASE_URL=postgres://postgres:postgres@localhost/grpc > .env

2. Definir el servicio gRPC para el CRUD

Preparamos una configuración mínima para este post en la sección pasada. Ahora definiremos el servicio gRPC con el método request y response. Haremos uso de los buffers de protocolo para usar los datos que hicimos con Postgresql.

Crearemos los archivos .proto en la carpeta proto. Usa estos comandos.

$mkdir proto $touch proto/user.proto

Primero definiremos el nombre de nuestro paquete, el cual es el que Tonic usa incluyendo los protos en las aplicaciones de cliente y servidor. En este caso será user.

syntax = "proto3"; package user;

Ahora definiremos nuestro servicio CRUD. Este servicio va a contener las llamadas actuales gRPC. Las usaremos para crear el ejemplo de CRUD de Rust con Tonic.

service Crud { // Use whatever name you want, this is for blog posts and not prouction files. rpc GetUser (UserRequest) returns (UserReply) {} // becomes get_user in impl functions in Rust files rpc ListUsers(Empty) returns (Users) {} rpc CreateUser (CreateUserRequest) returns (CreateUserReply) {} rpc UpdateUser (UpdateUserRequest) returns (UpdateUserReply) {} rpc DeleteUser (UserRequest) returns (DeleteUserReply) {} rpc DeleteUsers (Empty) returns (DeleteUserReply) {} }

Nada complicado de momento. Se que es un poco verboso pero, es para hacerlo mas explícito y fácil separando la lógica.

Si Tienes una mejor manera de hacerlo o si tienes más experiencia con gRPC no dudes en contactarme por Twitter o también podrías crear un issue en Github.

Finalmente especificaremos los tipos que usamos en el método RPC de nuestro CRUD. Los tipos RPC son definidos como mensajes que contienen campos por tipo. Algo similar a esto.

message Empty {} message UserRequest { string id = 1; } message UserReply { string id = 1; string first_name = 2; string last_name = 3; string date_of_birth = 4; } message CreateUserRequest { string first_name = 1; string last_name = 2; string date_of_birth = 3; } message CreateUserReply { string message = 1; } message UpdateUserRequest { string id = 1; string first_name = 2; string last_name = 3; string date_of_birth = 4; } message UpdateUserReply { string message = 1; } message DeleteUserReply { string message = 1; } message Users { repeated UserReply users = 1; }

Puedes ver que usé tipo string para date_of_birth en lugar de date. Lo hice porque no podía encontrar el ejemplo correcto para el tipo DATE en protobuf y hacerlo funcionar con Tonic,Buffers de protocolo y el sistema de tipos de Rust.

Si eres un experto con esto, o tienes más experiencia para hacerlo de una mejor manera, agradecería que me ayudes a corregir esto.

Al terminar nuestro archivo .proto de nuestro CRUD debería verse similar a este.

// user.proto syntax = "proto3"; package user; service Crud { rpc GetUser (UserRequest) returns (UserReply) {} rpc ListUsers(Empty) returns (Users) {} rpc CreateUser (CreateUserRequest) returns (CreateUserReply) {} rpc UpdateUser (UpdateUserRequest) returns (UpdateUserReply) {} rpc DeleteUser (UserRequest) returns (DeleteUserReply) {} rpc DeleteUsers (Empty) returns (DeleteUserReply) {} } message Empty {} message UserRequest { string id = 1; } message UserReply { string id = 1; string first_name = 2; string last_name = 3; string date_of_birth = 4; } message CreateUserRequest { string first_name = 1; string last_name = 2; string date_of_birth = 3; } message CreateUserReply { string message = 1; } message UpdateUserRequest { string id = 1; string first_name = 2; string last_name = 3; string date_of_birth = 4; } message UpdateUserReply { string message = 1; } message DeleteUserReply { string message = 1; } message Users { repeated UserReply users = 1; }

Encontré que trabajar con Rust no es fácil y se vuelve cada vez mas difícil de compilarlo sin errores cuando queremos especificar los tipos de datos de buffers de protocolo y Rust Postgresql al mismo tiempo.

Si deseas hacer tu propio proyecto de Rust Tonic con otros archivos proto. primero compila Tonic CRUD Example por Steadylearner para poder recolectar los archivos binarios y poder empezar editando de a pequeñas partes el código de Rust y las definiciones de protbuf que estan allí.


3. Preparar nuestro Cargo.toml para instalar dependencias

Configuramos el proyecto e hicimos el archivo protobuf para usar gRPC con Rust. Por lo tanto ya podemos escribir el código para de Rust con Tonic.

Lo primero será preparar las dependencias con Cargo.toml .

[package] name = "rust-tonic-crud-example" version = "0.1.0" authors = ["www.steadylearner.com"] edition = "2018" [dependencies] tonic = { version = "0.1.0-alpha.4", features = ["rustls"] } bytes = "0.4" prost = "0.5" prost-derive = "0.5" prost-types = "0.5.0" tokio = "=0.2.0-alpha.6" futures-preview = { version = "=0.3.0-alpha.19", default-features = false, features = ["alloc"]} async-stream = "0.1.2" http = "0.1" tower = "=0.3.0-alpha.2" serde = "1.0.101" serde_json = "1.0.41" serde_derive = "1.0.101" console = "0.9.0" # Database(Postgresql) postgres = { version = "0.15.2", features = ["with-chrono"] } dotenv = "0.15.0" chrono = "0.4.9" uuid = { version = "0.8.1", features = ["serde", "v4"] } # Help you use gRPC protobuf files in Rust. [build-dependencies] tonic-build = "0.1.0-alpha.4"

Hay muchas dependencias para este simple proyecto. pero, si te fijas en esta parte se hará mas sencillo de entender.

# Database(Postgresql) postgres = { version = "0.15.2", features = ["with-chrono"] } dotenv = "0.15.0" chrono = "0.4.9" uuid = { version = "0.8.1", features = ["serde", "v4"] }

Ese bloque corresponde a Rust Postgresql y el resto para Tonic.

incluimos tonic-build para hacer nuestro código gRPC del cliente y servidor.

Si no has usado gRPC o Tonic puede que te confundas. Pero, todos los lenguajes que usan gRPC tienen un proceso similar para usar las definiciones proto buffer con ellos.

Con Rust Tonic debemos incluirlo en el proceso build de nuestra aplicación. Lo configuráremos con build.rs en la raíz del crate.

fn main() -> Result<(), Box<dyn std::error::Error>> { tonic_build::compile_protos("proto/user.proto")?; Ok(()) }

Nada complicado de momento, solo necesitas modificar el nombre del archvio de otros ejemplos. De esta manera tonic-build compilará los archivos protobufs para que funcionen con los proyectos de Rust que integren gRPC.

Automáticamente creará algunos módulos de Rust para que puedas usarlos luego en tu código Rust dependiedo de las definiciones de los protobuf que hayas usado en los archivos protobuf. Puedes verificar esto con el código Rust que leeras dentro de poco.

Si deseas mas detalles, por favor consulta tonic-build.


4. Hacer el server gRPC con Tonic

El proceso de preparación terminó en la parte pasada. Finalmente Escribiremos nuestro código Rust, Empezando a construir el servidor Rust Tonic gRPC, de la siguiente manera.

// main.rs extern crate postgres; extern crate dotenv; extern crate chrono; // 1. pub mod user { tonic::include_proto!("user"); } use tonic::{transport::Server}; // 1. use user::{ server::{CrudServer}, }; extern crate uuid; extern crate console; use console::Style; mod db_connection; // 2. mod service; use crate::service::User; #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { let addr = "[::1]:50051".parse().unwrap(); // 3. let user = User::default(); let blue = Style::new() .blue(); println!("\nRust gRPC Server ready at {}", blue.apply_to(addr)); // 4. Server::builder().serve(addr, CrudServer::new(user)).await?; Ok(()) }

Esto será el punto de entrada cuando arranquemos nuestro servidor. La mayoria es para definir dependencias y módulos que usaremos.

El archivo es sencillo pero tiene unos puntos que debes entender.

1. Aquí es donde podremos usar los código autogenerados de tonic_build::compile_protos("proto/user.proto")?; y tonic-build = "0.1.0-alpha.4".

Puedes usar módulos hechos de forma similar a esta, así como handlers que crearemos con models.rs.

use user::{ server::{CrudServer}, };

2. El contenido del archivo db_connection debe ser similar a este.

use postgres::{Connection, TlsMode}; use dotenv::dotenv; use std::env; pub fn establish_connection() -> Connection { dotenv().ok(); let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); Connection::connect(database_url, TlsMode::None).unwrap() }

Puede ser reusado sin necesidad de modificarlo, lo separeremos en una función y lo llamaremos desde donde queramos.

3. Este es código de los creadores de Tonic. Deberías usarlo sin el prefijo de HTTP cuando uses CURL y el cliente gRPC.

4. Estamos usando console::Style; con println! para visitar fácilmente el servidor con exploradores y poder enviar comandos CURL.


5. Implementar el servicio gRPC de el CRUD con Rust Postgresql

En esta parte crearemos algunos handlers en service.rs mientras seguimos la definiciones que hicimos en el archivo user.proto. El resto del código será similar a esto.

El siguiente bloque es largo. Este bloque es para implemntar todas las operaciones del CRUD de gRPC con la base de datos Postgresql. Lee las partes que te interesen.

use chrono::*; use uuid::Uuid; use crate::db_connection::establish_connection; use tonic::{Request, Response, Status}; // Compare it with user.proto file, imported from the main.rs file use crate::user::{ server::Crud, CreateUserReply, CreateUserRequest, DeleteUserReply, Empty, UpdateUserReply, UpdateUserRequest, UserReply, UserRequest, Users, }; #[derive(Default)] pub struct User {} #[tonic::async_trait] impl Crud for User { // Compare it with the Crud service definition in user.proto file // The method GetUser becomes get_user etc async fn get_user(&self, request: Request<UserRequest>) -> Result<Response<UserReply>, Status> { println!("Got a request: {:#?}", &request); // request is private, so use this instead to get the data in it. let UserRequest { id } = &request.into_inner(); let conn = establish_connection(); // 1. let rows = &conn .query("SELECT * FROM users WHERE id = $1", &[&id]) .unwrap(); // println!("{:#?}", rows); // println!("{:#?}", rows.get(0)); // https://docs.rs/postgres/0.17.0-alpha.1/postgres/row/struct.Row.html let row = rows.get(0); println!("{:#?}", &row); // 2. let date_of_birth: NaiveDate = row.get(3); let reply = UserReply { id: row.get(0), first_name: row.get(1), last_name: row.get(2), // 2. date_of_birth: date_of_birth.to_string(), }; Ok(Response::new(reply)) } async fn list_users(&self, request: Request<Empty>) -> Result<Response<Users>, Status> { println!("Got a request: {:#?}", &request); let conn = establish_connection(); // 3. let mut v: Vec<UserReply> = Vec::new(); for row in &conn.query("SELECT * FROM users", &[]).unwrap() { let date_of_birth: NaiveDate = row.get(3); let user = UserReply { id: row.get(0), first_name: row.get(1), last_name: row.get(2), date_of_birth: date_of_birth.to_string(), }; v.push(user); } let reply = Users { users: v }; Ok(Response::new(reply)) } // Test with create_users, Rust compiler shows errors to help you. async fn create_user( &self, request: Request<CreateUserRequest>, ) -> Result<Response<CreateUserReply>, Status> { println!("Got a request: {:#?}", &request); // 4. let user_id = Uuid::new_v4().to_hyphenated().to_string(); let CreateUserRequest { first_name, last_name, date_of_birth, } = &request.into_inner(); // 5. let serialize_date_of_birth = NaiveDate::parse_from_str(date_of_birth, "%Y-%m-%d").unwrap(); // String to Date let conn = establish_connection(); // 6. let number_of_rows_affected = &conn.execute( "INSERT INTO users (id, first_name, last_name, date_of_birth) VALUES ($1, $2, $3, $4)", &[ &user_id, &first_name, &last_name, &serialize_date_of_birth, ] ) .unwrap(); let reply = if number_of_rows_affected == &(0 as u64) { CreateUserReply { message: format!( "Fail to create user with id {}.", &user_id ), } } else { CreateUserReply { message: format!( "Create {} user with id {}.", &number_of_rows_affected, &user_id ), } }; Ok(Response::new(reply)) } async fn update_user( &self, request: Request<UpdateUserRequest>, ) -> Result<Response<UpdateUserReply>, Status> { println!("Got a request: {:#?}", &request); let UpdateUserRequest { id, first_name, last_name, date_of_birth, } = &request.into_inner(); // 3. let serialize_date_of_birth = NaiveDate::parse_from_str(date_of_birth, "%Y-%m-%d").unwrap(); // String to Date let conn = establish_connection(); let number_of_rows_affected = &conn .execute( "UPDATE users SET first_name = $2, last_name = $3, date_of_birth = $4 WHERE id = $1", &[ &id, &first_name, &last_name, &serialize_date_of_birth, ] ) .unwrap(); let reply = if number_of_rows_affected == &(0 as u64) { UpdateUserReply { message: format!("Fail to update the user with id {}.", id), } } else { UpdateUserReply { message: format!("Update {} user with id {}", &number_of_rows_affected, &id), } }; Ok(Response::new(reply)) } async fn delete_user( &self, request: Request<UserRequest>, ) -> Result<Response<DeleteUserReply>, Status> { println!("Got a request: {:#?}", &request); let UserRequest { id } = &request.into_inner(); let conn = establish_connection(); let number_of_rows_affected = &conn .execute("DELETE FROM users WHERE id = $1", &[&id]) .unwrap(); let reply = if number_of_rows_affected == &(0 as u64) { DeleteUserReply { message: format!("Fail to delete the user with id {}.", id), } } else { DeleteUserReply { message: format!("Remove the user with id {}.", id), } }; Ok(Response::new(reply)) } async fn delete_users( &self, request: Request<Empty>, ) -> Result<Response<DeleteUserReply>, Status> { println!("Got a request: {:#?}", &request); let conn = establish_connection(); let rows = &conn.query("DELETE FROM users", &[]).unwrap(); let reply = DeleteUserReply { message: format!("Remove {} user data from the database.", rows.len()), }; Ok(Response::new(reply)) } }

Como podrás observar hay muchas líneas de código, así que será un poco complicado para empezar. Quiero que pruebes este proyecto con las partes de get_user y list_users. De esa manera podrías mejorarlos y consultarlo añadiéndole comentarios.

1. Cuando leemos la documentación de Rust Postgresql nos damos cuenta de que tiene comandos de execute y query. La diferencia es que execute nos devuelve el número de filas modificadas, mientras query nos devuelve datos. Deberías poder encontrar cual usar dependiendo de la necesidad que tengas.

2. Cuando usas Rust Postgresql puede que te salte un error similar a este.

cannot infer type the trait `postgres::types::FromSql` is not implemented for

Si es así, debes cambiar el tipo de la definición de datos para ayudar a que Rust sea compatible con postgresql con chrono .

let date_of_birth: NaiveDate = row.get(3);

Usamos el tipo de dato string para date_of_birth en el archivo user.proto. Debemos modificarlo de esta manera.

date_of_birth: date_of_birth.to_string(),

Este es el precio que debemos pagar para hacer funcionar Rust en conjunto con Postgresql y Protobuf. Puede que encuentres con una mejor manera de hacerlo.

3. Usamos la forma imperativa para crear la lista de usuarios siguiendo la documentación del autor. Fácilmente debes inferir que repeated de proto se vuelve vec en Rust.

message Users { repeated UserReply users = 1; }

También debes saber que el crate de Rust Postgresql requiere que incluyas &[] (vacío) cuando no hayan valores para pasar en los comandos SQL.

&conn.query("SELECT * FROM users", &[])

4. Crearemos un id aleatorio para los usuarios con la API Rust uuid. Debes incluir la v4 en el archivo cargo.toml para que funcione.

uuid = { version = "0.8.1", features = ["serde", "v4"] }

5. Usamos la API de chrono para pasar del tipo string a DATE y hacerlo compatible con postgresql.

6. usamos let number_of_rows_affected = &conn.execute y la lógica relevante para manejar el resultado de la base de datos proveniente de la petición hecha al cliente gRPC. Puedes observar que la lógica usada es muy similar in update_user, delete_user y delete_users.

Realmente espero que hayas leido todo el código del proyecto así como la documentación de cada herramienta. Si aún no has probado el proyecto corre el siguiente comando cargo run --release y verás algo similar a este mensaje.

Rust gRPC Server ready at [::1]:50051

Luego puedes probarlo con $curl [::1]:50051.

Si todo funciona bien verás esto.

Warning: Binary output can mess up your terminal. Use "--output -" to tell Warning: curl to output it to your terminal anyway, or consider "--output Warning: <FILE>" to save to a file.

Luego puedes escribir el código de Tonic del cliente gRPC de la siguiente manera.

pub mod user { tonic::include_proto!("user"); } use user::{client::CrudClient, UserRequest}; #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { let mut client = CrudClient::connect("http://[::1]:50051")?; let request = tonic::Request::new(UserRequest { id: "steadylearner".into(), }); let response = client.get_user(request).await?; println!("RESPONSE={:?}", response); let user_date_of_birth = &response.into_inner().date_of_birth; println!("{}", user_date_of_birth); Ok(()) }

Será fácil entender como funciona si ya leíste La guía oficial de Tonic. Compilalo si deseas y luego trata de mejorarlo después de que hayas probado todos los endpoints del CRUD con el cliente gRPC.


6. Usar el cliente gRPC para probarlo

Para esta parte quiero que ya tengas instalado el cliente gRPC en tu máquina. Deberías tener el archivo ejecutable BloomRPC version.AppImage en la carpeta release.

Puedes ejecutarlo manualmente, o guiarte para hacerlo con el editor de la siguiente manera.

Usa pwd para encontrar la ubicación del cliente gRPC, y $vim ~/.bashrc para incluir un alias de esta forma.

# gRPC alias grpc-client="grpc/bloomrpc/release/'BloomRPC version.AppImage'"

y usa este comando.

$source ~/.bashrc

Ahora puedes usarlo con $grpc-client cuando lo desees.

Se mostrará la aplicación de escritorio similar la del repositorio oficial.

Si has usado graphql antes, te darás cuenta de que la interfaz es muy similar, así como probar los endpoints con graphiql.

Primero necesitas incluir los archivo proto que hicimos anteriormente, por ejemplo user.proto.

Automáticamente se mostrarán los métodos que puedes usar.

Cada vez que hagas clic en un método, va a mostrarte el valor por defecto en la parte de editor. Puedes enviar peticiones con el valor por defecto o cambiarlo con los valores que quieras.

Para este ejemplo deberías tener cuidado cuando definas el valor de date_of_birth. Debes usar el tipo correcto de DATE para Rust y Postgresql.

Cuando se pause el proceso, puedes detenerlo fácilmente haciendo clic en el bot´pn que usaste para enviar la petición.

guiate con el siguiente ejemplo.

GetUser

{ "id": "steadylearner" }

ListUsers

{}

CreateUser

{ "first_name": "steady", "last_name": "learner", "date_of_birth": "%Y-%m-%d" }

UpdateUser

{ "id": "random-id", "first_name": "steadylearner", "last_name": "rust developer", "date_of_birth": "use-numbers-instead" }

DeleteUser

{ "id": "steadylearner" }

DeleteUsers

{}

Prueba los endpoints del servidor gRPC con tu propio código. Luego escribe un cliente Rust Tonic más complejo en varios archivos Rust.

También puedes usar otro servidores Rust para crear microservicios.


7. Conclusión

Espero que lo hayas logrado hacer funcionar. Te invito a que edites la definición de protobuf para crear tu propio proyecto y crees mas código Rust para manejar la base da datos.

Rust y Tonic me han ayudado a aprender y programar mejor gRPC. Pero, ha sido difícil encontrar ejemplo funcionales con integración de base de datos, y quería que este post fuese de ayuda.

Si deseas seguir mi último contenido, sigueme aquí Twitter o deja una estrella en Rust Full Stack.

Si necesitas contratar a un desarrollador, puedes contactarme.

Gracias.