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?")
Несколько моментов, на которые стоит обратить внимание в отправителе:
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.