Saltar al contenido
ES EN
Tema: automático

Ejercicio resuelto: protocolo de texto sobre TCP con buffer en Python

Simula un servidor TCP en Python que reensambla mensajes fragmentados con un buffer, los parsea y responde, con código probado, pistas y errores típicos.

Actualizado el

Código de Examen Python Redes Nivel examen Código probado

Código de Examen: Ejercicio resuelto: protocolo de texto sobre TCP con buffer en Python
Redes de Computadores · Examen finalEjercicio 4 · 2,5 puntos · 30 min

Una empresa quiere un pequeño servicio de configuración llamado KVT (clave-valor sobre texto). El cliente abre una conexión TCP y envía órdenes de texto; el servidor guarda pares clave-valor en memoria y contesta cada orden. No vas a usar sockets: el programa recibe por la entrada estándar los segmentos tal como los entregaría cada llamada a recv() y debe comportarse exactamente como lo haría el servidor real.

Recuerda que TCP es un flujo de bytes, no de mensajes: un segmento puede contener media orden, varias órdenes o el final de una y el principio de otra. En KVT cada mensaje termina con el delimitador \r\n; para que la entrada sea legible, en este ejercicio el delimitador se representa con el carácter |. Todo lo que haya entre dos delimitadores (o entre el inicio del flujo y el primero) es un mensaje.

Formato de entrada. La primera línea contiene un entero N (0 ≤ N ≤ 1000), el número de segmentos. Siguen N líneas, cada una con el contenido exacto de un segmento (entre 1 y 200 caracteres, sin el salto de línea final). Los espacios del segmento, incluidos los iniciales, forman parte de los datos y no deben eliminarse. Si la entrada está vacía, se considera N = 0.

Procesamiento de un mensaje. Primero se eliminan los espacios de sus extremos; si queda vacío, se ignora sin responder. Si no, la primera palabra es la orden (sin distinguir mayúsculas de minúsculas) y las claves sí distinguen mayúsculas. Las órdenes son: SET clave valor, donde el valor es todo lo que sigue a la clave tras saltar los espacios que la separan, y que puede contener espacios internos (responde OK); GET clave (responde VAL, un espacio y el valor); DEL clave (borra y responde OK); COUNT (responde VAL, un espacio y el número de claves almacenadas); QUIT (responde BYE y el servidor cierra la conexión).

Errores. Una orden que no sea ninguna de las anteriores responde ERR 400 comando desconocido. Una orden conocida con un número de argumentos incorrecto responde ERR 400 argumentos: SET necesita clave y valor, GET y DEL exactamente una clave, y COUNT y QUIT ninguno. Un GET o DEL sobre una clave inexistente responde ERR 404.

Formato de salida. Una línea por respuesta, en orden. Al final se escribe RESUMEN procesados=P pendientes=Q, donde P es el número de mensajes no vacíos atendidos y Q el número de caracteres recibidos que no se llegaron a procesar. Si se atiende un QUIT, Q cuenta todo lo que había tras su delimitador, incluidos los segmentos posteriores, que se descartan. Si el flujo se acaba sin QUIT, Q es la longitud del fragmento sin delimitador que quede en el buffer.

Entrada de ejemplo

4
SET color az
ul|GET col
or|GET talla|COUNT|DEL talla|SET talla
 M|GET talla|QUIT|GET color|

Salida esperada

OK
VAL azul
ERR 404
VAL 1
ERR 404
OK
VAL M
BYE
RESUMEN procesados=8 pendientes=10
Reensamblado correcto del flujo: buffer acumulado entre segmentos, extracción de todos los mensajes completos y conservación del fragmento final
1,0
Análisis de cada mensaje y semántica de SET, GET, DEL, COUNT y QUIT, incluido el valor de SET con espacios internos
0,8
Gestión de errores: orden desconocida, número de argumentos incorrecto, clave inexistente y mensajes vacíos ignorados
0,4
Línea RESUMEN exacta, con el cálculo de caracteres pendientes tanto al cerrar con QUIT como al acabar el flujo
0,3

Pistas

Pista 1 · ¿Por qué no puedo tratar cada línea como una orden?

Fíjate en el ejemplo: la primera orden llega partida entre dos segmentos. Necesitas una variable que sobreviva de un segmento al siguiente, un buffer, donde vas concatenando lo recibido. Solo cuando el buffer contiene un | tienes un mensaje completo que se puede procesar.

