martes, 18 de junio de 2024

Redes con Python: Programación de Sockets para la Comunicación. 7.- Envio de archivos binarios usando TCP y UDP

Para finalizar el tema de los sockets en Python vamos a ver como enviar archivos binarios usando ambos protocolos. Primeramente enviaremos un archivo binario, como puede ser una imagen usando TCP y luego veremos lo mismo usando UDP añadiendo la posibilidad de ver como enviar la imagen y ver como se va dibujando en tiempo real.

Enviando Imágenes Binarias de Cliente a Servidor con TCP

En este tutorial, te mostraré cómo enviar una imagen desde un cliente a un servidor utilizando TCP. Este método es útil cuando necesitas transferir archivos binarios de forma segura y fiable entre dos sistemas. A lo largo del post, te guiaré paso a paso por el código tanto del cliente como del servidor.

¿Qué es TCP y por qué usarlo?

TCP (Protocolo de Control de Transmisión) es uno de los protocolos fundamentales de Internet. Proporciona una comunicación fiable, ordenada y libre de errores entre aplicaciones que se ejecutan en hosts conectados a una red IP. A diferencia de UDP (Protocolo de Datagrama de Usuario), TCP asegura que los datos lleguen en el mismo orden en que se enviaron, lo que lo hace perfecto para la transferencia de archivos.

Código del Cliente

Vamos a empezar con el código del cliente. El objetivo del cliente es leer una imagen desde el disco y enviarla al servidor. En el mismo directorio donde crearé el siguiente archivo tengo una imagen de un gato, que es un archivo binario, en formato jpg cuyo archivo se llama gatin.jpg. Voy a crear un cliente que transmitirá este archivo a un servidor, donde se volverá a guardar con el nombre de gatin_out.jpg.

cliente.py

import socket

def send_image(file_path, host='localhost', port=12345):
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client_socket.connect((host, port))
    
    with open(file_path, 'rb') as f:
        data = f.read(1024)
        while data:
            client_socket.send(data)
            data = f.read(1024)
    
    # Enviar señal de finalización
    client_socket.send(b'END')
    print("Imagen enviada")
    client_socket.close()

