Ir al contenido
wannacode

Programación en red

En resumen: La programación en red son dos programas que hablan por la red: el servidor espera en una dirección IP y un puerto, el cliente se conecta, y los sockets envían los datos por TCP o UDP.

Problema

La caja y el almacén

La caja de la tienda tiene que preguntarle al almacén cuánto café queda, y el programa del almacén funciona en otra computadora. Un archivo no sirve: los dos programas no ven el disco del otro. Uno tiene que preguntar y el otro tiene que responder, en el momento, por la red.

Así funciona casi todo lo que te rodea: el navegador le pide una página al servidor, la app del banco le pide al banco tu saldo, un juego envía tus jugadas a los demás jugadores.

La idea

Dirección, puerto y dos formas de enviar

Para que los datos lleguen a un programa en otra computadora, necesitas dos cosas:

  • Una dirección IP: el número de la computadora en la red, por ejemplo 192.168.1.20 o 142.250.185.78. Tu propia computadora es siempre 127.0.0.1, con el nombre localhost.
  • Un puerto: un número del 0 al 65 535 para el programa de esa computadora. La dirección es el edificio, el puerto es el apartamento. HTTPS usa el 443, y nosotros usaremos el 5761 y los siguientes.

El programa que espera a que lo busquen es el servidor. El que busca es el cliente. Cómo viajan los datos lo decide el protocolo:

TCP UDP
Conexión primero se conectan, después hablan sin conexión, cada paquete es una carta aparte
Entrega todo llega, en el mismo orden, o te enteras de que no llegó un paquete puede perderse o llegar en otro orden
Velocidad más lento más rápido
Para qué la web, el correo, los pagos, los archivos voz y video en directo, juegos, sensores
En Java los extremos de una conexión se llaman sockets.
Más precisamente, para curiosos
  • La red se describe en capas. El modelo OSI tiene siete: física (bits por el cable o por el aire), de enlace de datos (la conexión con el dispositivo vecino), de red (direcciones IP y rutas), de transporte (TCP, UDP), de sesión, de presentación y de aplicación (HTTP, correo). En la práctica, internet usa el modelo TCP/IP, más sencillo, con cuatro capas. Tu programa trabaja en la de arriba: Java se encarga de todo lo de abajo.
  • IP entrega paquetes sueltos a una dirección, pero no promete que lleguen. TCP se apoya en IP y numera los paquetes, para poder ordenarlos y volver a pedir los que se perdieron.
  • Protocolos con los que te vas a encontrar: HTTP y HTTPS (la web; HTTPS es HTTP cifrado con TLS, cuyo nombre antiguo es SSL), WebSocket (una conexión en los dos sentidos para chats y notificaciones en el navegador), FTP (archivos), SSH (acceso seguro a una computadora remota).

Míralo

El servidor

El servidor del almacén recibe el nombre de un producto y responde con su precio:

static final Map<String, Double> PRICES = Map.of("café", 7.50, "té", 3.20);
static void serve(ServerSocket server) {
try (Socket client = server.accept();
BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
PrintWriter out = new PrintWriter(client.getOutputStream(), true)) {
String product = in.readLine();
System.out.println("El servidor recibió: " + product);
Double price = PRICES.get(product);
out.println(price == null ? "no existe ese producto" : product + ": " + price + " €");
} catch (IOException e) {
System.out.println("El servidor: " + e.getMessage());
}
}
  • ServerSocket escucha en un puerto. server.accept() espera hasta que se conecta un cliente y devuelve un Socket para la conexión con él.
  • El socket tiene dos flujos: getInputStream() para lo que llega, y getOutputStream() para lo que envías. Los envolvemos, como con los archivos: BufferedReader lee líneas, PrintWriter escribe líneas.
  • true en PrintWriter significa «envía después de cada println». Vas a ver por qué importa.
  • try-with-resources cierra la conexión y los flujos al final.

Míralo

El cliente

