Implementación de Servidores y Clientes TCP en Rust con TcpListener y TcpStream

En Rust, la biblioteca estándar proporciona TcpListener y TcpStream para construri aplicaciones de red orientadas a conexión. Estas abstraciones encapsulan operaciones de bajo nivel del sistema operativo, permitiendo desarrollar servidores y clientes TCP robustos sin recurrir a APIs externas.

Servidor TCP: TcpListener

TcpListener representa un socket pasivo que espera conexiones entrantes. Su ciclo típico implica vincular una dirección IP y puerto, aceptar conexiones y procesar cada flujo resultante.

Vinculación flexible

El método bind() admite múltiples estrategias: una dirección específica o una lista de alternativas para tolerancia a fallos.

use std::net::{SocketAddr, TcpListener};

fn iniciar_servidor() -> std::io::Result<TcpListener> {
    let candidatos = [
        SocketAddr::from(([127, 0, 0, 1], 3000)),
        SocketAddr::from(([127, 0, 0, 1], 3001)),
        SocketAddr::from(([0, 0, 0, 0], 3000)), // Escucha en todas las interfaces
    ];
    
    TcpListener::bind(&candidatos[..])
}

Aceptación de conexiones

accept() bloquea hasta que llega una conexión válida, devolviendo una tupla con el TcpStream y la dirección remota. Alternativamente, incoming() ofrece un iterador infinito sobre flujos entrantes:

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

fn manejar_conexiones() {
    let listener = iniciar_servidor().expect("No se pudo iniciar el servidor");
    
    for resultado in listener.incoming() {
        match resultado {
            Ok(mut flujo) => {
                let mut buffer = [0u8; 512];
                match flujo.read(&mut buffer) {
                    Ok(bytes_leidos) if bytes_leidos > 0 => {
                        let mensaje = String::from_utf8_lossy(&buffer[..bytes_leidos]);
                        println!("Recibido: {}", mensaje.trim());
                        
                        // Respuesta simple
                        flujo.write_all(b"ACK\n").ok();
                    }
                    _ => continue,
                }
            }
            Err(e) => eprintln!("Error al aceptar: {}", e),
        }
    }
}

Información de escucha

Para depuraicón o registro, local_addr() revela la dirección efectiva asignada (útil cuando se usa 0.0.0.0):

let direccion = listener.local_addr().unwrap();
println!("Escuchando en {}:{}", direccion.ip(), direccion.port());

Cliente TCP: TcpStream

TcpStream representa una conexión bidireccional activa. Permite establecer conexiones, enviar y recibir datos, y configurar tiempos de espera.

Conexión resiliente

Al igual que TcpListener::bind(), TcpStream::connect() acepta una secuencia de direcciones para intentos progresivos:

use std::net::{SocketAddr, TcpStream};

fn conectar_a_servicio() -> std::io::Result<TcpStream> {
    let destinos = [
        SocketAddr::from(([127, 0, 0, 1], 3000)),
        SocketAddr::from(([127, 0, 0, 1], 3001)),
    ];
    
    TcpStream::connect(&destinos[..])
}

Conexión con tiempo de espera

Para evitar bloqueos indefinidos durante la fase de handshake, se recomienda connect_timeout():

use std::time::Duration;

let flujo = TcpStream::connect_timeout(
    &([127, 0, 0, 1], 3000).into(),
    Duration::from_secs(3)
).expect("Conexión rechazada o agotada");

Operaciones de E/S eficientes

Lectura estructurada

Usar BufReader simplifica la lectura por líneas, evitando la gestión manual de buffers:

use std::io::{BufRead, BufReader, Write};
use std::net::TcpStream;

fn leer_por_lineas(flujo: TcpStream) {
    let lector = BufReader::new(flujo);
    
    for linea in lector.lines() {
        match linea {
            Ok(texto) => println!("Línea recibida: {}", texto),
            Err(e) => {
                eprintln!("Error de lectura: {}", e);
                break;
            }
        }
    }
}

Escritura optimizada

BufWriter reduce llamadas al sistema mediante buffering, mientras que write_all() garantiza que todos los bytes se envíen:

use std::io::{BufWriter, Write};
use std::net::TcpStream;

fn enviar_datos(flujo: TcpStream) {
    let mut escritor = BufWriter::new(flujo);
    
    for i in 0..5 {
        let mensaje = format!("Mensaje #{}\n", i);
        escritor.write_all(mensaje.as_bytes()).unwrap();
    }
    
    escritor.flush().unwrap(); // Vacía el buffer al socket
}

Control de tiempo de espera

Los métodos set_read_timeout() y set_write_timeout() configuran límites para operaciones bloqueantes. Es crucial notar que el tiempo de espera de escritura solo se activa si el buffer interno del kernel está saturado:

flujo.set_read_timeout(Some(Duration::from_millis(500))).unwrap();
flujo.set_write_timeout(Some(Duration::from_millis(500))).unwrap();

// Ahora read() y write() devolverán Err(std::io::ErrorKind::TimedOut)
// si exceden el umbral configurado

Cierre controlado de conexiones

La implementación de Drop libera automáticamente los recursos del socket al salir del ámbito. Para cierres protocolariamente correctos, se usa shutdown():

use std::net::Shutdown;

// Cerrar solo la dirección de escritura (enviar FIN)
flujo.shutdown(Shutdown::Write).unwrap();

// Cerrar ambas direcciones (equivalente a drop, pero explícito)
flujo.shutdown(Shutdown::Both).unwrap();

Etiquetas: Rust tcp network-programming

Publicado el 10-3 20:40