Apuntes DAM
Tema 4 de 6PSP · 2º DAM

Programación de Servicios y Procesos

Desarrollo de Servicios en Red

Implementación de servicios en red utilizando tecnologías Java.

45 min lecturaAvanzadoRevisado el

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)

Capa Aplicación
HTTP/HTTPSFTPSMTP/POP3/IMAPDNSSSHWebSocket
Capa Transporte
TCPUDP
Capa Red
IP (IPv4/IPv6)ICMP
Capa Enlace
EthernetWi-Fi (802.11)

Java trabaja principalmente en la capa de Aplicación y Transporte a través de java.net y java.net.http.

ProtocoloPuerto por defectoTransporteUso típico en Java
HTTP/HTTPS80 / 443TCPAPIs REST, servicios web, HttpClient (JDK 11+)
FTP21 (control)TCPTransferencia de ficheros, Apache Commons Net
SMTP25 / 587 / 465TCPEnvío de emails, Jakarta Mail (ex JavaMail)
POP3 / IMAP110 / 993TCPRecepción de emails, Jakarta Mail
DNS53UDP/TCPResolución de nombres, InetAddress.getByName()
SSH / SFTP22TCPAcceso seguro remoto, JSch / Apache SSHD
WebSocket80 / 443TCPComunicación bidireccional, Tyrus / Jakarta WebSocket
LDAP389 / 636TCPDirectorios 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.

HttpClientEjemplo.java
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

Java 11 introdujo el nuevo 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.
HTTP Crash Course & Exploration — Traversy Media


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)

⬛DETENIDO← estado actual

El proceso no existe. El puerto no está ocupado.

ESCUCHANDO
ATENDIENDO

Haz clic en cada estado para explorar el ciclo de vida

Diferencias entre Aplicación y Servicio

CaracterísticaAplicación convencionalServicio en red
Ciclo de vidaArranca, ejecuta una tarea, terminaArranca y permanece activo indefinidamente
InteracciónCon un único usuario a la vez (CLI/GUI)Con múltiples clientes simultáneos
InterfazPantalla, teclado, archivosPuerto TCP/UDP, protocolo de red
InicioManual por el usuarioAutomático al arrancar el sistema (systemd, Windows Services)
Contexto de ejecuciónSesión del usuario activoEn segundo plano, sin sesión de usuario
LoggingPuede escribir en consolaEscribe en ficheros de log / sistema de logging centralizado
ErroresPuede mostrarse al usuario con diálogoDebe recuperarse automáticamente o reiniciarse
Ejemplo JavaAplicación JavaFX, utilidad de línea de comandosServidor 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

En sistemas Linux/Unix, los puertos inferiores al 1024 (llamados well-known ports) están reservados y solo pueden ser abiertos por el usuario root. Por eso en desarrollo tus servicios Java escuchan en puertos altos como 8080 o 9090, y en producción un proxy inverso (Nginx, HAProxy) recibe en el 80/443 y redirige al proceso Java en el puerto alto.


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 / InterfazPaqueteRol en el servidor
ServerSocketjava.netEscucha en un puerto y acepta conexiones entrantes
Socketjava.netRepresenta una conexión activa con un cliente concreto
InetAddressjava.netRepresenta una dirección IP (resolución DNS, localhost, etc.)
InputStream / OutputStreamjava.ioFlujos de bytes para leer/escribir datos por el socket
BufferedReader / PrintWriterjava.ioLectura/escritura de texto por líneas sobre el socket
DataInputStream / DataOutputStreamjava.ioLectura/escritura de tipos primitivos Java serializados
ObjectInputStream / ObjectOutputStreamjava.ioSerialización/deserialización de objetos Java
Executors / ThreadPoolExecutorjava.util.concurrentGestión del pool de hilos para atender clientes
HttpServercom.sun.net.httpserverServidor HTTP ligero incluido en el JDK (no para producción)
AsynchronousServerSocketChanneljava.nio.channelsServidor 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)

new ServerSocket(8080)
serverSocket.accept() ← bloquea
Socket cliente = ...
getInputStream()
getOutputStream()
Procesar petición
Enviar respuesta
socket.close()
Volver a accept() →
→ conexión →
← datos ←
→ respuesta →

CLIENTE (Java)

new Socket("host", 8080)
Conexión TCP establecida
getOutputStream()
Enviar petición
getInputStream()
Leer respuesta
socket.close()
ServidorTCPSimple.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í.

ServidorCalculadora.java
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

Por defecto, 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.
Tu Primera Comunicación Cliente-Servidor en Java con Sockets TCP — DiscoDurodeRoer


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

○ Inactivo

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.

ServidorConcurrente.java
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.

