lunes, 3 de junio de 2024

Redes con Python: Programación de Sockets para la Comunicación. 5.- Parsing_serialización de datos.

Vamos a introducir en este post dos conceptos:

Parsing (análisis) y Serialización.

El parsing o análisis es el proceso de convertir los datos nuevamente en sus componentes. Toma un flujo de datos (estructurados) y luego hace algo basado en el contenido de ese flujo, generalmente creando una estructura en memoria basada en ella o ejecutando una secuencia de operaciones basada en ella.

La serialización es el proceso de convertir una estructura de datos en un conjunto de bytes, es decir, un formato que pueda almacenarse o transmitirse de forma segura y recrearse a partir del formato almacenado o transmitido. Generalmente se usa para referirse a la aplicación de este proceso a estructuras de datos complejas como árboles, objetos o gráficos, ya que no se prestan inherentemente a ser transmitidos como una simple cadena de datos binarios. Convertir un objeto en memoria a JSON o XML es un ejemplo de serialización.

Vamos a ver esta idea mejor con un ejemplo práctico.

Ponte en la piel de un programador que tiene que encontrar una solución al siguiente problema. Necesita comprobar todos los ordenadores de una red para asegurarse de que tienen instalada la última versión de Python.

Para ello va a crear un servidor y clientes en cada unos de los equipos. Cada cliente debería enviar la siguiente información al servidor:

- La fecha actual.

- El nombre de red del cliente.

- La versión de Python que el cliente está usando.

Con lo que ya conocemos sobre como crear un cliente usando el protocolo TCP/IP vamos a crear este programa de una forma muy básica. Crea un archivo, yo lo llamará cliente.py y añade el siguiente código:

cliente.py

import socket
from time import time, ctime
from platform import node
import pickle
from sys import version

fecha_actual = time()
nombre_red = node()
version_python = version

print("fecha_actual:", ctime(fecha_actual))
print ('Descripción:', node()) 
print(version_python)

Salida:

fecha_actual: Mon Jun  3 17:48:08 2024
Descripción: machine
3.10.12 (main, Nov 20 2023, 15:14:05) [GCC 11.4.0]


import socket.

Módulo para crear la conexión.

from time import time, ctime. 

La función time nos facilita la fecha actual en formato de tiempo Unix. Como queremos verlo más humanizado utilizamos el modulo ctime que nos trasforma el tiempo Unix en la fecha actual más legible.

from platform import node.

Con este módulo, podremos acceder a los datos de nuestro sistema tales como: el hardware, el sistema operativo, etc. utilizamos node para obtener simplemente el nombre de red de la máquina.

import pickle.

Librería para serializar datos es decir convertir datos en bytes o viceversa que pueden ser guardados o enviados.

from sys import version.

Con este módulo obtendremos la versión actual de Python en el sistema.

Ya tenemos todo lo necesario que el cliente tiene que enviar al servidor. Tenemos todos los datos en formato Str (texto); si hubiera algo en formato numérico, por ejemplo, habría que pasarlo a texto.

Vamos a prepararlo para enviar estos datos al servidor. Tenemos que crear el mensaje y como siempre codificarlo para poderlo enviar. Finalmente implantamos el código para conectarnos al servidor y enviar el mensaje.

cliente.py

from time import time, ctime
from platform import node
import pickle
from sys import version

fecha_actual = time()
nombre_red = node()
version_python = version

print("fecha_actual:", ctime(fecha_actual))
print ('Descripción:', node()) 
print(version_python)

from time import time, ctime
from platform import node
import pickle
from sys import version

fecha_actual_unix = time()

fecha_actual = ctime(fecha_actual_unix)
nombre_red = node()
version_python = version

print("fecha_actual:", fecha_actual)
print ('Descripción:', node()) 
print(version_python)

mensaje = fecha_actual + "\n" + nombre_red + "\n" + version_python
data = mensaje.encode()