Pista 2 · ¿Cómo saco varios mensajes de un mismo segmento?

Tras añadir un segmento al buffer, repite mientras quede algún delimitador: corta por el primero, procesa la parte izquierda y deja la derecha en el buffer. str.split("|", 1) o str.partition("|") hacen exactamente ese corte. Lo que quede al salir del bucle es un fragmento incompleto que debe esperar al siguiente segmento.

Pista 3 · ¿Cómo separo la orden sin romper el valor de SET?

Un split() sin límite trocea también el valor y pierde sus espacios internos. Con mensaje.split(None, 2) obtienes como mucho tres piezas: orden, clave y el resto intacto. El número de piezas te sirve además para validar los argumentos de cada orden.

Solución

Solución explicada

La solución separa dos responsabilidades, igual que se haría en un servidor real. La capa de framing (función simular) solo sabe acumular segmentos y cortar mensajes completos. La capa de aplicación (función atender) recibe un mensaje ya completo y decide la respuesta. Así cada parte se puede razonar y probar por separado, y cambiar el delimitador no obliga a tocar la lógica de las órdenes.

import sys

TERMINADOR = "|"


def atender(mensaje, almacen):
    """Procesa un mensaje completo y no vacio. Devuelve (respuesta, cerrar)."""
    partes = mensaje.split(None, 2)
    orden = partes[0].upper()
    args = partes[1:]
    if orden == "SET":
        if len(args) != 2:
            return "ERR 400 argumentos", False
        almacen[args[0]] = args[1]
        return "OK", False
    if orden in ("GET", "DEL"):
        if len(args) != 1:
            return "ERR 400 argumentos", False
        clave = args[0]
        if clave not in almacen:
            return "ERR 404", False
        if orden == "GET":
            return "VAL " + almacen[clave], False
        del almacen[clave]
        return "OK", False
    if orden == "COUNT":
        if args:
            return "ERR 400 argumentos", False
        return "VAL " + str(len(almacen)), False
    if orden == "QUIT":
        if args:
            return "ERR 400 argumentos", False
        return "BYE", True
    return "ERR 400 comando desconocido", False


def simular(segmentos):
    """Reensambla el flujo TCP y devuelve (respuestas, procesados, pendientes)."""
    almacen = {}
    buffer = ""
    respuestas = []
    procesados = 0
    for i, segmento in enumerate(segmentos):
        buffer += segmento
        while TERMINADOR in buffer:
            mensaje, buffer = buffer.split(TERMINADOR, 1)
            mensaje = mensaje.strip()
            if not mensaje:
                continue
            respuesta, cerrar = atender(mensaje, almacen)
            respuestas.append(respuesta)
            procesados += 1
            if cerrar:
                descartados = sum(len(s) for s in segmentos[i + 1:])
                return respuestas, procesados, len(buffer) + descartados
    return respuestas, procesados, len(buffer)


def main():
    lineas = sys.stdin.read().split("\n")
    n = int(lineas[0]) if lineas[0].strip() else 0
    segmentos = [linea.rstrip("\r") for linea in lineas[1:1 + n]]
    respuestas, procesados, pendientes = simular(segmentos)
    for respuesta in respuestas:
        print(respuesta)
    print(f"RESUMEN procesados={procesados} pendientes={pendientes}")


if __name__ == "__main__":
    main()

Lectura. En main se lee toda la entrada con sys.stdin.read() y se divide por saltos de línea. Es deliberado no usar strip() sobre los segmentos: solo se quita un posible \r final. En el ejemplo, el cuarto segmento empieza por un espacio que separa la clave talla del valor M; si se recortara, el buffer contendría SET tallaM y el servidor respondería ERR 400 argumentos en lugar de OK. Si la primera línea está vacía, n vale 0, y así funciona el caso de entrada vacía.

Framing. En simular, la variable buffer es el estado de la conexión. Cada segmento se concatena y, mientras haya un TERMINADOR, buffer.split(TERMINADOR, 1) separa el primer mensaje completo del resto. En el ejemplo, tras el primer segmento el buffer queda en SET color az y no se responde nada; con el segundo se completa SET color azul, se responde OK y queda GET col esperando. El tercer segmento cierra ese GET y trae tres órdenes más, que el bucle while atiende sin salir del mismo segmento.

