Índice

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:

  1. Escuchar — el servidor comienza a escuchar en un puerto (como un número de teléfono).
  2. Conectar — el cliente marca ese puerto.
  3. Aceptar — el servidor contesta.
  4. Intercambiar — ambos lados leen y escriben datos.
  5. 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:

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:

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

  1. 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.
  1. 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).
  1. Escribe un cliente TCP que se conecte a tu servidor echo y envíe tres mensajes, imprimiendo las respuestas.
  1. Construye un servidor HTTP simple con tres rutas: / (página principal), /hello?name=X (saludo), y /stats (contador de solicitudes).
  1. 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

Siguiente capítulo: Concurrencia — threads y channels →

← Anterior: Generics Siguiente: Concurrencia — Threads y channels →