Al cerrar el módulo 8, BiblioTech había dejado de esperar: importa el catálogo sin bloquear el menú, envía doscientos avisos con un pool acotado, protege su estado con ConcurrentHashMap y genera informes con cadenas de CompletableFuture. Y sin embargo seguía teniendo un límite que ninguna cantidad de hilos puede romper: todo ocurre dentro de una sola máquina. Marta Ruiz solo consulta el catálogo si se sienta delante de ese ordenador. Diego Alonso, desde otra planta, no puede. Nuria Vidal no puede pedir a un servicio externo los metadatos de una novedad.

Este módulo cambia eso. Pero antes de escribir la primera línea de código de red conviene entender qué hay debajo de una conexión, porque el código de red es el único código que escribirás en el que el fallo no es una posibilidad remota: es el estado normal del sistema. Un fichero local casi siempre está donde lo dejaste; una máquina al otro lado de la red puede estar apagada, saturada, tras un cortafuegos, con la dirección cambiada o simplemente lenta, y tu programa tiene que comportarse razonablemente en todos esos casos.

Esta lección no tiene apenas código de red —eso empieza en 09-02— pero es la que hace que el resto del módulo tenga sentido. Vas a entender el modelo cliente-servidor, el modelo por capas, qué es realmente una dirección IP y un puerto, cómo un nombre se convierte en una dirección, en qué se diferencian TCP y UDP y por qué esa diferencia decide el diseño de tu aplicación, y cómo se diseña un protocolo. Al final diseñaremos —sobre papel— el protocolo de consulta de catálogo de BiblioTech, que implementarás en las dos lecciones siguientes.

Nota sobre el enfoque. Esta lección explica conceptos de redes desde el punto de vista de quien va a programarlas, no desde el de quien va a administrar una infraestructura. No vas a encontrar aquí tablas de enrutamiento ni subredes al detalle; vas a encontrar exactamente lo que necesitas saber para que tu código de red no te sorprenda.

Contenido

  1. Qué significa que dos programas se comuniquen
  2. El modelo cliente-servidor y el modelo de igual a igual
  3. El modelo por capas: el sobre dentro del sobre
  4. Direcciones IP: IPv4, IPv6, localhost, privadas y públicas
  5. Puertos: por qué no basta con la dirección
  6. Qué es un socket
  7. DNS: de un nombre a una dirección
  8. InetAddress en la práctica
  9. TCP frente a UDP
  10. El saludo de tres vías, control de flujo y retransmisión
  11. Cuándo elegir cada uno
  12. Qué es un protocolo de aplicación y cómo se diseña
  13. El protocolo de BiblioTech
  14. Latencia frente a ancho de banda
  15. Herramientas de diagnóstico
  16. Qué puede fallar en una red
  17. Errores Comunes y Consejos
  18. Ejercicios

  1. Qué significa que dos programas se comuniquen

Hasta ahora, cuando dos partes de tu programa intercambiaban información, compartían memoria. Un hilo escribía en un ConcurrentHashMap y otro leía de él; el problema era la coordinación (candados, visibilidad, atomicidad), pero el dato estaba ahí, en la misma memoria, accesible en nanosegundos.

Cuando dos programas se comunican por red, nada de eso se cumple:

  • No comparten memoria. No hay objetos comunes. Solo se pueden enviar bytes.
  • No comparten tiempo. Lo que envías tarda en llegar, y a veces no llega.
  • No comparten confianza. El otro extremo puede ser un programa distinto, escrito en otro lenguaje, o directamente un atacante.
  • No comparten vida. El otro puede morir a mitad de la conversación sin avisarte.

De estas cuatro, la primera es la que más consecuencias tiene en el día a día: por la red solo viajan bytes. Un Libro de BiblioTech no viaja. Lo que viaja es una secuencia de bytes que, por acuerdo previo entre los dos programas, ambos saben interpretar como un libro. Ese acuerdo previo tiene nombre: protocolo.

En memoria (mismo proceso):     En red (dos procesos):

  Libro libro = catalogo         [67][79][78][83][85][76][84][65]...
      .buscar(isbn);              C   O   N   S   U   L   T   A
  // Es una referencia.           // Es una secuencia de bytes que
  // El objeto ya existe.         // alguien tiene que interpretar.

Todo el módulo 9 consiste, en el fondo, en aprender a convertir información en bytes, mandarlos por un tubo, y reconstruirla al otro lado sabiendo que el tubo puede romperse en cualquier momento.

  1. El modelo cliente-servidor y el modelo de igual a igual

Hay dos formas básicas de organizar quién habla con quién.

Cliente-servidor

Un programa —el servidor— se queda esperando peticiones en una dirección conocida. Otros programas —los clientes— toman la iniciativa, se conectan a esa dirección, piden algo y reciben una respuesta.

graph TD
    C1["Cliente<br/>portatil de Marta"] -->|CONSULTA 978-0000000001| S["Servidor BiblioTech<br/>192.168.1.50 puerto 9090"]
    C2["Cliente<br/>PC de Diego"] -->|LISTA| S
    C3["Cliente<br/>movil de Nuria"] -->|PRESTAR 978-0000000002| S
    S -->|200 OK ...| C1
    S -->|200 OK ...| C2
    S -->|404 NO ENCONTRADO| C3

Características:

  • Asimétrico. El servidor no llama a los clientes; espera. El cliente no espera; llama.
  • El servidor tiene dirección estable y conocida. El cliente no la necesita tener.
  • El servidor suele atender a muchos a la vez. Aquí es donde el módulo 8 se vuelve imprescindible.
  • Es el modelo de la web entera, del correo, de las bases de datos y del 95 % de lo que programarás.

BiblioTech va a ser un servidor: una máquina de Nexus Software con el catálogo, y todos los puestos de trabajo como clientes.

Igual a igual (peer-to-peer)

Todos los programas son a la vez cliente y servidor: cada uno puede iniciar una conversación y cada uno puede atender.

graph LR
    A["Nodo A"] <--> B["Nodo B"]
    B <--> C["Nodo C"]
    A <--> C
    C <--> D["Nodo D"]
    A <--> D

Características:

  • Simétrico. No hay un punto central.
  • Resistente: si cae un nodo, los demás siguen.
  • Complejo: descubrimiento de nodos, consistencia, seguridad. Mucho más difícil de programar.
  • Usos reales: BitTorrent, blockchains, algunos sistemas de mensajería y —a pequeña escala— el descubrimiento en red local que verás en 09-04, donde los puestos de trabajo se buscan entre sí sin que nadie les haya dicho dónde está el servidor.

En Java, ambos modelos se construyen con las mismas piezas (Socket y ServerSocket): un nodo P2P es simplemente un programa que tiene las dos.

  1. El modelo por capas: el sobre dentro del sobre

Cuando escribes una línea de texto en un socket, esa línea no viaja tal cual por el cable. Viaja envuelta, varias veces, como una carta metida en un sobre que a su vez va dentro de otro sobre, que va dentro de una saca.

Imagina que Marta Ruiz quiere mandar a Diego una nota que dice CONSULTA 978-0000000001:

  1. Escribe la nota (los datos de tu aplicación).
  2. La mete en un sobre con "Para: departamento de Sistemas, despacho 12" — eso identifica al programa destinatario dentro del edificio. Es la capa de transporte, y el número de despacho es el puerto.
  3. Ese sobre lo mete en otro con "Para: edificio Nexus, calle Mayor 3" — eso identifica a la máquina. Es la capa de red, y la dirección es la IP.
  4. Y ese sobre lo mete en la saca del camión que hace el trayecto hasta la siguiente oficina de correos. Es la capa de enlace, y la saca solo sirve para ese tramo concreto: en cada oficina se cambia de saca, pero los sobres interiores no se tocan.

Al llegar, se abre la saca, se abre el sobre exterior, se abre el interior y se lee la nota. Cada capa solo entiende su propio sobre y trata todo lo que hay dentro como contenido opaco.

graph TD
    subgraph EMISOR
    A1["Aplicacion<br/>CONSULTA 978-0000000001"] --> A2["Transporte<br/>anade puertos, secuencia"]
    A2 --> A3["Red<br/>anade IP origen y destino"]
    A3 --> A4["Enlace<br/>anade MAC, trama"]
    end
    A4 -->|bits por el cable| B4
    subgraph RECEPTOR
    B4["Enlace<br/>quita trama"] --> B3["Red<br/>quita cabecera IP"]
    B3 --> B2["Transporte<br/>quita cabecera TCP"]
    B2 --> B1["Aplicacion<br/>CONSULTA 978-0000000001"]
    end

Tabla de capas del modelo TCP/IP

Capa Qué añade Qué identifica Protocolos típicos Dónde la ves en Java
Aplicación Los datos con significado La operación que quieres HTTP, HTTPS, FTP, SMTP, DNS, SSH, tu protocolo PrintWriter, BufferedReader, HttpClient
Transporte Puerto origen y destino, números de secuencia, sumas de control El programa dentro de la máquina TCP, UDP Socket, ServerSocket, DatagramSocket
Red Dirección IP origen y destino, TTL La máquina en Internet IP, ICMP (ping), ARP InetAddress
Enlace Dirección MAC, comprobación de errores del tramo La tarjeta de red en el tramo local Ethernet, Wi-Fi, PPP NetworkInterface (poco)

