martes, 12 de febrero de 2013

[RT] Tarea 2: Implementación de un protocolo

Protocolo para el envió de mensajes a un buzón anónimo.

Propósito

El propósito del protocolo que diseñé es el envío de mensajes cortos vía Internet a un servidor que los almacena en el buzón correspondiente a cada usuario.

La aplicación

Se cuenta con un servidor programado en Python utilizando comunicación UDP, para ello utilicé la librería SocketServer. Para aplicar el protocolo UDP se declara una clase llamada UDPHandler que hereda el módulo SocketServer.BaseRequestHandler, que es un módulo con funciones base para procesar las peticiones al servidor pero en este caso especifico que lo quiero UDP.
Dentro tiene un método llamado handler donde se colocan los pasos que seguirá el servidor para procesar cada petición.
Ahora necesito el servidor UDP funcionando y, como lo quiero multiusuario, necesitamos importar la librería Threading, para ello es necesario implementar correctamente la siguiente clase, llamada ThreadingUDPServer que hereda a las clases SocketServer.ThreadingMixIn y SocketServer.ForkingUDPServer. La clase se queda vacía (pass). El threading server correra un hilo diferente cada vez que se conecte un cliente.

Ésta es la forma más fácil, simple y correcta de implementar un servidor UDP multiusuario.

Lo que acabo de explicar quedaría mas o  menos así en sintaxis:

import SocketServer import threading class MyUDPHandler(SocketServer.BaseRequestHandler): def handle(self): # Como se procesa la peticion # Pasos definidos por el usuario class ThreadingUDPServer(SocketServer.ThreadingMixIn, SocketServer.ForkingUDPServer): pass
El cliente realiza 3 peticiones básicas que son servidas por el servidor:
  • Descargar mensajes: El cliente especifica el ID del buzón para ver los mensajes guardados en el. El servidor lee la base de datos y envía los mensajes uno a uno.
  • Enviar mensajes: El cliente especifica el ID del buzón donde se dejará el mensaje. El servidor guarda el mensaje en el buzón indicado.
  • Obtener buzón: El cliente pide un ID para un nuevo buzón. El servidor genera un ID dinámico ente 1 y 999999.
  • Una opción salir cierra la conexión entre el cliente y el servidor, el servidor continúa corriendo.
Para simular los buzones se utilizan archivos donde el nombre corresponde al ID que se le proporciono al usuario al momento que lo solicito con la opción 3.


Sintaxis del protocolo


Básicamente, el protocolo utiliza 4 datos sencillos dentro de cada "paquete", los datos los nombre de la siguiente forma:
  • Acción: Indica el tipo de petición a servir por el servidor. El dato es de tipo entero.
  • ID1: Reservado para el ID del remitente que envía el mensaje. El dato es de tipo entero.
  • ID2: Contiene el ID del destinatario en el caso del envío de mensajes. En el caso de descarga contiene el ID del buzón a leer. El dato es de tipo entero.
  • Mensaje: Contiene el mensaje a enviar en el caso 2. Contiene el mensaje a descargar en el caso 1. Contiene el ID generado dinamicamente en el caso 3.
El "paquete" del protocolo se representa mediante una tupla de 4 elementos tuple(int, int, int, string), cada  entero ocupa 4 bytes y el mensaje esta limitado a 128 caracteres o 128 bytes, entonces el largo del paquete esta limitado a 140 bytes.
En el caso del empaquetado, si el mensaje es mayor a 128 caracteres entonces es recortado hasta el límite. Si el mensaje es menor a 128 se agrega un caracter de separación ( | ) y el resto se rellena con basura ( . ) porque el tamaño del mensaje es fijo siempre, el caracter de separación le permite al desempaquetador recuperar el mensaje y desechar la basura.

Para codificar los datos se utiliza la librería Struct de python que permite empaquetar una tupla de datos en una representación de bytes utilizando estructuras parecidas a las de C. Se necesita especificar el formato de codificación el cual es "i i i 128s" (int, int int 128string). Después utilizando la función Struct.pack(*datos) los datos se convierten en su presentación en bytes.

Después de empaquetar los datos se encaminan al servidor utilizando un canal UDP y especificando el host y puerto del servidor (localhost, 8080)

El desempaquetado realiza el proceso inverso, con el mismo formato de codificación pero en éste caso es para recuperar la información del paquete. Para ayudar a darle forma a los datos de nuevo se utiliza un diccionario donde se leen los elementos de la tupla recuperada, aquí es donde el caracter de separación agregado en el empaquetado ayuda a recuperar el mensaje y desechar los caracteres de relleno. Los campos del diccionario son: datos["accion"], datos["ID1"], datos["ID2"], datos["mensaje"].


