Gestión de bajo nivel con HttpClient y HttpServer
HttpClient y HttpServer son las abstracciones fundamentales de la pila de red en dart:io. Mientras que paquetes como http o dio proporcionan una capa de conveniencia para peticiones comunes, estas clases ofrecen el control total sobre el ciclo de vida de la conexión TCP y el protocolo HTTP. El diseño de dart:io para redes es puramente basado en streams: tanto el cuerpo de una petición como la respuesta no son buffers estáticos cargados en memoria, sino flujos de datos que permiten procesar payloads masivos con una huella de memoria mínima.
Esto es especialmente útil cuando necesitas implementar protocolos personalizados sobre HTTP, desarrollar proxies de alto rendimiento o manejar flujos de datos binarios (como video o archivos grandes) de forma eficiente. Sin embargo, este control manual exige una gestión meticulosa de los recursos: el cliente debe cerrar las peticiones para completar el handshake y el servidor debe cerrar los IOSink de las respuestas para liberar los descriptores de archivos y notificar al cliente que la transferencia ha terminado. Si ignoras la naturaleza asíncrona de estos flujos o no consumes los bytes correctamente, corres el riesgo de agotar los descriptores de archivos del sistema operativo o dejar sockets en estado ESTABLISHED de forma indefinida.
import 'dart:io';
import 'dart:convert';
Future<void> main() async {
// Inicializamos el servidor en la interfaz de loopback (localhost)
final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 8080);
// El servidor escucha peticiones como un Stream de HttpRequest
final serverSubscription = server.listen((HttpRequest request) async {
final response = request.response;
if (request.uri.path == '/api/status') {
// Configuramos headers antes de escribir el cuerpo
response.headers.set(HttpHeaders.contentType, 'application/json');
response.headers.set(HttpHeaders.connection, 'keep-alive');
response.statusCode = HttpStatus.ok;
// Escribimos directamente en el sink de la respuesta
response.write('{"status": "online", "engine": "dart_io"}');
// IMPORTANTE: Cerrar el sink para señalizar el fin de la transmisión
await response.close();
} else {
response.statusCode = HttpStatus.notFound;
await response.close();
}
});
final client = HttpClient();
try {
// Iniciamos una petición GET
final request = await client.getUrl(Uri.parse('http://localhost:8080/api/status'));
// request.close() es fundamental: envía los headers y el cuerpo (si lo hubiera)
// y nos devuelve el Future con la respuesta del servidor.
final response = await request.close();
// La respuesta es un Stream<List<int>>. Transformamos los bytes a texto
// usando utf8.decoder y unimos el stream en un solo String.
final body = await response.transform(utf8.decoder).join();
print('Cliente recibió: $body');
} catch (e) {
print('Error en la comunicación: $e');
} finally {
// Limpieza de recursos para evitar fugas de memoria y sockets huérfanos
await client.close();
await serverSubscription.cancel();
await server.close();
}
}
En el ejemplo anterior, el flujo de datos comienza con HttpServer.bind, que levanta un socket escuchando en el puerto 8080. Cuando llega una petición, server.listen dispara un callback que recibe un HttpRequest. Dentro de este, request.response actúa como un IOSink; es decir, un destino hacia donde podemos “inyectar” bytes mediante response.write. Es vital llamar a await response.close() para que el cliente reciba el marcador de fin de archivo (EOF) del protocolo HTTP.
Por el lado del cliente, el proceso es distinto: client.getUrl nos devuelve un HttpClientRequest. Este objeto es el encargado de configurar la cabecera de la petición. Sin embargo, la comunicación no se completa hasta que ejecutamos await request.close(). Este paso es el que efectivamente envía la petición al socket y nos devuelve el HttpClientResponse. Finalmente, para obtener el contenido, no podemos simplemente leer una propiedad, ya que los datos llegan de forma asíncrona; usamos response.transform(utf8.decoder).join() para transformar ese flujo de bytes crudos en un String legible mediante la decodificación UTF-8.
El error frecuente
Un error clásico al trabajar con HttpClient es olvidar que HttpClientRequest es un objeto que debe ser cerrado para que la petición se envíe correctamente y se reciba la respuesta.
// ERROR: El cliente se quedará esperando indefinidamente (hang)
final request = await client.get(Uri.parse('http://localhost:8080/api/data'));
// Olvidar el siguiente paso:
// await request.close();
// El código nunca llegará aquí porque la petición nunca se "disparó" formalmente
final response = await request.close();
En el servidor, el error equivalente es no cerrar el HttpResponse. Si escribes datos con request.response.write() pero no llamas a close(), el cliente recibirá los bytes pero la conexión permanecerá abierta, esperando más datos que nunca llegarán, lo que eventualmente agotará el pool de conexiones disponibles.
N° 78