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.20o142.250.185.78. Tu propia computadora es siempre127.0.0.1, con el nombrelocalhost. - 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 |
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 aservepor 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 connode:readline, yawait 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
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();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
- El servidor abre el puerto 5761 y empieza a escuchar.
- No hay hilo: Node llamará a serve cuando llegue un cliente. Mientras tanto, el programa sigue.
- El cliente se conecta. Node llama a serve con un socket para el cliente, y los dos lados pueden escribir y leer.
- El cliente escribe una línea: «café». Viaja por la conexión hasta el servidor.
- El cliente espera con await readLine(). El servidor lee la línea y la imprime.
- El servidor busca el precio y responde: «café: 7.5 €».
- El await readLine() del cliente devuelve la respuesta, y el cliente la imprime.
- socket.end() cierra la conexión, y server.close() cierra el puerto.
Más precisamente, para curiosos
- El programa completo, con los
import:
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 errorEADDRINUSE. Los errores llegan como un evento"error": sinsocket.on("error", …)el programa se detiene con ellos. - El texto viaja por la red como bytes.
writeyreadlineusan 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");listen(puerto, dirección) abre el puerto. A partir de ahí, Node llama a serve por cada cliente que se conecta: JavaScript no tiene accept().La idea
Muchos clientes a la vez
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();Cliente 1: mesa 4: dos cafésEco: mesa 4: dos cafésCliente 2: mesa 7: la cuentaEco: mesa 7: la cuentaMás precisamente, para curiosos
numberes compartido por todos los clientes, pero no hay condición de carrera: solo se ejecuta una función a la vez, y entrenumber++yconst n = numberno hay ningúnawait.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.
"\n". El servidor lee líneas, y solo hay una línea cuando llega un "\n". El servidor espera el final de la línea, y el cliente espera una respuesta. La corrección es socket.write("café\n").La idea
UDP: cartas sin conexión
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(); }});Temperatura: cocina 21.5 °CTemperatura: almacén 4.0 °CTemperatura: nevera 3.2 °CcreateSocket("udp4")crea un socket UDP. El receptor escucha en el puerto 5763 conbind(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éssend(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
localhostno 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>"))}`);Estado: 200Tipo: text/html; charset=utf-8Título: Example Domainfetch(dirección)envía una peticiónGETy te da una promesa de la respuesta.awaitla espera.response.text()lee el cuerpo como texto. También es una promesa, así que otra vezawait.- El estado te dice cómo fue:
200es éxito,404significa que no existe esa página,500es un error del servidor.fetchno lanza nada con un 404: revisaresponse.statusoresponse.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
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();/stock -> 200 café: 12 uds./prices -> 404 no existe esa dirección/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
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:
Eco: holaEco: qué talAdiós2. 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:
PEDIDO 42 ESTÁ LISTOLa hora tiene la forma HH:MM: trueComando desconocido: TIEMPOasync, 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):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!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
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Arrays: push, splice y sort · Map y Set · Funciones flecha · Async, await e hilos
- Continúa en: Entrada no confiable · Sesiones, cookies y CSRF · JWT y sus trampas · Diseño de API: contrato, versiones, solicitudes repetidas
- Ejercicios sobre el tema: Programas en el mundo real