HOST = "127.0.0.1"  # El nombre del servidor o IP del mismo
PORT = 8081  # El puerto usado por el servidor

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client_socket:
    client_socket.connect((HOST, PORT))
    client_socket.send(data)


Por otra parte tenemos que crear el código para que el servidor reciba cada una de las lecturas que le irán enviando los clientes. Un código muy sencillo.


server.py

import socket

HOST = '127.0.0.1' # Standard loopback interface address (localhost)
PORT = 8081 # Port to listen on (non-privileged ports are > 1023)

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:

    # Vincula el socket a una dirección y puerto específicos
    s.bind((HOST, PORT))

    # Escucha conexiones entrantes (el argumento especifica el tamaño máximo de la cola de conexiones)
    s.listen()

    print("Servidor escuchando en el puerto 8081...")

    # Aceptar una conexión
    client_socket, client_address = s.accept()
    with client_socket:
        print(f"Conexión aceptada de {client_address}")
        
        mensaje = "Hola,¡Gracias por conectarte!"
        data = client_socket.recv(1024)
        mensaje = data.decode()
        print(mensaje)

El código anterior debería decodificar los bytes recibidos del cliente y realizar un parsing del mensaje para transformarlo en sus datos originales. Incluso para un mensaje bastante simple como el nuestro el proceso de traspasar los bytes en sus datos originales es bastante farragoso. Es relativamente lento de ejecutar, difícil de cambiar y propenso a que se comentan errores.

Por eso podemos usar la serialización. Como dijimos al principio del post la serialización es el proceso de convertir datos en una serie de bytes para que puedan ser guardados o transmitidos. El ejemplo anterior es una forma simple de serialización. Para hacer la serialización más fácil se utiliza la librería pickle en Python.

Por ejemplo para enviar el mensaje podemos modificar el cliente. Utilizamos una tupla de Python para guardar la información y enviarla con Pickle, nos da igual cual sea el tipo de datos a enviar. Puede ser cualquier cosa una tupla, una lista, un diccionario etc.

cliente.py

import socket
from time import time, ctime
from platform import node
import pickle
from sys import version

fecha_actual_unix = time()

fecha_actual = ctime(fecha_actual_unix)
nombre_red = node()
version_python = version

print("fecha_actual:", fecha_actual)
print ('Descripción:', node()) 
print(version_python)

# mensaje = fecha_actual + "\n" + nombre_red + "\n" + version_python
# data = mensaje.encode()
mensaje = (fecha_actual, nombre_red, version_python)
# pickle.dumps((mensaje) convierte el mesaje en bytes sea cual sea el tipo de datos.
data = pickle.dumps(mensaje)

HOST = "127.0.0.1"  # El nombre del servidor o IP del mismo
PORT = 8081  # El puerto usado por el servidor

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client_socket:
    client_socket.connect((HOST, PORT))
    client_socket.sendall(data)
y lo mismo para esta versión muy sencilla del servidor creado.

server.py

import socket
import pickle

HOST = '127.0.0.1' # Standard loopback interface address (localhost)
PORT = 8081 # Port to listen on (non-privileged ports are > 1023)

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:

    # Vincula el socket a una dirección y puerto específicos
    s.bind((HOST, PORT))

    # Escucha conexiones entrantes (el argumento especifica el tamaño máximo de la cola de conexiones)
    s.listen()

    print("Servidor escuchando en el puerto 8081...")

    # Aceptar una conexión
    client_socket, client_address = s.accept()
    with client_socket:
        print(f"Conexión aceptada de {client_address}")
        
        mensaje = "Hola,¡Gracias por conectarte!"
        data = client_socket.recv(1024)
#         mensaje = data.decode()
        mensaje = pickle.loads(data)
        # hace lo contrario, convierte bytes a sus originales.
        print(mensaje)


Puedes encontrar el código de este post en este enlace de github.

En el siguiente post veremos. Protocolo UDP y diferencias con TCP.

No hay comentarios:

Publicar un comentario