Sobre el modelo OSI de siete capas. Puede que hayas oído hablar de siete capas (física, enlace, red, transporte, sesión, presentación, aplicación). El modelo OSI es un modelo de referencia académico; el que realmente implementa Internet es el TCP/IP de cuatro capas de la tabla. Conocer OSI es útil para entrevistas y para hablar con gente de infraestructura, pero para programar basta con las cuatro.

La consecuencia práctica de todo esto es enorme y conviene decirla explícitamente: tú solo programas la capa de aplicación. Cuando en 09-02 escribas escritor.println("CONSULTA " + isbn), el sistema operativo se encarga de todo lo demás. No verás una sola cabecera TCP ni una sola dirección MAC. Lo que sí verás son las consecuencias de esas capas: que los datos lleguen troceados, que haya un retardo, que una conexión se corte a mitad. Entender el modelo es entender por qué pasan esas cosas.

  1. Direcciones IP: IPv4, IPv6, localhost, privadas y públicas

Una dirección IP identifica una interfaz de red de una máquina. Hay dos versiones en uso.

IPv4

Cuatro números de 0 a 255 separados por puntos: 192.168.1.50. Son 32 bits, es decir, unos 4.300 millones de direcciones posibles — que se agotaron hace años, y por eso existe IPv6 (y NAT, más abajo).

192  .  168  .  1  .  50
 |       |      |     |
 8 bits  8      8     8    = 32 bits en total

IPv6

Ocho grupos de cuatro dígitos hexadecimales separados por dos puntos: 2001:0db8:85a3:0000:0000:8a2e:0370:7334. Son 128 bits, un número de direcciones absurdamente grande. Se permite abreviar los ceros:

2001:0db8:85a3:0000:0000:8a2e:0370:7334
2001:db8:85a3::8a2e:370:7334     <- misma direccion, abreviada
                ^^
                :: sustituye al grupo mas largo de ceros (solo una vez)

En Java, InetAddress maneja las dos de forma transparente: Inet4Address e Inet6Address son subclases suyas y casi nunca necesitarás distinguirlas.

Detalle práctico. En una URL, una dirección IPv6 se escribe entre corchetes para no confundir los : con el separador de puerto: http://[::1]:8080/catalogo.

localhost y 127.0.0.1

La dirección 127.0.0.1 (y su equivalente IPv6, ::1) es la interfaz de bucle invertido: se refiere siempre a la propia máquina. El nombre localhost apunta a ella.

Lo importante para ti: los paquetes enviados a localhost no salen a la red. Ni siquiera tocan la tarjeta de red; el sistema operativo los devuelve internamente. Por eso puedes probar todo este módulo en tu ordenador sin necesitar dos máquinas, sin conexión a Internet y con latencia casi cero. Todos los ejemplos del módulo se ejecutan contra localhost.

Privadas frente a públicas

Ciertos rangos están reservados para redes privadas y no son enrutables en Internet: cualquier router público descarta paquetes dirigidos a ellos.

Rango Notación Uso típico
10.0.0.010.255.255.255 10.0.0.0/8 Redes corporativas grandes
172.16.0.0172.31.255.255 172.16.0.0/12 Redes medianas, Docker
192.168.0.0192.168.255.255 192.168.0.0/16 Casi todos los routers domésticos
127.0.0.0127.255.255.255 127.0.0.0/8 Bucle invertido (la propia máquina)
169.254.0.0169.254.255.255 169.254.0.0/16 Autoconfiguración cuando falla el DHCP

La red interna de Nexus Software es 192.168.1.0/24, y el servidor de BiblioTech vivirá en 192.168.1.50. Eso significa que cualquier empleado dentro de la oficina puede conectarse a él, pero nadie desde fuera — lo cual, para un catálogo interno, es exactamente lo que se quiere.

Nota sobre NAT. Tu ordenador en casa tiene una dirección privada (192.168.1.23), pero navegas por Internet. Eso funciona gracias al NAT (traducción de direcciones de red): el router sustituye tu dirección privada por su única dirección pública al salir, y anota en una tabla qué conexión pertenece a quién para poder devolver las respuestas. La consecuencia que te afectará como programador es esta: detrás de un NAT puedes iniciar conexiones hacia fuera, pero nadie de fuera puede iniciar una conexión hacia ti, porque el router no sabría a qué máquina interna entregarla. Por eso los servidores necesitan dirección pública o reglas explícitas de redirección de puertos, y por eso los sistemas P2P tienen que hacer malabares para atravesar el NAT.

  1. Puertos: por qué no basta con la dirección

Si la IP identifica la máquina, ¿por qué hace falta algo más?

Porque en una máquina hay muchos programas que usan la red a la vez. En tu ordenador ahora mismo puede haber un navegador con quince pestañas, un cliente de correo, un servidor de desarrollo y BiblioTech. Cuando llega un paquete a la dirección de la máquina, el sistema operativo necesita saber a cuál de todos ellos entregarlo.

El puerto es ese número. Volviendo a la analogía: la IP es la calle y el número del edificio; el puerto es el número de despacho.

  • Es un número de 16 bits sin signo: de 0 a 65535.
  • Los puertos 0-1023 son bien conocidos y en sistemas tipo Unix requieren privilegios de administrador para escucharlos.
  • Los puertos 1024-49151 son registrados: asignados a aplicaciones concretas pero sin privilegios.
  • Los puertos 49152-65535 son efímeros: los que el sistema asigna automáticamente a tus conexiones salientes.

Puertos bien conocidos

Puerto Protocolo Para qué sirve
20 / 21 FTP Transferencia de ficheros (datos / control)
22 SSH Consola remota segura, scp, git sobre SSH
25 SMTP Envío de correo entre servidores
53 DNS Resolución de nombres
80 HTTP Web sin cifrar
443 HTTPS Web cifrada con TLS
3306 MySQL Base de datos
5432 PostgreSQL Base de datos
6379 Redis Caché en memoria
8080 HTTP alternativo Servidores de desarrollo, Tomcat
8443 HTTPS alternativo Igual, con TLS

Que HTTP viva en el 80 es lo que permite escribir http://ejemplo.com sin puerto: el navegador supone el 80 porque el esquema http lo implica. Si escribes http://ejemplo.com:8080, estás diciendo explícitamente otro despacho.

BiblioTech usará el puerto 9090 para su servidor de catálogo (TCP) y el 9091 para su servicio de descubrimiento (UDP). Son puertos altos, sin privilegios, y no chocan con nada habitual.

El puerto 0 es especial. Si pides el puerto 0 al crear un ServerSocket, el sistema te asigna uno libre cualquiera y luego puedes consultarlo con getLocalPort(). Es la técnica estándar en pruebas automatizadas para no chocar con puertos ocupados — la verás en el módulo 11 al hablar de JUnit.

  1. Qué es un socket

Ahora se puede definir con precisión la palabra que da nombre a la siguiente lección.

Un socket es un extremo de una comunicación, identificado por el par (dirección IP, puerto). Una conexión TCP queda identificada de forma única por cuatro valores: IP origen, puerto origen, IP destino, puerto destino.

Conexion de Marta al servidor de BiblioTech:

  (192.168.1.23 : 51422)  <----->  (192.168.1.50 : 9090)
   ^socket del cliente               ^socket del servidor
   puerto efimero, lo elige          puerto fijo y conocido
   el sistema operativo              lo elige el programador

Esta es la razón por la que mil clientes pueden conectarse al puerto 9090 del mismo servidor sin conflicto: aunque el destino sea idéntico en los mil casos, el origen (IP + puerto efímero) es distinto en cada uno, así que las cuádruplas son distintas y el sistema operativo nunca confunde una conexión con otra. Es una duda muy frecuente al empezar: no, el servidor no necesita "un puerto por cliente".

En Java, un socket TCP se representa con la clase java.net.Socket, y el objeto que acepta conexiones entrantes en el servidor es java.net.ServerSocket. La sorpresa agradable —y el motivo de que el módulo 7 fuera un requisito— es lo que un Socket te ofrece una vez conectado:

InputStream entrada = socket.getInputStream();
OutputStream salida = socket.getOutputStream();

Un InputStream y un OutputStream. Exactamente los mismos que usaste para leer y escribir ficheros. Los mismos decoradores (BufferedReader, InputStreamReader, PrintWriter), la misma necesidad de declarar el charset, el mismo try-with-resources. La red, una vez conectada, es E/S que ya sabes hacer. Lo único nuevo es cómo se establece y cómo se rompe esa conexión.

  1. DNS: de un nombre a una dirección

Nadie escribe 142.250.185.78 en el navegador. Escribe un nombre. El DNS (Sistema de Nombres de Dominio) es la guía telefónica distribuida que traduce nombres a direcciones.

sequenceDiagram
    participant App as Tu programa
    participant SO as Resolutor del SO
    participant DNS as Servidor DNS
    App->>SO: InetAddress.getByName("metadatos.nexussoftware.local")
    SO->>SO: mira /etc/hosts y la cache
    SO->>DNS: consulta el nombre (UDP puerto 53)
    DNS-->>SO: 192.168.1.60
    SO-->>App: InetAddress(192.168.1.60)

