Primeros pasos con Actix-Web: un framework para servidores web en Rust
A partir del reto de los cien días de código comencé a indagar sobre la creación de servidores web con Rust. Antes de elegir actix-web para dicha tarea estuve indagando un par de días sobre los frameworks existentes en dicho lenguage. Fue complicado elegir cuál utilizar debido a dos factores:
- Rust aún no cuenta con un entorno maduro para trabajar con código asíncrono (async) en su librería estándar. En ella sólo se ofrece una representación de código async a través del trait Future. Dado lo anterior los frameworks para servidores web basan su implementación en dos librerías externas: tokio y async-std, de las cuáles tokio parece mantener un desarrollo más activo en estos momentos. Actix-web está basado en dicha librería.
- En el mismo sentido de la madurez: aún no existe un framework para servidor web completamente consolidado en el mundo de Rust. Leyendo varias comparaciones en la red existen muchos artículos donde X y Y se mencionan como los mejores, otros donde Z y X son los ganadores, otros donde A y B son las opciones a elegir. Por lo que pude observar actix-web es el que cuenta con mayor cantidad de material didáctico.
La aplicación
Esta mini aplicación lleva acabo operaciones sobre un catálogo de álbumes musicales, ejemplificando los métodos GET, POST, PATCH y DELETE de HTTP. El catálogo está almacenado en una base de datos en la memoria local. La transferencia de datos se realiza utilizando el formato JSON.
Pueden encontrar el código de ejemplo en este repositorio 🦀.
Dependencias
Para este ejercicio necesitamos solamente actix-web y la librería serde para la serialización de datos en JSON.
// Cargo.toml
[...]
[dependencies]
actix-web = "4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
El servidor
Actix-web funciona mediante una instancia del servidor, responsable de servir las peticiones HTTP, y una instancia de aplicación, usada para registrar las rutas, aplicar transformaciones y validaciones, así como almacenar el estado de la aplicación. La aplicación es siempre activada a través del servidor.
// main.rs
[...]
#[actix_web::main]
async fn main() -> std::io::Result<()> {
[...]
**HttpServer::new**(move || {
**App::new()**
.service(...)
})
**.bind(("127.0.0.1", 8080))**?
.run()
.await
}
Como se observa en el fragmento de código anterior el servidor es ejecutado en el puerto 8080 de la dirección local en nuestra computadora.
El catálogo de álbumes y su manejo en la aplicación
En la aplicación un álbum musical está representado de la siguiente manera
//main.rs
[...]
#[derive(Debug, Deserialize, Serialize)]
struct **Album** {
**id**: String,
**title**: String,
**artist**: String,
**price**: f64,
}
La definición está precedida de macros que nos ayudarán en la serialización y deserialización de los datos en formato JSON, así como del traitDebug
que nos ayudará a imprimir un álbum en pantalla si así se desea.
Para almacenar el catálogo utilizaremos un vector de instancias de Album
como base de datos, es decir, una base de datos en la memoria local.
En Rust el manejo de referencias de memoria es algo que debe ser manejado de forma muy explícita para evitar fugas de datos. Dado que estamos manejando una mini base de datos en la memoria es necesario envolverla en estructuras que puedan ser compartidas con cada una de las instancias de la aplicación que se están generando. Actix-web lanza un nuevo hilo con la aplicación cada vez que recibe una petición web.
Para poder compartir el catálogo de forma correcta necesitamos envolverla en la structAppState
de actix-web de una forma segura usando un [Mutex](https://doc.rust-lang.org/std/sync/struct.Mutex.html)
. El Mutex
garantiza que la compartición de los datos en memoria se realice de forma correcta entre cada una de las instancias de la aplicación.
Finalmente para poder insertarla en la aplicación necesitamos envolverla con la estructura web::Data
de actix-web. Esta estructura es la que proporciona estado global a una aplicación.
En este punto cabe aclarar que el manejo de estado global en una aplicación multi-hilos es algo que en Rust resulta mucho más complicado que en la mayoría de los otros lenguajes de programación. Esta complicación radica en el compromiso que Rust tiene con la seguridad de los datos.
// main.rs
[...]
#[derive(Debug)]
struct **AppState** {
**albums**: **Mutex**<Vec<Album>>,
}
[...]
async fn main() -> std::io::Result<()> {
let **app_state** = **web::Data**::new(**AppState** {
albums: Mutex::new(vec![
Album {
id: "1".into(),
title: "Siembra".into(),
artist: "Willie Colón & Rubén Blades".into(),
price: 29.99,
},
Album {
id: "2".into(),
title: "Princesa Donashii".into(),
artist: "Princesa Donashii".into(),
price: 25.99,
},
Album {
id: "3".into(),
title: "El Silencio".into(),
artist: "Caifanes".into(),
price: 24.99,
},
Album {
id: "4".into(),
title: "Latin-Rock-Soul".into(),
artist: "Fania All Stars".into(),
price: 29.99,
},
]),
});
HttpServer::new(move || {
App::new()
**.app_data(app_state.clone())**
[...]
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
Las funciones de respuesta del servidor
Finalmente agregaremos la lógica necesaria para obtener el catálogo álbumes así como para obtener, añadir, actualizar y borrar un álbum del catálogo.
Las funciones que responden a una acción en una determinada ruta están decoradas por una macro de actix-web que indica el método y la ruta a la que dicha acción está asociada. Estas funciones deben retornar un tipo de datos que pueda ser convertido a la estructura HttpResponse
de actix-web.
**#[get("/albums")]**
async fn get_albums(data: web::Data<AppState>) ->**impl Responder** {
let albums = &data.albums;
web::Json(json!(albums))
}
En la función anterior se observa la función que retorna el catálogo completo en formato JSON. Para ello es necesario obtener una instancia del estado global, que es donde reside el catálogo y posteriormente transformarlo usando la instrucción json!
de serde
. En este caso la función retorna [impl Responder](https://docs.rs/actix-web/4.0.1/actix_web/trait.Responder.html)
que es convertido a HttpResponse
.
En caso de que sólo queramos retornar un código de estado de respuesta HTTP podemos usar HTTPResponseBuilder de actix-web con lo cual simplemente indicamos el código a retornar dependiendo del resultado de la acción.
**#[post("/albums")]**
async fn post_album(data: web::Data<AppState>, body: String) ->**HttpResponseBuilder** {
let mut albums = data.albums.lock().unwrap();
if let Ok(album) = serde_json::from_str(&body) {
albums.push(album);
**return HttpResponse::Ok();**
}
**HttpResponse::BadRequest()**
}
En el ejemplo anterior se puede observar cómo la aplicación retorna un código 404 (BadRequest
) si los datos enviados por el cliente no pueden ser transformados a una instancia de la estructura Album
con lo cual se implica que el formato es incorrecto.
En general es sencillo escribir un servidor web en Rust usando actix-web. La única complicación resultó al tener que manejar el estado de la aplicación con tantas envolturas de estructuras de datos debido a cómo Rust garantiza la seguridad de la información así como al estado prematuro de la implementación de async en Rust.
Si les interesa el tema pueden observar el código completo en el archivo main.rs
del repositorio.