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();