9.13. TCP-сокеты#
TCP-сокеты бывают двух видов, которые выглядят по-разному, но используют один и тот же базовый тип: клиентские сокеты, которые вызывают connect() для подключения к удалённому серверу, и серверные сокеты, которые вызывают bind(), listen() и accept() для приёма входящих подключений. Обе роли используют один и тот же класс socket, представленный на странице Объекты сокетов; различаются лишь вызываемые на них методы.
9.13.1. TCP-клиент#
Простейший клиент открывает соединение, отправляет запрос, читает ответ и закрывается:
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("192.168.1.20", 9000))
s.send(b"hello\n")
reply = s.recv(1024)
print("reply:", reply)
s.close()
connect() выполняет тройное рукопожатие, описанное на странице TCP – надёжный поток байтов, и возвращает управление, когда соединение установлено. send() записывает байты в соединение; recv() читает из него до заданного числа байтов. Когда приложение завершило работу, close() закрывает соединение.
Тот же скрипт, обёрнутый в идиому с инструкцией with из Объекты сокетов, так что сокет закрывается даже в случае возникновения исключения:
import socket
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect(("192.168.1.20", 9000))
s.send(b"hello\n")
print(s.recv(1024))
9.13.1.1. Чтение до полного получения данных#
Один вызов recv() возвращает до запрошенного числа байтов — он может вернуть меньше, потому что TCP представляет собой поток, а не последовательность сообщений. Приложению приходится продолжать чтение, пока оно не получит полный ответ:
chunks = []
while True:
chunk = s.recv(1024)
if not chunk: # empty bytes -> other side closed
break
chunks.append(chunk)
reply = b"".join(chunks)
Цикл завершается, когда recv() возвращает пустой объект bytes. Это происходит, когда другая сторона корректно закрыла свою половину соединения; приложение читает «конец потока» как то же самое, что и «конец сообщения», в протоколах такого рода.
9.13.1.2. Отправка до полной передачи данных#
К send() применима противоположная оговорка: он может отправить меньше байтов, чем запрошено, возвращая количество фактически записанных байтов. Для больших объёмов данных повторяйте отправку неотправленного остатка:
payload = some_big_bytes
while payload:
n = s.send(payload)
payload = payload[n:]
sendall() выполняет этот цикл внутри себя, поэтому большинство кода может просто вызвать его и избежать ручного повтора:
s.sendall(some_big_bytes)
9.13.2. TCP-сервер#
Серверная сторона состоит из четырёх шагов: занять порт, перевести сокет в режим прослушивания, принимать соединения одно за другим, обмениваться данными по каждому принятому сокету. Минимальный эхо-сервер:
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("0.0.0.0", 9000))
server.listen(1)
print("listening on port 9000")
while True:
conn, addr = server.accept()
print("connection from", addr)
while True:
data = conn.recv(1024)
if not data:
break
conn.send(data) # echo back
conn.close()
Шаг за шагом:
bind()занимает хост и порт на камере."0.0.0.0"принимает соединения на любом интерфейсе; замена этого значения на конкретный IP ограничивает прослушивание этим интерфейсом.listen()переводит сокет из обычного сокета в прослушивающий. Аргумент — это очередь ожидания (backlog): сколько ожидающих соединений MicroPython будет ставить в очередь, пока приложение занято. Выбирайте небольшое число; в большинстве случаев достаточно1.accept()блокируется, пока не подключится клиент, затем возвращает(conn, addr): новый сокет, представляющий это конкретное соединение, и адрес клиента. Сам прослушивающий сокет остаётся открытым для приёма новых подключений.Все байты обмена проходят через
conn— новый сокет. Чтение и запись используют те же вызовыrecv()/send(), что и на стороне клиента.Когда клиент закрывает соединение,
recv()возвращаетb""; внутренний цикл завершается, и сервер закрывает свою сторону с помощьюclose().
Внешний цикл while True возвращается к accept(), чтобы ожидать следующего клиента. В таком виде сервер обрабатывает по одному клиенту за раз; для параллельной работы с несколькими клиентами потребуются либо потоки, либо asyncio. Последнему посвящена следующая страница.
9.13.3. Распространённые ошибки#
Восприятие recv() как ориентированного на сообщения. Это не так. Два вызова
send(b"hi")могут прийти как одинrecv(4)со значениемb"hihi"или как дваrecv(2). Приложению приходится добавлять разметку (framing), если границы сообщений важны — символ новой строки, префикс длины, что угодно.Забывают повторять отправку при коротких передачах. Используйте
sendall()для всего, что больше нескольких сотен байтов.Забывают закрыть принятый сокет. Каждый
conn— это отдельный сокет; закрытие прослушивающего сокета не закрывает принятые. Блокиwithдля обоих делают такую ошибку маловероятной:while True: with server.accept()[0] as conn: # ... talk on conn ...
Повторная привязка к порту, всё ещё находящемуся в состоянии TIME_WAIT. Когда сервер перезапускается в течение нескольких секунд после закрытия,
bind()может завершиться ошибкой «address in use», потому что MicroPython всё ещё удерживает порт для предыдущего соединения. Вызовserver.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)передbind()устраняет это.
9.13.4. Что дальше#
Блокировка на accept() означает, что сервер может обслуживать лишь одного клиента за раз. Блокировка на recv() означает, что один медленный клиент подвешивает весь цикл. Стандартное решение на камере — asyncio: запускайте каждое соединение как отдельную задачу, позволяя циклу событий переключаться между ними. Следующая страница рассматривает asyncio-версии всего, что описано на этой.