RecursoRiesgoEstrategia de gestiónClase Java
HilosOOM si se crean sin límiteThreadPoolExecutor con maxPoolSizeExecutors, ThreadPoolExecutor
SocketsAgotamiento de file descriptorstry-with-resources, setSoTimeout()Socket, ServerSocket
MemoriaHeap lleno, GC pause stormsLimitar backlog, leer en streamingByteBuffer (NIO), InputStream
Conexiones BDPool agotadoHikariCP con maxPoolSizeDataSource (JDBC)
CPUHilos bloqueados indefinidamenteTimeouts en todas las operaciones I/OFuture.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).

GestorConexiones.java
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+

A partir de Java 21, el patrón thread-per-connection ya no es un compromiso. Los Virtual Threads de Project Loom son hilos ultra-ligeros gestionados por la JVM (no por el SO): puedes crear millones de ellos sin agotar recursos. La migration es sencilla: Executors.newVirtualThreadPerTaskExecutor() sustituye al pool tradicional sin cambiar el resto del código.
Java 19 Virtual Threads - JEP Café #11 — Java


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

ClienteTCP.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.

HerramientaTipoUsoEjemplo de comando
telnetCLIProbar servicios TCP de texto a manotelnet localhost 8080
nc (netcat)CLIVersión moderna de telnet, scriptableecho 'SUMA 5 3' | nc localhost 9000
curlCLIProbar servicios HTTP/HTTPScurl -X GET http://localhost:8080/api/v1/productos
PostmanGUIPruebas visuales de APIs RESTInterfaz gráfica con colecciones de peticiones
WiresharkSnifferCapturar y analizar paquetes de red a nivel byteFiltro: tcp.port == 8080
nmapScannerVerificar que los puertos están abiertos y escuchandonmap -p 8080 localhost
JUnit + TestcontainersTestTests de integración automatizados con servidor real@Test con socket cliente
WireMockMock serverSimular servicios externos HTTP en testswireMockServer.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.

ServidorCalculadoraTest.java
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

validacion-servicio.sh
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 || true

Usa 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).

HealthCheckServicio.java
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.

ClienteConReconexion.java
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).

LoggingServicio.java
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

En proyectos profesionales, nunca uses 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.
Logback vs SLF4J vs Log4J2 - what is the difference? Java Brains Brain Bytes — Java Brains


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).

ServidorDocumentado.java
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.

EstrategiaCómo escalaHerramienta JavaCuándo usarla
Thread pool grandeMás hilos por procesoThreadPoolExecutor, Virtual Threads (Java 21)Hasta ~10K conexiones simultáneas
NIO no bloqueanteUn hilo gestiona N conexionesjava.nio, Netty, Vert.xServicios de alto rendimiento I/O-bound
Escalado horizontalN instancias + balanceadorSpring Boot + Kubernetes + HAProxy/NginxAlta disponibilidad y tráfico variable
CachéReduce carga en el servidorRedis (Jedis/Lettuce), Caffeine (in-process)Datos que se leen mucho y cambian poco
Asincronía/colasDesacopla peticiones lentasKafka, RabbitMQ, Spring AMQPProcesamiento pesado, picos de carga
GraalVM Native ImageArranque instantáneo, menos RAMQuarkus, Micronaut, Spring NativeMicroservicios en contenedores pequeños
ServidorNIO.java
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?

Para la mayoría de servicios con cientos o pocos miles de conexiones simultáneas, el modelo de threads con un pool es más sencillo, más legible y suficientemente eficiente, especialmente con Virtual Threads de Java 21. Opta por NIO directo solo si necesitas manejar decenas de miles de conexiones simultáneas con latencia mínima y conoces bien su complejidad. Frameworks como Netty o Vert.x te dan el rendimiento de NIO con una API mucho más cómoda.

Checklist Final: Servicio Java en Red Listo para Producción

Vista previa — checklist de producción

✓Timeout en accept() y en todas las operaciones de I/O
✓try-with-resources para todos los sockets y streams
✓ThreadPoolExecutor con límites definidos
✓Logging estructurado (SLF4J + Logback)
✓Endpoint /health para orquestadores y balanceadores
✓Métricas expuestas (Micrometer / endpoint /metrics)
✓Shutdown hook para cierre ordenado (addShutdownHook)
✓Validación de todos los inputs del protocolo
✓Tests de integración automatizados (JUnit)
✓Javadoc completo del protocolo y API pública
✓Manejo de errores sin exponer stack traces al cliente
✓Límite de conexiones por IP (anti-DDoS básico)
ShutdownHook.java
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}
Running a XSS Attack + How to defend — Academind

¿Has encontrado un error, algo desactualizado o una explicación que no se entiende? Avísanos y lo corregimos.

¿Has terminado este tema?

Crea una cuenta gratis para guardar qué temas has terminado, subir de nivel y ganar medallas.

Guardar mi progreso