Puntos que importan al programar:

  • La resolución tarda. Es una llamada de red que puede añadir decenas o cientos de milisegundos la primera vez. Si haces getByName dentro de un bucle, lo pagas cada vez.
  • Se cachea. El sistema operativo y la propia JVM cachean resultados. La JVM tiene la propiedad networkaddress.cache.ttl para controlarlo; el valor por defecto en aplicaciones sin gestor de seguridad suele ser 30 segundos.
  • Puede fallar. Un nombre inexistente produce UnknownHostException, que es una IOException. Hay que capturarla.
  • Un nombre puede tener varias direcciones (balanceo de carga): por eso existe getAllByName, que devuelve un array.
  • El fichero /etc/hosts (en Windows, C:\Windows\System32\drivers\etc\hosts) tiene prioridad sobre el DNS. Es un truco habitual para probar en local un nombre de producción.

  1. InetAddress en la práctica

java.net.InetAddress es la clase que representa una dirección IP en Java. No tiene constructor público: se obtiene mediante métodos estáticos de fábrica.

Método Qué hace
InetAddress.getByName(String) Resuelve un nombre o interpreta una IP literal
InetAddress.getAllByName(String) Todas las direcciones asociadas a un nombre
InetAddress.getLocalHost() La dirección de la máquina actual
InetAddress.getLoopbackAddress() 127.0.0.1 / ::1, sin consultar nada
getHostAddress() La dirección en texto ("192.168.1.50")
getHostName() El nombre, haciendo resolución inversa si hace falta
getCanonicalHostName() El nombre canónico completo
isReachable(int millis) Intenta comprobar si responde, con tiempo límite
isLoopbackAddress() Si es 127.x.x.x o ::1
isSiteLocalAddress() Si es una dirección privada

Vamos a escribir la primera clase de red del módulo: una herramienta de diagnóstico que Nexus Software usará para comprobar dónde está y qué alcanza cada puesto de trabajo.

package com.nexussoftware.bibliotech.red;

import java.io.IOException;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.UnknownHostException;
import java.util.Enumeration;
import java.util.logging.Logger;

/**
 * Diagnostico de red de BiblioTech: responde a "donde estoy y a quien alcanzo".
 * No abre ninguna conexion: solo resuelve nombres y consulta interfaces.
 */
public final class DiagnosticoRed {

    private static final Logger LOG = Logger.getLogger(DiagnosticoRed.class.getName());

    private DiagnosticoRed() {
        // Clase de utilidades: no se instancia.
    }

    /** Muestra la direccion de esta maquina y las de sus interfaces. */
    public static void mostrarMaquinaLocal() {
        try {
            InetAddress local = InetAddress.getLocalHost();
            System.out.println("Nombre de esta maquina : " + local.getHostName());
            System.out.println("Direccion principal    : " + local.getHostAddress());
            System.out.println("Es privada             : " + local.isSiteLocalAddress());
        } catch (UnknownHostException e) {
            // Ocurre si la maquina no tiene nombre resoluble. No es fatal.
            LOG.warning("No se pudo determinar el nombre local: " + e.getMessage());
        }

        System.out.println();
        System.out.println("Interfaces de red disponibles:");
        try {
            Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
            while (interfaces.hasMoreElements()) {
                NetworkInterface ni = interfaces.nextElement();
                if (!ni.isUp()) {
                    continue;   // Ignoramos las interfaces apagadas.
                }
                Enumeration<InetAddress> direcciones = ni.getInetAddresses();
                while (direcciones.hasMoreElements()) {
                    InetAddress dir = direcciones.nextElement();
                    System.out.printf("  %-12s %-40s %s%n",
                            ni.getName(),
                            dir.getHostAddress(),
                            dir.isLoopbackAddress() ? "(bucle invertido)" : "");
                }
            }
        } catch (java.net.SocketException e) {
            LOG.warning("No se pudieron listar las interfaces: " + e.getMessage());
        }
    }

    /**
     * Resuelve un nombre a todas sus direcciones.
     * Devuelve false si el nombre no existe, en lugar de propagar la excepcion:
     * para una herramienta de diagnostico, "no resuelve" es un resultado, no un error.
     */
    public static boolean resolver(String nombre) {
        long inicio = System.nanoTime();
        try {
            InetAddress[] direcciones = InetAddress.getAllByName(nombre);
            long ms = (System.nanoTime() - inicio) / 1_000_000;
            System.out.println("Resolucion de '" + nombre + "' en " + ms + " ms:");
            for (InetAddress dir : direcciones) {
                System.out.println("   -> " + dir.getHostAddress());
            }
            return true;
        } catch (UnknownHostException e) {
            System.out.println("Resolucion de '" + nombre + "': NO RESUELVE");
            return false;
        }
    }

    /**
     * Comprueba si un equipo responde dentro del tiempo indicado.
     *
     * ATENCION: isReachable usa ICMP (como ping) si el proceso tiene privilegios,
     * y si no, intenta conectar al puerto 7 (echo). Muchos cortafuegos bloquean
     * ambas cosas, asi que un false NO demuestra que la maquina este caida.
     */
    public static boolean alcanzable(String nombre, int milisegundos) {
        try {
            InetAddress dir = InetAddress.getByName(nombre);
            boolean ok = dir.isReachable(milisegundos);
            System.out.printf("%-30s %s%n", nombre, ok ? "ALCANZABLE" : "sin respuesta");
            return ok;
        } catch (IOException e) {
            System.out.printf("%-30s ERROR: %s%n", nombre, e.getMessage());
            return false;
        }
    }

    public static void main(String[] args) {
        System.out.println("=== DIAGNOSTICO DE RED - BiblioTech ===\n");
        mostrarMaquinaLocal();

        System.out.println();
        resolver("localhost");
        resolver("servidor-que-no-existe.nexussoftware.local");

        System.out.println();
        alcanzable("localhost", 1000);
    }
}

Salida típica:

=== DIAGNOSTICO DE RED - BiblioTech ===

Nombre de esta maquina : puesto-marta
Direccion principal    : 192.168.1.23
Es privada             : true

Interfaces de red disponibles:
  lo           127.0.0.1                                (bucle invertido)
  lo           0:0:0:0:0:0:0:1                          (bucle invertido)
  eth0         192.168.1.23
  eth0         fe80:0:0:0:a00:27ff:fe4e:66a1

Resolucion de 'localhost' en 2 ms:
   -> 127.0.0.1
Resolucion de 'servidor-que-no-existe.nexussoftware.local': NO RESUELVE

localhost                      ALCANZABLE

Tres detalles que conviene fijar:

  1. getLocalHost() puede lanzar UnknownHostException en máquinas mal configuradas (contenedores sin entrada en /etc/hosts, sobre todo). No es un caso teórico; pasa a menudo en Docker.
  2. isReachable no es ping. Es un intento de comprobación que depende de privilegios y de cortafuegos. Un false significa "no lo he conseguido", no "la máquina está caída". Nunca bases una decisión importante solo en su resultado.
  3. La resolución de un nombre inexistente puede tardar segundos, no milisegundos, porque el resolutor reintenta. Otra razón para no meterla en un bucle caliente.

  1. TCP frente a UDP

Sobre la capa de red (IP) viven dos protocolos de transporte, y elegir entre ellos es la primera decisión de diseño de cualquier aplicación de red.

TCP: orientado a conexión y fiable

TCP (Protocolo de Control de Transmisión) ofrece a tu programa la ilusión de un tubo continuo y fiable entre los dos extremos, como si fuera un fichero abierto por ambos lados.

Garantiza:

  • Entrega: si un segmento se pierde, se retransmite. Si es imposible entregarlo, se te notifica con un error.
  • Orden: los bytes llegan en el mismo orden en que se enviaron.
  • Sin duplicados: los segmentos repetidos se descartan.
  • Control de flujo: si el receptor va lento, el emisor frena automáticamente.
  • Control de congestión: si la red está saturada, TCP reduce el ritmo para no empeorarla.

A cambio: hay que establecer la conexión antes de enviar nada (lo que cuesta un viaje de ida y vuelta), hay cabeceras más grandes, y las garantías introducen retardos — si un segmento se pierde, todo lo posterior espera a que llegue el retransmitido (bloqueo de cabecera de línea).

UDP: sin conexión y no fiable

UDP (Protocolo de Datagramas de Usuario) es casi transparente: coge tu bloque de bytes, le pone puertos y una suma de control, y lo suelta a la red.

No garantiza nada:

  • Un datagrama puede perderse y nadie te avisa.
  • Pueden llegar desordenados.
  • Puede llegar duplicado.
  • No hay control de flujo: puedes saturar al receptor sin enterarte.

A cambio: no hay establecimiento (envías al instante), la cabecera es de 8 bytes frente a los 20+ de TCP, y cada mensaje se conserva como unidad (si envías 100 bytes, el otro recibe 100 bytes o nada, nunca 60). Y permite enviar a muchos a la vez con difusión y multidifusión, cosa que TCP no puede hacer por definición.

Tabla comparativa

Criterio TCP UDP
Conexión previa Sí (saludo de tres vías) No
Entrega garantizada No
Orden garantizado No
Sin duplicados No
Control de flujo y congestión No
Tamaño de cabecera 20 bytes o más 8 bytes
Unidad de datos Flujo de bytes (sin fronteras) Datagrama (con fronteras)
Latencia inicial 1 viaje ida y vuelta antes de enviar Cero
Uno a muchos No Sí (difusión, multidifusión)
Clase en Java Socket / ServerSocket DatagramSocket / MulticastSocket

  1. El saludo de tres vías, control de flujo y retransmisión

