Apuntes DAM
Volver al inicio

Ejercicios resueltos de Programación de Servicios y Procesos

Los 13 ejercicios de Programación de Servicios y Procesos de la web, tema a tema: cada uno con su enunciado, los datos que necesitas (código de partida, ejemplos de entrada y salida o la base de datos) y, al final, la solución explicada.

Descargar el PDF

Programación Concurrente

1. Coordinador de procesos

Difícil · Programación Concurrente · apuntesdam.com/subject/prog-servicios/topic/concurrencia

El programa puede funcionar en dos modos. Si recibe los argumentos «trabajador desde hasta», es un proceso hijo: cuenta los primos de ese tramo y escribe el número (ya está hecho). Si no, es el coordinador: lee un límite y un número de procesos, reparte el intervalo [1, límite] en tantos tramos iguales como procesos (el último se queda con el resto) y lanza un proceso hijo por tramo con ProcessBuilder, ejecutando esta misma clase. Primero lanza todos y después lee la salida de cada uno en orden y espera a que termine. Muestra «Proceso i [desde, hasta]: n primos» y, al final, «Total: N primos hasta límite».

Código de partida (java)
import java.io.*;import java.util.*; public class Main {    static boolean esPrimo(long n) {        if (n < 2) return false;        for (long d = 2; d * d <= n; d++) if (n % d == 0) return false;        return true;    }     static long contar(long desde, long hasta) {        long c = 0;        for (long i = desde; i <= hasta; i++) if (esPrimo(i)) c++;        return c;    }     public static void main(String[] args) throws Exception {        // Modo trabajador: el proceso hijo cuenta su tramo y escribe el resultado        if (args.length == 3 && args[0].equals("trabajador")) {            System.out.println(contar(Long.parseLong(args[1]), Long.parseLong(args[2])));            return;        }        Scanner sc = new Scanner(System.in);        long limite = sc.nextLong();        int procesos = sc.nextInt();        // TODO: repartir [1, limite] en tramos, lanzar un proceso hijo por tramo,        //       leer su resultado y mostrar el resumen    }}

Ejemplo: 10 000 con 4 procesos

Entrada

10000 4

Salida esperada

Proceso 1 [1, 2500]: 367 primos
Proceso 2 [2501, 5000]: 302 primos
Proceso 3 [5001, 7500]: 281 primos
Proceso 4 [7501, 10000]: 279 primos
Total: 1229 primos hasta 10000

Ejemplo: Reparto con resto

Entrada

100 3

Salida esperada

Proceso 1 [1, 33]: 11 primos
Proceso 2 [34, 66]: 7 primos
Proceso 3 [67, 100]: 7 primos
Total: 25 primos hasta 100

2. Planificador de CPU: FCFS, SJF y Round Robin

Muy difícil · Programación Concurrente · apuntesdam.com/subject/prog-servicios/topic/concurrencia

El sistema operativo decide qué proceso usa la CPU en cada momento. La entrada es el quantum y, después, cada proceso con su identificador (una letra), su instante de llegada y su ráfaga de CPU. Implementa los tres algoritmos devolviendo la línea de tiempo: un carácter por unidad de tiempo con la letra del proceso que se ejecuta, o «-» si la CPU está libre. FCFS ejecuta los procesos enteros por orden de llegada (a igualdad, en el orden de la entrada). SJF, no expropiativo, cada vez que la CPU queda libre elige, de los que ya han llegado, el de ráfaga más corta (a igualdad, el primero de la entrada). Round Robin da turnos de como mucho quantum unidades con una cola FIFO: los procesos que llegan durante un turno entran en la cola antes que el proceso que acaba de ser expulsado. El método informe, ya hecho, calcula el tiempo de espera de cada proceso (fin − llegada − ráfaga) y la media.

Código de partida (java)
import java.util.*; public class Main {    record Proceso(String id, int llegada, int rafaga) { }     // Devuelve la línea de tiempo: una letra (id) por unidad de tiempo, o '-' si la CPU está libre    static String fcfs(List<Proceso> ps) {        // TODO: el primero que llega se ejecuta entero        return "";    }     static String sjf(List<Proceso> ps) {        // TODO: no expropiativo; de los que han llegado, el de ráfaga más corta        return "";    }     static String roundRobin(List<Proceso> ps, int quantum) {        // TODO: turnos de como mucho quantum unidades        return "";    }     // Calcula y muestra los tiempos de espera a partir de la línea de tiempo    static void informe(String nombre, String linea, List<Proceso> ps) {        StringBuilder sb = new StringBuilder(nombre + ": " + linea + " |");        double suma = 0;        for (Proceso p : ps) {            int fin = linea.lastIndexOf(p.id().charAt(0)) + 1;            int espera = fin - p.llegada() - p.rafaga();            suma += espera;            sb.append(" ").append(p.id()).append("=").append(espera);        }        sb.append(String.format(Locale.US, " | media %.2f", suma / ps.size()));        System.out.println(sb);    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int quantum = sc.nextInt();        List<Proceso> ps = new ArrayList<>();        while (sc.hasNext()) ps.add(new Proceso(sc.next(), sc.nextInt(), sc.nextInt()));        informe("FCFS", fcfs(ps), ps);        informe("SJF ", sjf(ps), ps);        informe("RR  ", roundRobin(ps, quantum), ps);    }}

Ejemplo: Ejemplo de clase

Entrada

2
A 0 5
B 1 3
C 2 1
D 3 2

Salida esperada

FCFS: AAAAABBBCDD | A=0 B=4 C=6 D=6 | media 4.00
SJF : AAAAACDDBBB | A=0 B=7 C=3 D=3 | media 3.25
RR  : AABBCAADDBA | A=6 B=6 C=2 D=4 | media 4.50

Ejemplo: CPU libre al principio

Entrada

3
A 2 4
B 3 2
C 10 1

Salida esperada

FCFS: --AAAABB--C | A=0 B=3 C=0 | media 1.00
SJF : --AAAABB--C | A=0 B=3 C=0 | media 1.00
RR  : --AAABBA--C | A=2 B=2 C=0 | media 1.33

Hilos y Multithreading en Java

3. Suma en paralelo con hilos

Difícil · Hilos y Multithreading en Java · apuntesdam.com/subject/prog-servicios/topic/hilos

La primera cifra de la entrada es el número de hilos y el resto, los números que hay que sumar. Reparte el array en tantos bloques consecutivos como hilos, lo más iguales posible: si no es divisible, los primeros bloques tienen un elemento más (con 10 números y 3 hilos: 4, 3 y 3). Cada hilo suma su bloque y guarda el resultado en su posición de parciales. El main debe esperar a que terminen todos antes de mostrar los resultados (esa parte ya está hecha). Un hilo puede quedarse sin elementos: entonces su parcial es 0.

Código de partida (java)
import java.util.*; public class Main {    public static void main(String[] args) throws InterruptedException {        Scanner sc = new Scanner(System.in);        int hilos = sc.nextInt();        List<Long> lista = new ArrayList<>();        while (sc.hasNextLong()) lista.add(sc.nextLong());        long[] datos = lista.stream().mapToLong(Long::longValue).toArray();         long[] parciales = new long[hilos];      // cada hilo escribe solo en su posición        // TODO: crear los hilos, repartir el array en bloques, arrancarlos y esperar a que terminen         long total = 0;        for (int i = 0; i < hilos; i++) {            System.out.println("Hilo " + i + ": " + parciales[i]);            total += parciales[i];        }        System.out.println("Total: " + total);    }}

Ejemplo: 3 hilos, 10 números

Entrada

3
1 2 3 4 5 6 7 8 9 10

Salida esperada

Hilo 0: 10
Hilo 1: 18
Hilo 2: 27
Total: 55

Ejemplo: Más hilos que números

Entrada

4
100 200 300

Salida esperada

Hilo 0: 100
Hilo 1: 200
Hilo 2: 300
Hilo 3: 0
Total: 600

4. Productor-consumidor con un búfer acotado

Muy difícil · Hilos y Multithreading en Java · apuntesdam.com/subject/prog-servicios/topic/hilos

Varios productores dejan números en un búfer de capacidad limitada y un consumidor los va sacando. Implementa los métodos sincronizados de BufferAcotado con wait y notifyAll: poner espera mientras el búfer está lleno y sacar espera mientras está vacío; los dos avisan a los demás hilos cuando cambian el estado. Usa un array circular (inicio y cantidad) para que sacar sea inmediato. El main, ya hecho, lanza los productores, espera a que terminen, mete un FIN para el consumidor y muestra cuántos elementos de cada productor se consumieron y la suma: si no se pierde ni se repite ningún elemento, cada productor aporta todos los suyos.

Código de partida (java)
import java.util.*; class BufferAcotado {    private final int[] datos;    private int inicio = 0, cantidad = 0;     BufferAcotado(int capacidad) {        datos = new int[capacidad];    }     // Espera mientras el búfer está lleno    public synchronized void poner(int v) throws InterruptedException {        // TODO    }     // Espera mientras el búfer está vacío    public synchronized int sacar() throws InterruptedException {        // TODO        return -1;    }} public class Main {    static final int FIN = -1;                    // «píldora venenosa»: avisa al consumidor de que no hay más     public static void main(String[] args) throws InterruptedException {        Scanner sc = new Scanner(System.in);        int capacidad = sc.nextInt(), productores = sc.nextInt(), porProductor = sc.nextInt();        BufferAcotado buffer = new BufferAcotado(capacidad);        int[] consumidosDe = new int[productores];        long[] suma = new long[1];         List<Thread> hilos = new ArrayList<>();        for (int p = 0; p < productores; p++) {            final int id = p;            hilos.add(new Thread(() -> {                try {                    // Cada valor codifica su productor: id * 1000 + número                    for (int k = 1; k <= porProductor; k++) buffer.poner(id * 1000 + k);                } catch (InterruptedException e) { Thread.currentThread().interrupt(); }            }));        }        Thread consumidor = new Thread(() -> {            try {                int v;                while ((v = buffer.sacar()) != FIN) {                    consumidosDe[v / 1000]++;                    suma[0] += v % 1000;                }            } catch (InterruptedException e) { Thread.currentThread().interrupt(); }        });        consumidor.start();        for (Thread h : hilos) h.start();        for (Thread h : hilos) h.join();        buffer.poner(FIN);        consumidor.join();         for (int p = 0; p < productores; p++) System.out.println("Productor " + p + ": " + consumidosDe[p] + " elementos consumidos");        System.out.println("Suma de los números: " + suma[0]);    }}

Ejemplo: Capacidad 2, 3 productores

Entrada

2 3 50

Salida esperada

Productor 0: 50 elementos consumidos
Productor 1: 50 elementos consumidos
Productor 2: 50 elementos consumidos
Suma de los números: 3825

Ejemplo: Capacidad 1

Entrada

1 4 25

Salida esperada

Productor 0: 25 elementos consumidos
Productor 1: 25 elementos consumidos
Productor 2: 25 elementos consumidos
Productor 3: 25 elementos consumidos
Suma de los números: 1300

Sockets en Java

5. Servidor de un protocolo de texto

Difícil · Sockets en Java · apuntesdam.com/subject/prog-servicios/topic/sockets

El main ya arranca un servidor TCP en un puerto libre y un cliente que le envía cada línea de la entrada y muestra la respuesta. Implementa atender, que gestiona la conexión de un cliente siguiendo este protocolo (una orden por línea, una respuesta por línea): «SUMA n1 n2 …» responde «OK total» (o «ERROR números no válidos»); «MAYUS texto» responde «OK TEXTO»; «INVERTIR texto» responde el texto al revés; «CONTAR» responde «OK n», con el número de órdenes atendidas antes en esta conexión (todas, incluidas las erróneas y otros CONTAR); «ADIOS» responde «OK hasta luego» y cierra. Las órdenes no distinguen mayúsculas y cualquier otra da «ERROR orden desconocida: X». Usa UTF-8 en los dos sentidos.

Código de partida (java)
import java.io.*;import java.net.*;import java.util.*; public class Main {    // Atiende a UN cliente: lee órdenes línea a línea y responde a cada una    static void atender(Socket cliente) throws IOException {        // TODO: leer cada línea, responder según el protocolo y terminar con ADIOS    }     public static void main(String[] args) throws Exception {        try (ServerSocket servidor = new ServerSocket(0)) {          // puerto libre cualquiera            Thread hiloServidor = new Thread(() -> {                try (Socket c = servidor.accept()) {                    atender(c);                } catch (IOException e) {                    System.out.println("Error del servidor: " + e.getMessage());                }            });            hiloServidor.start();             // Cliente: envía cada línea de la entrada y muestra la respuesta            Scanner sc = new Scanner(System.in);            try (Socket s = new Socket("localhost", servidor.getLocalPort());                 PrintWriter out = new PrintWriter(new OutputStreamWriter(s.getOutputStream(), "UTF-8"), true);                 BufferedReader in = new BufferedReader(new InputStreamReader(s.getInputStream(), "UTF-8"))) {                while (sc.hasNextLine()) {                    String orden = sc.nextLine();                    out.println(orden);                    String r;                    try {                        r = in.readLine();                    } catch (IOException e) {                        r = null;                                      // el servidor cerró la conexión                    }                    System.out.println("> " + orden);                    System.out.println("< " + (r == null ? "(conexión cerrada)" : r));                    if (r == null || orden.equals("ADIOS")) break;                }            }            hiloServidor.join();        }    }}

Ejemplo: Órdenes básicas

Entrada

SUMA 2 3 4
MAYUS hola mundo
INVERTIR socket
CONTAR
ADIOS

Salida esperada

> SUMA 2 3 4
< OK 9
> MAYUS hola mundo
< OK HOLA MUNDO
> INVERTIR socket
< OK tekcos
> CONTAR
< OK 3
> ADIOS
< OK hasta luego

Ejemplo: Errores

Entrada

suma 1 dos
BORRAR todo
CONTAR
adios

Salida esperada

> suma 1 dos
< ERROR números no válidos
> BORRAR todo
< ERROR orden desconocida: BORRAR
> CONTAR
< OK 2
> adios
< OK hasta luego

6. Mensajes fiables sobre UDP

Muy difícil · Sockets en Java · apuntesdam.com/subject/prog-servicios/topic/sockets

UDP no garantiza que los paquetes lleguen. El servidor de este reto lo simula: pierde uno de cada cuatro paquetes que recibe y una de cada tres respuestas. Implementa enviarFiable con el protocolo de parada y espera (stop-and-wait) que usan TCP y muchos juegos: envía el datagrama «seq:texto», espera como mucho ESPERA_MS milisegundos un datagrama «ACK seq» y, si no llega, reenvía el mismo mensaje, hasta MAX_INTENTOS veces. Devuelve el número de intentos que han hecho falta, o −1. Ignora cualquier ACK de otro número de secuencia. El servidor descarta los mensajes duplicados (cuando se pierde un ACK, el cliente reenvía algo que ya había llegado) gracias al número de secuencia, así que al final debe haber recibido todos los mensajes, en orden y una sola vez.

Código de partida (java)
import java.io.*;import java.net.*;import java.nio.charset.StandardCharsets;import java.util.*; public class Main {    static final int ESPERA_MS = 150, MAX_INTENTOS = 5;     // ---- Servidor (ya hecho): pierde paquetes a propósito para simular una red mala    static class Servidor extends Thread {        final DatagramSocket ds;        final List<String> recibidos = new ArrayList<>();        int paquetes = 0, duplicados = 0, ultimoSeq = 0;         Servidor() throws SocketException { ds = new DatagramSocket(0); }         public void run() {            byte[] buf = new byte[1024];            try {                while (true) {                    DatagramPacket p = new DatagramPacket(buf, buf.length);                    ds.receive(p);                    paquetes++;                    if (paquetes % 4 == 0) continue;                  // se pierde el paquete que llega                    String msg = new String(p.getData(), 0, p.getLength(), StandardCharsets.UTF_8);                    int seq = Integer.parseInt(msg.substring(0, msg.indexOf(':')));                    String datos = msg.substring(msg.indexOf(':') + 1);                    if (seq == ultimoSeq) duplicados++;                // ya lo teníamos: era una retransmisión                    else if (seq == ultimoSeq + 1) { ultimoSeq = seq; if (!datos.equals("FIN")) recibidos.add(datos); }                    if (paquetes % 3 == 0) continue;                  // se pierde el ACK de respuesta                    byte[] ack = ("ACK " + seq).getBytes(StandardCharsets.UTF_8);                    ds.send(new DatagramPacket(ack, ack.length, p.getSocketAddress()));                    if (datos.equals("FIN")) return;                }            } catch (IOException e) { }        }    }     // Envía un mensaje y espera su ACK; si no llega a tiempo, lo reenvía.    // Devuelve el número de intentos, o -1 si se agotan.    static int enviarFiable(DatagramSocket ds, SocketAddress destino, int seq, String texto) throws IOException {        // TODO        byte[] datos = (seq + ":" + texto).getBytes(StandardCharsets.UTF_8);        ds.send(new DatagramPacket(datos, datos.length, destino));        return 1;    }     public static void main(String[] args) throws Exception {        Servidor srv = new Servidor();        srv.start();        SocketAddress destino = new InetSocketAddress("localhost", srv.ds.getLocalPort());        Scanner sc = new Scanner(System.in);        List<String> mensajes = new ArrayList<>();        while (sc.hasNextLine()) { String l = sc.nextLine().trim(); if (!l.isEmpty()) mensajes.add(l); }        mensajes.add("FIN");        try (DatagramSocket ds = new DatagramSocket()) {            for (int i = 0; i < mensajes.size(); i++) {                int intentos = enviarFiable(ds, destino, i + 1, mensajes.get(i));                System.out.println("Mensaje " + (i + 1) + " «" + mensajes.get(i) + "»: "                        + (intentos < 0 ? "sin respuesta" : intentos + (intentos == 1 ? " intento" : " intentos")));            }        }        srv.join(2000);        srv.ds.close();        System.out.println("El servidor recibió en orden: " + String.join(" | ", srv.recibidos));        System.out.println("Retransmisiones duplicadas descartadas: " + srv.duplicados);    }}

Ejemplo: Cuatro mensajes

Entrada

hola
que
tal
estás

Salida esperada

Mensaje 1 «hola»: 1 intento
Mensaje 2 «que»: 1 intento
Mensaje 3 «tal»: 3 intentos
Mensaje 4 «estás»: 2 intentos
Mensaje 5 «FIN»: 3 intentos
El servidor recibió en orden: hola | que | tal | estás
Retransmisiones duplicadas descartadas: 3

Ejemplo: Uno solo

Entrada

ping

Salida esperada

Mensaje 1 «ping»: 1 intento
Mensaje 2 «FIN»: 1 intento
El servidor recibió en orden: ping
Retransmisiones duplicadas descartadas: 0

Desarrollo de Servicios en Red

7. Una API REST con HttpServer

Difícil · Desarrollo de Servicios en Red · apuntesdam.com/subject/prog-servicios/topic/servicios-red-java

Completa manejar para que el servidor ofrezca una API REST de tareas. GET /tareas devuelve 200 y la lista en JSON, ordenada por id: [{"id":1,"texto":"..."}]. POST /tareas con el texto como cuerpo crea una tarea con el siguiente id, responde 201 con la tarea en JSON y la cabecera Location; si el texto está vacío, 400 con {"error":"texto vacío"}. GET /tareas/{id} devuelve 200 con la tarea; DELETE /tareas/{id} la borra y responde 204 sin cuerpo. Si el id no es un número: 400 {"error":"id no válido"}; si no existe: 404 {"error":"no existe la tarea N"}. Cualquier otro método: 405 sin cuerpo. Usa los métodos responder y json, ya hechos. El main envía con HttpClient las peticiones de la entrada y muestra el código y el cuerpo.

Código de partida (java)
import com.sun.net.httpserver.*;import java.io.*;import java.net.*;import java.net.http.*;import java.nio.charset.StandardCharsets;import java.util.*;import java.util.concurrent.*;import java.util.concurrent.atomic.AtomicInteger; public class Main {    static final Map<Integer, String> tareas = new ConcurrentSkipListMap<>();   // id → texto, ordenadas por id    static final AtomicInteger siguienteId = new AtomicInteger(1);     static void responder(HttpExchange ex, int estado, String cuerpo) throws IOException {        byte[] b = cuerpo.getBytes(StandardCharsets.UTF_8);        ex.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");        ex.sendResponseHeaders(estado, b.length == 0 ? -1 : b.length);        if (b.length > 0) ex.getResponseBody().write(b);        ex.close();    }     static String json(int id, String texto) {        return "{\"id\":" + id + ",\"texto\":\"" + texto.replace("\\", "\\\\").replace("\"", "\\\"") + "\"}";    }     // Atiende /tareas y /tareas/{id}    static void manejar(HttpExchange ex) throws IOException {        // TODO: GET /tareas, POST /tareas, GET /tareas/{id}, DELETE /tareas/{id} y los errores        responder(ex, 501, "");    }     public static void main(String[] args) throws Exception {        HttpServer srv = HttpServer.create(new InetSocketAddress("localhost", 0), 0);        srv.createContext("/tareas", Main::manejar);        srv.start();        String base = "http://localhost:" + srv.getAddress().getPort();         HttpClient cliente = HttpClient.newHttpClient();        Scanner sc = new Scanner(System.in);        while (sc.hasNextLine()) {            String[] t = sc.nextLine().trim().split(" ", 3);       // MÉTODO ruta [cuerpo]            HttpRequest.BodyPublisher cuerpo = t.length > 2                    ? HttpRequest.BodyPublishers.ofString(t[2]) : HttpRequest.BodyPublishers.noBody();            HttpRequest req = HttpRequest.newBuilder(URI.create(base + t[1])).method(t[0], cuerpo).build();            HttpResponse<String> r = cliente.send(req, HttpResponse.BodyHandlers.ofString());            System.out.println(t[0] + " " + t[1] + " → " + r.statusCode() + (r.body().isEmpty() ? "" : " " + r.body()));        }        srv.stop(0);    }}

Ejemplo: Crear, listar y borrar

Entrada

POST /tareas Comprar pan
POST /tareas Estudiar PSP
GET /tareas
DELETE /tareas/1
GET /tareas

Salida esperada

POST /tareas → 201 {"id":1,"texto":"Comprar pan"}
POST /tareas → 201 {"id":2,"texto":"Estudiar PSP"}
GET /tareas → 200 [{"id":1,"texto":"Comprar pan"},{"id":2,"texto":"Estudiar PSP"}]
DELETE /tareas/1 → 204
GET /tareas → 200 [{"id":2,"texto":"Estudiar PSP"}]

Ejemplo: Errores

Entrada

GET /tareas/7
GET /tareas/abc
POST /tareas
PUT /tareas
DELETE /tareas/1

Salida esperada

GET /tareas/7 → 404 {"error":"no existe la tarea 7"}
GET /tareas/abc → 400 {"error":"id no válido"}
POST /tareas → 400 {"error":"texto vacío"}
PUT /tareas → 405
DELETE /tareas/1 → 404 {"error":"no existe la tarea 1"}

8. Disyuntor (circuit breaker) para un servicio inestable

Muy difícil · Desarrollo de Servicios en Red · apuntesdam.com/subject/prog-servicios/topic/servicios-red-java

Cuando un servicio remoto se cae, seguir llamándolo empeora las cosas: cada petición espera un tiempo de espera completo y sobrecarga al servicio cuando intenta recuperarse. El patrón disyuntor lo evita con tres estados. CERRADO: las llamadas pasan; tras umbralFallos fallos seguidos, pasa a ABIERTO y el contador de fallos vuelve a 0. ABIERTO: se rechazan las llamadas sin hacerlas durante espera instantes desde que se abrió. Pasado ese tiempo, la siguiente llamada se permite como prueba y el estado pasa a SEMIABIERTO. SEMIABIERTO: si la prueba sale bien, vuelve a CERRADO (y se reinician los fallos); si falla, vuelve a ABIERTO desde ese instante (aunque sea un solo fallo). Un éxito en CERRADO también reinicia el contador de fallos. Implementa permitir y registrar. La entrada es el umbral, la espera y una cadena con el resultado que daría el servicio en cada instante (E o F); el main simula el tiempo.

Código de partida (java)
import java.util.*; class CircuitBreaker {    enum Estado { CERRADO, ABIERTO, SEMIABIERTO }     private final int umbralFallos;       // fallos seguidos para abrir    private final int espera;             // instantes que permanece abierto    private Estado estado = Estado.CERRADO;    private int fallosSeguidos = 0;    private int abiertoDesde = 0;     CircuitBreaker(int umbralFallos, int espera) {        this.umbralFallos = umbralFallos;        this.espera = espera;    }     Estado estado() { return estado; }     // ¿Se puede llamar al servicio en el instante t? (puede pasar de ABIERTO a SEMIABIERTO)    boolean permitir(int t) {        // TODO        return true;    }     // Se informa del resultado de una llamada que se permitió    void registrar(boolean exito, int t) {        // TODO    }} public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        CircuitBreaker cb = new CircuitBreaker(sc.nextInt(), sc.nextInt());        String resultados = sc.next();               // una letra por instante: E = el servicio responde, F = falla        int llamadas = 0, rechazadas = 0;        for (int t = 0; t < resultados.length(); t++) {            String linea;            if (cb.permitir(t)) {                llamadas++;                boolean ok = resultados.charAt(t) == 'E';                cb.registrar(ok, t);                linea = (ok ? "llamada OK " : "llamada FALLA");            } else {                rechazadas++;                linea = "rechazada   ";            }            System.out.println("t=" + t + " " + linea + " → " + cb.estado());        }        System.out.println("Llamadas al servicio: " + llamadas + ", rechazadas sin llamar: " + rechazadas);    }}

Ejemplo: Caída y recuperación

Entrada

3 4
EEFFFEEEEEFEEE

Salida esperada

t=0 llamada OK  → CERRADO
t=1 llamada OK  → CERRADO
t=2 llamada FALLA → CERRADO
t=3 llamada FALLA → CERRADO
t=4 llamada FALLA → ABIERTO
t=5 rechazada    → ABIERTO
t=6 rechazada    → ABIERTO
t=7 rechazada    → ABIERTO
t=8 llamada OK  → CERRADO
t=9 llamada OK  → CERRADO
t=10 llamada FALLA → CERRADO
t=11 llamada OK  → CERRADO
t=12 llamada OK  → CERRADO
t=13 llamada OK  → CERRADO
Llamadas al servicio: 11, rechazadas sin llamar: 3

Ejemplo: La prueba falla

Entrada

3 2
FFFEFEFEEE

Salida esperada

t=0 llamada FALLA → CERRADO
t=1 llamada FALLA → CERRADO
t=2 llamada FALLA → ABIERTO
t=3 rechazada    → ABIERTO
t=4 llamada FALLA → ABIERTO
t=5 rechazada    → ABIERTO
t=6 llamada FALLA → ABIERTO
t=7 rechazada    → ABIERTO
t=8 llamada OK  → CERRADO
t=9 llamada OK  → CERRADO
Llamadas al servicio: 7, rechazadas sin llamar: 3

Programación Segura en Java

9. Guardar contraseñas con PBKDF2

Difícil · Programación Segura en Java · apuntesdam.com/subject/prog-servicios/topic/programacion-segura

Nunca se guarda una contraseña, ni cifrada: se guarda un hash lento con sal. Implementa guardar, que genera una sal aleatoria de 16 bytes con SecureRandom, deriva el hash con el método derivar (PBKDF2-HMAC-SHA256, 32 bytes) usando ITERACIONES y devuelve el texto «pbkdf2$iteraciones$sal$hash», con la sal y el hash en Base64. Implementa también verificar, que separa las cuatro partes (devuelve false si el formato no es correcto), vuelve a derivar el hash de la contraseña introducida con la misma sal y las mismas iteraciones guardadas y lo compara en tiempo constante con MessageDigest.isEqual. El main comprueba el formato, que dos guardados de la misma contraseña son distintos y la verificación; la orden VECTOR calcula un hash con una sal fija para comprobar que derivar se usa bien.

Código de partida (java)
import java.security.*;import java.security.spec.*;import java.util.*;import javax.crypto.*;import javax.crypto.spec.*; public class Main {    static final int ITERACIONES = 120_000;     // Deriva 32 bytes de la contraseña con PBKDF2-HMAC-SHA256    static byte[] derivar(String clave, byte[] sal, int iteraciones) throws GeneralSecurityException {        PBEKeySpec spec = new PBEKeySpec(clave.toCharArray(), sal, iteraciones, 256);        return SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(spec).getEncoded();    }     // Devuelve "pbkdf2$iteraciones$salBase64$hashBase64" con una sal aleatoria de 16 bytes    static String guardar(String clave) throws GeneralSecurityException {        // TODO        return clave;    }     // Comprueba una contraseña contra lo guardado, en tiempo constante    static boolean verificar(String clave, String guardado) throws GeneralSecurityException {        // TODO        return guardado.equals(clave);    }     public static void main(String[] args) throws Exception {        Scanner sc = new Scanner(System.in);        while (sc.hasNextLine()) {            String[] t = sc.nextLine().split(" ", 3);            switch (t[0]) {                case "VECTOR" -> {          // VECTOR salHex clave: resultado reproducible con 1000 iteraciones                    byte[] sal = HexFormat.of().parseHex(t[1]);                    System.out.println("Hash: " + HexFormat.of().formatHex(derivar(t[2], sal, 1000)));                }                case "PROBAR" -> {          // PROBAR claveGuardada claveIntroducida                    String g1 = guardar(t[1]), g2 = guardar(t[1]);                    String[] p = g1.split("\\
quot;);
System.out.println("Formato: " + p.length + " partes, algoritmo " + p[0] + ", " + (p.length > 1 ? p[1] : "?") + " iteraciones, sal de " + Base64.getDecoder().decode(p.length > 2 ? p[2] : "").length + " bytes, hash de " + Base64.getDecoder().decode(p.length > 3 ? p[3] : "").length + " bytes"); System.out.println("Dos guardados de la misma clave son distintos: " + !g1.equals(g2)); System.out.println("La clave guardada no aparece en el texto: " + !g1.contains(t[1])); System.out.println("Verificar «" + t[2] + "»: " + verificar(t[2], g1)); } } } }}

Ejemplo: Vector fijo

Entrada

VECTOR 000102030405060708090a0b0c0d0e0f patata

Salida esperada

Hash: e5816b525c317a12ddd9f411a9304dc0e131d02a179b24a02834f989d4613578

Ejemplo: Clave correcta

Entrada

PROBAR Clave.Segura-2025 Clave.Segura-2025

Salida esperada

Formato: 4 partes, algoritmo pbkdf2, 120000 iteraciones, sal de 16 bytes, hash de 32 bytes
Dos guardados de la misma clave son distintos: true
La clave guardada no aparece en el texto: true
Verificar «Clave.Segura-2025»: true

10. Tokens firmados con HMAC (como un JWT)

Muy difícil · Programación Segura en Java · apuntesdam.com/subject/prog-servicios/topic/programacion-segura

Tras iniciar sesión, muchas APIs entregan un token que el cliente envía en cada petición. Para que no se pueda falsificar, va firmado. Implementa crear: el token son tres partes separadas por puntos, cada una en Base64URL sin relleno: la cabecera {"alg":"HS256","typ":"JWT"}, los datos {"sub":"usuario","rol":"rol","exp":instante} (exactamente con ese formato, sin espacios) y la firma HMAC-SHA256 de «cabecera.datos» con la clave del servidor (usa el método hmac). Implementa verificar: si no hay tres partes o no se pueden decodificar, «INVÁLIDO: formato»; si la firma no coincide (compárala en tiempo constante), «INVÁLIDO: firma»; si ahora ≥ exp, «INVÁLIDO: caducado»; si todo está bien, «VÁLIDO usuario rol». Comprueba la firma antes de mirar los datos. El método campo, ya hecho, lee un valor del JSON de los datos.

Código de partida (java)
import java.nio.charset.StandardCharsets;import java.security.*;import java.util.*;import javax.crypto.*;import javax.crypto.spec.*; public class Main {    static final Base64.Encoder B64 = Base64.getUrlEncoder().withoutPadding();    static final Base64.Decoder D64 = Base64.getUrlDecoder();     static byte[] hmac(byte[] clave, String datos) throws GeneralSecurityException {        Mac mac = Mac.getInstance("HmacSHA256");        mac.init(new SecretKeySpec(clave, "HmacSHA256"));        return mac.doFinal(datos.getBytes(StandardCharsets.UTF_8));    }     // Token: base64url(cabecera) + "." + base64url(datos) + "." + base64url(HMAC de las dos primeras partes)    // La cabecera es {"alg":"HS256","typ":"JWT"} y los datos {"sub":"usuario","rol":"rol","exp":instante}    static String crear(byte[] clave, String usuario, String rol, long exp) throws GeneralSecurityException {        // TODO        return "";    }     // Devuelve "VÁLIDO usuario rol", o "INVÁLIDO: motivo" (formato, firma o caducado)    static String verificar(byte[] clave, String token, long ahora) throws GeneralSecurityException {        // TODO        return "VÁLIDO";    }     // Extrae un campo de un JSON plano y sencillo como el de los datos    static String campo(String json, String nombre) {        int i = json.indexOf("\"" + nombre + "\":");        if (i < 0) return null;        int ini = i + nombre.length() + 3;        if (json.charAt(ini) == '"') return json.substring(ini + 1, json.indexOf('"', ini + 1));        int fin = ini;        while (fin < json.length() && (Character.isDigit(json.charAt(fin)) || json.charAt(fin) == '-')) fin++;        return json.substring(ini, fin);    }     public static void main(String[] args) throws Exception {        byte[] clave = "clave-secreta-del-servidor-de-32b".getBytes(StandardCharsets.UTF_8);        byte[] otraClave = "otra-clave-que-no-es-la-buena-32".getBytes(StandardCharsets.UTF_8);        Scanner sc = new Scanner(System.in);        String usuario = sc.next(), rol = sc.next();        long exp = sc.nextLong();        String token = crear(clave, usuario, rol, exp);        System.out.println("Token: " + token);        while (sc.hasNext()) {            String prueba = sc.next();            long ahora = sc.nextLong();            String t = switch (prueba) {                case "normal" -> token;                case "otra-clave" -> crear(otraClave, usuario, rol, exp);                case "cambiar-rol" -> {               // el atacante edita los datos y deja la firma original                    String[] p = token.split("\\.");                    String datos = new String(D64.decode(p.length > 1 ? p[1] : ""), StandardCharsets.UTF_8).replace(rol, "admin");                    yield p[0] + "." + B64.encodeToString(datos.getBytes(StandardCharsets.UTF_8)) + "." + (p.length > 2 ? p[2] : "");                }                default -> "esto.no.es-un-token.valido";            };            System.out.println(prueba + " en t=" + ahora + ": " + verificar(clave, t, ahora));        }    }}

Ejemplo: Token válido y caducado

Entrada

ana alumna 1000
normal 500
normal 1000

Salida esperada

Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhbmEiLCJyb2wiOiJhbHVtbmEiLCJleHAiOjEwMDB9.fs9mjeocvakBG8JvEwlZgu3Yd5TgKgDUZUhGdUUXpKs
normal en t=500: VÁLIDO ana alumna
normal en t=1000: INVÁLIDO: caducado

Ejemplo: Ataques

Entrada

luis profesor 2000
cambiar-rol 10
otra-clave 10
basura 10

Salida esperada

Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJsdWlzIiwicm9sIjoicHJvZmVzb3IiLCJleHAiOjIwMDB9.IcQ4KtkPdyIVXmF3_Qw5jwIsK4X8GRGP0noHWUgIYKY
cambiar-rol en t=10: INVÁLIDO: firma
otra-clave en t=10: INVÁLIDO: firma
basura en t=10: INVÁLIDO: formato

Ejercicios largos

11. Contador de palabras en paralelo con un pool de hilos

Difícil · Java · 70 minutos · apuntesdam.com/ejercicios/java/contador-de-palabras-con-hilos

Contar las palabras de un texto enorme (las actas de un ayuntamiento, los registros de un servidor) se puede repartir: cada hilo cuenta un trozo y al final se suman los recuentos. Es el esquema «divide y vencerás» que usan desde los procesadores de varios núcleos hasta MapReduce en los centros de datos.

El error típico es que todos los hilos escriban en el mismo HashMap: no está preparado para accesos simultáneos y se pierden recuentos (o se corrompe). La forma limpia es que cada hilo cuente en su propio mapa y devuelva el resultado; el hilo principal es el único que los combina.

La prueba de que el programa es correcto es que el resultado no dependa del número de hilos ni del orden en que terminen: con 1, 3 u 8 hilos el informe de palabras tiene que ser exactamente el mismo.

Requisitos

  • La primera línea es el número de hilos, de 1 a 8 (si no, Número de hilos no válido: debe estar entre 1 y 8 y nada más). El resto es el texto; las líneas en blanco no cuentan. Si no queda ninguna línea, No hay texto que analizar.

  • Se usan tantos hilos como se pidan, pero nunca más que líneas. Las líneas se reparten en bloques consecutivos lo más iguales posible: si no salen exactos, los primeros bloques tienen una línea más. Cada bloque lo cuenta un hilo de un ExecutorService.

  • Una palabra es una secuencia de letras (de la a a la z, las vocales con tilde, la ü y la ñ), sin distinguir mayúsculas; cualquier otro carácter separa palabras.

  • Escribe Hilos: H · Líneas: L y una línea por bloque, en orden, con dos espacios delante: Bloque [i-j]: N palabras (o [i] si el bloque tiene una sola línea; las líneas se numeran desde 1 sin contar las vacías).

  • Después, Palabras: N · Distintas: D con el total de palabras y de palabras distintas, y Más frecuentes: seguido de las 5 palabras más repetidas (o las que haya) sin contar las palabras vacías de la tabla, cada una como el número en 4 caracteres alineado a la derecha, un espacio y la palabra. A igual número, por orden alfabético según compareTo.

Formato de la entrada

  • Línea 1: número de hilos (1 a 8). Resto: el texto.

Palabras vacías (no salen en «Más frecuentes»)
Palabras
de, la, que, el, en, y, a, los, se, del, las, un, por, con, no, una, su, para, es, al, lo, como

Ejemplo: Un texto con tres hilos

Entrada

3
La programación concurrente permite que varios hilos trabajen a la vez.
Cada hilo cuenta las palabras de su bloque de líneas.
Si los hilos comparten un mapa sin protección, se pierden datos.
Por eso cada hilo usa un mapa propio y devuelve el resultado.
El hilo principal espera con get() y suma los mapas parciales.
Con un hilo o con ocho hilos, el resultado debe ser el mismo.
La concurrencia bien hecha no cambia el resultado: solo el tiempo.

Salida esperada

Hilos: 3 · Líneas: 7
  Bloque [1-3]: 32 palabras
  Bloque [4-5]: 23 palabras
  Bloque [6-7]: 24 palabras
Palabras: 79 · Distintas: 56
Más frecuentes:
   4 hilo
   3 hilos
   3 resultado
   2 cada
   2 mapa

Ejemplo: Número de hilos no válido

Entrada

12
Un texto cualquiera

Salida esperada

Número de hilos no válido: debe estar entre 1 y 8
Código de partida (java)
import java.util.ArrayList;import java.util.HashMap;import java.util.List;import java.util.Map;import java.util.Scanner;import java.util.Set;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.Future; public class Main {    static final Set<String> VACIAS = Set.of("de", "la", "que", "el", "en", "y", "a", "los", "se", "del", "las", "un",            "por", "con", "no", "una", "su", "para", "es", "al", "lo", "como");     /** Lo que hace cada hilo: contar las palabras de su bloque en un mapa propio, sin compartir nada. */    static Map<String, Integer> contar(List<String> lineas) {        // TODO        return new HashMap<>();    }     public static void main(String[] args) throws Exception {        Scanner sc = new Scanner(System.in);        // TODO: lee el número de hilos y el texto, reparte los bloques en un pool de hilos y une los resultados    }}

12. Servidor de órdenes con sockets e hilos

Muy difícil · Java · 100 minutos · apuntesdam.com/ejercicios/java/servidor-de-ordenes-con-sockets

Muchos servicios de Internet (SMTP, FTP, Redis, POP3) usan protocolos de texto: el cliente envía una orden por línea y el servidor responde con otra línea que empieza por un código (OK, ERR). Son fáciles de depurar, porque se pueden probar a mano con telnet o netcat.

Vas a escribir un servidor de este tipo en Java. Escucha en un puerto, acepta conexiones y atiende a cada cliente en un hilo distinto, para que un cliente lento no bloquee a los demás. Tiene un pequeño almacén clave-valor compartido por todas las conexiones, al estilo de Redis, así que tiene que ser seguro para varios hilos.

Para poder probarlo sin dos programas, el mismo main arranca el servidor en un hilo y hace de cliente: lee las órdenes de la entrada, las envía por el socket y escribe lo que contesta el servidor. Una línea --- cierra la conexión y abre otra, como si llegara un cliente nuevo.

Requisitos

  • El servidor escucha en localhost en un puerto libre (puerto 0) y atiende cada conexión en un hilo nuevo. Lee líneas en UTF-8 y responde a cada una con una línea. Cuando recibe ADIOS responde y cierra la conexión.

  • Protocolo (las órdenes se reconocen en mayúsculas o minúsculas): SUMA a b → OK suma con enteros de hasta 9 cifras y signo opcional, o ERR números no válidos; MAYUS texto → OK TEXTO; INVIERTE texto → OK otxet (sin texto, ERR falta el texto); GUARDA clave valor (el valor puede tener espacios) → OK guardado u OK sustituido (antes: valor), y ERR uso: GUARDA clave valor si falta algo; LEE clave → OK valor o ERR no existe clave; CUENTA → OK número de claves; ADIOS → OK adiós. Una línea vacía es ERR línea vacía y cualquier otra orden ERR orden desconocida.

  • El cliente conecta al empezar y escribe == Cliente N conectado. Por cada línea de la entrada (incluidas las vacías) la envía, espera la respuesta y escribe > línea (solo > si la línea está vacía) y < respuesta. Una línea --- cierra la conexión actual y abre una nueva (con su mensaje de conexión); después de ADIOS, la siguiente línea también abre una conexión nueva.

  • Al terminar la entrada, el cliente cierra su conexión, se cierra el servidor y se escribe Servidor cerrado · claves guardadas: N.

  • El almacén es común a todas las conexiones: lo que guarda un cliente lo puede leer el siguiente.

Formato de la entrada

  • Una orden del protocolo por línea; --- abre una conexión nueva.

Ejemplo: Dos clientes que comparten el almacén

Entrada

SUMA 20 22
MAYUS hola, mundo
INVIERTE ñandú
GUARDA curso 2º DAM
GUARDA curso 1º DAW
CUENTA
ADIOS
LEE curso
GUARDA tutor Ana Gil
---
LEE tutor
LEE profe
CUENTA

Salida esperada

== Cliente 1 conectado
> SUMA 20 22
< OK 42
> MAYUS hola, mundo
< OK HOLA, MUNDO
> INVIERTE ñandú
< OK údnañ
> GUARDA curso 2º DAM
< OK guardado
> GUARDA curso 1º DAW
< OK sustituido (antes: 2º DAM)
> CUENTA
< OK 1
> ADIOS
< OK adiós
== Cliente 2 conectado
> LEE curso
< OK 1º DAW
> GUARDA tutor Ana Gil
< OK guardado
== Cliente 3 conectado
> LEE tutor
< OK Ana Gil
> LEE profe
< ERR no existe profe
> CUENTA
< OK 2
Servidor cerrado · claves guardadas: 2

Ejemplo: Errores del protocolo

Entrada

SUMA 1
SUMA uno dos
MAYUS

GUARDA solo
BORRA todo
suma -5 3

Salida esperada

== Cliente 1 conectado
> SUMA 1
< ERR números no válidos
> SUMA uno dos
< ERR números no válidos
> MAYUS
< ERR falta el texto
>
< ERR línea vacía
> GUARDA solo
< ERR uso: GUARDA clave valor
> BORRA todo
< ERR orden desconocida
> suma -5 3
< OK -2
Servidor cerrado · claves guardadas: 0
Código de partida (java)
import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.io.OutputStreamWriter;import java.io.PrintWriter;import java.net.ServerSocket;import java.net.Socket;import java.nio.charset.StandardCharsets;import java.util.Map;import java.util.Scanner;import java.util.concurrent.ConcurrentHashMap; public class Main {    static final Map<String, String> almacen = new ConcurrentHashMap<>();     /** La respuesta del protocolo a una línea. */    static String responder(String linea) {        // TODO: SUMA, MAYUS, INVIERTE, GUARDA, LEE, CUENTA y ADIOS        return "ERR sin hacer";    }     /** Atiende a un cliente: una respuesta por línea hasta ADIOS o hasta que cierre la conexión. */    static void atender(Socket socket) {        // TODO    }     public static void main(String[] args) throws Exception {        // TODO: arranca el servidor en un hilo, conecta el cliente y envía las líneas de la entrada    }}

13. Almacén de contraseñas seguro con PBKDF2 y sal

Difícil · Java · 70 minutos · apuntesdam.com/ejercicios/java/almacen-de-contrasenas-con-pbkdf2

Guardar contraseñas en texto plano, o con un hash rápido como MD5 o SHA-256 sin más, es uno de los fallos de seguridad más graves y más frecuentes. Si alguien roba la base de datos, con un hash rápido puede probar miles de millones de contraseñas por segundo.

La forma correcta es usar una función de derivación lenta a propósito, como PBKDF2, bcrypt o Argon2, con una sal aleatoria distinta para cada usuario. La sal hace que dos usuarios con la misma contraseña tengan hashes distintos (y que no sirvan las tablas precalculadas), y las miles de iteraciones hacen que cada intento cueste tiempo.

El inicio de sesión también tiene sus reglas: comparar los hashes en tiempo constante, bloquear la cuenta tras varios fallos seguidos y responder lo mismo si el usuario no existe que si la contraseña es incorrecta, para que nadie pueda averiguar qué usuarios hay. Como la sal es aleatoria, el programa nunca escribe hashes: solo el resultado de cada operación.

Requisitos

  • Cada línea es una orden con sus argumentos separados por espacios (la orden en mayúsculas o minúsculas; las líneas vacías se ignoran). Antes de responder se escribe > ORDEN usuario: (la orden en mayúsculas y solo el primer argumento, nunca la contraseña) y después la respuesta en la misma línea. Una orden desconocida responde orden desconocida.

  • REGISTRA usuario contraseña: el usuario ya existe; o, si la contraseña no cumple la política, contraseña rechazada, motivo con el primer motivo: debe tener al menos 10 caracteres, debe tener mayúsculas, minúsculas y números, no puede contener el nombre de usuario (sin distinguir mayúsculas); si no, guarda una sal aleatoria de 16 bytes y el hash PBKDF2WithHmacSHA256 (10.000 iteraciones, 256 bits) y responde registrado.

  • ENTRA usuario contraseña: si el usuario no existe o la contraseña no es correcta, usuario o contraseña incorrectos; al tercer fallo seguido, usuario o contraseña incorrectos; cuenta bloqueada tras 3 fallos; con la cuenta bloqueada, cuenta bloqueada aunque la contraseña sea buena; si es correcta, bienvenido y el contador de fallos vuelve a 0.

  • CAMBIA usuario actual nueva: no se puede cambiar la contraseña si el usuario no existe, está bloqueado o la actual no es correcta; después, la política (contraseña rechazada, motivo) y la nueva contraseña debe ser distinta; si todo va bien, nueva sal y nuevo hash, y contraseña cambiada.

  • DESBLOQUEA usuario → desbloqueada (y fallos a 0) o no existe. COMPARA u1 u2 → ¿mismo hash que u2? sí/no o no existe alguno de los usuarios. INFO usuario → PBKDF2WithHmacSHA256, 10000 iteraciones, sal de 16 bytes, hash de 32 bytes (con , bloqueada al final si lo está) o no existe.

  • Una orden con un número de argumentos que no es el suyo responde uso: … con su formato (uso: REGISTRA usuario contraseña, uso: ENTRA usuario contraseña, uso: CAMBIA usuario actual nueva).

  • Las contraseñas se comparan con MessageDigest.isEqual, nunca con Arrays.equals ni comparando textos.

Formato de la entrada

  • Una orden por línea: REGISTRA, ENTRA, CAMBIA, DESBLOQUEA, COMPARA o INFO.

Ejemplo: Registro, inicio de sesión y bloqueo

Entrada

REGISTRA ana Corta1
REGISTRA ana sinmayusculas123
REGISTRA ana MiAnaClave2026
REGISTRA ana Gatos.Tigres.2026
REGISTRA eva Gatos.Tigres.2026
REGISTRA ana OtraClave99999
COMPARA ana eva
ENTRA ana gatos.tigres.2026
ENTRA nadie Gatos.Tigres.2026
ENTRA ana Gatos.Tigres.2026
ENTRA eva mal
ENTRA eva mal
ENTRA eva mal
ENTRA eva Gatos.Tigres.2026
INFO eva

Salida esperada

> REGISTRA ana: contraseña rechazada, debe tener al menos 10 caracteres
> REGISTRA ana: contraseña rechazada, debe tener mayúsculas, minúsculas y números
> REGISTRA ana: contraseña rechazada, no puede contener el nombre de usuario
> REGISTRA ana: registrado
> REGISTRA eva: registrado
> REGISTRA ana: el usuario ya existe
> COMPARA ana: ¿mismo hash que eva? no
> ENTRA ana: usuario o contraseña incorrectos
> ENTRA nadie: usuario o contraseña incorrectos
> ENTRA ana: bienvenido
> ENTRA eva: usuario o contraseña incorrectos
> ENTRA eva: usuario o contraseña incorrectos
> ENTRA eva: usuario o contraseña incorrectos; cuenta bloqueada tras 3 fallos
> ENTRA eva: cuenta bloqueada
> INFO eva: PBKDF2WithHmacSHA256, 10000 iteraciones, sal de 16 bytes, hash de 32 bytes, bloqueada

Ejemplo: Cambio de contraseña y desbloqueo

Entrada

REGISTRA luis Montañas.2026
CAMBIA luis Equivocada1 Nueva.Clave.27
CAMBIA luis Montañas.2026 Montañas.2026
CAMBIA luis Montañas.2026 corta
CAMBIA luis Montañas.2026 Nueva.Clave.27
ENTRA luis Montañas.2026
ENTRA luis Nueva.Clave.27
DESBLOQUEA luis
INFO luis

Salida esperada

> REGISTRA luis: registrado
> CAMBIA luis: no se puede cambiar la contraseña
> CAMBIA luis: la nueva contraseña debe ser distinta
> CAMBIA luis: contraseña rechazada, debe tener al menos 10 caracteres
> CAMBIA luis: contraseña cambiada
> ENTRA luis: usuario o contraseña incorrectos
> ENTRA luis: bienvenido
> DESBLOQUEA luis: desbloqueada
> INFO luis: PBKDF2WithHmacSHA256, 10000 iteraciones, sal de 16 bytes, hash de 32 bytes
Código de partida (java)
import java.security.MessageDigest;import java.security.SecureRandom;import java.util.HashMap;import java.util.Map;import java.util.Scanner;import javax.crypto.SecretKeyFactory;import javax.crypto.spec.PBEKeySpec; public class Main {    static final String ALGORITMO = "PBKDF2WithHmacSHA256";    static final int ITERACIONES = 10_000;    // en producción, cientos de miles: aquí menos para que las pruebas vayan rápido    static final int MAX_FALLOS = 3;     static class Cuenta {        byte[] sal;        byte[] hash;        int fallos;        boolean bloqueada;    }     static final Map<String, Cuenta> cuentas = new HashMap<>();    static final SecureRandom azar = new SecureRandom();     static byte[] derivar(String contrasena, byte[] sal) throws Exception {        PBEKeySpec spec = new PBEKeySpec(contrasena.toCharArray(), sal, ITERACIONES, 256);        return SecretKeyFactory.getInstance(ALGORITMO).generateSecret(spec).getEncoded();    }     public static void main(String[] args) throws Exception {        Scanner sc = new Scanner(System.in);        // TODO: REGISTRA, ENTRA, CAMBIA, DESBLOQUEA, COMPARA e INFO    }}