Mostrando entradas con la etiqueta socket. Mostrar todas las entradas
Mostrando entradas con la etiqueta socket. Mostrar todas las entradas

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.

lunes, 20 de mayo de 2024

Redes con Python: Programación de Sockets para la Comunicación. 1.- Usando Sockets con Python.

Python tiene una API que nos permite comunicarnos con otras programas a través de una red. Vamos a empezar viendo como usar esta API para crear un servidor y usar tu navegador para conectarnos a él. Lo primero que vamos a hacer es crear un nuevo programa de Python al que llamaremos server.py

Para poder usar el API tenemos que importar el módulo socket, de esta forma:

server.py

import socket
Esta línea importa el módulo "socket" que nos proporciona acceso a la interfaz de sockets de red de bajo nivel. Nos va a permitir realizar operaciones de red, como establecer conexiones y trasferir datos entre sistemas.

Luego crearemos el socket. Lo llamaré server_socket y tendrá unos parámetros que van a determinar el tipo de protocolo utilizado.

server.py

import socket

server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

  • socket.socket(): Es la función que crea un nuevo socket.
  • socket.AF_INET: Especifica la familia de direcciones que el socket puede usar. AF_INET significa que el socket utilizará direcciones IPv4. Si quisieras usar direcciones IPv6, usarías AF_INET6.
  • socket.SOCK_STREAM: Especifica el tipo de socket. SOCK_STREAM indica que se va a usar un socket de flujo, lo que significa que se trata de un socket orientado a la conexión y basado en TCP (Transmission Control Protocol). TCP es un protocolo que proporciona una conexión confiable y orientada a la conexión para el intercambio de datos.

En conclusión, hemos creado un socket TCP/IP. A continuación tenemos que vincular ese socket con una dirección y un puerto especifico. 

server.py

import socket

server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM
server_socket.bind(('0.0.0.0', 8081))
Usaremos la dirección 0.0.0.0 y el puerto 8081. 

La dirección 0.0.0.0 se utiliza en la función bind() de los sockets para indicar que el socket debe aceptar conexiones en todas las interfaces de red disponibles del sistema. Es una dirección especial que no se refiere a una sola dirección IP específica, sino a todas las direcciones IP configuradas en el dispositivo. Vamos a explorar esto con más detalle:

Contexto de Uso de 0.0.0.0

  • 0.0.0.0: Acepta conexiones en todas las interfaces de red del sistema. Esto significa que el servidor será accesible desde cualquier dirección IP que tenga el sistema (por ejemplo, desde localhost, direcciones privadas, y direcciones públicas, si están configuradas). También es equivalente a dejar este argumento en blanco "".
  • 127.0.0.1: Acepta conexiones solo desde la interfaz de loopback (localhost). Esto significa que el servidor solo será accesible desde el mismo sistema u ordenador donde está ejecutándose y no desde otras máquinas.
  • Dirección IP específica (como 192.168.1.10): Acepta conexiones solo en esa dirección IP específica. Esto puede ser útil si solo quieres que tu servidor esté accesible a través de una interfaz de red particular.
Es decir cualquier conexión que se solicite a nuestro ordenador activará el socket en el puerto 8081.

Por otra parte, los puertos se utilizan normalmente para identificar el propósito de la conexión. Se puede utilizar cualquier número entre el 0 y el 65535. No obstante ten en cuenta que los puertos entre el 0 y el 1023 se les llama "Puertos Bien Conocidos" y suelen estar restringidos a usos específicos. Por ejemplo:

puertos en un ordenador


Podríamos haber usado cualquier otro puerto, pero hemos optado por el 8081 que se suele utilizar para testear.

Preparemos al servidor para que se ponga a escuchar las peticiones entrantes. Vamos también a poner un mensaje en la pantalla, que indique que el servidor está esperando la conexión y podemos también limitar el número de conexiones entrantes. (5 en este caso)

server.py

import socket

server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Vincula el socket a una dirección y puerto específicos
server_socket.bind(('0.0.0.0', 8081))

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

print("Servidor escuchando en el puerto 8080...")
Ya tenemos al servidor esperando a las conexiones. Una vez que tenga una petición tiene que aceptarlas. Así que vamos a prepararlo todo:

server.py

import socket

# Creamos el socket
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Vincula el socket a una dirección y puerto específicos
server_socket.bind(('0.0.0.0', 8081))

# Escucha conexiones entrantes (el argumento especifica el 
# número máximo de solicitudes de conexión en cola)
server_socket.listen(5)

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

# Aceptar una conexión
client_socket, client_address = server_socket.accept()
print(f"Conexión aceptada de {client_address}")

El programa está esperando que se realice una conexión y cuando esto se produce, se devuelven dos parámetros. 

El primero "client_socketes un nuevo objeto de socket que representa la conexión específica con el cliente que ha hecho la solicitud de conexión. Este socket se usa para enviar y recibir datos a y desde el cliente. Cada vez que un cliente se conecta al servidor, el método accept() crea un nuevo socket para manejar esa conexión individual, permitiendo que el servidor continúe escuchando nuevas conexiones en el socket original.

Para que quede claro. Un socket_server no manda ningún dato. No recibe ningún dato. Solo produce sockets clientes. Cada client_socket es creado en respuesta a algún otro socket cliente que hace connect al host y puerto al que estamos vinculado. Tan pronto como hemos creado ese cliente_socket volvemos a escuchar por más conexiones. Los dos "clientes" son libres de conversar entre ellos - están usando algún puerto asignado dinámicamente que será reciclado cuando la conversación termine.

Cualquier software que sea capaz de conectarse mediante TCP podrá establecer una conexión con nuestro programa de servidor, como un navegador web o un terminal. Ahora, en el siguiente paso, vamos a crear tu propio cliente, pero antes vamos a probar que nuestro programa funciona usando tu navegador web para conectarse. Así que ejecutemos nuestro programa.

El segundo "client_address" es una tupla que contiene la dirección IP y el número de puerto del cliente que ha establecido la conexión con el servidor. Este parámetro proporciona información sobre la ubicación de red del cliente.

Pondremos un mensaje en pantalla para indicar que se ha realizado la conexión.


>>> python3 server.py
Servidor escuchando en el puerto 8081...

Podemos ver que está esperando una conexión. Abre un navegador web y ve a la dirección 127.0.0.1, que es la dirección local de tu ordenador. Usa el puerto 8081 porque ese es el puerto en el que programa está escuchando. (http://127.0.0.1:8081/)

>>> python3 server.py
Servidor escuchando en el puerto 8081...
Conexión aceptada de ('127.0.0.1', 45438)
Aunque el navegador web no muestra ninguna información, podemos ver que está conectado. ¿Por que el navegador no muestra nada? ¿Qué tendrías que hacer para que el navegador responda?  Lo veremos en el próximo post.

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