El saludo de tres vías

Antes de que viaje un solo byte de tus datos, TCP establece la conexión con tres mensajes:

sequenceDiagram
    participant C as Cliente (Marta)
    participant S as Servidor BiblioTech
    Note over C,S: Establecimiento
    C->>S: SYN (quiero conectar, mi secuencia empieza en x)
    S->>C: SYN + ACK (de acuerdo, la mia en y, confirmo tu x)
    C->>S: ACK (confirmo tu y)
    Note over C,S: Conexion establecida: ya se pueden enviar datos
    C->>S: CONSULTA 978-0000000001
    S->>C: 200 OK Java Efectivo
    Note over C,S: Cierre
    C->>S: FIN
    S->>C: ACK
    S->>C: FIN
    C->>S: ACK

Consecuencias prácticas:

  • Abrir una conexión cuesta un viaje de ida y vuelta completo. Si el servidor está a 80 ms, solo el establecimiento son 80 ms antes de mandar un byte útil. Por eso las bibliotecas HTTP modernas reutilizan conexiones (lo verás en 09-06 con HttpClient) y por eso abrir un socket nuevo por cada petición es un error de rendimiento clásico.
  • El servidor responde SYN+ACK aunque tu programa no haya llamado a accept(). El sistema operativo mantiene una cola de conexiones ya establecidas esperando a que tu código las recoja: es el backlog de 09-03.
  • Si el puerto está cerrado, el sistema responde con RST en lugar de SYN+ACK, y en Java eso se convierte en ConnectException: Connection refused — inmediata, no un tiempo agotado. Distinguir "rechazado" (hay una máquina, no hay nadie escuchando) de "tiempo agotado" (no hay respuesta: máquina caída o cortafuegos que descarta en silencio) es diagnóstico puro.
  • El cierre son cuatro mensajes, no tres, porque cada sentido se cierra por separado. De ahí sale el concepto de media conexión (shutdownOutput) que verás en 09-02.

Retransmisión

Cada segmento enviado debe ser confirmado. Si el emisor no recibe confirmación dentro de un plazo estimado, reenvía. Ese plazo se recalcula constantemente midiendo el tiempo de ida y vuelta.

Emisor                        Receptor
  |--- segmento 1 --------------->|   ok
  |<-- ACK 1 ---------------------|
  |--- segmento 2 ----X           |   perdido en la red
  |     (espera... sin ACK)       |
  |--- segmento 2 (reenvio) ----->|   ok
  |<-- ACK 2 ---------------------|

Lo que ves desde Java: nada. Tu read() simplemente tarda un poco más. Esa es exactamente la propuesta de valor de TCP, y también la razón por la que un read() sin setSoTimeout puede quedarse parado mucho más de lo que imaginas.

Control de flujo

El receptor anuncia en cada confirmación cuánto espacio le queda en su buffer (la ventana). Si anuncia cero, el emisor se detiene hasta que se libere sitio.

Lo que ves desde Java: si el otro extremo no lee, tu write() acabará bloqueándose. Es una sorpresa habitual: mucha gente cree que escribir en un socket nunca bloquea porque "solo se copia a un buffer". Se copia a un buffer, sí, pero ese buffer es finito y se llena si el otro no consume.

  1. Cuándo elegir cada uno

Situación Elección Por qué
Transferir un fichero TCP Un byte perdido corrompe el fichero entero
Consultar el catálogo de BiblioTech TCP La respuesta debe llegar completa y en orden
Petición web (HTTP/1.1, HTTP/2) TCP Igual
Base de datos TCP Igual, y además la sesión tiene estado
Vídeo o audio en directo UDP Un fotograma perdido es un parpadeo; esperar su retransmisión sería peor
Videojuego de acción UDP La posición de hace 200 ms ya no sirve; interesa la de ahora
Consulta DNS UDP Una pregunta, una respuesta, cabe en un datagrama; si se pierde, se repite
Telemetría, métricas UDP Miles por segundo; perder alguna es irrelevante
Descubrimiento en red local UDP Necesita difusión, y TCP no puede hacer difusión
Sincronización de reloj (NTP) UDP Latencia mínima; el protocolo ya tolera pérdidas

La regla mental que funciona: ¿el dato de hace un momento sigue valiendo, o hay que esperarlo sí o sí? Si retransmitir un dato viejo es peor que perderlo, UDP. Si perderlo rompe el significado, TCP.

El caso híbrido. HTTP/3 y QUIC, los protocolos web más recientes, funcionan sobre UDP e implementan por su cuenta fiabilidad, orden y control de congestión, precisamente para librarse del bloqueo de cabecera de línea de TCP. Es la prueba de que "no fiable" no significa "peor": significa "las garantías las pones tú, a tu medida".

BiblioTech usará los dos: TCP para el catálogo (09-02 y 09-03) y UDP para el descubrimiento y la telemetría (09-04).

  1. Qué es un protocolo de aplicación y cómo se diseña

Un protocolo de aplicación es el acuerdo sobre qué bytes significan qué. TCP te da un tubo de bytes fiable, pero un tubo de bytes no dice dónde acaba un mensaje y empieza el siguiente, ni qué es una petición y qué una respuesta. Eso lo pones tú.

Texto frente a binario

Aspecto Protocolo de texto Protocolo binario
Legibilidad Se depura con telnet y nc Requiere herramientas
Tamaño Mayor Menor
Velocidad de análisis Menor Mayor
Facilidad de implementación Alta Media/baja
Problemas de codificación Sí (charset, saltos de línea) No
Ejemplos HTTP/1.1, SMTP, IMAP, Redis HTTP/2, gRPC, protocolos de juegos

Para aprender y para la mayoría de aplicaciones internas, texto. Es lo que haremos: podrás hablar con el servidor de BiblioTech usando telnet, que es una ayuda pedagógica impagable.

El problema central: ¿dónde acaba un mensaje?

TCP es un flujo de bytes sin fronteras. Si el cliente hace dos escrituras, el servidor puede recibirlas en una sola lectura, o partidas en tres. Esto se llama a veces "el problema del pegado de mensajes" y es la causa número uno de bugs sutiles en protocolos caseros.

El cliente escribe:      "CONSULTA 111\n"  y luego  "LISTA\n"

El servidor puede leer:  "CONSULTA 111\nLISTA\n"       (todo junto)
                    o:   "CONSUL"  ...  "TA 111\nLIS"  ...  "TA\n"   (partido)

Hay tres soluciones clásicas:

Técnica Cómo funciona Ventajas Inconvenientes
Delimitador Un byte o secuencia marca el fin (\n) Simplísimo, legible El delimitador no puede aparecer en los datos
Longitud previa Primero N bytes con el tamaño, luego el cuerpo Datos binarios sin restricción Hay que leer en dos fases; hay que limitar N
Cierre de conexión El mensaje acaba cuando se cierra Trivial Una sola respuesta por conexión

HTTP usa las tres a la vez: delimitador (\r\n) para las cabeceras, Content-Length (longitud previa) para el cuerpo, y en HTTP/1.0 el cierre de conexión.

En Java, la solución por delimitador de línea es casi gratis: BufferedReader.readLine() ya se encarga de acumular hasta encontrar el salto de línea, y PrintWriter.println() de añadirlo. Por eso los protocolos de línea son tan cómodos y por eso el de BiblioTech será uno de ellos.

Cuidado con readLine(). Devuelve null cuando el otro extremo cierra. Ese null es la única señal de fin de conversación que vas a tener, y no comprobarlo produce un NullPointerException en el primer cliente que se desconecte. Lo verás demostrado en 09-02.

Las cinco preguntas de todo protocolo

Cuando diseñes uno, contesta explícitamente:

  1. ¿Quién habla primero? (¿el cliente pide, o el servidor saluda?)
  2. ¿Dónde acaba cada mensaje? (delimitador, longitud, cierre)
  3. ¿Qué codificación de caracteres? (UTF-8, siempre, explícito)
  4. ¿Cómo se señala un error? (códigos, como HTTP; o texto libre, que es peor)
  5. ¿Cómo termina la conversación? (comando de despedida, cierre, tiempo de inactividad)

  1. El protocolo de BiblioTech

Con eso, ya podemos diseñar el protocolo que implementarás en 09-02 (cliente) y 09-03 (servidor). Se llamará BTCP (BiblioTech Catalog Protocol), versión 1.

Especificación

BTCP/1 - Protocolo de consulta de catalogo de BiblioTech
=========================================================

Transporte : TCP, puerto 9090
Codificacion : UTF-8
Delimitador : una linea por mensaje, terminada en \n
Inicia : el servidor envia una linea de saludo al conectar
Fin : el cliente envia SALIR, o el servidor cierra por inactividad (60 s)

--- SALUDO (servidor -> cliente, al conectar) ---

  200 BIBLIOTECH BTCP/1

--- PETICIONES (cliente -> servidor) ---

  CONSULTA <isbn>              Datos de un material por ISBN
  LISTA                        Todos los materiales del catalogo
  PRESTAR <isbn> <empleado>    Registrar un prestamo
  SALIR                        Terminar la conversacion

