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 для всього, що описано на цій.