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.