--- RESPUESTAS (servidor -> cliente) ---

  Toda respuesta empieza por un codigo numerico de 3 digitos
  seguido de un texto. Las respuestas de varias lineas indican
  cuantas hay y terminan con una linea que contiene solo un punto.

  200 OK <datos>               Exito, respuesta de una linea
  201 LISTA <n>                Exito, siguen n lineas y luego "."
  400 PETICION INVALIDA <por que>
  404 NO ENCONTRADO <isbn>
  409 NO DISPONIBLE <isbn>
  500 ERROR INTERNO
  221 ADIOS                    Respuesta a SALIR; el servidor cierra

--- FORMATO DE UN MATERIAL ---

  <isbn>|<titulo>|<tipo>|<disponible>

  Ejemplo: 978-0000000001|Java Efectivo|LIBRO|true

Una sesión de ejemplo

S: 200 BIBLIOTECH BTCP/1
C: CONSULTA 978-0000000001
S: 200 OK 978-0000000001|Java Efectivo|LIBRO|true
C: LISTA
S: 201 LISTA 3
S: 978-0000000001|Java Efectivo|LIBRO|true
S: 978-0000000002|Patrones de Diseno|LIBRO|false
S: 978-0000000003|Refactorizacion|LIBRO|true
S: .
C: PRESTAR 978-0000000002 Diego Alonso
S: 409 NO DISPONIBLE 978-0000000002
C: CONSULTA 999
S: 404 NO ENCONTRADO 999
C: PIDEME UN CAFE
S: 400 PETICION INVALIDA comando desconocido: PIDEME
C: SALIR
S: 221 ADIOS
   [el servidor cierra la conexion]

Fíjate en las decisiones y en por qué se toman:

  • Códigos numéricos de tres dígitos. Copiados descaradamente de HTTP y SMTP, y por buenas razones: el cliente puede decidir con el primer dígito (2 = bien, 4 = culpa tuya, 5 = culpa mía) sin analizar el texto, y el texto puede cambiarse o traducirse sin romper a nadie.
  • | como separador de campos. Elegido porque no aparece en títulos ni en ISBN. Si pudiera aparecer, habría que escaparlo — el mismo problema que ya resolviste con el CSV en 07-07.
  • Una respuesta por petición. Simplifica enormemente al cliente: escribe una línea, lee una respuesta.
  • La respuesta múltiple anuncia su tamaño y además se cierra con .. El número permite reservar y detectar truncamientos; el punto final permite terminar aunque el número fuera erróneo. Es redundante a propósito.
  • El servidor saluda primero. Así el cliente confirma que ha llegado al sitio correcto y a un servidor que habla su versión del protocolo antes de enviar nada.
  • Hay un comando de despedida. Sin él, la única forma de terminar limpiamente sería cerrar el socket, y el servidor no podría distinguir un cierre ordenado de una caída.

Este es el contrato. En 09-02 escribirás el cliente contra él (probándolo con nc mientras no exista el servidor), y en 09-03 el servidor multicliente que lo implementa.

  1. Latencia frente a ancho de banda

Dos magnitudes distintas que se confunden constantemente y que gobiernan el rendimiento de todo lo que programes en red.

Latencia Ancho de banda
Qué mide Cuánto tarda el primer byte en llegar Cuántos bytes por segundo caben
Unidad milisegundos Mbit/s, MB/s
Analogía Cuánto tarda el camión en llegar Cuánto cabe en el camión
Lo limita La distancia física y los saltos La capacidad del enlace
Se mejora con Acercar los datos (cachés, CDN) Contratar más línea

La clave: el ancho de banda se puede comprar; la latencia, no. Está limitada por la velocidad de la luz. Madrid–Nueva York son unos 5.800 km; ida y vuelta por fibra son como mínimo unos 60 ms, y en la práctica 90-120 ms. Ninguna optimización de tu código va a bajar de ahí.

Órdenes de magnitud que conviene tener interiorizados:

Operación Tiempo aproximado
Acceso a memoria RAM 100 nanosegundos
Lectura de un SSD 100 microsegundos (1.000×)
Ida y vuelta en la misma red local 0,5 milisegundos (5.000×)
Ida y vuelta a un servidor del mismo país 10-30 milisegundos
Ida y vuelta intercontinental 100-200 milisegundos (2.000.000×)

Compara la primera fila con la última: la red es un millón de veces más lenta que la memoria. De aquí salen tres reglas de diseño que verás aplicadas todo el módulo:

  1. Minimiza el número de viajes, no el número de bytes. Una petición que devuelve 100 registros bate a cien peticiones de un registro, aunque muevan lo mismo.
  2. Solapa las esperas. Si tienes que hacer diez consultas independientes, hazlas en paralelo. Esto es exactamente lo que aprendiste en el módulo 8 y lo que harás en 09-06 con sendAsync y allOf.
  3. Reutiliza las conexiones. Abrir una cuesta un viaje completo. Es la razón de ser del pool interno de HttpClient.

Nota: el orden de bytes de red. Cuando envías un int de 4 bytes, ¿va primero el byte más significativo o el menos significativo? Distintas arquitecturas de CPU eligen distinto (big-endian frente a little-endian), así que los protocolos de red fijan uno: big-endian, también llamado orden de bytes de red. La buena noticia para ti es que DataOutputStream.writeInt() y DataInputStream.readInt() de Java ya escriben y leen en big-endian, y ByteBuffer usa big-endian por defecto, así que si usas esas clases en los dos extremos no tienes que pensarlo. Solo importa si hablas con un programa en C que haya escrito bytes crudos, en cuyo caso él tendrá que usar htonl/ntohl.

  1. Herramientas de diagnóstico

Programar redes sin herramientas de diagnóstico es programar a ciegas. Estas cinco resuelven la mayoría de las dudas, y las usarás durante todo el módulo.

ping — ¿llega algo hasta esa máquina?

ping -c 4 localhost
PING localhost (127.0.0.1) 56(84) bytes of data.
64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.041 ms
64 bytes from localhost (127.0.0.1): icmp_seq=2 ttl=64 time=0.055 ms
--- localhost ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3060ms
rtt min/avg/max/mdev = 0.041/0.050/0.061/0.008 ms

Mide latencia y pérdida. Un ping que falla no demuestra que la máquina esté caída: muchísimos cortafuegos bloquean ICMP por política. Y un ping que funciona no demuestra que tu servicio esté vivo: solo que la máquina responde.

traceroute / tracert — ¿por dónde va?

traceroute example.com

Muestra cada salto intermedio con su latencia. Sirve para localizar dónde se degrada una conexión.

ss / netstat — ¿quién está escuchando en qué puerto?

ss -tlnp                  # Linux moderno: TCP, escuchando, numerico, con proceso
netstat -an | grep 9090   # Portable, mas antiguo
State   Recv-Q  Send-Q   Local Address:Port    Peer Address:Port  Process
LISTEN  0       50             0.0.0.0:9090         0.0.0.0:*     users:(("java",pid=4711,fd=7))

Esta es la herramienta que resuelve el error más frecuente del módulo: BindException: Address already in use. Te dice exactamente qué proceso tiene ocupado tu puerto. Si es una ejecución anterior de tu propio servidor que no cerraste, la matas y listo.

nc (netcat) — la navaja suiza

Actúa de cliente o de servidor TCP/UDP genérico. Es como tener un Socket y un ServerSocket en la línea de órdenes.

# Como servidor: escucha en el 9090 y muestra lo que llegue.
nc -l 9090

# Como cliente: conecta y te deja escribir lineas a mano.
nc localhost 9090

# Comprobar si un puerto esta abierto, sin enviar nada.
nc -zv localhost 9090

En 09-02 escribirás el cliente de BiblioTech antes de tener servidor, y lo probarás contra nc -l 9090 haciendo tú de servidor a mano. Es la mejor forma de entender un protocolo.

telnet — cliente de protocolos de texto

telnet localhost 9090

Igual que nc para TCP, pero presente desde hace décadas y con eco local, lo que hace más cómodo teclear. En 09-03 probarás el servidor de BiblioTech con él.

curl — cliente HTTP

curl -v https://example.com
curl -X POST -H "Content-Type: application/json" -d '{"isbn":"978-0000000001"}' http://localhost:8080/api
curl -i -s -o /dev/null -w "%{http_code}\n" http://localhost:9090/

En 09-05 y 09-06 lo usarás para comparar lo que hace tu código Java con lo que hace un cliente de referencia. La opción -v muestra las cabeceras enviadas y recibidas, que es justo lo que necesitas ver.

  1. Qué puede fallar en una red

Termina esta lección con lo que más te va a marcar como programador de red. Existe una lista célebre de falacias de la computación distribuida: suposiciones que todo el mundo hace al principio y que son todas falsas.

Falacia Realidad Consecuencia en tu código
La red es fiable Los paquetes se pierden y los cables se desconectan Toda operación de red va en try/catch
La latencia es cero Es un millón de veces la de la memoria Minimiza viajes; nunca en un bucle caliente
El ancho de banda es infinito Está compartido y es limitado No mandes lo que no necesitas
La red es segura Cualquiera en el camino puede leer y modificar TLS, validar toda la entrada
La topología no cambia Cambia constantemente No caches una IP para siempre
Hay un solo administrador Hay muchos, con políticas distintas Los cortafuegos te bloquearán sin avisar
El coste de transporte es cero Serializar y deserializar cuesta CPU Formatos y tamaños importan
La red es homogénea No lo es No supongas nada del otro extremo

