34 - Construyendo un web server en Rust desde cero - parte 3 multihilo y concurrencia

34 - Construyendo un web server en Rust desde cero - parte 3 multihilo y concurrencia

¡Bienvenidos de nuevo a Rustaceo.es! En las entregas anteriores, construimos un servidor web en Rust que puede manejar rutas y responder dinámicamente a solicitudes HTTP. Ahora es momento de optimizar el rendimiento permitiendo que maneje múltiples conexiones simultáneamente utilizando multihilo.


🚀 Objetivos de esta parte #

  1. Implementar un pool de threads para manejar múltiples solicitudes concurrentes.
  2. Mejorar la eficiencia evitando bloqueos en conexiones entrantes.
  3. Asegurar la seguridad de los datos compartidos.

⚙️ Introducción al multihilo en Rust #

Por defecto, nuestro servidor maneja una conexión a la vez, lo que lo hace ineficiente cuando hay múltiples clientes. Para solucionarlo, crearemos un pool de threads, lo que nos permitirá procesar varias peticiones simultáneamente.


🏗 Implementando un pool de threads #

Comenzaremos definiendo una estructura para administrar un grupo de threads que procesarán las conexiones entrantes.

📌 Definiendo la estructura del pool #

use std::sync::{Arc, Mutex};
use std::thread;
use std::sync::mpsc;

struct ThreadPool {
    workers: Vec<Worker>,
    sender: mpsc::Sender<Job>,
}

impl ThreadPool {
    fn nuevo(tamano: usize) -> ThreadPool {
        let (sender, receiver) = mpsc::channel();
        let receiver = Arc::new(Mutex::new(receiver));
        
        let mut workers = Vec::with_capacity(tamano);
        for id in 0..tamano {
            workers.push(Worker::nuevo(id, Arc::clone(&receiver)));
        }
        
        ThreadPool { workers, sender }
    }
    
    fn ejecutar<F>(&self, tarea: F)
    where F: FnOnce() + Send + 'static {
        let tarea = Box::new(tarea);
        self.sender.send(tarea).unwrap();
    }
}

type Job = Box<dyn FnOnce() + Send + 'static>;

Aquí:

  • ThreadPool::nuevo(tamano): Crea un conjunto de workers que estarán esperando tareas.
  • mpsc::channel(): Permite comunicación entre el servidor y los threads.
  • Arc<Mutex<T>>: Facilita el acceso seguro a los trabajos desde múltiples threads.

🔄 Creando los workers #

Cada worker representará un hilo que ejecutará tareas cuando estén disponibles.

struct Worker {
    id: usize,
    thread: Option<thread::JoinHandle<()>>,
}

impl Worker {
    fn nuevo(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
        let thread = thread::spawn(move || {
            loop {
                let tarea = receiver.lock().unwrap().recv().unwrap();
                println!("Worker {} ejecutando tarea", id);
                tarea();
            }
        });
        
        Worker { id, thread: Some(thread) }
    }
}
  • Cada Worker toma una tarea del receiver y la ejecuta.
  • thread::spawn() permite que cada worker funcione en paralelo.
  • loop asegura que los workers sigan ejecutando nuevas tareas mientras el servidor esté en ejecución.

🔗 Integrando el pool en el servidor #

Ahora modificamos main.rs para usar nuestro ThreadPool y manejar múltiples conexiones simultáneamente.

use std::net::TcpListener;
use std::io::{Read, Write};
use std::net::TcpStream;

fn manejar_conexion(mut stream: TcpStream) {
    let mut buffer = [0; 1024];
    stream.read(&mut buffer).unwrap();
    let respuesta = "HTTP/1.1 200 OK\r\n\r\n¡Servidor Pokémon Multihilo!";
    stream.write(respuesta.as_bytes()).unwrap();
    stream.flush().unwrap();
}

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
    let pool = ThreadPool::nuevo(4);
    
    println!("Servidor Pokémon Multihilo en http://127.0.0.1:7878");
    
    for stream in listener.incoming() {
        let stream = stream.unwrap();
        pool.ejecutar(move || {
            manejar_conexion(stream);
        });
    }
}

🛠 Explicación de los cambios #

  1. Creamos un pool de 4 threads usando ThreadPool::nuevo(4).
  2. Cada conexión es manejada por un worker, en lugar de bloquear el programa principal.
  3. Mejoramos el rendimiento y la escalabilidad al permitir múltiples clientes concurrentes.

🏆 Conclusión #

Con esta implementación, nuestro Servidor Pokémon ahora puede manejar múltiples solicitudes de manera concurrente, haciéndolo más rápido y escalable.

🎯 Resumen de lo aprendido #

✔ Implementamos un pool de threads para manejar múltiples conexiones. ✔ Utilizamos Rust seguro con Arc<Mutex<T>> para gestionar tareas concurrentes. ✔ Integración en un servidor funcional que ahora soporta múltiples clientes simultáneamente.

🔮 Próximo paso: Puedes seguir mejorando el servidor añadiendo manejo de errores, soporte para HTTPS o un backend de almacenamiento para guardar datos de los Pokémon.

¡Hasta la próxima, Rustaceos!