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