4.1 Protocolos Estándar de Comunicación
En el día a día del desarrollo de aplicaciones distribuidas, rara vez empezamos desde cero: existe un ecosistema rico de protocolos estándar de comunicaciónque definen exactamente cómo deben intercambiar información dos extremos de una red. Estos protocolos son el resultado de décadas de experiencia colectiva, plasmados en documentos técnicos abiertos llamados RFC (Request for Comments), publicados por el IETF (Internet Engineering Task Force). Conocerlos y saber elegir el adecuado en cada situación es una competencia fundamental de cualquier desarrollador de aplicaciones en red.
La pila de protocolos de internet se organiza en capas. Java, a través de su API de sockets y las librerías de java.net, te permite trabajar a distintos niveles: desde los sockets TCP/UDP de bajo nivel hasta clientes HTTP de alto nivel incluidos en el JDK moderno. Entender en qué capa opera cada protocolo te ayudará a elegir la abstracción correcta.
Protocolos a Nivel de Aplicación
Los protocolos de capa de aplicación son los que definen el formato y la semántica de los mensajes intercambiados entre aplicaciones. Se construyen sobre los protocolos de transporte (TCP o UDP) y proporcionan servicios de alto nivel listos para usar en tus programas. En Java, la mayoría disponen de una implementación nativa en el JDK o en librerías ampliamente adoptadas.
Vista previa — pila de protocolos de internet (haz clic para explorar)
Java trabaja principalmente en la capa de Aplicación y Transporte a través de java.net y java.net.http.
| Protocolo | Puerto por defecto | Transporte | Uso típico en Java |
|---|---|---|---|
| HTTP/HTTPS | 80 / 443 | TCP | APIs REST, servicios web, HttpClient (JDK 11+) |
| FTP | 21 (control) | TCP | Transferencia de ficheros, Apache Commons Net |
| SMTP | 25 / 587 / 465 | TCP | Envío de emails, Jakarta Mail (ex JavaMail) |
| POP3 / IMAP | 110 / 993 | TCP | Recepción de emails, Jakarta Mail |
| DNS | 53 | UDP/TCP | Resolución de nombres, InetAddress.getByName() |
| SSH / SFTP | 22 | TCP | Acceso seguro remoto, JSch / Apache SSHD |
| WebSocket | 80 / 443 | TCP | Comunicación bidireccional, Tyrus / Jakarta WebSocket |
| LDAP | 389 / 636 | TCP | Directorios corporativos, JNDI / UnboundID LDAP SDK |
Ventajas del Uso de Protocolos Estándar
Usar protocolos estandarizados en lugar de inventar protocolos propietarios no es solo una cuestión de comodidad: tiene implicaciones directas en la mantenibilidad, escalabilidad y seguridad de tus servicios en producción. En el contexto de un entorno empresarial real, las ventajas son determinantes.
✓ Interoperabilidad
Un servicio que habla HTTP puede ser consumido desde un cliente Java, Python, JavaScript, Go o cualquier otro lenguaje. No existe ningún acoplamiento entre cliente y servidor más allá del contrato del protocolo.
✓ Infraestructura existente
Proxies, firewalls, balanceadores de carga, CDNs y herramientas de monitorización ya entienden protocolos estándar. Tu servicio se integra inmediatamente en cualquier infraestructura corporativa.
✓ Seguridad auditada
Los protocolos estándar han sido analizados por miles de expertos en seguridad. Sus vulnerabilidades son conocidas y existen parches. Un protocolo inventado en casa no tiene esa garantía.
✓ Herramientas de diagnóstico
curl, Postman, Wireshark, nmap: depurar un servicio HTTP o TCP es inmediato con herramientas universalmente disponibles.
Interoperabilidad en la Práctica
La interoperabilidad es la capacidad de sistemas distintos de comunicarse y colaborar sin necesidad de adaptar su implementación interna. En Java, los protocolos estándar son el pegamento que une sistemas heterogéneos en arquitecturas modernas de microservicios o integraciones empresariales.
1import java.net.URI;
2import java.net.http.HttpClient;
3import java.net.http.HttpRequest;
4import java.net.http.HttpResponse;
5import java.time.Duration;
6
7/**
8 * Ejemplo de cliente HTTP usando el HttpClient nativo de Java 11+.
9 * Este cliente puede comunicarse con cualquier servidor HTTP del mundo,
10 * independientemente de en qué lenguaje esté implementado.
11 */
12public class HttpClientEjemplo {
13
14 public static void main(String[] args) throws Exception {
15
16 // Crear un cliente HTTP con configuración reutilizable
17 // HttpClient es thread-safe y se recomienda crear una sola instancia
18 HttpClient cliente = HttpClient.newBuilder()
19 .connectTimeout(Duration.ofSeconds(10))
20 .followRedirects(HttpClient.Redirect.NORMAL)
21 .version(HttpClient.Version.HTTP_2) // Prefiere HTTP/2 si el servidor lo soporta
22 .build();
23
24 // Construir la petición HTTP GET
25 HttpRequest peticion = HttpRequest.newBuilder()
26 .uri(URI.create("https://api.github.com/repos/openjdk/jdk"))
27 .header("Accept", "application/json")
28 .header("User-Agent", "MiApp-Java/1.0")
29 .timeout(Duration.ofSeconds(30))
30 .GET()
31 .build();
32
33 // Enviar la petición y obtener la respuesta
34 // El segundo argumento define cómo se maneja el cuerpo de la respuesta
35 HttpResponse<String> respuesta = cliente.send(
36 peticion,
37 HttpResponse.BodyHandlers.ofString()
38 );
39
40 // El protocolo HTTP garantiza que cualquier servidor responde con un código de estado
41 System.out.println("Código HTTP: " + respuesta.statusCode()); // Ej: 200
42 System.out.println("Cabeceras: " + respuesta.headers().map());
43 System.out.println("Cuerpo (primeros 200 chars): "
44 + respuesta.body().substring(0, Math.min(200, respuesta.body().length())));
45
46 // También es posible hacer la petición de forma asíncrona
47 cliente.sendAsync(peticion, HttpResponse.BodyHandlers.ofString())
48 .thenApply(HttpResponse::body)
49 .thenAccept(body -> System.out.println("Respuesta asíncrona recibida"))
50 .join();
51 }
52}HTTP/1.1 vs HTTP/2 vs HTTP/3 en Java
HttpClient con soporte nativo para HTTP/2, que permite multiplexar múltiples peticiones sobre una sola conexión TCP y reduce significativamente la latencia. HTTP/3 (sobre QUIC/UDP) está disponible en algunas implementaciones experimentales pero aún no forma parte del JDK estándar. Para la mayoría de servicios empresariales, HTTP/2 con TLS es la elección óptima.4.2 Servicios en Red
Antes de escribir una sola línea de código, es fundamental entender qué es exactamente un servicio en red y en qué se diferencia de una aplicación convencional. Esta distinción conceptual guiará todas las decisiones de diseño que tomarás a lo largo del ciclo de vida del servicio: desde cómo gestionar las conexiones hasta cómo garantizar la disponibilidad en producción.
Concepto de Servicio
Un servicio es un proceso que se ejecuta de forma continua en segundo plano, escuchando en un puerto de red y respondiendo a peticiones de múltiples clientes. A diferencia de un programa convencional que ejecuta una tarea y termina, un servicio tiene una vida propia: arranca, espera, atiende, y sigue disponible indefinidamente hasta que se le ordena parar.
En el mundo Linux/Unix, los servicios se denominan demonios(daemons) y su nombre suele terminar en d:httpd, sshd, mysqld. En Windows se llaman servicios de Windows y se gestionan desde el Administrador de servicios.
Vista previa — ciclo de vida de un servicio en red (interactivo)
El proceso no existe. El puerto no está ocupado.
Haz clic en cada estado para explorar el ciclo de vida
Diferencias entre Aplicación y Servicio
| Característica | Aplicación convencional | Servicio en red |
|---|---|---|
| Ciclo de vida | Arranca, ejecuta una tarea, termina | Arranca y permanece activo indefinidamente |
| Interacción | Con un único usuario a la vez (CLI/GUI) | Con múltiples clientes simultáneos |
| Interfaz | Pantalla, teclado, archivos | Puerto TCP/UDP, protocolo de red |
| Inicio | Manual por el usuario | Automático al arrancar el sistema (systemd, Windows Services) |
| Contexto de ejecución | Sesión del usuario activo | En segundo plano, sin sesión de usuario |
| Logging | Puede escribir en consola | Escribe en ficheros de log / sistema de logging centralizado |
| Errores | Puede mostrarse al usuario con diálogo | Debe recuperarse automáticamente o reiniciarse |
| Ejemplo Java | Aplicación JavaFX, utilidad de línea de comandos | Servidor HTTP, daemon de procesamiento de mensajes |
Casos de Uso Reales
En un entorno empresarial moderno te encontrarás con servicios en red en múltiples capas de la arquitectura. Identificarlos y entender su rol te ayudará a diseñar sistemas más coherentes.
🌐 API REST
Un servidor HTTP que expone recursos (clientes, pedidos, productos) a través de endpoints JSON. Escucha en un puerto (p. ej. 8080) y atiende peticiones GET, POST, PUT, DELETE de múltiples aplicaciones cliente.
📧 Servidor de correo SMTP
Servicio que escucha en el puerto 25/587 y acepta mensajes de correo electrónico para enrutarlos o almacenarlos. Un daemon como Postfix o un servicio Java que usa Jakarta Mail como relay interno.
🔄 Procesador de mensajes
Un daemon que consume mensajes de una cola (RabbitMQ, Kafka) y los procesa de forma asíncrona. Muy común en arquitecturas event-driven para desacoplar productores y consumidores.
📁 Servidor de ficheros FTP/SFTP
Servicio que permite a otros sistemas depositar o recoger ficheros de forma automatizada. Típico en integraciones B2B donde se intercambian informes, facturas o datos de inventario en lotes.
🔍 Servicio de monitorización
Un agente que expone métricas de la aplicación (JVM heap, número de peticiones, latencia) a través de un endpoint HTTP para que herramientas como Prometheus las recojan periódicamente.
🔐 Servidor de autenticación
Servicio que implementa OAuth2/OIDC para centralizar la autenticación de múltiples aplicaciones. Spring Authorization Server o Keycloak son implementaciones Java muy extendidas.
¿Puerto < 1024? Solo root puede abrirlo
4.3 Programación de Servidores de Servicios
La programación de servidores en Java se basa en el modelo de sockets, una abstracción que representa un extremo de una comunicación bidireccional sobre la red. Un socket TCP tiene una dirección IP y un puerto, y puede enviar y recibir flujos de bytes. El JDK proporciona las clases necesarias en el paquetejava.net sin necesidad de librerías externas para los casos más comunes.
Librerías y Clases Fundamentales
El JDK incluye un conjunto de clases bien definido para la programación de redes. Conocerlas antes de empezar a programar te evitará inventar soluciones a problemas ya resueltos.
| Clase / Interfaz | Paquete | Rol en el servidor |
|---|---|---|
| ServerSocket | java.net | Escucha en un puerto y acepta conexiones entrantes |
| Socket | java.net | Representa una conexión activa con un cliente concreto |
| InetAddress | java.net | Representa una dirección IP (resolución DNS, localhost, etc.) |
| InputStream / OutputStream | java.io | Flujos de bytes para leer/escribir datos por el socket |
| BufferedReader / PrintWriter | java.io | Lectura/escritura de texto por líneas sobre el socket |
| DataInputStream / DataOutputStream | java.io | Lectura/escritura de tipos primitivos Java serializados |
| ObjectInputStream / ObjectOutputStream | java.io | Serialización/deserialización de objetos Java |
| Executors / ThreadPoolExecutor | java.util.concurrent | Gestión del pool de hilos para atender clientes |
| HttpServer | com.sun.net.httpserver | Servidor HTTP ligero incluido en el JDK (no para producción) |
| AsynchronousServerSocketChannel | java.nio.channels | Servidor asíncrono no bloqueante (NIO.2, Java 7+) |
Diseño de un Servidor TCP en Java
El patrón básico de un servidor TCP en Java siempre sigue la misma estructura: crear unServerSocket, esperar conexiones con accept(), y procesar cada conexión con su flujo de entrada/salida. Veamos primero la versión más sencilla para entender el concepto, y después evolucionaremos hacia implementaciones más robustas.
Vista previa — flujo de comunicación Servidor ↔ Cliente TCP
SERVIDOR (Java)
CLIENTE (Java)
1import java.io.*;
2import java.net.*;
3
4/**
5 * Servidor TCP básico — versión monothread (un cliente a la vez).
6 * Este servidor implementa un servicio de eco: devuelve al cliente
7 * exactamente el mismo texto que recibe, en mayúsculas.
8 *
9 * Limitación: mientras atiende a un cliente, los demás esperan.
10 * En la sección 4.4 veremos cómo solucionar esto con hilos.
11 */
12public class ServidorTCPSimple {
13
14 private static final int PUERTO = 8080;
15 private static final String COMANDO_SALIDA = "FIN";
16
17 public static void main(String[] args) {
18
19 System.out.println("[Servidor] Iniciando en puerto " + PUERTO + "...");
20
21 // ServerSocket escucha en el puerto indicado.
22 // El parámetro "backlog" (50 por defecto) es el tamaño
23 // de la cola de conexiones pendientes de aceptar.
24 try (ServerSocket serverSocket = new ServerSocket(PUERTO)) {
25
26 System.out.println("[Servidor] Listo. Esperando conexiones...");
27
28 // Bucle infinito: el servidor nunca para por sí solo
29 while (true) {
30
31 // accept() BLOQUEA el hilo hasta que un cliente conecta.
32 // Devuelve un Socket que representa ESA conexión concreta.
33 Socket socketCliente = serverSocket.accept();
34
35 String clienteInfo = socketCliente.getInetAddress().getHostAddress()
36 + ":" + socketCliente.getPort();
37 System.out.println("[Servidor] Cliente conectado desde " + clienteInfo);
38
39 // Obtener los flujos de entrada y salida del socket
40 // Se usa try-with-resources para cerrar el socket al terminar
41 try (
42 // BufferedReader para leer líneas de texto del cliente
43 BufferedReader entrada = new BufferedReader(
44 new InputStreamReader(socketCliente.getInputStream()));
45
46 // PrintWriter con autoFlush=true para enviar al cliente
47 PrintWriter salida = new PrintWriter(
48 socketCliente.getOutputStream(), true)
49 ) {
50 String lineaRecibida;
51
52 // readLine() devuelve null si el cliente cierra la conexión
53 while ((lineaRecibida = entrada.readLine()) != null) {
54 System.out.println("[Servidor] Recibido de " + clienteInfo + ": " + lineaRecibida);
55
56 // Protocolo de salida: si el cliente envía "FIN", cerramos
57 if (COMANDO_SALIDA.equalsIgnoreCase(lineaRecibida.trim())) {
58 salida.println("ADIOS");
59 break;
60 }
61
62 // Servicio de eco en mayúsculas
63 String respuesta = "ECO: " + lineaRecibida.toUpperCase();
64 salida.println(respuesta);
65 }
66
67 } catch (IOException e) {
68 System.err.println("[Servidor] Error con cliente " + clienteInfo + ": " + e.getMessage());
69 }
70
71 System.out.println("[Servidor] Cliente " + clienteInfo + " desconectado.");
72 // El socketCliente se cierra automáticamente al salir del try
73 }
74
75 } catch (IOException e) {
76 System.err.println("[Servidor] Error fatal: " + e.getMessage());
77 // En producción aquí se registraría en un sistema de logging (SLF4J, Log4j2)
78 }
79 }
80}Gestión de Peticiones — Protocolo Propio sobre TCP
TCP es un protocolo de flujo de bytes: no tiene concepto de mensaje ni de petición/respuesta. Cuando programas un servicio TCP, tú defines el protocolo de aplicación que determina cómo delimitar los mensajes (por salto de línea, por longitud prefijada, por marcador especial), qué comandos acepta el servidor y qué respuestas envía. Este diseño es tan importante como el código en sí.
1import java.io.*;
2import java.net.*;
3
4/**
5 * Servidor de calculadora con protocolo de texto propio.
6 * Protocolo definido:
7 * Petición: "OPERACION numero1 numero2
8"
9 * Respuesta: "OK resultado
10" o "ERROR mensaje
11"
12 *
13 * Ejemplos de uso (con telnet o nuestro cliente Java):
14 * > SUMA 10 5 → OK 15.0
15 * > RESTA 10 5 → OK 5.0
16 * > MULT 10 5 → OK 50.0
17 * > DIV 10 0 → ERROR Division por cero
18 * > DESCONOCIDO → ERROR Comando desconocido
19 */
20public class ServidorCalculadora {
21
22 public static void main(String[] args) throws IOException {
23 try (ServerSocket server = new ServerSocket(9000)) {
24 System.out.println("[Calculadora] Servidor iniciado en puerto 9000");
25
26 while (true) {
27 try (Socket cliente = server.accept();
28 BufferedReader in = new BufferedReader(
29 new InputStreamReader(cliente.getInputStream()));
30 PrintWriter out = new PrintWriter(cliente.getOutputStream(), true)) {
31
32 out.println("BIENVENIDO Servidor Calculadora v1.0");
33 String linea;
34
35 while ((linea = in.readLine()) != null) {
36 String respuesta = procesarComando(linea.trim().toUpperCase());
37 out.println(respuesta);
38 if ("ADIOS".equals(respuesta)) break;
39 }
40 }
41 }
42 }
43 }
44
45 /**
46 * Parsea y ejecuta un comando del protocolo de la calculadora.
47 * @param comando Línea de texto recibida del cliente
48 * @return Respuesta a enviar al cliente
49 */
50 private static String procesarComando(String comando) {
51 if (comando.equals("FIN")) return "ADIOS";
52
53 // Dividir la línea en partes: "SUMA 10 5" → ["SUMA", "10", "5"]
54 String[] partes = comando.split("\s+");
55
56 if (partes.length != 3) {
57 return "ERROR Sintaxis incorrecta. Usa: OPERACION num1 num2";
58 }
59
60 try {
61 double a = Double.parseDouble(partes[1]);
62 double b = Double.parseDouble(partes[2]);
63
64 return switch (partes[0]) {
65 case "SUMA" -> "OK " + (a + b);
66 case "RESTA" -> "OK " + (a - b);
67 case "MULT" -> "OK " + (a * b);
68 case "DIV" -> b == 0 ? "ERROR Division por cero"
69 : "OK " + (a / b);
70 case "MOD" -> b == 0 ? "ERROR Modulo por cero"
71 : "OK " + (a % b);
72 default -> "ERROR Comando desconocido: " + partes[0];
73 };
74 } catch (NumberFormatException e) {
75 return "ERROR Los operandos deben ser numeros";
76 }
77 }
78}SO_TIMEOUT: evita que el servidor se quede bloqueado para siempre
accept() y readLine() pueden bloquearse indefinidamente. En producción siempre configura un timeout con serverSocket.setSoTimeout(milisegundos) y socketCliente.setSoTimeout(milisegundos). Así el servidor puede detectar clientes que se han desconectado silenciosamente (half-open connections) y liberar los recursos.4.4 Comunicaci ón Simultánea
El servidor de la sección anterior tenía una limitación crítica para un entorno de producción: solo podía atender a un cliente a la vez. Si cien usuarios intentaban conectarse simultáneamente, 99 de ellos esperarían en la cola hasta que el primero terminara. Para un servicio real, esto es inaceptable. La solución es la concurrencia: atender múltiples clientes en paralelo usando hilos de ejecución (threads).
Atención a Múltiples Clientes — Patrón Thread-per-Connection
El patrón más sencillo de concurrencia para servidores es el thread-per-connection: por cada conexión aceptada, se crea un nuevo hilo que gestiona esa conexión de principio a fin. El hilo principal sigue en el bucle de accept() listo para aceptar la siguiente conexión sin esperar a que la actual termine.
Vista previa — servidor concurrente (pool de hilos)
Servidor
accept() loop
T-1
libre
T-2
192.168.1.20
POST /api/orders
T-3
libre
T-4
libre
T-5
192.168.1.50
DELETE /api/cache
T-6
libre
Haz clic en el servidor para cambiar el estado de T-1. El pool tiene 6 hilos; 3 activos y 3 libres.
1import java.io.*;
2import java.net.*;
3import java.util.concurrent.*;
4import java.util.concurrent.atomic.AtomicInteger;
5
6/**
7 * Servidor TCP concurrente usando un pool de hilos.
8 * Diseño: el hilo principal solo acepta conexiones (accept loop).
9 * Cada conexión se delega a un hilo del pool para su procesamiento.
10 *
11 * Ventajas del pool frente a "new Thread() por conexión":
12 * - Evita crear/destruir hilos continuamente (costoso)
13 * - Limita el número de hilos (evita agotar los recursos del sistema)
14 * - Reutiliza hilos ya creados (mejor rendimiento)
15 */
16public class ServidorConcurrente {
17
18 private static final int PUERTO = 8080;
19 private static final int MAX_HILOS = 50; // máximo de conexiones simultáneas
20 private static final int BACKLOG = 100; // cola de conexiones pendientes
21 private static final AtomicInteger contador = new AtomicInteger(0);
22
23 public static void main(String[] args) throws IOException {
24
25 // ThreadPoolExecutor con:
26 // - 10 hilos core (siempre activos)
27 // - hasta 50 hilos máximos
28 // - hilos extra se destruyen si inactivos 60 segundos
29 // - cola de tareas pendientes de hasta 100 conexiones
30 ExecutorService pool = new ThreadPoolExecutor(
31 10, MAX_HILOS,
32 60L, TimeUnit.SECONDS,
33 new ArrayBlockingQueue<>(100),
34 new ThreadPoolExecutor.CallerRunsPolicy() // si el pool está lleno, el hilo main procesa
35 );
36
37 try (ServerSocket serverSocket = new ServerSocket(PUERTO, BACKLOG)) {
38 serverSocket.setReuseAddress(true); // permite reusar el puerto inmediatamente tras reinicio
39 System.out.printf("[Servidor] Escuchando en :%d con pool de hasta %d hilos%n",
40 PUERTO, MAX_HILOS);
41
42 // Bucle principal: solo acepta conexiones y las delega al pool
43 while (!Thread.currentThread().isInterrupted()) {
44 Socket clienteSocket = serverSocket.accept();
45 int id = contador.incrementAndGet();
46 System.out.printf("[Servidor] Conexión #%d desde %s%n",
47 id, clienteSocket.getRemoteSocketAddress());
48
49 // Enviar la tarea al pool: el hilo principal queda libre inmediatamente
50 pool.submit(new GestorCliente(clienteSocket, id));
51 }
52
53 } finally {
54 // Shutdown limpio: espera a que los hilos activos terminen
55 pool.shutdown();
56 try {
57 if (!pool.awaitTermination(30, TimeUnit.SECONDS)) {
58 pool.shutdownNow();
59 }
60 } catch (InterruptedException e) {
61 pool.shutdownNow();
62 Thread.currentThread().interrupt();
63 }
64 }
65 }
66
67 /**
68 * Tarea ejecutada por un hilo del pool para gestionar una conexión de cliente.
69 * Implementa Runnable para ser compatible con ExecutorService.
70 */
71 static class GestorCliente implements Runnable {
72
73 private final Socket socket;
74 private final int id;
75
76 GestorCliente(Socket socket, int id) {
77 this.socket = socket;
78 this.id = id;
79 }
80
81 @Override
82 public void run() {
83 String clienteInfo = socket.getRemoteSocketAddress().toString();
84 System.out.printf("[Hilo-%d] Iniciando sesión con %s%n", id, clienteInfo);
85
86 // setSoTimeout: si el cliente no envía datos en 30s, lanzamos SocketTimeoutException
87 try {
88 socket.setSoTimeout(30_000);
89 } catch (SocketException e) {
90 System.err.printf("[Hilo-%d] No se pudo configurar timeout%n", id);
91 }
92
93 try (socket;
94 BufferedReader in = new BufferedReader(
95 new InputStreamReader(socket.getInputStream()));
96 PrintWriter out = new PrintWriter(socket.getOutputStream(), true)) {
97
98 out.println("CONECTADO OK servidor_concurrente/1.0");
99 String linea;
100 while ((linea = in.readLine()) != null) {
101 if ("FIN".equalsIgnoreCase(linea.trim())) {
102 out.println("ADIOS");
103 break;
104 }
105 // Procesamiento: en un servicio real aquí iría la lógica de negocio
106 out.println("ACK: " + linea.trim().toUpperCase());
107 }
108
109 } catch (SocketTimeoutException e) {
110 System.out.printf("[Hilo-%d] Timeout: %s inactivo%n", id, clienteInfo);
111 } catch (IOException e) {
112 System.out.printf("[Hilo-%d] Desconexión abrupta de %s: %s%n",
113 id, clienteInfo, e.getMessage());
114 } finally {
115 System.out.printf("[Hilo-%d] Sesión con %s terminada%n", id, clienteInfo);
116 }
117 }
118 }
119}Gestión Eficiente de Recursos
Un servidor en producción gestiona recursos escasos: hilos del sistema operativo, descriptores de fichero (cada socket abierto consume uno), memoria del heap de la JVM. Un diseño descuidado puede provocar que el servidor se quede sin recursos y deje de aceptar conexiones. Las siguientes estrategias son esenciales para cualquier servicio Java en producción.
| Recurso | Riesgo | Estrategia de gestión | Clase Java |
|---|---|---|---|
| Hilos | OOM si se crean sin límite | ThreadPoolExecutor con maxPoolSize | Executors, ThreadPoolExecutor |
| Sockets | Agotamiento de file descriptors | try-with-resources, setSoTimeout() | Socket, ServerSocket |
| Memoria | Heap lleno, GC pause storms | Limitar backlog, leer en streaming | ByteBuffer (NIO), InputStream |
| Conexiones BD | Pool agotado | HikariCP con maxPoolSize | DataSource (JDBC) |
| CPU | Hilos bloqueados indefinidamente | Timeouts en todas las operaciones I/O | Future.get(timeout, ...) |
Control de Conexiones — Sincronización y Estado Compartido
Cuando múltiples hilos atienden clientes simultáneamente, puede surgir la necesidad de compartir estado entre ellos: llevar un contador de clientes conectados, mantener una lista de sesiones activas, o limitar el número de conexiones por IP. Estas operaciones deben protegerse para evitar condiciones de carrera (race conditions).
1import java.net.*;
2import java.util.*;
3import java.util.concurrent.*;
4import java.util.concurrent.atomic.*;
5
6/**
7 * Gestor de conexiones activas — compartido entre todos los hilos del servidor.
8 * Usa estructuras thread-safe para evitar condiciones de carrera.
9 *
10 * Este es un componente de infraestructura que el GestorCliente usa para:
11 * - Registrarse al conectar / darse de baja al desconectar
12 * - Limitar conexiones por IP (anti-DDoS básico)
13 * - Proporcionar estadísticas al endpoint de monitorización
14 */
15public class GestorConexiones {
16
17 // AtomicInteger: operaciones de incremento/decremento son atómicas (thread-safe)
18 private final AtomicInteger totalConexiones = new AtomicInteger(0);
19 private final AtomicInteger conexionesActivas = new AtomicInteger(0);
20 private final AtomicLong totalPeticionesServidas = new AtomicLong(0);
21
22 // ConcurrentHashMap: Map thread-safe sin bloqueos generales
23 // Clave: IP del cliente, Valor: número de conexiones activas desde esa IP
24 private final ConcurrentHashMap<String, AtomicInteger> conexionesPorIp =
25 new ConcurrentHashMap<>();
26
27 // Límite de conexiones simultáneas por IP (anti-DDoS básico)
28 private final int MAX_CONEXIONES_POR_IP;
29
30 // CopyOnWriteArraySet: optimizado para lecturas frecuentes, escrituras raras
31 // Almacena los sockets activos para poder cerrarlos todos en shutdown
32 private final Set<Socket> socketsActivos = new CopyOnWriteArraySet<>();
33
34 public GestorConexiones(int maxConexionesPorIp) {
35 this.MAX_CONEXIONES_POR_IP = maxConexionesPorIp;
36 }
37
38 /**
39 * Intenta registrar una nueva conexión.
40 * @return true si se acepta la conexión, false si se rechaza por límite de IP
41 */
42 public boolean registrarConexion(Socket socket) {
43 String ip = socket.getInetAddress().getHostAddress();
44
45 // getOrDefault + computeIfAbsent: atómico en ConcurrentHashMap
46 AtomicInteger contadorIp = conexionesPorIp
47 .computeIfAbsent(ip, k -> new AtomicInteger(0));
48
49 // Verificar el límite antes de incrementar
50 int actual = contadorIp.incrementAndGet();
51 if (actual > MAX_CONEXIONES_POR_IP) {
52 contadorIp.decrementAndGet(); // revertir el incremento
53 System.out.printf("[Gestor] Rechazada conexión desde %s: límite de %d alcanzado%n",
54 ip, MAX_CONEXIONES_POR_IP);
55 return false;
56 }
57
58 socketsActivos.add(socket);
59 totalConexiones.incrementAndGet();
60 conexionesActivas.incrementAndGet();
61
62 System.out.printf("[Gestor] Conexión registrada: %s (activas: %d, IPs únicas: %d)%n",
63 ip, conexionesActivas.get(), conexionesPorIp.size());
64 return true;
65 }
66
67 /**
68 * Desregistra una conexión al cerrarse.
69 */
70 public void desregistrarConexion(Socket socket) {
71 String ip = socket.getInetAddress().getHostAddress();
72
73 socketsActivos.remove(socket);
74 conexionesActivas.decrementAndGet();
75 totalPeticionesServidas.incrementAndGet();
76
77 AtomicInteger contadorIp = conexionesPorIp.get(ip);
78 if (contadorIp != null && contadorIp.decrementAndGet() <= 0) {
79 conexionesPorIp.remove(ip); // limpiar entradas vacías
80 }
81 }
82
83 /**
84 * Cierra todas las conexiones activas (para shutdown ordenado).
85 */
86 public void cerrarTodas() {
87 System.out.printf("[Gestor] Cerrando %d conexiones activas...%n", socketsActivos.size());
88 socketsActivos.forEach(socket -> {
89 try { socket.close(); } catch (Exception ignored) { }
90 });
91 }
92
93 // Getters para el endpoint de monitorización
94 public int getConexionesActivas() { return conexionesActivas.get(); }
95 public long getTotalConexiones() { return totalConexiones.get(); }
96 public long getTotalPeticionesServidas() { return totalPeticionesServidas.get(); }
97 public int getIpsUnicas() { return conexionesPorIp.size(); }
98}Virtual Threads (Project Loom) — Java 21+
Executors.newVirtualThreadPerTaskExecutor() sustituye al pool tradicional sin cambiar el resto del código.4.5 Clientes de Servicios
Todo servicio que desarrolles necesita ser probado y consumido. La programación del cliente es la otra cara de la moneda: mientras el servidor escucha y responde, el cliente inicia la conexión y envía peticiones. En Java, programar un cliente TCP es incluso más sencillo que programar el servidor: basta con instanciar un Socket con la dirección y el puerto del servidor.
Cliente TCP en Java
1import java.io.*;
2import java.net.*;
3
4/**
5 * Cliente TCP genérico para conectarse a cualquier servicio TCP.
6 * Este cliente se puede usar para probar el ServidorCalculadora
7 * o cualquier otro servicio con protocolo de texto basado en líneas.
8 */
9public class ClienteTCP {
10
11 private final String host;
12 private final int puerto;
13
14 public ClienteTCP(String host, int puerto) {
15 this.host = host;
16 this.puerto = puerto;
17 }
18
19 /**
20 * Envía una lista de comandos al servidor y recibe las respuestas.
21 * @param comandos Lista de strings a enviar, uno por conexión
22 */
23 public void ejecutarComandos(String... comandos) {
24
25 // new Socket(host, puerto) intenta conectar inmediatamente
26 // Lanza ConnectException si el servidor no está disponible
27 try (Socket socket = new Socket()) {
28
29 // Intentar la conexión con timeout de 5 segundos
30 // Evita que el cliente espere indefinidamente si el servidor no responde
31 socket.connect(new InetSocketAddress(host, puerto), 5000);
32 socket.setSoTimeout(10_000); // timeout de lectura: 10 segundos
33
34 System.out.printf("[Cliente] Conectado a %s:%d%n", host, puerto);
35
36 try (BufferedReader in = new BufferedReader(
37 new InputStreamReader(socket.getInputStream()));
38 PrintWriter out = new PrintWriter(socket.getOutputStream(), true)) {
39
40 // Leer mensaje de bienvenida
41 String bienvenida = in.readLine();
42 System.out.println("[Servidor] " + bienvenida);
43
44 // Enviar comandos y leer respuestas
45 for (String comando : comandos) {
46 System.out.println("[Cliente] Enviando: " + comando);
47 out.println(comando);
48
49 String respuesta = in.readLine();
50 System.out.println("[Servidor] " + respuesta);
51
52 if (respuesta != null && respuesta.startsWith("ADIOS")) {
53 System.out.println("[Cliente] Servidor confirmó cierre");
54 break;
55 }
56 }
57 }
58
59 } catch (ConnectException e) {
60 System.err.printf("[Cliente] No se pudo conectar a %s:%d — ¿está el servidor activo?%n",
61 host, puerto);
62 } catch (SocketTimeoutException e) {
63 System.err.println("[Cliente] Timeout: el servidor tardó demasiado en responder");
64 } catch (IOException e) {
65 System.err.println("[Cliente] Error de I/O: " + e.getMessage());
66 }
67 }
68
69 public static void main(String[] args) {
70 ClienteTCP cliente = new ClienteTCP("localhost", 9000);
71
72 // Probar el ServidorCalculadora
73 cliente.ejecutarComandos(
74 "SUMA 10 5",
75 "RESTA 100 37",
76 "MULT 6 7",
77 "DIV 15 4",
78 "DIV 10 0", // Error esperado: división por cero
79 "POTENCIA 2 10", // Comando desconocido → ERROR
80 "FIN" // Cierre limpio
81 );
82 }
83}Pruebas de Funcionamiento
Probar un servicio en red correctamente implica verificar no solo el caso feliz (happy path), sino también el comportamiento ante condiciones adversas: servidor caído, mensajes malformados, timeouts, cierres abruptos. Existen varias herramientas, desde las más básicas disponibles en cualquier sistema hasta frameworks de testing completos.
| Herramienta | Tipo | Uso | Ejemplo de comando |
|---|---|---|---|
| telnet | CLI | Probar servicios TCP de texto a mano | telnet localhost 8080 |
| nc (netcat) | CLI | Versión moderna de telnet, scriptable | echo 'SUMA 5 3' | nc localhost 9000 |
| curl | CLI | Probar servicios HTTP/HTTPS | curl -X GET http://localhost:8080/api/v1/productos |
| Postman | GUI | Pruebas visuales de APIs REST | Interfaz gráfica con colecciones de peticiones |
| Wireshark | Sniffer | Capturar y analizar paquetes de red a nivel byte | Filtro: tcp.port == 8080 |
| nmap | Scanner | Verificar que los puertos están abiertos y escuchando | nmap -p 8080 localhost |
| JUnit + Testcontainers | Test | Tests de integración automatizados con servidor real | @Test con socket cliente |
| WireMock | Mock server | Simular servicios externos HTTP en tests | wireMockServer.start() |
Herramientas Cliente — Tests de Integración con JUnit
En un proyecto profesional, las pruebas manuales con telneto curl son útiles para el desarrollo inicial, pero no escalan. La automatización con JUnit te permite ejecutar cientos de casos de prueba en segundos, integrarlo en tu pipeline CI/CD y detectar regresiones de forma inmediata.
1import org.junit.jupiter.api.*;
2import java.io.*;
3import java.net.*;
4import java.util.concurrent.*;
5
6/**
7 * Tests de integración del ServidorCalculadora.
8 * Levantamos el servidor real en un hilo separado antes de los tests
9 * y lo cerramos al terminar.
10 *
11 * Dependencia: JUnit 5 (junit-jupiter)
12 */
13@TestInstance(TestInstance.Lifecycle.PER_CLASS)
14class ServidorCalculadoraTest {
15
16 private ServerSocket serverSocket;
17 private ExecutorService executor;
18
19 /**
20 * Antes de todos los tests: levantar el servidor en background.
21 */
22 @BeforeAll
23 void arrancarServidor() throws IOException {
24 // Puerto 0 → el SO asigna un puerto libre automáticamente (evita conflictos)
25 serverSocket = new ServerSocket(0);
26 executor = Executors.newCachedThreadPool();
27
28 // Arrancar el servidor en un hilo daemon
29 Thread servidorThread = new Thread(() -> {
30 while (!serverSocket.isClosed()) {
31 try {
32 Socket cliente = serverSocket.accept();
33 executor.submit(() -> new ServidorCalculadora.GestorCliente(cliente).run());
34 } catch (IOException e) {
35 if (!serverSocket.isClosed()) {
36 System.err.println("Error en accept: " + e.getMessage());
37 }
38 }
39 }
40 });
41 servidorThread.setDaemon(true);
42 servidorThread.start();
43 }
44
45 @AfterAll
46 void pararServidor() throws IOException {
47 serverSocket.close();
48 executor.shutdownNow();
49 }
50
51 /**
52 * Helper: enviar un comando y obtener la respuesta.
53 */
54 private String enviarComando(String comando) throws IOException {
55 int puerto = serverSocket.getLocalPort();
56 try (Socket socket = new Socket("localhost", puerto);
57 PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
58 BufferedReader in = new BufferedReader(
59 new InputStreamReader(socket.getInputStream()))) {
60 in.readLine(); // mensaje de bienvenida
61 out.println(comando);
62 return in.readLine();
63 }
64 }
65
66 @Test
67 @DisplayName("Suma de dos enteros positivos")
68 void testSuma() throws IOException {
69 Assertions.assertEquals("OK 15.0", enviarComando("SUMA 10 5"));
70 }
71
72 @Test
73 @DisplayName("División por cero debe retornar ERROR")
74 void testDivisionPorCero() throws IOException {
75 String respuesta = enviarComando("DIV 10 0");
76 Assertions.assertTrue(respuesta.startsWith("ERROR"),
77 "Se esperaba ERROR, se recibió: " + respuesta);
78 }
79
80 @Test
81 @DisplayName("Operando no numérico debe retornar ERROR")
82 void testOperandoInvalido() throws IOException {
83 String respuesta = enviarComando("SUMA abc 5");
84 Assertions.assertTrue(respuesta.startsWith("ERROR"));
85 }
86
87 @Test
88 @DisplayName("Comando desconocido devuelve ERROR")
89 void testComandoDesconocido() throws IOException {
90 String respuesta = enviarComando("RAIZ 25");
91 Assertions.assertTrue(respuesta.startsWith("ERROR"));
92 }
93
94 @Test
95 @DisplayName("Comando FIN cierra la conexión")
96 void testComandoFin() throws IOException {
97 Assertions.assertEquals("ADIOS", enviarComando("FIN"));
98 }
99
100 @Test
101 @DisplayName("Múltiples comandos en la misma conexión")
102 void testMultiplesComandos() throws IOException {
103 int puerto = serverSocket.getLocalPort();
104 try (Socket socket = new Socket("localhost", puerto);
105 PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
106 BufferedReader in = new BufferedReader(
107 new InputStreamReader(socket.getInputStream()))) {
108
109 in.readLine(); // bienvenida
110
111 out.println("SUMA 1 1");
112 Assertions.assertEquals("OK 2.0", in.readLine());
113
114 out.println("MULT 3 4");
115 Assertions.assertEquals("OK 12.0", in.readLine());
116
117 out.println("RESTA 100 55");
118 Assertions.assertEquals("OK 45.0", in.readLine());
119 }
120 }
121}Validación del Servicio con curl y telnet
1#!/bin/bash
2# Script de validación rápida de un servicio en red
3
4echo "=== Validación del ServidorCalculadora (puerto 9000) ==="
5
6# 1. Verificar que el puerto está escuchando
7echo -n "[1] Verificando puerto 9000... "
8if nc -z -w2 localhost 9000 2>/dev/null; then
9 echo "✓ Puerto abierto"
10else
11 echo "✗ Puerto cerrado o servidor inactivo"
12 exit 1
13fi
14
15# 2. Probar una operación básica con netcat
16echo -n "[2] Probando SUMA 5 3... "
17RESPUESTA=$(echo -e "SUMA 5 3
18FIN" | nc -w3 localhost 9000 | grep "OK|ERROR" | head -1)
19if [[ "$RESPUESTA" == "OK 8.0" ]]; then
20 echo "✓ Correcto: $RESPUESTA"
21else
22 echo "✗ Respuesta inesperada: $RESPUESTA"
23fi
24
25# 3. Probar error esperado
26echo -n "[3] Probando división por cero... "
27RESPUESTA=$(echo -e "DIV 10 0
28FIN" | nc -w3 localhost 9000 | grep "OK|ERROR" | head -1)
29if [[ "$RESPUESTA" == ERROR* ]]; then
30 echo "✓ Error manejado correctamente: $RESPUESTA"
31else
32 echo "✗ Debería haber devuelto ERROR: $RESPUESTA"
33fi
34
35# 4. Prueba de carga básica: 10 conexiones simultáneas
36echo "[4] Prueba de concurrencia (10 conexiones simultáneas)..."
37EXITOS=0
38for i in $(seq 1 10); do
39 RESPUESTA=$(echo -e "SUMA $i $i
40FIN" | nc -w3 localhost 9000 | grep "OK" | head -1) &
41 ESPERADO="OK $(echo "scale=1; $i + $i" | bc)"
42done
43wait
44echo " Prueba de concurrencia completada."
45
46echo ""
47echo "=== Validación HTTP (si el servicio expone un endpoint REST) ==="
48# Para un servidor HTTP en puerto 8080:
49curl -s -o /dev/null -w "Status: %{http_code}, Tiempo: %{time_total}s
50" http://localhost:8080/api/v1/health
51
52curl -s http://localhost:8080/api/v1/health | python3 -m json.tool 2>/dev/null || trueUsa nc (netcat) en lugar de telnet
telnet está desapareciendo de los sistemas modernos por razones de seguridad.nc (netcat) es su sustituto natural: más versátil, scriptable y disponible en todas las distribuciones Linux. En macOS y Windows (WSL) también está disponible. Para probar un servicio HTTP, curl es la elección estándar con opciones como -v (verbose), -H (cabeceras) y -d (cuerpo POST).4.6 Disponibilidad y Eficiencia
Un servicio en producción no solo debe funcionar: debe funcionar siempre, incluso cuando algo falla. La disponibilidad se mide habitualmente como porcentaje del tiempo que el servicio está operativo. El famoso "five nines" (99,999% de disponibilidad) equivale a solo 5 minutos de inactividad al año. Alcanzar estos niveles requiere diseñar proactivamente para el fallo, no reactivamente.
Vista previa — panel de monitorización de puertos (haz clic para cambiar estado)
Puerto 8080
UP
Puerto 8443
UP
Puerto 9090
DEGRADED
Puerto 5432
DOWN
Haz clic en cada servicio para ciclar entre estados: OK → DEGRADED → DOWN
Comprobación de Disponibilidad — Health Check
Un health check es un mecanismo que permite a herramientas externas (orquestadores como Kubernetes, balanceadores de carga, sistemas de monitorización) verificar si tu servicio está sano y listo para recibir tráfico. En Java hay varias formas de implementarlo, desde el más simple (intentar una conexión TCP) hasta el más completo (un endpoint HTTP que verifica todas las dependencias).
1import com.sun.net.httpserver.*;
2import java.io.*;
3import java.net.*;
4import java.util.*;
5import java.util.concurrent.*;
6
7/**
8 * Servicio de health check HTTP integrado en el servidor.
9 * Expone los siguientes endpoints:
10 *
11 * GET /health → 200 OK si el servicio está sano, 503 si no
12 * GET /health/ready → 200 OK si listo para recibir tráfico
13 * GET /health/live → 200 OK si el proceso está vivo (siempre)
14 * GET /metrics → Estadísticas del servicio en formato Prometheus
15 *
16 * Compatible con Kubernetes liveness/readiness probes y Spring Boot Actuator style.
17 */
18public class HealthCheckServicio {
19
20 private final int puerto;
21 private final HttpServer httpServer;
22 private volatile boolean listo = false;
23 private volatile int conexionesActivas = 0;
24 private volatile long peticionesServidas = 0;
25 private final long inicioMs = System.currentTimeMillis();
26
27 public HealthCheckServicio(int puerto) throws IOException {
28 this.puerto = puerto;
29 this.httpServer = HttpServer.create(new InetSocketAddress(puerto), 0);
30 registrarHandlers();
31 }
32
33 private void registrarHandlers() {
34 // Health check general
35 httpServer.createContext("/health", exchange -> {
36 Map<String, Object> estado = new LinkedHashMap<>();
37 estado.put("status", "UP");
38 estado.put("uptime_ms", System.currentTimeMillis() - inicioMs);
39 estado.put("conexiones_activas", conexionesActivas);
40 estado.put("peticiones_servidas", peticionesServidas);
41 estado.put("memoria_heap_mb",
42 Runtime.getRuntime().totalMemory() / 1_048_576);
43
44 // Verificar dependencias (BD, servicios externos...)
45 boolean bdOk = verificarBaseDatos();
46 estado.put("base_datos", bdOk ? "UP" : "DOWN");
47
48 int statusCode = bdOk ? 200 : 503;
49 responderJson(exchange, estado, statusCode);
50 });
51
52 // Readiness probe: ¿está listo para recibir tráfico?
53 httpServer.createContext("/health/ready", exchange -> {
54 if (listo) {
55 responderTexto(exchange, "READY", 200);
56 } else {
57 responderTexto(exchange, "NOT READY — inicializando", 503);
58 }
59 });
60
61 // Liveness probe: ¿está el proceso vivo?
62 httpServer.createContext("/health/live", exchange ->
63 responderTexto(exchange, "ALIVE", 200));
64
65 // Endpoint de métricas estilo Prometheus
66 httpServer.createContext("/metrics", exchange -> {
67 long uptimeSec = (System.currentTimeMillis() - inicioMs) / 1000;
68 String metricas = String.format(
69 "# HELP app_connections_active Conexiones TCP activas
70"
71 + "app_connections_active %d
72"
73 + "# HELP app_requests_total Total de peticiones servidas
74"
75 + "app_requests_total %d
76"
77 + "# HELP app_uptime_seconds Tiempo en línea en segundos
78"
79 + "app_uptime_seconds %d
80"
81 + "# HELP jvm_memory_heap_bytes Heap JVM en uso
82"
83 + "jvm_memory_heap_bytes %d
84",
85 conexionesActivas, peticionesServidas, uptimeSec,
86 Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()
87 );
88 byte[] cuerpo = metricas.getBytes();
89 exchange.getResponseHeaders().set("Content-Type", "text/plain; version=0.0.4");
90 exchange.sendResponseHeaders(200, cuerpo.length);
91 exchange.getResponseBody().write(cuerpo);
92 exchange.getResponseBody().close();
93 });
94 }
95
96 private void responderJson(HttpExchange exchange, Map<String, Object> data, int status)
97 throws IOException {
98 // Serialización manual sencilla (en producción usa Jackson o Gson)
99 StringBuilder sb = new StringBuilder("{");
100 data.forEach((k, v) -> {
101 sb.append(""").append(k).append("":");
102 if (v instanceof String) sb.append(""").append(v).append(""");
103 else sb.append(v);
104 sb.append(",");
105 });
106 if (sb.charAt(sb.length() - 1) == ',') sb.deleteCharAt(sb.length() - 1);
107 sb.append("}");
108 byte[] cuerpo = sb.toString().getBytes();
109 exchange.getResponseHeaders().set("Content-Type", "application/json");
110 exchange.sendResponseHeaders(status, cuerpo.length);
111 exchange.getResponseBody().write(cuerpo);
112 exchange.getResponseBody().close();
113 }
114
115 private void responderTexto(HttpExchange exchange, String texto, int status)
116 throws IOException {
117 byte[] cuerpo = texto.getBytes();
118 exchange.sendResponseHeaders(status, cuerpo.length);
119 exchange.getResponseBody().write(cuerpo);
120 exchange.getResponseBody().close();
121 }
122
123 private boolean verificarBaseDatos() {
124 // En producción: intentar obtener una conexión del pool y ejecutar "SELECT 1"
125 // Aquí simulamos que la BD siempre está disponible
126 return true;
127 }
128
129 public void iniciar() {
130 httpServer.setExecutor(Executors.newFixedThreadPool(4));
131 httpServer.start();
132 listo = true;
133 System.out.printf("[HealthCheck] Endpoints disponibles en http://localhost:%d/%n", puerto);
134 System.out.printf(" GET /health, /health/ready, /health/live, /metrics%n");
135 }
136
137 // Llamar desde el GestorConexiones para actualizar métricas
138 public void actualizarMetricas(int conexiones, long peticiones) {
139 this.conexionesActivas = conexiones;
140 this.peticionesServidas = peticiones;
141 }
142}Gestión de Caídas — Reconexión Automática
Los servicios caen: fallo de hardware, actualización del sistema operativo, memoria agotada, bug crítico. Los clientes que consumen tu servicio deben diseñarse para recuperarse automáticamente de estas caídas mediante estrategias de reconexión con backoff exponencial: cada intento fallido espera el doble que el anterior, para no saturar un servidor que ya está bajo presión.
1import java.io.*;
2import java.net.*;
3import java.util.concurrent.TimeUnit;
4
5/**
6 * Cliente TCP con reconexión automática y backoff exponencial.
7 * Patrón recomendado para clientes de servicios críticos en producción.
8 *
9 * Backoff exponencial: si falla, espera 1s, luego 2s, luego 4s, 8s, 16s...
10 * Con jitter: añade un factor aleatorio para evitar que todos los clientes
11 * reconecten al mismo tiempo (thundering herd problem).
12 */
13public class ClienteConReconexion {
14
15 private static final String HOST = "localhost";
16 private static final int PUERTO = 8080;
17 private static final int MAX_REINTENTOS = 10;
18 private static final long ESPERA_BASE_MS = 1_000; // 1 segundo
19 private static final long ESPERA_MAX_MS = 60_000; // máximo 60 segundos
20
21 private Socket socket;
22 private BufferedReader lector;
23 private PrintWriter escritor;
24
25 /**
26 * Intentar conectar con backoff exponencial.
27 * @return true si la conexión se establece, false si se agota el número de reintentos
28 */
29 public boolean conectar() {
30 int intento = 0;
31 long espera = ESPERA_BASE_MS;
32
33 while (intento < MAX_REINTENTOS) {
34 try {
35 System.out.printf("[Cliente] Intento %d/%d conectando a %s:%d...%n",
36 intento + 1, MAX_REINTENTOS, HOST, PUERTO);
37
38 socket = new Socket();
39 socket.connect(new InetSocketAddress(HOST, PUERTO), 5000);
40 socket.setSoTimeout(30_000);
41
42 lector = new BufferedReader(
43 new InputStreamReader(socket.getInputStream()));
44 escritor = new PrintWriter(socket.getOutputStream(), true);
45
46 System.out.printf("[Cliente] ✓ Conectado al intento %d%n", intento + 1);
47 return true;
48
49 } catch (IOException e) {
50 intento++;
51 if (intento >= MAX_REINTENTOS) {
52 System.err.printf("[Cliente] ✗ Agotados %d reintentos. Rendición.%n",
53 MAX_REINTENTOS);
54 return false;
55 }
56
57 // Backoff exponencial con jitter (factor aleatorio ±20%)
58 long jitter = (long)(espera * 0.2 * (Math.random() * 2 - 1));
59 long esperaConJitter = Math.min(espera + jitter, ESPERA_MAX_MS);
60 System.out.printf("[Cliente] ⏳ Reintentando en %.1f segundos (%s)%n",
61 esperaConJitter / 1000.0, e.getMessage());
62
63 try {
64 TimeUnit.MILLISECONDS.sleep(esperaConJitter);
65 } catch (InterruptedException ie) {
66 Thread.currentThread().interrupt();
67 return false;
68 }
69
70 // Doblar el tiempo de espera para el siguiente intento
71 espera = Math.min(espera * 2, ESPERA_MAX_MS);
72 }
73 }
74 return false;
75 }
76
77 /**
78 * Enviar un mensaje y recibir respuesta.
79 * Si la conexión está rota, intenta reconectar automáticamente.
80 */
81 public String enviar(String mensaje) throws IOException {
82 // Intentar enviar; si falla, reconectar y reintentar una vez
83 for (int intento = 0; intento < 2; intento++) {
84 try {
85 if (socket == null || socket.isClosed()) {
86 if (!conectar()) throw new IOException("No se pudo reconectar al servidor");
87 }
88 escritor.println(mensaje);
89 return lector.readLine();
90
91 } catch (IOException e) {
92 if (intento == 0) {
93 System.out.println("[Cliente] Conexión perdida, reconectando...");
94 cerrar();
95 } else {
96 throw e;
97 }
98 }
99 }
100 throw new IOException("Error irrecuperable");
101 }
102
103 public void cerrar() {
104 try { if (socket != null) socket.close(); } catch (IOException ignored) { }
105 socket = null;
106 lector = null;
107 escritor = null;
108 }
109
110 public static void main(String[] args) throws IOException {
111 ClienteConReconexion cliente = new ClienteConReconexion();
112
113 // Conectar al servidor (con reintentos si no está disponible)
114 if (!cliente.conectar()) {
115 System.err.println("No se pudo conectar al servicio");
116 return;
117 }
118
119 // Usar el cliente normalmente; se reconecta solo si la conexión cae
120 String[] comandos = { "SUMA 10 5", "MULT 3 7", "FIN" };
121 for (String cmd : comandos) {
122 String respuesta = cliente.enviar(cmd);
123 System.out.printf(" %s → %s%n", cmd, respuesta);
124 }
125
126 cliente.cerrar();
127 }
128}Monitorización del Servicio
Un servicio no monitorizado es un servicio invisible: no sabes si funciona, si se degrada o cuándo va a fallar. La monitorización en producción se basa en tres pilares: logs (qué ha pasado), métricas (cómo funciona numéricamente) y trazas (cómo fluye una petición a través del sistema).
1import java.io.*;
2import java.time.*;
3import java.time.format.*;
4import java.util.logging.*;
5
6/**
7 * Configuración de logging estructurado para un servicio en red.
8 * En producción se usa SLF4J + Logback o Log4j2.
9 * Aquí usamos java.util.logging (JUL) del JDK sin dependencias externas.
10 *
11 * Niveles de log (de menor a mayor gravedad):
12 * FINEST / FINER / FINE → DEBUG detallado (solo desarrollo)
13 * INFO → Eventos normales del ciclo de vida
14 * WARNING → Situaciones anómalas pero recuperables
15 * SEVERE → Errores críticos que requieren atención
16 */
17public class LoggingServicio {
18
19 // Obtener el logger con el nombre de la clase
20 // SLF4J equivalente: private static final Logger log = LoggerFactory.getLogger(LoggingServicio.class);
21 private static final Logger logger = Logger.getLogger(LoggingServicio.class.getName());
22
23 public static void configurarLogging() {
24 // Formato de log: timestamp [nivel] mensaje
25 System.setProperty("java.util.logging.SimpleFormatter.format",
26 "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL [%4$s] %5$s%6$s%n");
27
28 // Nivel mínimo de log (en producción: INFO; en desarrollo: FINE)
29 Logger raiz = Logger.getLogger("");
30 raiz.setLevel(Level.INFO);
31 raiz.getHandlers()[0].setLevel(Level.INFO);
32
33 // Fichero de log rotativo: logs/servicio.log.0, .1, .2...
34 try {
35 FileHandler fileHandler = new FileHandler(
36 "logs/servicio-%g.log", // %g = número de rotación
37 10 * 1024 * 1024, // 10 MB por fichero
38 5, // máximo 5 ficheros de rotación
39 true // append = true
40 );
41 fileHandler.setFormatter(new SimpleFormatter());
42 raiz.addHandler(fileHandler);
43 } catch (IOException e) {
44 logger.warning("No se pudo configurar el log a fichero: " + e.getMessage());
45 }
46 }
47
48 /**
49 * Ejemplo de logging estructurado en los distintos puntos del ciclo de vida.
50 */
51 public static void ejemploEventosLog(String clienteIp, int puerto) {
52 // Inicio del servicio
53 logger.info(String.format("Servicio iniciado en :%d", puerto));
54
55 // Nueva conexión (INFO: evento normal)
56 logger.info(String.format("Nueva conexión desde %s", clienteIp));
57
58 // Petición recibida (FINE: debug, solo visible en modo verbose)
59 logger.fine(String.format("Petición recibida: SUMA 10 5 desde %s", clienteIp));
60
61 // Situación anómala recuperable (WARNING)
62 logger.warning(String.format("Timeout de lectura para cliente %s — conexión cerrada", clienteIp));
63
64 // Error crítico (SEVERE)
65 logger.severe("Error al abrir ServerSocket: puerto 8080 ya en uso");
66
67 // Log con excepción (incluye el stack trace)
68 try {
69 throw new IOException("Error simulado para ejemplo");
70 } catch (IOException e) {
71 logger.log(Level.SEVERE, "Error de I/O inesperado al procesar petición", e);
72 }
73 }
74
75 /**
76 * En producción, usar SLF4J con Logback o Log4j2:
77 *
78 * pom.xml:
79 * <dependency>
80 * <groupId>org.slf4j</groupId>
81 * <artifactId>slf4j-api</artifactId>
82 * <version>2.0.9</version>
83 * </dependency>
84 * <dependency>
85 * <groupId>ch.qos.logback</groupId>
86 * <artifactId>logback-classic</artifactId>
87 * <version>1.4.14</version>
88 * </dependency>
89 *
90 * Uso:
91 * import org.slf4j.Logger;
92 * import org.slf4j.LoggerFactory;
93 * private static final Logger log = LoggerFactory.getLogger(MiServicio.class);
94 *
95 * log.info("Nueva conexión desde {}", clienteIp); // {} es el placeholder de SLF4J
96 * log.warn("Timeout para cliente {}, intentos: {}", clienteIp, reintentos);
97 * log.error("Error crítico al procesar petición", excepcion);
98 */
99}SLF4J + Logback: el estándar de facto para logging en Java
System.out.println() para logging. El estándar es SLF4J como API (lo que usas en el código) con Logback como implementación (lo que configuras en producción). Esta separación permite cambiar el sistema de logging sin tocar el código de la aplicación. Spring Boot lo incluye automáticamente.4.7 Documentación y Mantenimiento
El código sin documentar es código que solo tú puedes mantener hoy, y que mañana nadie —incluido tú mismo— entenderá. En el contexto de los servicios en red, la documentación es doblemente crítica: el protocolo que implementa tu servicio es un contrato con todos sus clientes, y cualquier cambio no documentado romperá esos clientes. Documentar bien un servicio Java involucra varios niveles, desde el Javadoc del código hasta la especificación del protocolo.
Documentación Técnica con Javadoc
Javadoc es el sistema estándar de documentación en Java. Los comentarios /** ... */ sobre clases, métodos y campos se procesan automáticamente para generar documentación HTML navegable. La regla de oro es documentar el contrato (qué hace el método, qué parámetros espera, qué devuelve, qué excepciones puede lanzar), no la implementación (cómo lo hace, que se lee del propio código).
1import java.io.*;
2import java.net.*;
3import java.util.concurrent.*;
4
5/**
6 * Servidor TCP de calculadora con soporte para múltiples clientes simultáneos.
7 *
8 * <h2>Protocolo de comunicación (versión 1.2)</h2>
9 * <p>El protocolo está basado en texto ASCII, una instrucción por línea,
10 * terminada con {@code
11}.</p>
12 *
13 * <h3>Peticiones del cliente</h3>
14 * <pre>
15 * OPERACION operando1 operando2
16
17 * </pre>
18 * Donde OPERACION es uno de: {@code SUMA}, {@code RESTA}, {@code MULT}, {@code DIV}, {@code MOD}.
19 * Los operandos deben ser números decimales válidos.
20 *
21 * <h3>Respuestas del servidor</h3>
22 * <pre>
23 * OK resultado
24 → operación realizada con éxito
25 * ERROR descripcion
26 → error en la operación
27 * ADIOS
28 → confirma el cierre de la conexión
29 * </pre>
30 *
31 * <h3>Ejemplo de sesión</h3>
32 * <pre>
33 * C: SUMA 10 5
34 * S: OK 15.0
35 * C: DIV 8 0
36 * S: ERROR Division por cero
37 * C: FIN
38 * S: ADIOS
39 * </pre>
40 *
41 * <h2>Configuración</h2>
42 * <ul>
43 * <li>Puerto por defecto: {@value #PUERTO_DEFAULT}</li>
44 * <li>Hilos máximos: {@value #MAX_HILOS}</li>
45 * <li>Timeout de sesión: {@value #TIMEOUT_MS} ms</li>
46 * </ul>
47 *
48 * @author Equipo de Desarrollo DAM
49 * @version 1.2
50 * @since 2024-01-15
51 * @see ClienteTCP Para implementaciones de cliente de referencia
52 */
53public class ServidorDocumentado {
54
55 /** Puerto TCP en el que el servidor escucha por defecto. */
56 public static final int PUERTO_DEFAULT = 9000;
57
58 /** Número máximo de hilos simultáneos para atender clientes. */
59 private static final int MAX_HILOS = 50;
60
61 /**
62 * Tiempo máximo de inactividad de un cliente antes de cerrar la sesión,
63 * expresado en milisegundos.
64 */
65 private static final int TIMEOUT_MS = 30_000;
66
67 private final int puerto;
68 private final ExecutorService pool;
69 private ServerSocket serverSocket;
70 private volatile boolean activo = false;
71
72 /**
73 * Crea un nuevo servidor en el puerto por defecto ({@value #PUERTO_DEFAULT}).
74 */
75 public ServidorDocumentado() {
76 this(PUERTO_DEFAULT);
77 }
78
79 /**
80 * Crea un nuevo servidor en el puerto especificado.
81 *
82 * @param puerto El número de puerto TCP (1024–65535 para usuarios no root)
83 * @throws IllegalArgumentException Si el puerto está fuera del rango válido
84 */
85 public ServidorDocumentado(int puerto) {
86 if (puerto < 1 || puerto > 65535) {
87 throw new IllegalArgumentException("Puerto inválido: " + puerto
88 + " (debe estar entre 1 y 65535)");
89 }
90 this.puerto = puerto;
91 this.pool = Executors.newFixedThreadPool(MAX_HILOS);
92 }
93
94 /**
95 * Inicia el servidor y comienza a aceptar conexiones.
96 * Este método bloquea el hilo llamante hasta que se llame a {@link #detener()}.
97 *
98 * @throws IOException Si no se puede abrir el puerto (p. ej. ya está en uso)
99 */
100 public void iniciar() throws IOException {
101 serverSocket = new ServerSocket(puerto);
102 activo = true;
103 System.out.printf("Servidor iniciado en puerto %d%n", puerto);
104
105 while (activo) {
106 try {
107 Socket cliente = serverSocket.accept();
108 pool.submit(new GestorCliente(cliente, TIMEOUT_MS));
109 } catch (SocketException e) {
110 if (activo) throw e; // Error real; si !activo, es el cierre controlado
111 }
112 }
113 }
114
115 /**
116 * Detiene el servidor de forma ordenada.
117 * Las conexiones activas se completan antes del cierre; no se aceptan nuevas.
118 * El método retorna cuando todos los hilos han terminado o tras un timeout de 10 segundos.
119 */
120 public void detener() {
121 activo = false;
122 try {
123 if (serverSocket != null) serverSocket.close();
124 } catch (IOException ignored) { }
125
126 pool.shutdown();
127 try {
128 pool.awaitTermination(10, TimeUnit.SECONDS);
129 } catch (InterruptedException e) {
130 pool.shutdownNow();
131 Thread.currentThread().interrupt();
132 }
133 System.out.println("Servidor detenido.");
134 }
135
136 /**
137 * Devuelve el puerto en el que el servidor está escuchando.
138 *
139 * @return El número de puerto TCP
140 */
141 public int getPuerto() { return puerto; }
142
143 /**
144 * Indica si el servidor está activo y aceptando conexiones.
145 *
146 * @return {@code true} si está activo, {@code false} en caso contrario
147 */
148 public boolean isActivo() { return activo; }
149
150 // GestorCliente sería una clase interna con su propio Javadoc...
151 private record GestorCliente(Socket socket, int timeout) implements Runnable {
152 @Override
153 public void run() {
154 // implementación...
155 }
156 }
157}Buenas Prácticas en Servicios Java en Red
Diseña para el fallo desde el primer día
Añade timeouts a TODAS las operaciones de I/O. Nunca asumas que el cliente responderá o que la red funcionará. Usa try-with-resources para todos los sockets y streams.
Define el protocolo por escrito antes de codificar
Documenta el protocolo: qué comandos acepta, qué respuestas envía, cuál es el formato exacto, qué errores puede retornar. Este documento es el contrato entre servidor y clientes.
Valida siempre la entrada del cliente
Nunca confíes en los datos que llegan por la red. Valida tipos, rangos, longitudes y codificación. Un cliente malicioso podría enviar datos diseñados para crashear tu servidor o explotar una vulnerabilidad.
Instrumenta desde el inicio (logs + métricas)
Sin observabilidad, operar un servicio en producción es ceguera total. Añade logging estructurado (SLF4J), métricas de rendimiento (Micrometer) y un endpoint de health check desde el primer día.
Versionado del protocolo
Incluye un número de versión en tu protocolo desde el inicio. Cuando necesites hacer cambios incompatibles, los clientes podrán negociar la versión y coexistirán la versión antigua y la nueva durante la migración.
Tests de integración automatizados
Cada caso de uso del protocolo debe tener su test automatizado: caso feliz, errores esperados, timeout, cierre abrupto del cliente. Estos tests se ejecutan en el CI/CD y previenen regresiones.
Escalabilidad — Del Servidor Simple al Clúster
El servidor que has programado en este tema escala verticalmente (más CPU, más RAM, más hilos) hasta cierto punto. Cuando un solo servidor no basta, la siguiente evolución es el escalado horizontal: múltiples instancias del mismo servicio detrás de un balanceador de carga. Java, con frameworks como Spring Boot y la JVM moderna, está especialmente bien preparado para este modelo de despliegue.
| Estrategia | Cómo escala | Herramienta Java | Cuándo usarla |
|---|---|---|---|
| Thread pool grande | Más hilos por proceso | ThreadPoolExecutor, Virtual Threads (Java 21) | Hasta ~10K conexiones simultáneas |
| NIO no bloqueante | Un hilo gestiona N conexiones | java.nio, Netty, Vert.x | Servicios de alto rendimiento I/O-bound |
| Escalado horizontal | N instancias + balanceador | Spring Boot + Kubernetes + HAProxy/Nginx | Alta disponibilidad y tráfico variable |
| Caché | Reduce carga en el servidor | Redis (Jedis/Lettuce), Caffeine (in-process) | Datos que se leen mucho y cambian poco |
| Asincronía/colas | Desacopla peticiones lentas | Kafka, RabbitMQ, Spring AMQP | Procesamiento pesado, picos de carga |
| GraalVM Native Image | Arranque instantáneo, menos RAM | Quarkus, Micronaut, Spring Native | Microservicios en contenedores pequeños |
1import java.io.*;
2import java.net.*;
3import java.nio.*;
4import java.nio.channels.*;
5import java.util.*;
6
7/**
8 * Servidor NIO no bloqueante — alternativa de alto rendimiento al servidor con hilos.
9 * Con NIO, un único hilo puede gestionar miles de conexiones simultáneas usando el
10 * patrón Selector/Channel, evitando el overhead de crear/destruir hilos por conexión.
11 *
12 * Arquitectura:
13 * - ServerSocketChannel: acepta conexiones (no bloqueante)
14 * - Selector: monitoriza múltiples canales simultáneamente (epoll/kqueue en SO)
15 * - SelectionKey: asocia un canal con las operaciones que queremos monitorizar
16 *
17 * Nota: para producción, frameworks como Netty abstraen esta complejidad.
18 * Este ejemplo muestra los fundamentos para entender cómo funcionan por dentro.
19 */
20public class ServidorNIO {
21
22 public static void main(String[] args) throws IOException {
23
24 // Selector: el multiplexor que monitoriza todos los canales
25 Selector selector = Selector.open();
26
27 // ServerSocketChannel: el canal de escucha (equivale a ServerSocket)
28 ServerSocketChannel serverChannel = ServerSocketChannel.open();
29 serverChannel.bind(new InetSocketAddress(8080));
30 serverChannel.configureBlocking(false); // ← MODO NO BLOQUEANTE: clave de NIO
31
32 // Registrar el canal en el selector para el evento OP_ACCEPT
33 serverChannel.register(selector, SelectionKey.OP_ACCEPT);
34
35 System.out.println("[NIO] Servidor iniciado en puerto 8080 (modo no bloqueante)");
36
37 while (true) {
38 // select() bloquea hasta que al menos un canal tenga un evento listo
39 // El OS notifica a Java (epoll en Linux, kqueue en macOS, IOCP en Windows)
40 int canalesListos = selector.select(1000); // timeout de 1 segundo
41
42 if (canalesListos == 0) continue;
43
44 Set<SelectionKey> keys = selector.selectedKeys();
45 Iterator<SelectionKey> it = keys.iterator();
46
47 while (it.hasNext()) {
48 SelectionKey key = it.next();
49 it.remove(); // siempre eliminar la key procesada del conjunto
50
51 if (!key.isValid()) continue;
52
53 if (key.isAcceptable()) {
54 // Nueva conexión entrante
55 ServerSocketChannel server = (ServerSocketChannel) key.channel();
56 SocketChannel cliente = server.accept();
57 if (cliente != null) {
58 cliente.configureBlocking(false);
59 // Registrar para leer cuando el cliente envíe datos
60 cliente.register(selector, SelectionKey.OP_READ,
61 ByteBuffer.allocate(1024)); // attach: buffer de lectura
62 System.out.println("[NIO] Nueva conexión: "
63 + cliente.getRemoteAddress());
64 }
65
66 } else if (key.isReadable()) {
67 // Datos disponibles para leer sin bloquear
68 SocketChannel cliente = (SocketChannel) key.channel();
69 ByteBuffer buffer = (ByteBuffer) key.attachment();
70 buffer.clear();
71
72 int bytesLeidos = cliente.read(buffer);
73 if (bytesLeidos == -1) {
74 // Cliente cerró la conexión
75 System.out.println("[NIO] Cliente desconectado: "
76 + cliente.getRemoteAddress());
77 key.cancel();
78 cliente.close();
79 } else {
80 buffer.flip();
81 String mensaje = new String(buffer.array(), 0, bytesLeidos).trim();
82 System.out.println("[NIO] Recibido: " + mensaje);
83
84 // Enviar respuesta (eco) de vuelta al cliente
85 ByteBuffer respuesta = ByteBuffer.wrap(
86 ("ECO: " + mensaje + "
87").getBytes());
88 cliente.write(respuesta);
89 }
90 }
91 }
92 }
93 }
94}¿Cuándo usar NIO vs Threads?
Checklist Final: Servicio Java en Red Listo para Producción
Vista previa — checklist de producción
1/**
2 * Cierre ordenado del servidor mediante un Shutdown Hook.
3 * La JVM ejecuta el hook cuando recibe SIGTERM (kill normal, Ctrl+C, systemctl stop).
4 * Permite completar conexiones activas, hacer flush de los logs y liberar recursos.
5 */
6public class AppPrincipal {
7
8 public static void main(String[] args) throws IOException {
9 ServidorDocumentado servidor = new ServidorDocumentado(8080);
10 HealthCheckServicio healthCheck = new HealthCheckServicio(8081);
11
12 // Registrar el hook de cierre ANTES de iniciar el servidor
13 // La JVM llama a este hook cuando detecta una señal de cierre (SIGTERM, SIGINT)
14 Runtime.getRuntime().addShutdownHook(new Thread(() -> {
15 System.out.println("[App] Señal de cierre recibida. Iniciando shutdown limpio...");
16
17 try {
18 // 1. Dejar de aceptar nuevas conexiones
19 servidor.detener();
20
21 // 2. Dar tiempo a las conexiones activas para terminar (máx. 30 segundos)
22 Thread.sleep(2000);
23
24 // 3. Registrar el cierre en los logs y hacer flush
25 System.out.println("[App] Shutdown completado. Hasta luego.");
26
27 } catch (InterruptedException e) {
28 Thread.currentThread().interrupt();
29 }
30 }, "shutdown-hook"));
31
32 // Iniciar servicios
33 healthCheck.iniciar();
34 servidor.iniciar(); // bloquea hasta detener()
35 }
36}