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
Los extremos de una conexión se llaman sockets. En Node los crea el módulo node:net.
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: Node y el sistema operativo se encargan 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:

// await readLine() espera la siguiente línea del socket (undefined cuando la conexión está cerrada).
function lineReader(socket) {
const lines = readline.createInterface({ input: socket })[Symbol.asyncIterator]();
return async () => (await lines.next()).value;
}
const PRICES = new Map([["café", 7.50], ["té", 3.20]]);
async function serve(client) {
const readLine = lineReader(client);
const product = await readLine();
console.log(`El servidor recibió: ${product}`);
const price = PRICES.get(product);
client.end(price === undefined ? "no existe ese producto\n" : `${product}: ${price} €\n`);
}
  • No hay hilo ni accept(). Node llama a serve por cada cliente que se conecta y le da un socket para la conexión (client).
  • El socket es un flujo: los bytes llegan en trozos, a medida que llegan. lineReader (arriba) los junta en líneas con node:readline, y await readLine() espera la siguiente línea.
  • client.end(texto) envía el último texto y cierra la conexión. El texto viaja por la red en UTF-8. El "\n" del final le dice al otro lado que la línea terminó. Vas a ver por qué importa.

Míralo

El cliente

El cliente se conecta con net.connect(puerto, dirección) y usa los mismos lineReader y write. Aquí el servidor y el cliente están en un solo programa: Node atiende a los dos lados en un solo hilo, alternándolos mientras cada uno espera. Así puedes probarlos sin una segunda computadora:
const server = net.createServer(serve);
server.listen(5761, "localhost");
await once(server, "listening");
const socket = net.connect(5761, "localhost");
const readLine = lineReader(socket);
socket.write("café\n");
console.log(`El cliente recibió: ${await readLine()}`);
socket.end();
server.close();
Salida
El servidor recibió: café
El cliente recibió: café: 7.5 €
server.listen(5761, "localhost") abre el puerto, y await once(server, "listening") espera hasta que está abierto. 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 await readLine(), y el servidor lee la línea y responde. Si cambian de papel y los dos esperan para leer, esperarán para siempre.

const server = net.createServer(serve);
server.listen(5761, "localhost");
await once(server, "listening");
const socket = net.connect(5761, "localhost");
const readLine = lineReader(socket);
socket.write("café\n");
console.log(`El cliente recibió: ${await readLine()}`);
socket.end();
server.close();
Paso a paso, en palabras
  1. El servidor abre el puerto 5761 y empieza a escuchar.
  2. No hay hilo: Node llamará a serve cuando llegue un cliente. Mientras tanto, el programa sigue.
  3. El cliente se conecta. Node llama a serve con un socket para el cliente, y los dos lados pueden escribir y leer.
  4. El cliente escribe una línea: «café». Viaja por la conexión hasta el servidor.
  5. El cliente espera con await readLine(). El servidor lee la línea y la imprime.
  6. El servidor busca el precio y responde: «café: 7.5 €».
  7. El await readLine() del cliente devuelve la respuesta, y el cliente la imprime.
  8. socket.end() cierra la conexión, y server.close() cierra el puerto.
Más precisamente, para curiosos
  • El programa completo, con los import:
prices.js
import net from "node:net";
import readline from "node:readline";
import { once } from "node:events";
// await readLine() espera la siguiente línea del socket (undefined cuando la conexión está cerrada).
function lineReader(socket) {
const lines = readline.createInterface({ input: socket })[Symbol.asyncIterator]();
return async () => (await lines.next()).value;
}
const PRICES = new Map([["café", 7.50], ["té", 3.20]]);
async function serve(client) {
const readLine = lineReader(client);
const product = await readLine();
console.log(`El servidor recibió: ${product}`);
const price = PRICES.get(product);
client.end(price === undefined ? "no existe ese producto\n" : `${product}: ${price} €\n`);
}
const server = net.createServer(serve);
server.listen(5761, "localhost");
await once(server, "listening");
const socket = net.connect(5761, "localhost");
const readLine = lineReader(socket);
socket.write("café\n");
console.log(`El cliente recibió: ${await readLine()}`);
socket.end();
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, el socket da un error ECONNREFUSED. Si otro programa ya ocupa el puerto, el servidor da un error EADDRINUSE. Los errores llegan como un evento "error": sin socket.on("error", …) el programa se detiene con ellos.
  • El texto viaja por la red como bytes. write y readline usan UTF-8 por defecto, así que las tildes, la ñ y el € llegan intactos.

Pruébalo

Abre el puerto

¿Qué método abre el puerto, para que el servidor pueda esperar clientes?

const server = net.createServer(serve);
server.(5761, "localhost");

La idea

Muchos clientes a la vez