El cliente se conecta con new Socket(dirección, puerto) y usa los mismos dos flujos. Aquí el servidor y el cliente están en un solo programa: el servidor funciona en su propio hilo, y main es el cliente. Así puedes probarlos sin una segunda computadora:
ServerSocket server = new ServerSocket(5761);
new Thread(() -> serve(server)).start();
try (Socket socket = new Socket("localhost", 5761);
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {
out.println("café");
System.out.println("El cliente recibió: " + in.readLine());
}
server.close();
Salida
El servidor recibió: café
El cliente recibió: café: 7.5 €
El puerto se abre con new ServerSocket(5761) en main, antes de que arranque el hilo. Así, cuando el cliente se conecta, el servidor ya está escuchando.

Los dos lados se turnan: el cliente escribe una línea y espera una respuesta con readLine(), y el servidor lee la línea y responde. Si cambian de papel y los dos esperan para leer, esperarán para siempre.

ServerSocket server = new ServerSocket(5761);
new Thread(() -> serve(server)).start();
try (Socket socket = new Socket("localhost", 5761);
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {
out.println("café");
System.out.println("El cliente recibió: " + in.readLine());
}
server.close();
Paso a paso, en palabras
  1. El servidor abre el puerto 5761 y empieza a escuchar.
  2. En su propio hilo, el servidor llama a accept() y espera a un cliente.
  3. El cliente se conecta. accept() devuelve un Socket, y cada lado tiene dos flujos: uno para escribir y otro para leer.
  4. El cliente escribe una línea: «café». Viaja por la conexión hasta el flujo de entrada del servidor.
  5. El cliente espera en readLine(). El servidor lee la línea y la imprime.
  6. El servidor busca el precio y responde: «café: 7.5 €».
  7. El readLine() del cliente devuelve la respuesta, y el cliente la imprime.
  8. try cierra el socket, y server.close() cierra el puerto.
Más precisamente, para curiosos
  • El programa completo, con los import:
Main.java
import java.io.*;
import java.net.*;
import java.util.Map;
public class Main {
static final Map<String, Double> PRICES = Map.of("café", 7.50, "té", 3.20);
static void serve(ServerSocket server) {
try (Socket client = server.accept();
BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
PrintWriter out = new PrintWriter(client.getOutputStream(), true)) {
String product = in.readLine();
System.out.println("El servidor recibió: " + product);
Double price = PRICES.get(product);
out.println(price == null ? "no existe ese producto" : product + ": " + price + " €");
} catch (IOException e) {
System.out.println("El servidor: " + e.getMessage());
}
}
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(5761);
new Thread(() -> serve(server)).start();
try (Socket socket = new Socket("localhost", 5761);
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {
out.println("café");
System.out.println("El cliente recibió: " + in.readLine());
}
server.close();
}
}
  • En dos archivos separados y dos terminales es lo mismo: primero arrancas el servidor, después el cliente. Si no hay ningún servidor en el puerto, new Socket lanza ConnectException: Connection refused. Si otro programa ya ocupa el puerto, new ServerSocket lanza BindException: Address already in use.
  • El texto viaja por la red como bytes. Java 18 y las versiones más nuevas convierten el texto a UTF-8 por defecto, así que las tildes, la ñ y el € llegan intactos.

Pruébalo

Espera a un cliente

¿Qué método espera hasta que se conecta un cliente?

ServerSocket server = new ServerSocket(5761);
Socket client = server.();

La idea

Muchos clientes a la vez

Mientras el servidor habla con un cliente, nadie llama a accept() y los demás esperan. Por eso un servidor real hace un bucle infinito: acepta un cliente, se lo da a un hilo nuevo y vuelve enseguida a accept():
static void acceptAll(ServerSocket server) {
int number = 0;
while (true) {
try {
Socket client = server.accept();
number++;
int n = number;
new Thread(() -> handle(client, n)).start();
} catch (IOException e) {
return;
}
}
}
static void handle(Socket client, int number) {
try (client;
BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
PrintWriter out = new PrintWriter(client.getOutputStream(), true)) {
String line;
while ((line = in.readLine()) != null) {
System.out.println("Cliente " + number + ": " + line);
out.println("Eco: " + line);
}
} catch (IOException e) {
System.out.println("Cliente " + number + " se desconectó: " + e.getMessage());
}
}
handle responde a cada línea hasta que el cliente cierra la conexión: entonces readLine() devuelve null. Dos clientes, uno detrás de otro:
ServerSocket server = new ServerSocket(5762);
Thread acceptor = new Thread(() -> acceptAll(server));
acceptor.setDaemon(true);
acceptor.start();
for (String order : new String[] { "mesa 4: dos cafés", "mesa 7: la cuenta" }) {
try (Socket socket = new Socket("localhost", 5762);
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {
out.println(order);
System.out.println(in.readLine());
}
}
server.close();
Salida
Cliente 1: mesa 4: dos cafés
Eco: mesa 4: dos cafés
Cliente 2: mesa 7: la cuenta
Eco: mesa 7: la cuenta
Más precisamente, para curiosos
  • setDaemon(true) convierte el hilo en un hilo «en segundo plano»: el programa termina cuando termina main, sin esperar al bucle infinito. Un servidor real no lo pone en segundo plano: tiene que funcionar hasta que lo detengas.
  • try (client; ...) mete el socket que ya existe en el try-with-resources, para que se cierre al final.
  • Con miles de clientes, un hilo para cada uno sale caro. Entonces el bucle que acepta les da los clientes a un grupo de hilos (thread pool) de la lección anterior: pool.execute(() -> handle(client, n)).

Pruébalo

Los dos esperan

El cliente envía «café» y espera una respuesta. El servidor es el mismo, pero el programa se queda colgado y no imprime nada. ¿En qué línea está el error?

Elige la línea con el error.

La idea

UDP: cartas sin conexión

Los sensores de la tienda envían la temperatura cada minuto. Si se pierde una lectura, la siguiente llega un minuto después, así que aquí UDP viene bien. No hay conexión ni flujos: envías un DatagramPacket (bytes más una dirección y un puerto) a través de un DatagramSocket:
DatagramSocket receiver = new DatagramSocket(5763);
try (DatagramSocket sensor = new DatagramSocket()) {
InetAddress address = InetAddress.getByName("localhost");
for (String reading : new String[] { "cocina 21.5", "almacén 4.0", "nevera 3.2" }) {
byte[] data = reading.getBytes();
sensor.send(new DatagramPacket(data, data.length, address, 5763));
}
}
byte[] buffer = new byte[256];
for (int i = 0; i < 3; i++) {
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
receiver.receive(packet);
String text = new String(packet.getData(), 0, packet.getLength());
System.out.println("Temperatura: " + text + " °C");
}
receiver.close();
Salida
Temperatura: cocina 21.5 °C
Temperatura: almacén 4.0 °C
Temperatura: nevera 3.2 °C
  • El receptor escucha en el puerto 5763 con new DatagramSocket(5763). El emisor usa new DatagramSocket() sin puerto: el sistema le da uno libre.
  • receive espera un paquete y lo escribe en el búfer. getLength() te dice cuántos bytes llegaron.
  • Nadie confirma que el paquete llegó. En localhost no se pierden, pero en una red real sí puede pasar.
Más precisamente, para curiosos

Si el receptor tiene que responder, toma la dirección y el puerto del emisor del propio paquete, con packet.getAddress() y packet.getPort(), y envía allí un paquete nuevo. Un paquete tiene un tamaño máximo (menos de 64 KB, y por internet conviene quedarse en unos 1400 bytes), así que UDP no sirve para archivos.

La idea

HTTP: el idioma de la web

Cuando abres una página, el navegador hace una conexión TCP con el servidor y envía una petición HTTP de texto: «dame /». El servidor devuelve una respuesta: un estado, unas cabeceras y un cuerpo (HTML, JSON, una imagen). En Java esto lo hace HttpClient:

import java.net.URI;
import java.net.http.*;
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder(URI.create("https://example.com")).build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("Estado: " + response.statusCode());
System.out.println("Tipo: " + response.headers().firstValue("content-type").orElse("?"));
String body = response.body();
System.out.println("Título: " + body.substring(body.indexOf("<title>") + 7, body.indexOf("</title>")));
Salida (con internet)
Estado: 200
Tipo: text/html; charset=utf-8
Título: Example Domain
  • HttpRequest.newBuilder(URI) describe la petición, que por defecto es GET.
  • send envía la petición y espera la respuesta. BodyHandlers.ofString() dice «dame el cuerpo como texto».
  • El estado te dice cómo fue: 200 es éxito, 404 significa que no existe esa página, 500 es un error del servidor.

La revisión del código del sitio se hace sin internet, así que este programa se muestra con la salida de una ejecución con red. Sin red, send lanza IOException.

Más precisamente, para curiosos
La propia Java también tiene un servidor HTTP (com.sun.net.httpserver). Así puedes probar HttpClient sin internet, en localhost:
HttpServer server = HttpServer.create(new InetSocketAddress("localhost", 5764), 0);
server.createContext("/stock", exchange -> {
byte[] answer = "café: 12 uds.".getBytes();
exchange.sendResponseHeaders(200, answer.length);
exchange.getResponseBody().write(answer);
exchange.close();
});
server.start();
HttpClient client = HttpClient.newHttpClient();
for (String path : new String[] { "/stock", "/prices" }) {
HttpRequest request = HttpRequest.newBuilder(URI.create("http://localhost:5764" + path)).build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(path + " -> " + response.statusCode() + " " + response.body());
}
server.stop(0);
Salida
/stock -> 200 café: 12 uds.
/prices -> 404 <h1>404 Not Found</h1>No context found for request
La dirección /prices no tiene manejador, así que el servidor devuelve 404 por su cuenta. En proyectos reales, los servidores web se hacen con bibliotecas como Spring Boot. Están construidas sobre los mismos sockets e hilos.

Ejercicio

Servidores en localhost

Resuélvelos en tu editor. El servidor y el cliente pueden estar en un solo programa (el servidor en un hilo) o en dos programas separados, ejecutados en dos terminales.

1. Servidor de eco. El servidor devuelve cada línea con «Eco: » delante, hasta que recibe «fin». Entonces responde «Adiós» y cierra la conexión. El cliente envía «hola», «qué tal» y «fin», e imprime las respuestas:

Salida
Eco: hola
Eco: qué tal
Adiós

2. Un servidor con comandos. MAYUS texto devuelve el texto en mayúsculas, HORA devuelve la hora actual como 14:05 (LocalTime.now() y DateTimeFormatter.ofPattern("HH:mm")), y cualquier otra cosa devuelve «Comando desconocido: » y el comando. La hora cambia, así que el cliente solo comprueba su forma con una expresión regular:

Salida
PEDIDO 42 ESTÁ LISTO
La hora tiene la forma HH:MM: true
Comando desconocido: TIEMPO
3. Un chat para dos. Ana abre el servidor en el puerto 5767, Carlos se conecta. Cada uno está en su propio hilo y envía sus frases preparadas, esperando una respuesta después de cada una:
Salida
Carlos recibió: ¡Hola, Carlos! ¿Estás libre a las 18:00?
Ana recibió: Sí, allí estaré.
Carlos recibió: Genial, hasta pronto.
Ana recibió: ¡Hasta luego!
Después hazlo de verdad: dos programas, cada uno con dos hilos. Un hilo lee del teclado y envía, y el otro lee del socket e imprime. Así puedes escribir mientras esperas una respuesta.

Test

Ponte a prueba

1. ¿A qué apunta el puerto?
2. ¿Cuándo es UDP mejor opción que TCP?
3. ¿Qué hace server.accept()?
4. El cliente escribe con println, pero el servidor no recibe nada, y los dos esperan. ¿Cuál es la causa más común?
5. HttpResponse.statusCode() devuelve 404. ¿Qué significa?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·