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

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
| Paso | segmento | mensaje | respuesta | buffer |
|---|---|---|---|---|
| 1 | 0 | SET color az | ||
| 2 | 1 | SET color azul|GET col | ||
| 3 | 1 | SET color azul | OK | GET col |
| 4 | 2 | GET color|GET talla|COUNT|DEL talla|SET talla | ||
| 5 | 2 | GET color | VAL azul | GET talla|COUNT|DEL talla|SET talla |
| 6 | 2 | GET talla | ERR 404 | COUNT|DEL talla|SET talla |
| 7 | 2 | COUNT | VAL 1 | DEL talla|SET talla |
| 8 | 2 | DEL talla | ERR 404 | SET talla |
| 9 | 3 | SET talla M|GET talla|QUIT|GET color| | ||
| 10 | 3 | SET talla M | OK | GET talla|QUIT|GET color| |
| 11 | 3 | GET talla | VAL M | QUIT|GET color| |
| 12 | 3 | QUIT | BYE | GET color| |
Casos de prueba
| Caso | Entrada | Salida esperada | Salida real | Resultado |
|---|---|---|---|---|
| Ejemplo: órdenes partidas y QUIT con datos detrás | 4
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=10 | OK
VAL azul
ERR 404
VAL 1
ERR 404
OK
VAL M
BYE
RESUMEN procesados=8 pendientes=10 | OK |
| Valores con espacios, minúsculas, errores y mensajes vacíos | 3
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=0 | 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=0 | OK |
| Entrada vacía | (vacía) | RESUMEN procesados=0 pendientes=0 | RESUMEN procesados=0 pendientes=0 | OK |
| Fin del flujo con fragmento incompleto | 2
SET k1 v1|SET k2 v2|SET k1 nuevo|GET k1|COU
NT|DEL k2|GET k2|SET k3 | OK
OK
OK
VAL nuevo
VAL 2
OK
ERR 404
RESUMEN procesados=7 pendientes=6 | OK
OK
OK
VAL nuevo
VAL 2
OK
ERR 404
RESUMEN procesados=7 pendientes=6 | OK |
| QUIT temprano con segmentos sin leer | 3
GET x|quit now|QUIT|SET a
b|
COUNT| | ERR 404
ERR 400 argumentos
BYE
RESUMEN procesados=3 pendientes=14 | ERR 404
ERR 400 argumentos
BYE
RESUMEN procesados=3 pendientes=14 | OK |
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 azen 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 segmentoM|...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 conjoin, 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
- 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.
- Publicación original.
Artículo generado mediante AI.larebelion · Revisado por Kernel


