9.12. Sockets UDP

El tráfico UDP en Python se envía y recibe con dos métodos sobre un socket de datagramas: sendto() para lanzar un datagrama a un destino elegido, y recvfrom() para recibir un datagrama y averiguar de dónde vino. Cada llamada mueve un mensaje autocontenido; no hay estado de conexión.

9.12.1. Enviar un datagrama

El envío UDP más sencillo es una línea de Python sobre un constructor de socket:

import socket

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.sendto(b"hello", ("192.168.1.20", 9000))
s.close()

Eso envía b"hello" al puerto 9000 en 192.168.1.20 y se desentiende. MicroPython elige un puerto de origen efímero; el script no tiene que vincular nada.

Enviar la misma carga útil a muchos destinos es simplemente un bucle: el socket es reutilizable entre envíos y no hay ninguna conexión que establecer:

targets = [
    ("192.168.1.20", 9000),
    ("192.168.1.21", 9000),
    ("192.168.1.22", 9000),
]

with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
    for addr in targets:
        s.sendto(b"hello", addr)

9.12.2. Recibir un datagrama

Para recibir datagramas, el socket tiene que reclamar un puerto conocido que los emisores usarán como destino. Esa es la llamada bind():

import socket

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(("0.0.0.0", 9000))

while True:
    data, src = s.recvfrom(1024)
    print("from", src, "got", data)

La dirección "0.0.0.0" significa «todas las interfaces IPv4 de la cámara»: sea cual sea la interfaz Wi-Fi o Ethernet por la que entren los paquetes, el puerto 9000 pertenece a este socket.

El argumento 1024 de recvfrom() es el número máximo de bytes que se leen en el búfer retornado. Los datagramas UDP que superen este tamaño serán truncados; elige el valor para que coincida con el datagrama más grande que espere la aplicación.

recvfrom() retorna (data, src): los bytes recibidos y la dirección del emisor. La dirección del emisor es a la que hay que responder, lo que facilita escribir un pequeño protocolo de solicitud/respuesta:

while True:
    request, src = s.recvfrom(1024)
    if request == b"ping":
        s.sendto(b"pong", src)

Por defecto, recvfrom() se bloquea hasta que llega un datagrama. Los patrones para que no se bloquee (tiempos de espera, sockets no bloqueantes, asyncio) están en Sockets con asyncio.

9.12.3. Una solicitud y una respuesta

Dos scripts breves: uno envía una solicitud, otro recibe y responde.

El receptor:

import socket

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(("0.0.0.0", 9000))

while True:
    req, src = s.recvfrom(64)
    print("got", req, "from", src)
    s.sendto(b"ack: " + req, src)

El emisor:

import socket

with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
    s.settimeout(2.0)                              # 2 s reply window
    s.sendto(b"ping", ("192.168.1.20", 9000))
    try:
        reply, _ = s.recvfrom(64)
        print("reply:", reply)
    except OSError:
        print("no reply in 2 s -- packet lost?")

Algunas cosas que vale la pena destacar en el emisor:

  • Sin bind() ni connect(). Los clientes UDP simplemente envían.

  • settimeout() pone un plazo a la llamada de recepción. Si no llega ninguna respuesta en dos segundos, la llamada lanza OSError en vez de bloquearse para siempre, una forma natural de detectar un paquete perdido.

  • El bloque with cierra el socket automáticamente.

9.12.4. Límites de tamaño de los datagramas

En teoría, los datagramas UDP pueden llegar a unos 64 KB, pero el límite práctico es mucho menor. Cada enlace de la ruta entre el emisor y el receptor tiene una Unidad Máxima de Transmisión (MTU), es decir, el mayor bloque de bytes que ese enlace puede transportar en un solo fotograma. Tanto Ethernet como Wi-Fi limitan esto a alrededor de 1500 bytes, y casi todas las rutas de internet se remontan a ese límite en algún punto.

Cuando un datagrama supera la MTU de un enlace que tiene que atravesar, la capa de red lo divide en fragmentos más pequeños y los reensambla en el destino. UDP en sí nunca ve la división, pero los fragmentos tienen varias propiedades inconvenientes:

  • Si se pierde un solo fragmento, todo el datagrama se descarta en el receptor: no hay retransmisión por fragmento. La probabilidad de pérdida crece con la cantidad de fragmentos.

  • Algunas redes y cortafuegos descartan por completo los paquetes fragmentados, tratándolos como sospechosos.

  • El reensamblaje cuesta memoria en el receptor, que en un microcontrolador escasea.

La regla práctica en la cámara: mantén los mensajes UDP muy por debajo de los 1500 bytes. Unos 1400 bytes dejan espacio para los encabezados IP y UDP, cualquier sobrecarga de tunelización que añada la ruta y las pequeñas variaciones de MTU entre enlaces Ethernet, Wi-Fi y VPN. Las aplicaciones que necesiten enviar más que eso deberían fragmentar los datos en la capa de aplicación o cambiar a TCP, que gestiona la división y el reensamblaje automáticamente.

9.12.5. Errores comunes

  • Olvidar que UDP puede perder paquetes. El código que funciona perfectamente en una red local tranquila a veces falla de formas sutiles en una más ocupada o más amplia. Diseña siempre teniendo en cuenta la posibilidad de que el mensaje no haya llegado.

  • Receptor no vinculado antes de que el emisor envíe. Un datagrama enviado a un puerto en el que nadie está escuchando se descarta silenciosamente. Inicia primero el receptor.

  • Enviar un datagrama mayor que la MTU de la ruta. Consulta la sección anterior: mantén los mensajes por debajo de ~1400 bytes.

Los patrones anteriores cubren casi todos los usos de UDP a los que recurre la cámara. La página siguiente hace lo equivalente para TCP.