Модуль socket
Работа с сокетами
Модуль socket предоставляет IPv4-сокеты TCP, UDP и RAW поверх lwIP. Количество сокетов ограничено сетевыми пулами контроллера; исчерпание ресурсов вызывает OSError. Закрывайте сокеты явно в finally.
Адреса и ограничения сети
Для bind, connect и sendto используется пара ('IPv4', port). IPv4 — четыре десятичных октета 0…255, порт — целое 0…65535. Пустая строка вместо IP, доменное имя, неполный адрес или октет больше 255 не принимаются. Используйте '0.0.0.0' для привязки ко всем локальным IPv4-адресам; порт 0 при bind выбирает свободный порт автоматически.
Для доменного имени сначала вызовите getaddrinfo либо gethostbyname. IPv6 в этой сборке не поддерживается, хотя константа AF_INET6 экспортирована.
UDP дополнительно ограничен встроенным firewall контроллера. Входящие пакеты на произвольные UDP-порты и ответы с произвольных портов отбрасываются до lwIP. Разрешены предусмотренные прошивкой службы, в частности ответы DNS с исходного порта 53 и SNTP с порта 123. Один bind не открывает произвольный UDP-порт в firewall. Отправка UDP может завершиться успешно, а recvfrom — таймаутом из-за фильтрации ответа.
При диагностике смотрите UdpPortErr в HTTP /txt?eth и статистику UDP в /txt?lwip. В стендовых тестах проверен обмен через временный UDP echo-сервер компьютера на разрешённом исходном порту 53; это не DNS-сервис и не универсальный UDP-сервер контроллера.
Разрешение имён
getaddrinfo
socket.getaddrinfo(host, port[, family[, type[, proto[, flags]]]])
Принимает строку с доменным именем либо числовым IPv4 и целый порт 0…65535. Возвращает список с одним элементом:
(AF_INET, SOCK_STREAM, 0, '', ('IPv4', port))
Поддерживаемые ограничения: family — 0 или AF_INET, type — 0 или SOCK_STREAM, proto=0, flags=0. Другие значения лишь вызывают предупреждение, а не обеспечивают фильтрацию или IPv6. Все параметры передаются позиционно.
При ошибке разрешения возникает OSError с отрицательным кодом lwIP. Отдельного класса gaierror нет.
import socket
address = socket.getaddrinfo('telematic.dev', 80, socket.AF_INET,
socket.SOCK_STREAM)[0][4]
print(address)
gethostbyname
socket.gethostbyname(host) возвращает IPv4 в строковом виде. При числовом IPv4 DNS-запрос не требуется. Ошибка разрешения вызывает OSError; здесь код преобразуется в номер ошибки сокета, поэтому не следует считать коды этой функции и getaddrinfo одинаковыми.
import socket
assert socket.gethostbyname('192.168.50.108') == '192.168.50.108'
Для доменных имён нужен настроенный и доступный DNS-сервер. DNS-вызовы блокирующие; settimeout конкретного сокета их не ограничивает. Остановка сценария во время разрешения имени может дождаться штатного завершения/таймаута DNS. В регрессиях проверены числовые адреса; это не проверка доступности внешнего DNS-сервера.
Создание сокета
socket.socket([family[, type[, proto]]])
Аргументы позиционные. По умолчанию family=AF_INET, type=SOCK_STREAM, proto=0.
AF_INET,SOCK_STREAM— TCP;AF_INET,SOCK_DGRAM— UDP;AF_INET,SOCK_RAW— RAW, гдеprotoзадаёт номер IP-протокола 0…255, например 1 для ICMP.
Для TCP/UDP протокол определяется типом сокета; обычно третий аргумент не нужен. Константы IPPROTO_TCP, IPPROTO_UDP и IPPROTO_ICMP не экспортированы. Для обычного ICMP используйте net.ping.
Привязка и соединения
bind
sock.bind(address) привязывает локальный адрес. Для TCP/UDP используйте свободный порт. Занятый порт вызывает OSError; наличие SO_REUSEADDR не гарантирует успешную привязку при любых существующих соединениях.
listen
sock.listen([backlog]) переводит TCP-сокет в режим ожидания подключений. По умолчанию backlog=2; допустимо целое 0…255. Значения 0 и 1 используют один слот очереди. Для больших значений выделяется дополнительная память; фактическая вместимость также ограничена пулом TCP. Повторный вызов listen на уже слушающем сокете вызывает OSError.
accept
sock.accept() возвращает (conn, address): новый TCP-сокет и пару IPv4/порт клиента. Принятый сокет наследует timeout listener. Его нужно закрывать отдельно. Метод учитывает блокирующий/неблокирующий режим и timeout.
connect
sock.connect(address) устанавливает TCP-соединение. Для UDP/RAW задаёт адрес партнёра для последующего send.
У TCP в неблокирующем режиме незавершённое соединение вызывает OSError(EINPROGRESS). В этой реализации EINPROGRESS также возвращается при истечении установленного ожидания connect; попытка соединения при этом может продолжаться в lwIP. Если она больше не нужна, закройте сокет.
Передача данных
send
sock.send(data) принимает байтовый буфер и возвращает число отправленных байт. У TCP результат может быть меньше длины данных. Для UDP требуется предварительный connect.
sendall
sock.sendall(data) отправляет все данные TCP либо вызывает исключение. Возвращает None. При ошибке часть данных уже могла уйти. Timeout применяется к отдельным операциям отправки, а не ко всему вызову суммарно. Для UDP метод не реализован; используйте send/sendto.
sendto
sock.sendto(data, address) отправляет UDP-датаграмму (либо RAW-пакет) и возвращает число байт. Путь UDP/RAW отклоняет длину больше 65535; это технический предел буфера, а не гарантия возможности отправки IP-пакета такого размера. Фактические ограничения ниже и зависят от заголовков IP, MTU и свободной памяти.
recv
sock.recv(bufsize) возвращает bytes длиной не больше bufsize. Требуется неотрицательное целое; буфер выделяется из кучи Python. Ограничивайте размер ожидаемыми данными. Отрицательные размеры вызывают ValueError.
Для TCP короткое чтение допустимо, а b'' после получения всех данных означает закрытие соединения партнёром. Для UDP читается одна датаграмма; если буфер меньше пакета, оставшийся хвост теряется. В UDP пустая датаграмма не означает закрытие соединения.
recvfrom
sock.recvfrom(bufsize) возвращает (data, address), где data — bytes, адрес — (IPv4, port). Правила длины и усечения датаграммы такие же, как у recv. Входящий UDP-буфер хранит одну ожидающую датаграмму; новые пакеты до её чтения могут отбрасываться.
TCP — поток байтов: один send партнёра не соответствует одному recv. Для прикладных сообщений задайте формат длины или разделитель.
Блокировка и ошибки
settimeout
sock.settimeout(None) # Неограниченное ожидание, исходный режим.
sock.settimeout(1.5) # Ожидание в секундах.
sock.settimeout(0) # Неблокирующий режим.
Конечный timeout должен быть неотрицательным; после перевода в миллисекунды значение должно быть меньше 4294967295. Дробная часть миллисекунды отбрасывается, поэтому положительное значение меньше 1 мс также включает неблокирующий режим. Отрицательное число, NaN и бесконечность вызывают ValueError.
Чтение/запись без готовых данных в неблокирующем режиме вызывает OSError(EAGAIN), истечение ожидания чтения/записи — OSError(ETIMEDOUT). Особенность connect описана выше. Проверяйте exc.args[0]; отдельного класса socket.timeout нет.
setblocking
sock.setblocking(True) равнозначен settimeout(None), а setblocking(False) — settimeout(0). Вызов с True снимает ранее установленный конечный timeout.
close
sock.close() освобождает ресурсы; повторный вызов безопасен. Операции передачи и setsockopt после закрытия вызывают OSError. Нативные ресурсы недостижимого сокета освобождаются также сборщиком мусора, но явный close предпочтителен. Закрытый сокет повторно не используется — создайте новый.
Опции и потоковый интерфейс
setsockopt
Основная поддерживаемая опция:
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
SO_REUSEADDR включает/выключает возможность повторного использования адреса. Также экспортированы IPPROTO_IP, IP_ADD_MEMBERSHIP, IP_DROP_MEMBERSHIP; аргумент для членства — 8 байт: IPv4 группы, затем IPv4 интерфейса. Наличие IGMP-опций не отменяет ограничений firewall и не подтверждает работу прикладного multicast в данной сети.
В реализации есть расширение setsockopt(SOL_SOCKET, 20, callback): callable вызывается через планировщик MicroPython с сокетом в качестве аргумента при поступлении TCP-данных/закрытии партнёром или появлении подключения listener. None отключает callback. Уведомления могут объединяться; это не отдельный вызов на каждый пакет. Для UDP/RAW таких уведомлений нет. Исполнение callback при совпадении с GC не проверено отдельным нагрузочным тестом.
makefile, read и write
makefile(...) возвращает тот же сокет, а не независимый файловый объект: закрытие любого из этих ссылочных объектов закрывает общее соединение. Доступны потоковые read, readinto, readline, write. Учитывайте timeout и собственные границы сообщений. Для гарантированной передачи полного TCP-буфера используйте sendall.
Примеры
TCP-клиент
Адрес и порт замените на ваш TCP-сервер.
import socket
sock = socket.socket()
sock.settimeout(2)
try:
sock.connect(('192.168.50.101', 8080))
sock.sendall(b'hello\n')
print(sock.recv(256)) # Одно чтение, а не гарантия целого ответа.
finally:
sock.close()
TCP echo-сервер для одного клиента
import socket
server = socket.socket()
try:
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(('0.0.0.0', 19001))
server.listen(1)
server.settimeout(30)
client, address = server.accept()
try:
client.settimeout(2)
data = client.recv(256)
if data:
client.sendall(data)
finally:
client.close()
finally:
server.close()
На контроллере проверены TCP-клиент, частичные чтения, передача 4096 байт, accept с одним ожидающим соединением, сброс непринятого клиента, закрытие очереди listener, UDP-обмен в пределах firewall, отклонение неверных аргументов и финализация сокетов. Успешное разрешение домена внешним DNS, multicast и все варианты RAW-пакетов в этот набор проверок не входили.