Networking — Servidores TCP
Programas que hablan
Cada programa que has escrito hasta ahora se ejecuta solo. Lee archivos, computa cosas e imprime resultados. Pero la mayoría del software actual está conectado en red — habla con otros programas a través de internet.
Cuando abres un sitio web, tu navegador (un programa) envía una solicitud a un servidor (otro programa). El servidor lee la solicitud, la procesa y envía una respuesta. Esa conversación ocurre sobre TCP (Protocolo de Control de Transmisión) — la base de internet.
En este capítulo, aprenderás a escribir ambos lados: clientes que envían solicitudes y servidores que las manejan.
Conceptos básicos de TCP
TCP funciona como una llamada telefónica:
- Escuchar — el servidor comienza a escuchar en un puerto (como un número de teléfono).
- Conectar — el cliente marca ese puerto.
- Aceptar — el servidor contesta.
- Intercambiar — ambos lados leen y escriben datos.
- Cerrar — cualquier lado cuelga.
Un puerto es un número (0–65535) que identifica un servicio en una máquina. Los servidores web típicamente usan el puerto 80 (HTTP) o 443 (HTTPS). Para desarrollo, usamos puertos como 8080 o 9000.
Iniciar un servidor TCP
fn main() { let server: int = tcp_listen("0.0.0.0", 9000) print("Servidor escuchando en puerto 9000...") let client: int = tcp_accept(server) print("¡Cliente conectado!") tcp_write(client, "¡Hola desde Nyx!\n") tcp_close(client) tcp_close(server) }
Desglosemos esto:
tcp_listen("0.0.0.0", 9000)— empieza a escuchar en todas las interfaces, puerto 9000. Devuelve un socket de servidor (un entero).tcp_accept(server)— espera a que un cliente se conecte. Esto bloquea — el programa se pausa aquí hasta que alguien se conecte. Devuelve un socket de cliente.tcp_write(client, ...)— envía datos al cliente.tcp_close(...)— cierra la conexión.
Para probar esto, ejecuta el servidor en una terminal, y en otra:
echo "hola" | nc localhost 9000
Verás "¡Hola desde Nyx!" impreso por nc.
Leer datos del cliente
fn main() { let server: int = tcp_listen("0.0.0.0", 9000) print("Servidor echo en puerto 9000...") let client: int = tcp_accept(server) let mensaje: String = tcp_read_line(client) print("Recibido: " + mensaje) tcp_write(client, "Echo: " + mensaje + "\n") tcp_close(client) tcp_close(server) }
tcp_read_line(client) lee datos del cliente hasta que encuentra un carácter de salto de línea. Esto es perfecto para protocolos basados en texto.
Para leer bytes crudos, usa tcp_read(client, max_bytes) que lee hasta max_bytes bytes.
Un servidor que maneja múltiples clientes
Los ejemplos anteriores manejan un cliente y luego se detienen. Un servidor real sigue ejecutándose:
fn main() { let server: int = tcp_listen("0.0.0.0", 9000) print("Servidor echo ejecutándose en puerto 9000...") while 1 > 0 { let client: int = tcp_accept(server) let linea: String = tcp_read_line(client) if linea.length() > 0 { print("Recibido: " + linea) tcp_write(client, "Echo: " + linea + "\n") } tcp_close(client) } }
Este bucle infinito acepta un cliente, lee una línea, la devuelve como echo, cierra la conexión, y espera al siguiente cliente. Este es un servidor secuencial — maneja un cliente a la vez. (En el Capítulo 18, aprenderás a manejar muchos clientes concurrentemente.)
Escribir un cliente TCP
El otro lado de la conversación — un programa que se conecta a un servidor:
fn main() { let sock: int = tcp_connect("127.0.0.1", 9000) if sock < 0 { print("No se pudo conectar") return 0 } tcp_write(sock, "¡Hola servidor!\n") let respuesta: String = tcp_read_line(sock) print("El servidor dijo: " + respuesta) tcp_close(sock) }
tcp_connect(host, puerto) se conecta a un servidor y devuelve un socket. Si la conexión falla, devuelve un valor negativo.
Construir una respuesta HTTP a mano
HTTP (el protocolo de la web) está construido sobre TCP. Una respuesta HTTP es solo texto con un formato específico:
fn http_ok(body: String) -> String { return "HTTP/1.1 200 OK\r\nContent-Length: " + int_to_string(body.length()) + "\r\n\r\n" + body } fn main() { let server: int = tcp_listen("0.0.0.0", 8080) print("Servidor HTTP en http://localhost:8080") while 1 > 0 { let client: int = tcp_accept(server) // Leer la solicitud (la primera línea es suficiente) let linea_solicitud: String = tcp_read_line(client) print("Solicitud: " + linea_solicitud) // Enviar una respuesta let respuesta: String = http_ok("<h1>¡Hola desde Nyx!</h1>") tcp_write(client, respuesta) tcp_close(client) } }
Abre http://localhost:8080 en un navegador y verás "¡Hola desde Nyx!" — una página web servida por tu programa.
Usar la biblioteca HTTP
Construir HTTP a mano es educativo, pero Nyx proporciona una forma mucho más fácil:
import { http_serve, http_response } from "std/http" fn al_recibir_solicitud(request: Array) -> String { let metodo: String = request[1] let ruta: String = request[2] if ruta == "/" { return http_response(200, "¡Bienvenido a Nyx!") } if ruta == "/about" { return http_response(200, "Nyx es un lenguaje compilado.") } return http_response(404, "No Encontrado") } fn main() { print("Servidor ejecutándose en http://localhost:8080") http_serve(8080, al_recibir_solicitud) }
http_serve maneja toda la lógica TCP por ti. Tu función recibe un array de solicitud parseado y devuelve un string de respuesta. El array de solicitud contiene:
request[1]— método (GET, POST, etc.)request[2]— ruta (/about, /api/data, etc.)request[3]— headers (array plano de pares clave-valor)request[4]— body (para solicitudes POST)
Resolución DNS
Antes de conectarte a un servidor remoto, podrías necesitar resolver un nombre de host:
fn main() { let ip: String = resolve("example.com") print("example.com está en " + ip) let sock: int = tcp_connect(ip, 80) tcp_write(sock, "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n") let respuesta: String = tcp_read(sock, 4096) print(respuesta) tcp_close(sock) }
resolve(hostname) devuelve la dirección IP como un string.
Ejemplo práctico: un servidor clave-valor
Construyamos un servidor simple que almacena y recupera valores:
fn main() { var almacen: Map = Map.new() let server: int = tcp_listen("0.0.0.0", 7000) print("Servidor KV en puerto 7000") while 1 > 0 { let client: int = tcp_accept(server) let linea: String = tcp_read_line(client) if linea.startsWith("SET ") { let resto: String = linea.substring(4, linea.length()) let espacio: int = resto.indexOf(" ") if espacio > 0 { let clave: String = resto.substring(0, espacio) let valor: String = resto.substring(espacio + 1, resto.length()) almacen.insert(clave, valor) tcp_write(client, "OK\n") } } if linea.startsWith("GET ") { let clave: String = linea.substring(4, linea.length()) if almacen.contains(clave) { tcp_write(client, almacen.get(clave) + "\n") } else { tcp_write(client, "NO ENCONTRADO\n") } } tcp_close(client) } }
Pruébalo:
echo "SET nombre Alice" | nc localhost 7000 # OK echo "GET nombre" | nc localhost 7000 # Alice echo "GET desconocido" | nc localhost 7000 # NO ENCONTRADO
Ejercicios
- Escribe un servidor echo que lea líneas de un cliente y devuelva cada línea en mayúsculas. Pista: convierte cada carácter usando aritmética ASCII.
- Escribe un "servidor de tiempo" que responda a cualquier conexión con el mensaje "Servidor activo: N conexiones atendidas" (cuenta cuántos clientes se han conectado).
- Escribe un cliente TCP que se conecte a tu servidor echo y envíe tres mensajes, imprimiendo las respuestas.
- Construye un servidor HTTP simple con tres rutas:
/(página principal),/hello?name=X(saludo), y/stats(contador de solicitudes).
- Construye un servidor tipo chat donde el servidor lee líneas de un cliente y responde según palabras clave: "hola" → "¡Qué tal!", "adiós" → "¡Hasta luego!", cualquier otra cosa → "No entiendo."
Resumen
tcp_listen(host, puerto)inicia un servidor TCP. Devuelve un socket de servidor.tcp_accept(server)espera a un cliente. Devuelve un socket de cliente.tcp_connect(host, puerto)se conecta a un servidor. Devuelve un socket.tcp_read(sock, max)lee hastamaxbytes.tcp_read_line(sock)lee una línea.tcp_write(sock, datos)envía datos.tcp_close(sock)cierra una conexión.resolve(hostname)convierte un nombre de host a una dirección IP.- HTTP es texto sobre TCP. El
http_servede Nyx maneja el protocolo por ti. - Los servidores secuenciales manejan un cliente a la vez — la concurrencia viene a continuación.
Siguiente capítulo: Concurrencia — threads y channels →