De todas ellas, la primera es la que debe cambiar tu forma de escribir código. El código de red no falla ocasionalmente: falla siempre, y la única pregunta es con qué frecuencia.

Esto conecta directamente con el módulo 6. Todas las clases de red del JDK lanzan IOException o subclases suyas, y son excepciones comprobadas — el compilador te obliga a tratarlas. Eso no es una molestia burocrática: es el diseño del lenguaje diciéndote la verdad sobre el mundo.

// Lo que NO debe hacerse nunca, por comodo que parezca.
try {
    socket.connect(direccion);
} catch (IOException e) {
    // Silencio. El programa sigue como si nada.
}

// Lo que corresponde, con la estrategia por capas de 06-07.
try {
    socket.connect(direccion, TIEMPO_LIMITE_MS);
} catch (SocketTimeoutException e) {
    // Fallo TRANSITORIO: la maquina puede estar solo saturada.
    // Tiene sentido reintentar con espera creciente.
    LOG.warning("Tiempo agotado conectando a " + direccion + "; se reintentara");
    throw new BiblioTechException("El servidor de catalogo no responde", e);
} catch (ConnectException e) {
    // Fallo PERMANENTE mientras no cambie algo: no hay nadie escuchando.
    // Reintentar en bucle solo gasta CPU.
    LOG.severe("Conexion rechazada por " + direccion + ": el servidor no esta arrancado");
    throw new BiblioTechException("El servidor de catalogo no esta disponible", e);
} catch (IOException e) {
    // Cualquier otro fallo de red.
    LOG.log(Level.SEVERE, "Fallo de red al conectar", e);
    throw new BiblioTechException("Error de comunicacion con el catalogo", e);
}

Tres ideas que arrastrarás todo el módulo:

  1. Distingue transitorio de permanente. Un tiempo agotado merece reintento; una conexión rechazada o un nombre inexistente, no. Reintentar lo irreintentable es un antipatrón caro.
  2. Pon siempre un tiempo límite. Sin setSoTimeout ni setConnectTimeout, un read() puede quedarse parado hasta que el sistema operativo se rinda, y eso puede ser horas. Un hilo bloqueado para siempre es un hilo perdido, y con un pool acotado, un pool que se agota.
  3. Traduce el fallo a tu dominio en la frontera. Que CatalogoRemoto lance BiblioTechException y no SocketException es lo que permite que el menú no sepa nada de sockets — exactamente la estrategia por capas de 06-07.

Errores Comunes y Consejos

Confundir IP con puerto, o creer que el servidor necesita un puerto por cliente. Ya lo hemos visto: la conexión se identifica por la cuádrupla completa. Mil clientes, un solo puerto de servidor.

Cachear una InetAddress para toda la vida del programa. Las direcciones cambian: DHCP, migraciones, balanceadores. Resuelve el nombre cuando lo necesites y deja que el caché del sistema haga su trabajo, en lugar de guardar tú la IP en un static final.

Usar getLocalHost() para saber "mi dirección pública". No lo es. Devuelve la dirección de la máquina en su red, que detrás de un NAT es privada. Para conocer tu dirección pública hace falta que te lo diga alguien de fuera.

Suponer que isReachable() equivale a ping. Depende de privilegios y cortafuegos. Úsalo como pista, nunca como decisión.

Elegir UDP "porque es más rápido". UDP es más rápido en el caso feliz. Si necesitas fiabilidad y la implementas encima, acabarás escribiendo una versión peor de TCP. Elige UDP cuando no necesites las garantías, no cuando quieras ahorrarte los 20 bytes de cabecera.

Diseñar un protocolo sin decidir dónde acaba un mensaje. El bug aparecerá el día que un mensaje se parta en dos lecturas, que es el día que tengas tráfico real. Decide el delimitador antes de escribir la primera línea.

No fijar la codificación en el protocolo. Si el cliente escribe en UTF-8 y el servidor lee en la codificación por defecto de la plataforma, Patrones de Diseño llegará como Patrones de Diseño. En el protocolo se documenta, y en el código se pone explícito. Siempre.

Escribir código de red sin tiempos límite. El caso que lo demuestra: un servidor cuyo read() se bloquea porque el cliente se fue sin cerrar (se le acabó la batería al portátil). Sin setSoTimeout, ese hilo del pool no vuelve nunca. Con diez clientes así, el pool de diez hilos está muerto y el servidor deja de aceptar a nadie, sin un solo error en el log.

Probar solo en localhost y dar por hecho que funciona. En localhost la latencia es cero, no hay pérdidas, no hay MTU pequeña y no hay cortafuegos. Muchos bugs de red solo aparecen fuera. Prueba también entre dos máquinas reales cuando puedas, y al menos ten presente qué te está ocultando el bucle invertido.

Consejo transversal. Cuando algo no funcione, diagnostica capa a capa y de abajo arriba: ¿la máquina responde (ping)? ¿el puerto está escuchando (ss -tlnp)? ¿se puede conectar (nc -zv)? ¿el protocolo responde lo que esperas (telnet o curl -v)? Cada pregunta descarta una capa entera y el problema aparece en dos minutos en lugar de en dos horas.

Ejercicios

Ejercicio 1: Inventario de red de Nexus Software

Escribe una clase InventarioRed con un método analizar(String... nombres) que, para cada nombre recibido:

  • lo resuelva a una o varias direcciones, midiendo el tiempo de resolución en milisegundos;
  • indique de cada dirección si es IPv4 o IPv6, si es de bucle invertido y si es privada;
  • si no resuelve, lo indique sin lanzar excepción.

Debe imprimir una tabla alineada y terminar con un resumen de cuántos nombres resolvieron y cuántos no. Pruébalo con localhost, 127.0.0.1, tu propia máquina y un nombre inventado.

Ejercicio 2: Analizador del protocolo BTCP

Escribe una clase AnalizadorBtcp con un método estático String describir(String linea) que reciba una línea del protocolo BTCP/1 definido en esta lección y devuelva una descripción legible. Debe:

  • distinguir peticiones (CONSULTA, LISTA, PRESTAR, SALIR) de respuestas (empiezan por tres dígitos);
  • para una respuesta, indicar la familia según el primer dígito (2 = éxito, 4 = error del cliente, 5 = error del servidor);
  • validar que CONSULTA lleva exactamente un argumento y PRESTAR al menos dos;
  • devolver una descripción de error para cualquier cosa que no encaje.

Escribe un main que lo pruebe con la sesión de ejemplo de la sección 13. No uses la API de Streams.

Ejercicio 3: Estimador de coste de red

Escribe una clase EstimadorRed con un método estimar(int numeroPeticiones, int bytesPorPeticion, int latenciaMs, double anchoBandaMbps) que calcule y compare tres estrategias para traer los datos de N materiales del catálogo:

  1. Secuencial: N peticiones, una detrás de otra.
  2. Paralela con 8 conexiones: N peticiones repartidas en 8 vías simultáneas.
  3. Por lotes: 1 sola petición que devuelve los N materiales.

Para cada una, calcula el tiempo total (latencia + transferencia) y muéstralo en una tabla con el factor de mejora respecto a la secuencial. Considera que cada petición secuencial cuesta un viaje de ida y vuelta completo (la latencia) más el tiempo de transferir sus bytes.

Soluciones

Solución 1

package com.nexussoftware.bibliotech.red;

import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.UnknownHostException;

/**
 * Inventario de red: resuelve una lista de nombres y clasifica sus direcciones.
 * Herramienta de diagnostico interna de Nexus Software.
 */
public class InventarioRed {

    /** Resultado de resolver un nombre. Se usa solo para el resumen final. */
    private int resueltos = 0;
    private int fallidos = 0;

    public void analizar(String... nombres) {
        System.out.printf("%-40s %-42s %-6s %-8s %-8s %s%n",
                "NOMBRE", "DIRECCION", "VER", "BUCLE", "PRIVADA", "MS");
        System.out.println("-".repeat(115));

        for (String nombre : nombres) {
            analizarUno(nombre);
        }

        System.out.println("-".repeat(115));
        System.out.printf("Resueltos: %d   Fallidos: %d   Total: %d%n",
                resueltos, fallidos, resueltos + fallidos);
    }

    private void analizarUno(String nombre) {
        long inicio = System.nanoTime();
        InetAddress[] direcciones;
        try {
            // getAllByName devuelve TODAS las direcciones del nombre:
            // un servicio balanceado puede tener varias.
            direcciones = InetAddress.getAllByName(nombre);
        } catch (UnknownHostException e) {
            // No resolver es un resultado valido para una herramienta de
            // diagnostico: se informa y se sigue, no se propaga.
            long ms = (System.nanoTime() - inicio) / 1_000_000;
            System.out.printf("%-40s %-42s %-6s %-8s %-8s %d%n",
                    nombre, "NO RESUELVE", "-", "-", "-", ms);
            fallidos++;
            return;
        }
        long ms = (System.nanoTime() - inicio) / 1_000_000;
        resueltos++;

        boolean primera = true;
        for (InetAddress dir : direcciones) {
            // La version se deduce del tipo concreto: Inet4Address o Inet6Address
            // son las dos unicas subclases de InetAddress en el JDK.
            String version;
            if (dir instanceof Inet4Address) {
                version = "IPv4";
            } else if (dir instanceof Inet6Address) {
                version = "IPv6";
            } else {
                version = "?";
            }

            System.out.printf("%-40s %-42s %-6s %-8s %-8s %s%n",
                    primera ? nombre : "",            // el nombre solo en la 1a fila
                    dir.getHostAddress(),
                    version,
                    dir.isLoopbackAddress() ? "si" : "no",
                    dir.isSiteLocalAddress() ? "si" : "no",
                    primera ? String.valueOf(ms) : "");
            primera = false;
        }
    }

