9.12. UDP-сокеты

UDP-трафик в Python отправляется и принимается с помощью двух методов датаграммного сокета: sendto() для отправки датаграммы в выбранное место назначения и recvfrom() для приёма датаграммы и определения её источника. Каждый вызов передаёт одно самодостаточное сообщение; состояния соединения нет.

9.12.1. Отправка датаграммы

Простейшая отправка UDP — это одна строка Python поверх конструктора сокета:

import socket

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

Это отправляет b"hello" на порт 9000 по адресу 192.168.1.20 и завершается. MicroPython выбирает эфемерный исходный порт; скрипту не нужно ничего привязывать.

Отправка одних и тех же данных по множеству адресов — это просто цикл: сокет можно переиспользовать между отправками, и нет соединения, которое нужно устанавливать:

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. Приём датаграммы

Чтобы принимать датаграммы, сокет должен занять известный порт, который отправители будут использовать в качестве места назначения. Это и есть вызов 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)

Адрес "0.0.0.0" означает «каждый интерфейс IPv4 на камере» — какой бы интерфейс Wi-Fi или Ethernet ни принимал пакеты, порт 9000 принадлежит этому сокету.

Аргумент 1024 в recvfrom() — это максимальное число байтов для чтения в возвращаемый буфер. UDP-датаграммы больше этого размера будут усечены; выбирайте значение в соответствии с самой большой датаграммой, которую ожидает приложение.

recvfrom() возвращает (data, src): принятые байты и адрес отправителя. Адрес отправителя — это то, на что нужно отвечать, что упрощает написание небольшого протокола запрос/ответ:

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

По умолчанию recvfrom() блокируется, пока не придёт датаграмма. Способы избежать блокировки — таймауты, неблокирующие сокеты, asyncio — описаны на странице Сокеты с asyncio.

9.12.3. Запрос и ответ

Два коротких скрипта: один отправляет запрос, другой принимает и отвечает.

Получатель:

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)

Отправитель:

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

Несколько моментов, на которые стоит обратить внимание в отправителе:

  • Нет ни bind(), ни connect(). UDP-клиенты просто отправляют.

  • settimeout() устанавливает предельный срок для вызова приёма. Если ответ не приходит в течение двух секунд, вызов вызывает OSError, а не блокируется бесконечно — естественный способ обнаружить потерянный пакет.

  • Блок with автоматически закрывает сокет.

9.12.4. Ограничения размера датаграммы

Теоретически UDP-датаграммы могут достигать примерно 64 КБ, но на практике предел значительно меньше. Каждое звено пути между отправителем и получателем имеет максимальный размер передаваемого блока (Maximum Transmission Unit, MTU) — наибольший единый блок байтов, который это звено может передать в одном кадре. Ethernet и Wi-Fi оба ограничивают его примерно 1500 байтами, и почти любой интернет-путь где-то упирается в этот предел.

Когда датаграмма превышает MTU звена, которое ей нужно пройти, сетевой уровень разбивает её на меньшие фрагменты и повторно собирает их в месте назначения. Сам UDP никогда не видит этого разбиения, но фрагменты обладают рядом неудобных свойств:

  • Если теряется хотя бы один фрагмент, вся датаграмма отбрасывается у получателя — повторной передачи отдельных фрагментов не существует. Вероятность потери растёт с числом фрагментов.

  • Некоторые сети и межсетевые экраны полностью отбрасывают фрагментированные пакеты, считая их подозрительными.

  • Повторная сборка требует памяти у получателя, которой на микроконтроллере не хватает.

Практическое правило на камере: держите UDP-сообщения значительно меньше 1500 байтов. Около 1400 байтов оставляет место для заголовков IP и UDP, любых накладных расходов на туннелирование, добавляемых путём, и небольших вариаций MTU между звеньями Ethernet, Wi-Fi и VPN. Приложениям, которым нужно отправлять больше, следует либо разбивать данные на уровне приложения, либо переходить на TCP, который обрабатывает разбиение и повторную сборку автоматически.

9.12.5. Распространённые ошибки

  • Забывают, что UDP может терять пакеты. Код, который прекрасно работает в спокойной локальной сети, иногда даёт неуловимые сбои в более загруженной или более крупной. Всегда проектируйте с учётом возможности того, что сообщение не дошло.

  • Получатель не привязан до того, как отправитель начнёт отправлять. Датаграмма, отправленная на порт, который никто не слушает, молча отбрасывается. Сначала запускайте получателя.

  • Отправка датаграммы больше MTU пути. См. предыдущий раздел — держите сообщения меньше ~1400 байтов.

Приведённые выше шаблоны охватывают почти все случаи применения UDP, к которым прибегает камера. Следующая страница делает то же самое для TCP.