Semántica del protocolo


Como ya expliqué, existen 4 comandos dentro de la aplicación, el servidor solo responde a 3 comandos.

El comando Descargar es especial, es la única opción que permite enviar varios paquetes a la vez alternadamente. Es decir, del lado del servidor, si se desea leer un buzón y el buzón contiene más de un mensaje entonces se leerá el buzón hasta el final y los mensajes se almacenarán en un buffer temporal (list), después por cada mensaje se armará un paquete y se enviarán uno a uno, al final se añadirá un paquete adicional con el mensaje EOT (end-of-transmision).
Del lado del cliente, la aplicación pedirá el ID del buzón que se desea leer y se guardará en el campo ID2 y el campo acción se rellenará con el número 1, los campos ID1 y mensaje permanecen vacíos  La aplicación enviará la petición, creará un buffer temporal (list), y esperará el stream de información. Una vez comenzado el stream desempaquetará todos los mensajes y buscará por la expresión EOT, cuando sea recibida comenzará a vaciar el buffer mostrando al usuario uno a uno los mensajes en la pantalla.

El comando Enviar pide al usuario el ID del buzón destinatario y se guardará en el campo ID2, se pedirá también el mensaje que será empaquetado y se guardará en el campo mensaje. En campo acción se guardará un 2 y el campo ID1 permanecerá vacío.
Del lado del servidor, desempaqueta y recupera la información, buscará por el buzón indicado en el campo ID2 y si existe se abrirá para guardar el mensaje

El comando Obtener no pedirá ningún dato, enviará un paquete donde solo el campo acción contendrá un número 3, los demás campos estarán vacíos.
Del lado del servidor se desempaquetan y recuperan los datos, se generará un ID dinamicamente (random.randint). El servidor responde con paquete igual, solo que ahora en el campo mensaje irá el ID correspondiente al buzón creado y asignado. El usuario puede compartir éste ID para que le envíen mensajes o para recuperar los mensaje de su buzón.

Salir cierra la conexión entre el cliente y el servidor.


Uso de mensajes


Existe un manejo básico de errores y excepciones.

En Descargar el cliente no cuenta con soporte. Del lado del servidor, si  no existe el buzón, se enviarán los mismos paquetes de la misma forma, un stream de 2 mensajes, el primer mensaje contiene la expresión "[X] Error, se intentaron decargar los mensajes de un buzon inexistente (buzon=ID2)", el segundo es la expresión "EOT". El cliente recibirá los datos como si se tratará de una descarga normal pero recibirá el mensaje de error.

En Enviar el cliente no cuenta con soporte. Del lado del servidor, si no existe el buzón se regresará un paquete con el mensaje "[X] Error, se intento dejar un mensaje en un buzon inexistente (buzon=ID2)". El cliente desempaqueta y recupera la información y muestra el mensaje de error en pantalla.

En Obtener no se cuenta con soporte de ambos lados. El servidor se encargará de crear siempre un ID y enviarlo.

De forma general, si hay un error de conexión en ambas partes se genera el mensaje "[X] Error al enviar la peticion (error=ErrorCode)", donde el error code corresponde al código de la excepción capturada por Python en los bloques try...except...

El servidor almacena un LOG de actividades, con los siguientes mensajes.
  • Operación exitosa:
    • [O] Se decargaron los mensajes del buzon [ID2]
    • [O] Se dejo un mensaje en el buzon [ID2]"
    • [O] Nuevo buzon creado [id=ID2]
  • Operación fallida (el servidor los envía como cualquier paquete):
    • [X] Error, se intentaron decargar los mensajes de un buzon inexistente [buzon=ID2]
    • [X] Error, se intento dejar un mensaje en un buzon inexistente [buzon=ID2]
    • [X] Error al enviar la peticion [error=ID2]
El cliente muestra los siguientes mensajes, pero no los almacena en ningú lado:
  • Operación exitosa:
    • [O] Se decargaron [NUM] mensajes
    • [O] Se dejo un mensaje en el buzon [ID2]"
    • [O] Su nuevo buzon es [%s]
  • Operación fallida (el cliente los recibe como respuesta normal del servidor):
    • [X] Error, se intentaron decargar los mensajes de un buzon inexistente [buzon=ID2]
    • [X] Error, se intento dejar un mensaje en un buzon inexistente [buzon=ID2]
    • [X] Error al enviar la peticion [error=ID2]

 

Diagramas







Ejecución





Código

protocolo.py


cliente.py



servidor.py


Referencias:

1 comentario: