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.