9.12. Sockets UDP

O tráfego UDP em Python é enviado e recebido com dois métodos em um socket de datagrama: sendto() para disparar um datagrama a um destino escolhido e recvfrom() para receber um datagrama e descobrir de onde ele veio. Cada chamada move uma mensagem autocontida; não há estado de conexão.

9.12.1. Enviando 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()

Isso envia b"hello" para a porta 9000 em 192.168.1.20 e segue adiante. O MicroPython escolhe uma porta de origem efêmera; o script não precisa vincular nada.

Enviar a mesma carga útil para muitos destinos é apenas um laço – o socket é reutilizável entre os envios e não há conexã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. Recebendo um datagrama

Para receber datagramas, o socket precisa reivindicar uma porta conhecida que os remetentes usarão como destino. É essa 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 “toda interface IPv4 na câmera” – seja qual for a interface Wi-Fi ou Ethernet que traz os pacotes, a porta 9000 pertence a esse socket.

O argumento 1024 de recvfrom() é o número máximo de bytes a ler para o buffer retornado. Datagramas UDP acima desse tamanho serão truncados; escolha o valor para corresponder ao maior datagrama que a aplicação espera.

recvfrom() retorna (data, src): os bytes recebidos e o endereço do remetente. O endereço do remetente é para onde responder, o que facilita escrever um pequeno protocolo de requisição/resposta:

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

Por padrão, recvfrom() bloqueia até que um datagrama chegue. Os padrões para fazê-lo não bloquear – timeouts, sockets não bloqueantes, asyncio – estão em Sockets com asyncio.

9.12.3. Uma requisição e uma resposta

Dois scripts curtos: um envia uma requisição, o outro recebe e responde.

O 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)

O remetente:

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

Algumas coisas que vale a pena observar no remetente:

  • Nenhum bind() e nenhum connect(). Clientes UDP apenas enviam.

  • settimeout() impõe um prazo à chamada de recebimento. Se nenhuma resposta chegar em dois segundos, a chamada levanta OSError em vez de bloquear para sempre – uma forma natural de detectar um pacote perdido.

  • O bloco with fecha o socket automaticamente.

9.12.4. Limites de tamanho de datagrama

Em teoria, datagramas UDP podem ter até cerca de 64 KB, mas o limite prático é muito menor. Cada enlace no caminho entre o remetente e o receptor tem uma Maximum Transmission Unit (MTU) – o maior bloco único de bytes que aquele enlace pode transportar em um quadro. Ethernet e Wi-Fi limitam isso a cerca de 1500 bytes, e quase todo caminho na internet remonta a esse limite em algum ponto.

Quando um datagrama excede a MTU de um enlace que ele precisa atravessar, a camada de rede o divide em fragmentos menores e os remonta no destino. O próprio UDP nunca enxerga a divisão, mas os fragmentos têm várias propriedades inconvenientes:

  • Se qualquer fragmento for perdido, o datagrama inteiro é descartado no receptor – não há retransmissão por fragmento. A probabilidade de perda cresce com a contagem de fragmentos.

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

  • A remontagem custa memória no receptor, que em um microcontrolador é escassa.

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

9.12.5. Armadilhas comuns

  • Esquecer que o UDP pode perder pacotes. Código que funciona perfeitamente em uma rede local tranquila às vezes falha de maneiras sutis em uma rede mais movimentada ou mais ampla. Sempre projete considerando a possibilidade de a mensagem não ter chegado.

  • Receptor não vinculado antes de o remetente enviar. Um datagrama enviado a uma porta em que ninguém está escutando é descartado silenciosamente. Inicie o receptor primeiro.

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

Os padrões acima cobrem quase todo uso de UDP que a câmera precisa. A próxima página faz o equivalente para o TCP.