9.12. Sockets UDP

O tráfego UDP em Python é enviado e recebido com dois métodos num socket de datagramas: sendto() para enviar um datagrama para um destino escolhido, e recvfrom() para receber um datagrama e saber de onde veio. Cada chamada move uma mensagem autónoma; não existe estado de ligação.

9.12.1. Enviar um datagrama

O envio UDP mais simples é uma linha de Python sobre um construtor de socket:

import socket

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

Isto envia b"hello" para a porta 9000 em 192.168.1.20 e termina. O MicroPython escolhe uma porta de origem efémera; o script não precisa de fazer bind a nada.

Enviar o mesmo payload para vários destinos é apenas um ciclo – o socket é reutilizável entre envios e não há nenhuma ligação a estabelecer:

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. Receber um datagrama

Para receber datagramas, o socket tem de reservar uma porta conhecida que os emissores utilizarão como destino. É isso que faz a chamada 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)

O endereço "0.0.0.0" significa «todas as interfaces IPv4 da câmara» – qualquer interface Wi-Fi ou Ethernet que receba pacotes, a porta 9000 pertence a este socket.

O argumento 1024 de recvfrom() é o número máximo de bytes a ler para o buffer devolvido. Os datagramas UDP acima deste tamanho serão truncados; escolha o valor de acordo com o maior datagrama que a aplicação espera receber.

recvfrom() devolve (data, src): os bytes recebidos e o endereço do remetente. O endereço do remetente é para onde responder, facilitando a escrita de um pequeno protocolo de pedido/resposta:

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

Por omissão, recvfrom() bloqueia até que um datagrama chegue. Os padrões para evitar o bloqueio – timeouts, sockets não bloqueantes, asyncio – estão em Sockets com asyncio.

9.12.3. Um pedido e uma resposta

Dois scripts curtos: um envia um pedido, o outro recebe e responde.

O recetor:

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)

O emissor:

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?")

Alguns pontos a destacar no emissor:

  • Sem bind() e sem connect(). Os clientes UDP simplesmente enviam.

  • settimeout() define um prazo para a chamada de receção. Se não chegar nenhuma resposta em dois segundos, a chamada lança OSError em vez de bloquear indefinidamente – uma forma natural de detetar um pacote perdido.

  • O bloco with fecha o socket automaticamente.

9.12.4. Limites de tamanho dos datagramas

Os datagramas UDP podem ter até cerca de 64 KB em teoria, mas o limite prático é muito menor. Cada ligação no caminho entre o emissor e o recetor tem uma Maximum Transmission Unit (MTU) – o maior bloco único de bytes que essa ligação consegue transportar num fotograma. Ethernet e Wi-Fi limitam ambos a cerca de 1500 bytes, e quase todos os caminhos na internet remontam a esse limite nalgum ponto.

Quando um datagrama excede a MTU de uma ligação que tem de atravessar, a camada de rede divide-o em fragmentos mais pequenos e remonta-os no destino. O próprio UDP nunca vê a divisão, mas os fragmentos têm várias propriedades inconvenientes:

  • Se qualquer um dos fragmentos se perder, o datagrama completo é descartado no recetor – não há retransmissão por fragmento. A probabilidade de perda aumenta com o número de fragmentos.

  • Algumas redes e firewalls descartam pacotes fragmentados por completo, tratando-os como suspeitos.

  • A remontagem consome memória no recetor, que num microcontrolador é escassa.

A regra prática na câmara: mantenha as mensagens UDP bem abaixo dos 1500 bytes. Cerca de 1400 bytes deixa margem para os cabeçalhos IP e UDP, qualquer sobrecarga de tunelamento que o caminho acrescente e pequenas variações de MTU entre ligações Ethernet, Wi-Fi e VPN. As aplicações que precisam de enviar mais do que isso devem ou fragmentar os dados na camada de aplicação ou mudar para TCP, que trata da divisão e remontagem automaticamente.

9.12.5. Armadilhas comuns

  • Esquecer que o UDP pode perder pacotes. Código que funciona perfeitamente numa rede local tranquila pode falhar de formas subtis numa rede mais movimentada ou mais ampla. Projete sempre tendo em conta a possibilidade de a mensagem não ter chegado.

  • Recetor não ligado antes de o emissor enviar. Um datagrama enviado para uma porta sem ninguém a ouvir é descartado silenciosamente. Inicie o recetor primeiro.

  • Enviar um datagrama maior do que a MTU do caminho. Consulte a secção anterior – mantenha as mensagens abaixo de ~1400 bytes.

Os padrões acima cobrem quase todos os casos de utilização de UDP na câmara. A próxima página faz o equivalente para TCP.