    public static void main(String[] args) {
        InventarioRed inventario = new InventarioRed();

        // Si nos pasan nombres por linea de ordenes, usamos esos.
        if (args.length > 0) {
            inventario.analizar(args);
            return;
        }

        String miMaquina;
        try {
            miMaquina = InetAddress.getLocalHost().getHostName();
        } catch (UnknownHostException e) {
            miMaquina = "localhost";   // Respaldo razonable si la maquina no tiene nombre.
        }

        inventario.analizar(
                "localhost",
                "127.0.0.1",
                miMaquina,
                "servidor-inexistente.nexussoftware.local");
    }
}

Salida típica:

NOMBRE                                   DIRECCION                                  VER    BUCLE    PRIVADA  MS
-------------------------------------------------------------------------------------------------------------
localhost                                127.0.0.1                                  IPv4   si       no       3
127.0.0.1                                127.0.0.1                                  IPv4   si       no       0
puesto-marta                             192.168.1.23                               IPv4   no       si       1
servidor-inexistente.nexussoftware.local NO RESUELVE                                -      -        -        41
-------------------------------------------------------------------------------------------------------------
Resueltos: 3   Fallidos: 1   Total: 4

Comentarios. Fíjate en tres cosas de la salida real: 127.0.0.1 resuelve en 0 ms porque no hay consulta DNS (es una IP literal, solo se interpreta); el nombre inexistente tarda 41 ms, mucho más que los que sí resuelven, porque el resolutor consulta y espera antes de rendirse — en una red mal configurada esto puede ser de segundos; y localhost es de bucle invertido pero no privada, porque isSiteLocalAddress() solo mira los rangos 10/8, 172.16/12 y 192.168/16.

Solución 2

package com.nexussoftware.bibliotech.red;

/**
 * Analizador didactico del protocolo BTCP/1 de BiblioTech.
 * Convierte una linea del protocolo en una descripcion legible.
 * Se usa para depurar trazas de sesiones capturadas con nc o telnet.
 */
public final class AnalizadorBtcp {

    private AnalizadorBtcp() {
    }

    public static String describir(String linea) {
        if (linea == null || linea.isBlank()) {
            return "LINEA VACIA (no valida en BTCP/1)";
        }

        String limpia = linea.strip();

        // Una respuesta empieza siempre por tres digitos.
        if (esCodigoRespuesta(limpia)) {
            return describirRespuesta(limpia);
        }

        // El marcador de fin de respuesta multiple.
        if (limpia.equals(".")) {
            return "FIN de respuesta multiple";
        }

        return describirPeticion(limpia);
    }

    /** Comprueba que los tres primeros caracteres son digitos. */
    private static boolean esCodigoRespuesta(String linea) {
        if (linea.length() < 3) {
            return false;
        }
        for (int i = 0; i < 3; i++) {
            if (!Character.isDigit(linea.charAt(i))) {
                return false;
            }
        }
        // Tras los tres digitos debe venir el final o un espacio.
        return linea.length() == 3 || linea.charAt(3) == ' ';
    }

    private static String describirRespuesta(String linea) {
        String codigo = linea.substring(0, 3);
        String texto = linea.length() > 4 ? linea.substring(4) : "";

        // El primer digito determina la familia: la gracia de los codigos
        // numericos es poder decidir sin analizar el texto.
        String familia = switch (codigo.charAt(0)) {
            case '2' -> "EXITO";
            case '4' -> "ERROR DEL CLIENTE";
            case '5' -> "ERROR DEL SERVIDOR";
            default  -> "FAMILIA DESCONOCIDA";
        };

        String detalle = switch (codigo) {
            case "200" -> "respuesta correcta de una linea";
            case "201" -> "siguen n lineas y luego un punto";
            case "221" -> "despedida; el servidor va a cerrar";
            case "400" -> "la peticion no se entiende";
            case "404" -> "el material no existe en el catalogo";
            case "409" -> "el material existe pero no esta disponible";
            case "500" -> "fallo interno del servidor";
            default    -> "codigo no definido en BTCP/1";
        };

        return "RESPUESTA " + codigo + " [" + familia + "] " + detalle
                + (texto.isEmpty() ? "" : " -> '" + texto + "'");
    }

    private static String describirPeticion(String linea) {
        // split con limite 2 en PRESTAR no serviria: necesitamos separar
        // el comando y luego el resto, porque el nombre del empleado
        // puede llevar espacios ("Diego Alonso").
        int primerEspacio = linea.indexOf(' ');
        String comando = primerEspacio < 0 ? linea : linea.substring(0, primerEspacio);
        String resto = primerEspacio < 0 ? "" : linea.substring(primerEspacio + 1).strip();

        switch (comando) {
            case "CONSULTA":
                if (resto.isEmpty()) {
                    return "PETICION INVALIDA: CONSULTA requiere un ISBN";
                }
                if (resto.contains(" ")) {
                    return "PETICION INVALIDA: CONSULTA admite un solo argumento, recibido '"
                            + resto + "'";
                }
                return "PETICION CONSULTA del material con ISBN " + resto;

            case "LISTA":
                if (!resto.isEmpty()) {
                    return "PETICION INVALIDA: LISTA no admite argumentos";
                }
                return "PETICION LISTA: catalogo completo";

            case "PRESTAR":
                int espacio = resto.indexOf(' ');
                if (espacio < 0) {
                    return "PETICION INVALIDA: PRESTAR requiere ISBN y empleado";
                }
                String isbn = resto.substring(0, espacio);
                String empleado = resto.substring(espacio + 1).strip();
                if (empleado.isEmpty()) {
                    return "PETICION INVALIDA: falta el empleado en PRESTAR";
                }
                return "PETICION PRESTAR del ISBN " + isbn + " a '" + empleado + "'";

            case "SALIR":
                if (!resto.isEmpty()) {
                    return "PETICION INVALIDA: SALIR no admite argumentos";
                }
                return "PETICION SALIR: fin de la conversacion";

            default:
                return "PETICION INVALIDA: comando desconocido '" + comando + "'";
        }
    }

    public static void main(String[] args) {
        String[] sesion = {
                "200 BIBLIOTECH BTCP/1",
                "CONSULTA 978-0000000001",
                "200 OK 978-0000000001|Java Efectivo|LIBRO|true",
                "LISTA",
                "201 LISTA 3",
                "978-0000000001|Java Efectivo|LIBRO|true",
                ".",
                "PRESTAR 978-0000000002 Diego Alonso",
                "409 NO DISPONIBLE 978-0000000002",
                "CONSULTA 999",
                "404 NO ENCONTRADO 999",
                "PIDEME UN CAFE",
                "400 PETICION INVALIDA comando desconocido: PIDEME",
                "CONSULTA",
                "CONSULTA 111 222",
                "SALIR",
                "221 ADIOS"
        };

        for (String linea : sesion) {
            System.out.printf("%-55s | %s%n", linea, describir(linea));
        }
    }
}

Comentarios. Tres decisiones merecen atención. La primera: esCodigoRespuesta no se limita a mirar tres dígitos, sino que exige que después venga un espacio o el final de línea; sin eso, la línea de datos 978-0000000001|Java Efectivo|... empieza por tres dígitos y se clasificaría como respuesta. La segunda: en PRESTAR no se usa split(" ") porque el nombre del empleado lleva espacios; se busca el primer separador a mano y el resto se toma entero. Y la tercera: fíjate en que la línea de datos 978-...|Java Efectivo|... cae en el default de describirPeticion y se marca como comando desconocido. Es correcto: fuera del contexto de una respuesta 201, esa línea no es válida. Analizar un protocolo con estado exige recordar en qué punto de la conversación estás, y eso es justo lo que hará el cliente real de 09-02.

Solución 3

package com.nexussoftware.bibliotech.red;

/**
 * Estimador del coste de red de las distintas formas de traer N materiales
 * del catalogo. Sirve para justificar por que las peticiones por lotes ganan.
 */
public class EstimadorRed {

    /** Resultado de una estrategia. Un record simple (04-07). */
    public record Estimacion(String nombre, double milisegundos) {
    }

