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-statement зі сторінки Об’єкти сокетів, щоб сокет закривався навіть у разі виникнення помилки:
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()переводить сокет зі звичайного в режим прослуховування. Аргумент – це черга – скільки очікуючих з’єднань 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)s. Якщо межі повідомлень мають значення, застосунок повинен додати обрамлення – символ нового рядка, префікс довжини тощо.Забування повторного надсилання при коротких відправках. Використовуйте
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 для всього, що описано на цій.