if __name__ == '__main__':
    send_image('gatin.jpg')
  • Creación del Socket: Creamos un socket TCP/IP con socket.socket(socket.AF_INET, socket.SOCK_STREAM).

  • Conexión al Servidor: Nos conectamos al servidor especificado en host y port.

  • Lectura y Envío de la Imagen: Abrimos el archivo de imagen en modo binario (rb) y leemos bloques de 1024 bytes. Estos bloques se envían al servidor hasta que se hayan leído todos los datos.

  • Señal de Finalización: Después de enviar todos los datos, enviamos una señal de finalización (b'END').

  • Cierre del Socket: Cerramos el socket del cliente.


  • Código del Servidor

    Ahora, vamos a ver el código del servidor. El servidor recibirá la imagen enviada por el cliente y la guardará en el disco.

    servidor.py

    import socket
    
    def start_server(host='0.0.0.0', port=12345):
        server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        server_socket.bind((host, port))
        server_socket.listen(1)
        print(f"Servidor escuchando en {host}:{port}")
        
        client_socket, addr = server_socket.accept()
        print(f"Conexión establecida con {addr}")
        
        with open('gatin_out.png', 'wb') as f:
            while True:
                data = client_socket.recv(1024)
                if data == b'END':
                    break
                f.write(data)
        
        print("Imagen recibida y guardada como 'gatin_out.png'")
        client_socket.close()
        server_socket.close()
        print("Servidor cerrado")
    
    if __name__ == '__main__':
        start_server()


    Creación del Socket: Creamos un socket TCP/IP con socket.socket(socket.AF_INET, socket.SOCK_STREAM).

    Configuración del Servidor: Enlazamos el socket a la dirección y puerto especificados con bind((host, port)).

    Escucha de Conexiones: Ponemos el servidor en modo de escucha con listen(1), esperando conexiones entrantes.

    Aceptación de Conexión: Aceptamos una conexión entrante con accept(), que devuelve un nuevo socket y la dirección del cliente.

    Recepción de la Imagen: Abrimos un archivo en modo binario (wb) para guardar los datos recibidos. En un bucle, recibimos bloques de datos de 1024 bytes y los escribimos en el archivo. El bucle termina cuando recibimos la señal de finalización (b'END').

    Cierre del Socket: Cerramos los sockets del cliente y del servidor.

    Ejecuta primero el archivo del servidor (servidor.py) y a continuación el del cliente (cliente.py). Si todo ha ido bien tendrás en el lado del servidor un archivo llamado gatin_out.png.


    Enviando Imágenes Binarias de Cliente a Servidor con UDP.


    Advertencia. Como ya hemos comentado varias veces en estos post sobre sockets el usar el protocolo UDP implica que no hay garantía de que todos los datos enviados lleguen, ni que lleguen en el mismo orden en el que se enviaron. 

    Por tanto si lo que buscas es que el archivo llegue tal cual es mejor usar TCP. No obstante a efectos de ver como usar este protocolo vamos a enviar el archivo igual que hicimos anteriormente.


    Código del cliente.


    cliente_UDP.py

    import socket
    
    def udp_client(server_host, server_port, file_path, buffer_size):
        client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        
        with open(file_path, 'rb') as f:
            while (chunk := f.read(buffer_size)):
                # := operador walrus introducido a partir de python 3.8
                # asigna y devuelve el contendido de la variable.
                client_socket.sendto(chunk, (server_host, server_port))
        
        client_socket.sendto(b"END", (server_host, server_port))
        print(f"Imagen enviada a {server_host}:{server_port}")
    
        client_socket.close()
    
    # Parámetros del cliente
    server_host = '127.0.0.1'
    server_port = 12345
    file_path = 'gatin.jpg'
    buffer_size = 1024
    
    udp_client(server_host, server_port, file_path, buffer_size)


    Un cliente UDP para enviar imágenes

    Este código escrito en Python define una función udp_client que sirve para enviar una imagen a un servidor utilizando el protocolo UDP (User Datagram Protocol). Veamos paso a paso cómo funciona:

    Importando librerías:

    • import socket: Esta línea importa la librería socket que provee herramientas para la comunicación por red.

    La función udp_client:

    • def udp_client(server_host, server_port, file_path, buffer_size):: Esta línea define una función llamada udp_client que toma cuatro argumentos:
      • server_host: La dirección del servidor al que se conectará (en este caso, '127.0.0.1' que representa la máquina local).
      • server_port: El puerto del servidor donde se escucharán los mensajes (en este caso, 12345).
      • file_path: La ruta del archivo que se enviará (en este caso, 'gatin.jpg' que es la imagen del gato).
      • buffer_size: El tamaño de los bloques en los que se dividirá la imagen para su envío (en este caso, 1024 bytes).

    Creando el socket:

    • client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM): Esta línea crea un socket para la comunicación UDP.
      • socket.AF_INET: Indica que se utilizará el protocolo de internet (IPv4).
      • socket.SOCK_DGRAM: Indica que se trata de un socket no orientado a la conexión, propio del protocolo UDP.

    Abriendo el archivo y enviando por bloques:

    • with open(file_path, 'rb') as f:: Abre el archivo especificado en modo lectura binario ('rb').
    • while (chunk := f.read(buffer_size))::
      • f.read(buffer_size): Lee un bloque de buffer_size bytes del archivo y lo asigna a la variable chunk.
      • := (Python 3.8+): Este operador (walrus) asigna el valor leído a chunk y también lo devuelve para la condición del while.
    • client_socket.sendto(chunk, (server_host, server_port)): Envía el bloque leído (chunk) al servidor en la dirección (server_host, server_port).

    Enviando el mensaje de fin y cerrando el socket:

    • client_socket.sendto(b"END", (server_host, server_port)): Envía un mensaje final al servidor indicando el término de la transmisión (en este caso, "END" como bytes b"END").
    • client_socket.close(): Cierra el socket del cliente.

    Ejecutando el cliente:

    • # Parámetros del cliente: Define los valores por defecto para los argumentos de la función udp_client.
    • udp_client(server_host, server_port, file_path, buffer_size): Llama a la función udp_client con los parámetros definidos anteriormente, enviando la imagen gatin.jpg al servidor local en el puerto 12345 usando bloques de 1024 bytes.
    • print(f"Imagen enviada a {server_host}:{server_port}"): Imprime un mensaje de confirmación indicando que la imagen se ha enviado al servidor.

    Este código permite enviar una imagen a un servidor que esté esperando por paquetes UDP. Ten en cuenta que se necesita un servidor compatible con UDP para recibir la imagen transmitida por este cliente así que vamos con ello.

    Código del servidor.


    servidor_UDP.py

    import socket
    
    def udp_server(host, port, buffer_size, output_file):
        server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        server_socket.bind((host, port))
        
        print(f"Servidor UDP escuchando en {host}:{port}")
        
        with open(output_file, 'wb') as f:
            while True:
                data, addr = server_socket.recvfrom(buffer_size)
                if data == b"END":
                    print("Transferencia completa.")
                    break
                f.write(data)
                print(f"Recibiendo datos de {addr}")
    
        server_socket.close()
    
    # Parámetros del servidor
    host = '127.0.0.1'
    port = 12345
    buffer_size = 1024
    output_file = 'gatin_out.jpg'
    
    udp_server(host, port, buffer_size, output_file)

    En los ejemplos gatin.jpg es una imagen en formato jpg que está en el mismo directorio que ambos archivos.

    Este código implementa un servidor UDP (User Datagram Protocol). Vamos a desglosarlo paso a paso:

    - Importación de módulos:

        - El código comienza importando el módulo socket, que proporciona funciones para crear y administrar sockets de red.

        - Los sockets son canales de comunicación bidireccionales que permiten la transferencia de datos entre dispositivos a través de una red.

    - Definición de la función udp_server:

        - La función udp_server acepta cuatro parámetros:

            - host: la dirección IP en la que el servidor escuchará (en este caso, 127.0.0.1 se refiere a la dirección local del equipo).

            - port: el número de puerto en el que el servidor estará disponible (aquí, 12345).

            - buffer_size: el tamaño del búfer utilizado para recibir datos (en bytes, aquí 1024).

            - output_file: el nombre del archivo en el que se guardarán los datos recibidos (por ejemplo, 'gatin_out.jpg').

        - La función crea un socket UDP utilizando socket.AF_INET para indicar que se utilizará IPv4 y socket.SOCK_DGRAM para especificar que es un socket UDP.

        - Luego, enlaza el socket al host y port especificados.

    - Bucle principal:

        - El servidor entra en un bucle infinito (while True) para recibir datos.

        - Cuando recibe datos del cliente (usando server_socket.recvfrom(buffer_size)), verifica si los datos son igual a "END". Si es así, imprime "Transferencia completa" y sale del bucle.

        - De lo contrario, escribe los datos recibidos en el archivo especificado (output_file) y muestra la dirección del cliente desde la que se recibieron los datos.

    - Cierre del socket:

        - Después de salir del bucle, el servidor cierra el socket con server_socket.close().


    Transmitiendo imágenes a través de UDP en Python: Un enfoque práctico

    Enviar imágenes de una máquina a otra a través de UDP en Python puede ser un desafío interesante debido a las características y limitaciones inherentes del protocolo UDP. En este artículo, exploraremos cómo implementar una aplicación básica para enviar y recibir imágenes utilizando sockets UDP y la biblioteca Pygame para la visualización.

    Entendiendo UDP y sus desafíos

    UDP (User Datagram Protocol) es un protocolo de red que ofrece una comunicación rápida y sin conexión entre aplicaciones pero no garantiza la entrega de los mensajes ni el orden de los mismos. Esto lo hace ideal para aplicaciones donde la velocidad es crucial pero no tanto la integridad absoluta de los datos, como el streaming de video o audio en tiempo real.

    Implementación del servidor (receptor)

    Comencemos con el código del servidor que recibirá la imagen enviada desde el cliente. Para que funcinoe tienes que instalar los módulos pygame y PILLOW (PIL):

    pip install pygame

    pip install pilllow

    Código del receptor.

    import socket import pickle from time import sleep import pygame # Parámetros del servidor host = '127.0.0.1' port = 12345 buffer_size = 2048 # Configuración de Pygame para la visualización pygame.init() screen = None # Configuración del socket UDP server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) server_socket.bind((host, port)) print(f"Servidor UDP escuchando en {host}:{port}") while True: data, addr = server_socket.recvfrom(buffer_size) mensaje = pickle.loads(data) # Finaliza la transmisión si se recibe "END" if mensaje == "END": print("Transferencia completa.") break if not screen: # Se reciben las dimensiones de la imagen width, height = mensaje print(f"Las dimensiones de la imagen son {width},{height}") screen = pygame.display.set_mode((width, height)) pygame.display.set_caption("Imagen recibida") else: # Se reciben los píxeles y se actualiza la pantalla index = mensaje[0] pixel = mensaje[1] x = index[0] y = index[1] screen.set_at((x, y), pixel) pygame.display.flip() # Cierre de conexiones y finalización de Pygame server_socket.close() pygame.quit() print("Conexión finalizada.")

    Explicación del código del servidor:

    1. Configuración inicial: Importamos los módulos necesarios y configuramos los parámetros del servidor UDP.

    2. Inicialización de Pygame: Se inicializa Pygame para poder visualizar la imagen recibida.

    3. Bucle principal: El servidor entra en un bucle infinito donde espera recibir datos del cliente.

    4. Recepción de datos: Cuando llega un datagrama UDP, se deserializa usando pickle para obtener la información sobre las dimensiones de la imagen o los píxeles.

    5. Visualización de la imagen: Si es la primera vez que se recibe un mensaje (dimensiones de la imagen), se configura la pantalla de Pygame con estas dimensiones. Para mensajes posteriores, se actualiza la pantalla con los píxeles recibidos.

    6. Finalización: Cuando se recibe el mensaje "END", se sale del bucle y se cierran las conexiones.

    Implementación del cliente (emisor)

    A continuación, veamos el código del cliente que enviará la imagen al servidor.

    import socket import pickle from time import sleep from PIL import Image # Carga de la imagen image = Image.open("gatin.jpg") width, height = image.size # Configuración del socket UDP udp_client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # Envío de las dimensiones de la imagen dimension = (width, height) data = pickle.dumps(dimension) udp_client.sendto(data, ("127.0.0.1", 12345)) sleep(1) # Espera para asegurar que el servidor esté listo # Envío de cada píxel de la imagen for y in range(height): for x in range(width): pos = (x, y) rgba = image.getpixel(pos) message = (pos, rgba) data = pickle.dumps(message) udp_client.sendto(data, ("127.0.0.1", 12345)) sleep(0.005) # Pequeña pausa para simular el envío gradual # Envío del mensaje de finalización mensaje = "END" data = pickle.dumps(mensaje) udp_client.sendto(data, ("127.0.0.1", 12345)) # Cierre del socket del cliente udp_client.close()

    Explicación del código del cliente:

    1. Carga de la imagen: Utilizamos Pillow (PIL) para cargar la imagen "gatin.jpg" y obtener sus dimensiones.

    2. Configuración del socket UDP: Creamos un socket UDP para la comunicación con el servidor.

    3. Envío de las dimensiones: Convertimos las dimensiones a una cadena de bytes usando pickle y las enviamos al servidor.

    4. Envío de los píxeles: Iteramos sobre cada píxel de la imagen, obtenemos su valor RGBA, lo empaquetamos en un mensaje y lo enviamos al servidor.

    5. Finalización: Enviamos un mensaje "END" para indicar al servidor que la transmisión ha terminado.

    6. Cierre del socket: Finalmente, cerramos el socket del cliente.

    Problemas comunes con UDP en la transmisión de imágenes

    1. Pérdida de píxeles: Debido a la naturaleza no garantizada de UDP, algunos datagramas pueden perderse durante la transmisión, lo que resulta en la pérdida de píxeles en la imagen recibida.

    2. Retardo en la recepción: Para evitar saturar la red, es común introducir pequeñas pausas (como sleep(0.005)) entre el envío de cada píxel. Esto introduce un pequeño retardo en la transmisión, pero ayuda a mejorar la fiabilidad al permitir que los datagramas se entreguen de manera más efectiva.

    En conclusión, aunque UDP ofrece una manera eficiente de transmitir datos como imágenes en tiempo real, es importante tener en cuenta sus limitaciones y diseñar la aplicación para manejar posibles pérdidas de datos y asegurar una transmisión fluida y confiable.

    Puedes encontrar el código en este enlace de Github.


    No hay comentarios:

    Publicar un comentario