Mensajes vacíos. El strip() sí se aplica aquí, al mensaje ya cortado, porque el enunciado lo exige. Un || o un mensaje formado solo por espacios produce una cadena vacía y el continue lo salta sin incrementar procesados. El test de órdenes mixtas incluye ambos casos y por eso cuenta 9 mensajes atendidos, aunque el flujo contenga 11 delimitadores.

Capa de aplicación. En atender, mensaje.split(None, 2) divide por cualquier secuencia de espacios en, como mucho, tres piezas. Para set saludo hola mundo produce la orden, la clave y hola mundo con sus dos espacios internos, que es justo lo que devuelve el GET posterior. Solo la orden pasa por upper(); la clave se usa tal cual, porque distingue mayúsculas. La lista args sirve para validar: GET a b deja dos argumentos y se rechaza, igual que Count extra o quit now.

Cierre y resumen. atender devuelve una tupla cuyo segundo elemento indica si hay que cerrar. Cuando QUIT devuelve True, simular calcula los pendientes como lo que queda en buffer más la longitud de todos los segmentos posteriores (segmentos[i + 1:]). En el ejemplo queda GET color| en el buffer, 10 caracteres, y no hay más segmentos. En el test de QUIT temprano quedan SET a (5) y dos segmentos de 3 y 6 caracteres: 14. Si no hay QUIT, como en el test de fragmento final, se devuelve la longitud del buffer: SET k3, 6.

Traza paso a paso · generada ejecutando el código

Pasosegmentomensajerespuestabuffer
10SET color az
21SET color azul|GET col
31SET color azulOKGET col
42GET color|GET talla|COUNT|DEL talla|SET talla
52GET colorVAL azulGET talla|COUNT|DEL talla|SET talla
62GET tallaERR 404COUNT|DEL talla|SET talla
72COUNTVAL 1DEL talla|SET talla
82DEL tallaERR 404SET talla
93SET talla M|GET talla|QUIT|GET color|
103SET talla MOKGET talla|QUIT|GET color|
113GET tallaVAL MQUIT|GET color|
123QUITBYEGET color|

Casos de prueba

CasoEntradaSalida esperadaSalida realResultado
Ejemplo: órdenes partidas y QUIT con datos detrás4 SET color az ul|GET col or|GET talla|COUNT|DEL talla|SET talla M|GET talla|QUIT|GET color|OK VAL azul ERR 404 VAL 1 ERR 404 OK VAL M BYE RESUMEN procesados=8 pendientes=10OK VAL azul ERR 404 VAL 1 ERR 404 OK VAL M BYE RESUMEN procesados=8 pendientes=10OK
Valores con espacios, minúsculas, errores y mensajes vacíos3 set saludo hola mundo |get saludo|PING|GET|GET a b|| | Count extra|del saludo|COUNT| QUIT|OK VAL hola mundo ERR 400 comando desconocido ERR 400 argumentos ERR 400 argumentos ERR 400 argumentos OK VAL 0 BYE RESUMEN procesados=9 pendientes=0OK VAL hola mundo ERR 400 comando desconocido ERR 400 argumentos ERR 400 argumentos ERR 400 argumentos OK VAL 0 BYE RESUMEN procesados=9 pendientes=0OK
Entrada vacía(vacía)RESUMEN procesados=0 pendientes=0RESUMEN procesados=0 pendientes=0OK
Fin del flujo con fragmento incompleto2 SET k1 v1|SET k2 v2|SET k1 nuevo|GET k1|COU NT|DEL k2|GET k2|SET k3OK OK OK VAL nuevo VAL 2 OK ERR 404 RESUMEN procesados=7 pendientes=6OK OK OK VAL nuevo VAL 2 OK ERR 404 RESUMEN procesados=7 pendientes=6OK
QUIT temprano con segmentos sin leer3 GET x|quit now|QUIT|SET a b| COUNT|ERR 404 ERR 400 argumentos BYE RESUMEN procesados=3 pendientes=14ERR 404 ERR 400 argumentos BYE RESUMEN procesados=3 pendientes=14OK

Salidas reales: código compilado con Python 3.13.16 y ejecutado en un contenedor aislado el 30 de octubre de 2026.

Complejidad

