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

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.


    viernes, 7 de junio de 2024

    Redes con Python: Programación de Sockets para la Comunicación. 6.- Usando UDP en Python y diferencias con TCP

    El usar sockets para comunicarse en las redes de ordenadores reduce la complejidad de usar diferentes protocolos, ya que permite al programador abstraerse de como realmente se envían los datos. El usar el protocolo UDP es muy similar a usar TCP aunque difiere un poco.

    Recordemos que TCP es un protocolo orientado a la conexión, lo que significa que se establece una conexión entre dos dispositivos antes de que se inicie la comunicación entre ambos. UDP, por otro lado, es un protocolo sin conexión lo que significa que no se establece una conexión antes de que comience la comunicación. 

    TCP garantiza que los datos se entreguen de manera ordenada y sin errores. Esto se logra mediante un mecanismo de confirmación y retransmisión de paquetes. TCP también utiliza un mecanismo de control de congestión para garantizar que la red no se sature con demasiados paquetes.

    UDP, por otro lado, no proporciona garantías de entrega de paquetes. Los paquetes pueden perderse, duplicarse o entregarse en un orden diferente al que se enviaron. UDP es más rápido que TCP ya que no hay necesidad de establecer una conexión y no se utiliza ningún mecanismo de control de congestión.

    En palabras más claras:

     Un socket de flujo (TCP) es como una llamada telefónica: un lado hace la llamada, el otro responde, se saludan (SYN/ACK en TCP) y luego intercambian información. Una vez que terminan, se despiden (FIN/ACK en TCP). Si un lado no escucha una despedida, normalmente volverá a llamar, ya que esto es un evento inesperado; generalmente el cliente se reconectará al servidor. Hay una garantía de que los datos no llegarán en un orden diferente al que se enviaron, y hay una garantía razonable de que los datos no estarán dañados.

    Un socket UDP es como pasar una nota en clase. Considera el caso en el que no estás directamente al lado de la persona a la que le estás pasando la nota; la nota viajará de persona a persona. Puede que no llegue a su destino, y puede que esté modificada para cuando llegue. Si pasas dos notas a la misma persona, pueden llegar en un orden que no pretendías, ya que la ruta que toman las notas a través del aula puede no ser la misma; una persona podría no pasar una nota tan rápido como otra, etc.

    Así que usas un socket de flujo cuando tener la información en orden y sin daños es importante. Los protocolos de transferencia de archivos son un buen ejemplo aquí. ¡No querrás descargar un archivo con su contenido aleatoriamente desordenado y dañado!

    Usarías un socket de datagrama (UDP) cuando el orden es menos importante que la entrega oportuna (piensa en VoIP o protocolos de juegos), cuando no quieres la sobrecarga adicional de un flujo (por esto el DNS es principalmente un protocolo de datagrama, para que los servidores puedan responder a muchas solicitudes a la vez muy rápidamente), o cuando no te importa demasiado si los datos nunca llegan a su destino.

    Para ampliar el caso de VoIP/juegos, tales protocolos incluyen su propio mecanismo de ordenación de datos. Pero si un paquete se daña o se pierde, no quieres esperar a que el protocolo de flujo (generalmente TCP) emita una solicitud de reenvío; necesitas recuperarte rápidamente. El TCP puede tardar hasta varios minutos en recuperarse, y para protocolos en tiempo real como juegos o VoIP, incluso tres segundos pueden ser inaceptables. Usar un protocolo de datagrama como UDP permite que el software se recupere de tal evento extremadamente rápido, simplemente ignorando los datos perdidos o re-solicitándolos antes de lo que lo haría TCP.

    Si comparas el código para crear un servidor TCP y UDP que reciban datos de un cliente y vuelvan a enviar esos mismos datos de vuelta, puedes ver que en el código para crear un servidor UDP no se utiliza ni el método listen para estar a la escucha de clientes, ni el método accept para aceptar la conexión.

    Servidor TCP

    import socket
    
    tcp_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    tcp_server.bind(("0.0.0.0", 8081))
    
    tcp_server.listen()
    connection_socket, address = tcp_server.accept()
    
    data = connection_socket.recv(1024)
    connection_socket.send(data)

    Servidor UDP

    import socket

    udp_server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    udp_server.bind(("0.0.0.0", 20001))

    data, client_address = udp_server.recvfrom(1024)
    udp_server.sendto(data, client_address)

    UDP también envía datagramas (o paquetes) completos de información, en lugar de un flujo de bytes como TCP. Sin embargo no se garantiza que estos paquetes lleguen en el mismo orden en el que fueron enviados. Ahora bien, si llegan lo harán completos. Esto significa que como programador no tendrás que preocuparte por terminar o dividir los mensajes. Un único mensaje enviado será un único mensaje recibido. Las otras diferencias están relacionadas con la configuración del socket y el envío-recepción de datos sin conexión.

    Vamos a verlo paso a paso:

    1.- Cuando se crea el socket, su tipo se establece como socket.SOCK_DGRAM.

    udp_server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

    2.- De manera similar a TCP, bind se usa para configurar el puerto para aceptar datos de todas las direcciones de red, 0.0.0.0 usando el puerto 20001

    udp_server.bind(("0.0.0.0", 20001))

    Hemos usado el puerto 20001, ya que se usa a menudo para realizar pruebas usando UDP, pero puedes usar cualquier puerto. De manera similar a TCP, los puertos del 0 al 1023 normalmente se evitan, ya que son puertos que se usan para otras finalidades.

    3.- Como no hay ningún socket de conexión para recibir datos, se utiliza la función recvfrom, que no sólo devuelve los datos recibidos, si no también la dirección de donde provienen los datos:

    data, client_address = udp_server.recvfrom(1024)

    La variable client_address contendrá una tupla con la dirección IP y el puerto. El valor 1024 hace referencia a cuantos bytes se referirán. Como UDP envía paquetes de información, este valor debe ser lo suficientemente grande para poder recibir un paquete completo. Se generará un error si no se recibe un paquete completo de datos. 

    4.- Cuando se envían los datos, se utiliza la función sendto y se proporciona un dirección específica. 

    udp_server.sendto(data, client_address)

    En el ejemplo anterior los datos se vuelven a enviar de regreso a la dirección desde donde se recibieron. Sin embargo, tu puedes especificar cualquier dirección y el socket la enviaría, por ejemplo:

    udp_server.sendto(data, ("169.192.1.2", 9999))

    En el caso de que un socket envíe datos y otro socket no espere recibirlos, simplemente se pierden.


    Con lo que hemos visto te planteo un reto. ¿Puedes crear un cliente que se conecte a este servidor UDP que hemos creado?

    Tu programa deberá:

    • crear un socket UDP
    • Usar sendto para enviar un mensaje a la dirección y puerto del servidor UDP.
    • Usar recvfrom para obtener el mensaje desde el servidor.
    • Imprimir el mensaje devuelto por pantalla.

     Si al final te das por vencido, puedes ver una forma de hacerlo a través de este enlace en Pastebin.

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

    En el próximo post "Envío de archivos binarios usando TCP y UDP"