    public void estimar(int numeroPeticiones, int bytesPorPeticion,
                        int latenciaMs, double anchoBandaMbps) {

        // Tiempo de transferir los bytes de UNA peticion.
        // Mbps son megabits por segundo: hay que pasar bytes a bits (x8)
        // y megabits a bits (x1.000.000).
        double bitsPorPeticion = bytesPorPeticion * 8.0;
        double bitsPorMs = anchoBandaMbps * 1_000_000.0 / 1000.0;
        double msTransferenciaUna = bitsPorPeticion / bitsPorMs;

        // --- Estrategia 1: secuencial ---
        // Cada peticion cuesta un viaje completo (latencia) mas su transferencia.
        double secuencial = numeroPeticiones * (latenciaMs + msTransferenciaUna);

        // --- Estrategia 2: paralela con 8 conexiones ---
        // Las 8 vias avanzan a la vez, asi que el tiempo es el de la via
        // mas cargada: se reparten las peticiones redondeando hacia arriba.
        int vias = 8;
        int porVia = (numeroPeticiones + vias - 1) / vias;
        double paralela = porVia * (latenciaMs + msTransferenciaUna);

        // --- Estrategia 3: un solo lote ---
        // Un unico viaje de ida y vuelta, y toda la transferencia junta.
        double lote = latenciaMs + msTransferenciaUna * numeroPeticiones;

        Estimacion[] resultados = {
                new Estimacion("Secuencial (" + numeroPeticiones + " viajes)", secuencial),
                new Estimacion("Paralela x8 (" + porVia + " viajes por via)", paralela),
                new Estimacion("Por lotes (1 viaje)", lote)
        };

        System.out.printf("N=%d  bytes/peticion=%d  latencia=%d ms  ancho=%.1f Mbps%n",
                numeroPeticiones, bytesPorPeticion, latenciaMs, anchoBandaMbps);
        System.out.println();
        System.out.printf("%-36s %12s %10s%n", "ESTRATEGIA", "TIEMPO (ms)", "MEJORA");
        System.out.println("-".repeat(60));
        for (Estimacion e : resultados) {
            double mejora = secuencial / e.milisegundos();
            System.out.printf("%-36s %12.1f %9.1fx%n", e.nombre(), e.milisegundos(), mejora);
        }
        System.out.println();
        System.out.printf("Bytes totales movidos en los tres casos: %d (identico)%n",
                (long) numeroPeticiones * bytesPorPeticion);
    }

    public static void main(String[] args) {
        EstimadorRed estimador = new EstimadorRed();

        System.out.println("=== Caso 1: red local de Nexus Software ===");
        estimador.estimar(100, 200, 1, 1000.0);

        System.out.println();
        System.out.println("=== Caso 2: servicio externo de metadatos (otro continente) ===");
        estimador.estimar(100, 200, 120, 50.0);
    }
}

Salida:

=== Caso 1: red local de Nexus Software ===
N=100  bytes/peticion=200  latencia=1 ms  ancho=1000,0 Mbps

ESTRATEGIA                            TIEMPO (ms)     MEJORA
------------------------------------------------------------
Secuencial (100 viajes)                     100,2       1,0x
Paralela x8 (13 viajes por via)              13,0       7,7x
Por lotes (1 viaje)                           1,2      86,2x

Bytes totales movidos en los tres casos: 20000 (identico)

=== Caso 2: servicio externo de metadatos (otro continente) ===
N=100  bytes/peticion=200  latencia=120 ms  ancho=50,0 Mbps

ESTRATEGIA                            TIEMPO (ms)     MEJORA
------------------------------------------------------------
Secuencial (100 viajes)                   12003,2      1,0x
Paralela x8 (13 viajes por via)            1560,4      7,7x
Por lotes (1 viaje)                         123,2     97,5x

Bytes totales movidos en los tres casos: 20000 (identico)

Comentarios. Este es el ejercicio más importante de la lección, porque el resultado es contraintuitivo y decide diseños. Los tres casos mueven exactamente los mismos 20.000 bytes, y sin embargo el más lento tarda 12 segundos y el más rápido 123 milisegundos: una diferencia de casi cien veces. Lo que cambia no es el volumen, sino el número de viajes.

Compara además los dos escenarios. En la red local, la secuencial tarda 100 ms — molesto pero soportable. Contra un servicio a 120 ms de distancia, la misma estrategia tarda doce segundos, y ningún aumento de ancho de banda lo arregla: la transferencia real son 0,2 ms de los 120,2 de cada viaje. Por eso las dos técnicas que funcionan son agrupar (una petición en lugar de cien) y solapar (ocho a la vez), y por eso el módulo 8 era requisito de este. En 09-06 harás exactamente la estrategia paralela con sendAsync y allOf.

Conclusión

Esta lección ha puesto los cimientos de todo el módulo. Ya no vas a escribir código de red a ciegas.

Sabes qué significa realmente que dos programas se comuniquen: no comparten memoria, ni tiempo, ni confianza, ni vida, y por la red solo viajan bytes que ambos extremos tienen que saber interpretar. Conoces los dos modelos de organización —cliente-servidor, asimétrico y con dirección conocida, que es el de BiblioTech y el del 95 % de lo que programarás; y de igual a igual, simétrico y resistente pero mucho más complejo— y sabes que en Java ambos se construyen con las mismas piezas.

Entiendes el modelo por capas como sobres dentro de sobres: la de aplicación lleva tus datos, la de transporte identifica el programa con el puerto, la de red identifica la máquina con la IP, y la de enlace solo cubre el tramo siguiente. Y sabes la consecuencia práctica: tú solo programas la capa de aplicación, pero sufres las consecuencias de todas las demás.

Manejas las direcciones IP —IPv4 de 32 bits, IPv6 de 128, 127.0.0.1 como bucle invertido que ni siquiera toca la tarjeta de red, y los rangos privados que hacen que Nexus Software funcione en 192.168.1.0/24 con NAT para salir— y los puertos: 16 bits, de 0 a 65535, con los bien conocidos por debajo de 1024 y los efímeros por encima de 49152. Con la idea que más dudas resuelve: un socket es el par (IP, puerto), una conexión TCP es una cuádrupla, y por eso mil clientes caben en un solo puerto de servidor.

Sabes cómo un nombre se convierte en dirección a través del DNS, que tarda, se cachea y puede fallar con UnknownHostException; y has escrito tu primera clase de red, DiagnosticoRed, que resuelve nombres, lista interfaces y comprueba alcance, con las tres advertencias que la acompañan: getLocalHost() falla en contenedores, isReachable() no es ping, y resolver un nombre inexistente puede tardar segundos.

Y sobre todo has entendido la decisión que abre este módulo: TCP frente a UDP. TCP te da un tubo fiable y ordenado a cambio de un saludo de tres vías que cuesta un viaje entero, de retransmisiones que hacen que tus lecturas tarden más sin que te enteres, y de un control de flujo que puede llegar a bloquear tus escrituras si el otro no lee. UDP no te da nada de eso, y por eso es lo correcto cuando el dato viejo ya no vale: vídeo, juegos, telemetría, DNS y —lo que usará BiblioTech— descubrimiento en red local por difusión, algo que TCP simplemente no puede hacer.

Sabes qué es un protocolo de aplicación, por qué el texto gana en claridad y el binario en tamaño, y cuál es su problema central: TCP no tiene fronteras de mensaje, así que hay que ponerlas con un delimitador, con una longitud previa o con el cierre de la conexión. Y has diseñado uno completo, BTCP/1: saludo del servidor, comandos CONSULTA, LISTA, PRESTAR y SALIR, respuestas con códigos de tres dígitos cuya primera cifra ya dice si fue bien, respuestas múltiples que anuncian su tamaño y se cierran con un punto, UTF-8 explícito y delimitador de línea.

Tienes interiorizada la diferencia entre latencia —que no se puede comprar porque la limita la velocidad de la luz— y ancho de banda, con la cifra que lo resume todo: la red es un millón de veces más lenta que la memoria, y de ahí salen las tres reglas que gobernarán tu código: minimiza viajes, solapa esperas, reutiliza conexiones. Tu propio estimador te ha enseñado que los mismos 20.000 bytes tardan doce segundos o ciento veintitrés milisegundos según cómo los pidas.

Conoces las cinco herramientas con las que se diagnostica de verdad —ping, traceroute, ss, nc y curl— y el método: capa a capa, de abajo arriba. Y aceptas la premisa que hace bueno a un programador de red: el código de red no falla ocasionalmente, falla siempre, así que se escribe suponiendo el fallo, distinguiendo lo transitorio de lo permanente, poniendo tiempo límite a todo y traduciendo el fallo de red a una BiblioTechException en la frontera, tal como aprendiste en 06-07.

En la próxima lección, Sockets, se acaba la teoría. Vas a abrir tu primera conexión desde Java con la clase Socket, y vas a descubrir algo que ya intuyes: una vez conectado, getInputStream() y getOutputStream() te devuelven exactamente los flujos del módulo 7, con los mismos decoradores, el mismo charset explícito y el mismo try-with-resources. Verás cómo conectar con tiempo límite, cómo cerrar media conexión, qué significa cada excepción de red y qué opciones tiene un socket. Y verás demostrado, en directo, el bug número uno de todo el que empieza con sockets: olvidar el vaciado del buffer y dejar al otro extremo esperando para siempre. Al terminar tendrás el cliente de BiblioTech hablando BTCP/1 — probado contra un nc -l 9090 en el que harás tú de servidor a mano, mientras llega el servidor de verdad en 09-03.

Curso de Programación en Java

Módulo 1: Introducción a Java

Módulo 2: Flujo de Control

Módulo 3: Programación Orientada a Objetos

Módulo 4: Programación Orientada a Objetos Avanzada

Módulo 5: Estructuras de Datos y Colecciones

Módulo 6: Manejo de Excepciones

Módulo 7: Entrada/Salida de Archivos

Módulo 8: Multihilo y Concurrencia

Módulo 9: Redes

Módulo 10: Temas Avanzados

Módulo 11: Frameworks y Librerías de Java

Módulo 12: Construcción de Aplicaciones del Mundo Real

© Copyright 2026. Todos los derechos reservados