Sea L el número total de caracteres recibidos y M el número de mensajes. Cada orden de la capa de aplicación cuesta O(longitud del mensaje) por el split más O(1) en media por el acceso al diccionario. El framing hace en cada segmento una búsqueda del delimitador y un corte por mensaje; como cada split copia el resto del buffer, en el peor caso un segmento con k mensajes cuesta O(k · 200). Con segmentos de como mucho 200 caracteres eso es una constante, y el total es O(L).

Hay un caso degenerado que conviene conocer: un mensaje muy largo repartido en muchos segmentos hace que buffer += segmento copie una cadena cada vez mayor, lo que da O(L²) en el peor caso. Un servidor real lo evita con un bytearray y un índice de lectura, o buscando el delimitador solo en la parte recién llegada. La memoria es O(L) en el peor caso para el buffer, más O(C) para el almacén, siendo C el tamaño de los pares guardados.

Errores típicos

  • Tratar cada segmento como un mensaje completo. Es suponer que TCP conserva los límites de los mensajes, y falla en cuanto una orden llega partida, como SET color az en el ejemplo.
  • Hacer segmento.split("|") sobre cada segmento por separado y descartar el último trozo, perdiendo así el fragmento que debía completarse con el segmento siguiente.
  • Leer los segmentos con input().strip(). Se pierden los espacios iniciales, que son datos: el segmento  M|... del ejemplo se pegaría a la clave anterior.
  • Usar split() sin límite para analizar SET, lo que trocea el valor y obliga a reconstruirlo con join, perdiendo los espacios dobles internos.
  • Pasar a mayúsculas el mensaje entero en vez de solo la orden, de modo que las claves dejan de distinguir mayúsculas y los valores se alteran.
  • Al procesar QUIT, contar como pendiente solo lo que queda en el buffer y olvidar los segmentos que todavía no se habían leído.

Variantes

Framing por longitud en lugar de por delimitador

El profesor puede cambiar el protocolo para que cada mensaje vaya precedido de su longitud en decimal y dos puntos, por ejemplo 5:COUNT. La función atender no cambia. En simular, el bucle pasa a tener dos estados: si aún no se conoce la longitud, se busca el : y se convierte a entero lo anterior; si ya se conoce, se espera a que el buffer tenga al menos esa cantidad de caracteres y se corta exactamente ahí. La ventaja es que el contenido puede incluir cualquier carácter, también el delimitador.

Límite de tamaño del buffer

Otra pregunta habitual: si el buffer supera, por ejemplo, 512 caracteres sin contener un delimitador, el servidor responde ERR 413 y cierra. Basta con comprobar len(buffer) al salir del bucle while de cada segmento y tratarlo como un cierre, igual que QUIT, calculando los pendientes de la misma forma. Así un cliente que nunca envía el delimitador no puede hacer crecer la memoria del servidor sin límite.

En los temarios

El modelo de capas y, en particular, la relación entre la capa de transporte y la de aplicación que trabaja este ejercicio forman parte del temario de varias asignaturas latinoamericanas. En la Universidad de los Andes, ISIS3204 Infraestructura de Comunicaciones, el programa pone especial énfasis en la pila TCP/IP como implementación real de los conceptos de redes. La planificación de Redes de Datos de la Universidad Tecnológica Nacional (FRC) incluye entre sus contenidos el protocolo TCP/IP, la capa de transporte y la capa de aplicación.

También aparecen contenidos de redes en las sumillas de Ingeniería Informática de la Pontificia Universidad Católica del Perú, que recogen la asignatura Redes de computadoras junto a la comunicación entre procesos, y en el programa de IIC2333 Sistemas Operativos y Redes de la Pontificia Universidad Católica de Chile. Para repasar el diccionario que actúa como almacén del servidor, puede servir la colección de ejercicios de diccionarios, sets y tuplas en Python.

Historial de cambios

  1. Reescrito con la plantilla de Código de Examen: enunciado de examen, pistas, solución explicada, casos de prueba y código compilado y ejecutado; versión en inglés enlazada.
  2. Publicación original.

Artículo generado mediante AI.larebelion · Revisado por Kernel

Kernel

· Dirección de tecnología · España

«El framing aprueba el examen; decidir cuánto buffer aceptas por conexión es lo que mantiene vivo el servidor en producción.»