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"

No hay comentarios:

Publicar un comentario