Aquí JavaScript es más sencillo. No hay bucle para aceptar: Node llama a handle por cada cliente nuevo. Mientras un cliente espera, Node atiende a los demás, todo en un solo hilo:
let number = 0;
async function handle(client) {
number++;
const n = number;
for await (const line of readline.createInterface({ input: client })) {
console.log(`Cliente ${n}: ${line}`);
client.write(`Eco: ${line}\n`);
}
}
handle responde a cada línea hasta que el cliente cierra la conexión: entonces for await termina. Dos clientes, uno detrás de otro:
const server = net.createServer(handle);
server.listen(5762, "localhost");
await once(server, "listening");
for (const order of ["mesa 4: dos cafés", "mesa 7: la cuenta"]) {
const socket = net.connect(5762, "localhost");
const readLine = lineReader(socket);
socket.write(`${order}\n`);
console.log(await readLine());
socket.end();
}
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
  • number es compartido por todos los clientes, pero no hay condición de carrera: solo se ejecuta una función a la vez, y entre number++ y const n = number no hay ningún await.
  • server.close() deja de aceptar conexiones nuevas. Node termina el programa cuando también se cierran las antiguas.
  • Este único hilo basta para miles de clientes, porque la mayor parte del tiempo están esperando a la red. Si el servidor tiene que calcular mucho, el trabajo pesado va a un Worker de la lección anterior.

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. Los paquetes los envía el módulo node:dgram (datagram, un paquete suelto):
const receiver = dgram.createSocket("udp4");
receiver.bind(5763, "localhost");
await once(receiver, "listening");
const sensor = dgram.createSocket("udp4");
sensor.connect(5763, "localhost");
await once(sensor, "connect");
for (const reading of ["cocina 21.5", "almacén 4.0", "nevera 3.2"]) {
sensor.send(reading);
}
let count = 0;
receiver.on("message", (data) => {
console.log(`Temperatura: ${data} °C`);
count++;
if (count === 3) {
receiver.close();
sensor.close();
}
});
Salida
Temperatura: cocina 21.5 °C
Temperatura: almacén 4.0 °C
Temperatura: nevera 3.2 °C
  • createSocket("udp4") crea un socket UDP. El receptor escucha en el puerto 5763 con bind(5763, "localhost").
  • El emisor llama a connect(5763, "localhost"): con UDP esto no establece una conexión, solo recuerda adónde van los paquetes. Después send(texto) envía un paquete.
  • Cada paquete llega como un evento "message". Después del tercero, el programa cierra los dos sockets; si no, Node esperaría más paquetes para siempre.
  • 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 segundo parámetro de "message": (data, rinfo) => receiver.send(respuesta, rinfo.port, rinfo.address). 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 Node (y en el navegador) esto lo hace fetch, sin ningún import:

const response = await fetch("https://example.com");
console.log(`Estado: ${response.status}`);
console.log(`Tipo: ${response.headers.get("content-type")}`);
const body = await response.text();
console.log(`Título: ${body.slice(body.indexOf("<title>") + 7, body.indexOf("</title>"))}`);
Salida (con internet)
Estado: 200
Tipo: text/html; charset=utf-8
Título: Example Domain
  • fetch(dirección) envía una petición GET y te da una promesa de la respuesta. await la espera.
  • response.text() lee el cuerpo como texto. También es una promesa, así que otra vez await.
  • El estado te dice cómo fue: 200 es éxito, 404 significa que no existe esa página, 500 es un error del servidor. fetch no lanza nada con un 404: revisa response.status o response.ok.

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, fetch lanza TypeError: fetch failed.

Más precisamente, para curiosos
El propio Node también tiene un servidor HTTP (node:http). Así puedes probar fetch sin internet, en localhost:
const server = http.createServer((request, response) => {
if (request.url !== "/stock") {
response.writeHead(404).end("no existe esa dirección");
return;
}
response.writeHead(200, { "content-type": "text/plain; charset=utf-8" }).end("café: 12 uds.");
});
server.listen(5764, "localhost");
await once(server, "listening");
for (const path of ["/stock", "/prices"]) {
const response = await fetch(`http://localhost:5764${path}`);
console.log(`${path} -> ${response.status} ${await response.text()}`);
}
server.close();
Salida
/stock -> 200 café: 12 uds.
/prices -> 404 no existe esa dirección
Node llama a la función del servidor por cada petición. Para /prices devuelve 404, y fetch te lo da como una respuesta normal. En proyectos reales, los servidores web se hacen con bibliotecas como Express. Están construidas sobre el mismo node:http.

Ejercicio

Servidores en localhost

Resuélvelos en tu editor. El servidor y el cliente pueden estar en un solo programa 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 (new Date().toTimeString().slice(0, 5)), 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 es una función async, las dos arrancan juntas con Promise.all, y cada uno envía sus frases preparadas, esperando una respuesta después de cada una (await once(server, "connection") espera al primer cliente):
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 lee líneas del teclado (readline con input: process.stdin) y las envía, e imprime las líneas del socket en cuanto llegan. No necesitas un segundo hilo: las dos cosas llegan como eventos en el bucle de eventos. 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 net.createServer(serve)?
4. El cliente envía socket.write("café"), y el servidor lee líneas. Los dos esperan. ¿Por qué?
5. response.status de fetch es 404. ¿Qué significa?

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