К концу руководства вы освоите основные функции и методы модуля Python socket, научитесь применять пользовательский класс для отправки сообщений и данных между конечными точками и работать со всем этим в собственных клиент-серверных приложениях. Материалом делимся к старту курса по Fullstack-разработке на Python.

Сеть при этом может быть логической, локальной сетью компьютера или физически подключённой к внешней сети, с собственными подключениями к другим сетям. Очевидный пример — интернет, к которому подключаются через провайдера.

Примеры протестированы на Python 3.10, но подойдёт и версия 3.6 или новее. Исходным код поможет использовать это руководство по максимуму.

Сети и сокеты — большие темы, по которым написаны томá литературы. Если они вам в новинку, переварить терминологию со всеми подробностями может быть трудно. Но с этим руководством всё получится!

История сокетов


История у сокетов давняя. Их применение началось с ARPANET в 1971 году и продолжилось в 1983-м, когда в операционной системе Berkeley Software Distribution (BSD) появился API под названием «сокеты Беркли».

В 1990-х годах вместе со Всемирной паутиной возникло и сетевое программирование. Преимущества сокетов и первых подключаемых сетей применялись не только в веб-серверах и браузерах — широко стали применяться клиент-серверные приложения разных типов и размеров.

Базовые протоколы API сокетов развивались многие годы, появились и новые, а низкоуровневый API остался прежним.

Самые распространённые сегодня приложения с сокетами — это клиент-серверные приложения, где одна сторона действует как сервер и ожидает подключения клиентов. Именно такое приложение вы напишете благодаря руководству. А конкретнее, сосредоточимся на API сокетов для интернет-сокетов. Иногда их называют сокетами Беркли, или сокетами BSD. Есть и сокеты домена Unix, которые используются для взаимодействия между процессами внутри только одного компьютера.

Обзор API сокетов


В модуле socket есть интерфейс к API сокетов Беркли.

Вот основные функции и методы этого API:

  • .socket()
  • .bind()
  • .listen()
  • .accept()
  • .connect()
  • .connect_ex()
  • .send()
  • .recv()
  • .close()

В Python имеется удобный и последовательный API, напрямую сопоставленный с системными вызовами, то есть аналогами функций из списка выше на C. В следующем разделе вы узнаете, как эти функции используются вместе.

Кроме того, в стандартной библиотеке Python есть классы, которые упрощают применение этих функций. Хотя в этом руководстве socketserver не рассматривается, с этим фреймворком для сетевых серверов можно ознакомиться по ссылке.

Доступно много модулей, где реализованы интернет-протоколы уровня выше, например HTTP и SMTP. Обзор этих протоколов смотрите в разделе документации Python «Интернет-протоколы и их поддержка».

TCP-сокеты


С помощью socket.socket() вы создадите объект сокета с указанием типа сокета socket.SOCK_STREAM. При этом по умолчанию применяется протокол управления передачей (TCP). Возможно, это то, что вам нужно.

Но зачем вам TCP? Вот его особенности:

  • TCP надёжен. Отброшенные в сети пакеты обнаруживаются и повторно передаются отправителем.
  • Данные доставляются с сохранением порядка очерёдности. В приложении данные считываются в порядке их записи отправителем.

Для сравнения: сокеты, которые создаются через socket.SOCK_DGRAM протокола пользовательских датаграмм ненадёжны: данные могут считываться получателем с изменением порядка очерёдности записей отправителя. Почему это важно? Сети — это система негарантированной доставки. Нет гарантии, что данные дойдут до места назначения или что отправленные данные будут получены.

Сетевые устройства — маршрутизаторы и коммутаторы — также обладают конечной полосой пропускания и собственными, системными ограничениями. Как на клиентах и на серверах, у них есть процессоры, память, шины и интерфейсные буферы пакетов. С TCP при этом не нужно беспокоиться о потере пакетов, поступлении данных с изменением порядка очерёдности пакетов данных, а также о других подводных камнях.

Чтобы разобраться лучше, ознакомьтесь с последовательностью вызовов API сокетов и с потоком данных TCP.

Ниже слева сервер, а справа клиент:

Поток TCP-сокетов
Поток TCP-сокетов. В центре изображения показан обмен данными между клиентом и сервером с помощью вызовов .send() и .recv().

Внизу соответствующие сокеты закрываются на клиенте и на сервере. (источник изображения)

Начиная с верхнего левого угла, показаны серверные вызовы API на сервере, которые настраивают «прослушиваемый» сокет:

  • socket()
  • .bind()
  • .listen()
  • .accept()

Этим сокетом, как следует из его названия, прослушиваются подключения от клиентов. Чтобы принять или завершить такое подключение, на сервере вызывается .accept().

А чтобы установить подключение к серверу и инициировать трёхэтапное рукопожатие, на клиенте вызывается .connect(). Процесс рукопожатия важен, ведь он гарантирует доступность каждой стороны подключения в сети, то есть то, что клиент может связаться с сервером, и наоборот. Возможно, только один хост, клиент или сервер может связаться с другим.

Эхо-клиент и эхо-сервер


Теперь, когда вы узнали об API сокетов и взаимодействии клиента и сервера, вы готовы создать свои первые клиент и сервер. Начнём с примера, где полученное сервером сообщение просто возвращается клиенту, как эхо.

Эхо-сервер



Вот он:
# echo-server.py

import socket

HOST = "127.0.0.1"  # Standard loopback interface address (localhost)
PORT = 65432  # Port to listen on (non-privileged ports are > 1023)

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.bind((HOST, PORT))
    s.listen()
    conn, addr = s.accept()
    with conn:
        print(f"Connected by {addr}")
        while True:
            data = conn.recv(1024)
            if not data:
                break
            conn.sendall(data)

Не пытайтесь понять весь код сразу. В этих нескольких строках много чего происходит. И это только отправная точка, здесь можно увидеть базовый сервер в деле. Но что же происходит в вызове нашего API?

С помощью socket.socket() создаётся объект сокета, которым поддерживается тип контекстного менеджера, который используется в операторе with. Вызывать s.close() не нужно:

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    pass  # Use the socket object without calling s.close().

Передаваемые в socket() аргументы — это константы, используемые для указания семейства адресов и типа сокетов. AF_INET — это семейство интернет-адресов для IPv4. SOCK_STREAM — это тип сокета для TCP и протокол, который будет использоваться для передачи сообщений в сети.

Метод .bind() применяется для привязки сокета к конкретному сетевому интерфейсу и номеру порта:

# echo-server.py

# ...

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.bind((HOST, PORT))
    # ...

Передаваемые в .bind() значения зависят от семейства адресов сокета. В этом примере используется socket. AF_INET (IPv4). Поэтому принимается кортеж с двумя значениями: (host, port).

host может быть именем хоста, IP-адресом или пустой строкой. Если используется IP-адрес, то host должен быть строкой адреса формата IPv4. IP-адрес 127.0.0.1 — это стандартный IPv4-адрес для интерфейса «внутренней петли», когда к серверу подключаются только процессы в хосте. Если передавать пустую строку, подключения на сервере принимаются во всех доступных интерфейсах IPv4.

port — это номер TCP-порта для приёма подключений от клиентов. Это должно быть целое число от 1 до 65535(0 резервируется). В некоторых системах, если номер порта меньше 1024, могут потребоваться привилегии суперпользователя.

Относительно использования имён хостов с .bind() есть замечание:

«Если в хостовой части адреса сокета IPv4/v6 использовать имя хоста, программа может стать непредсказуемой: Python использует первый возвращаемый из разрешения DNS адрес. Адрес сокета будет разрешён в фактический адрес IPv4/v6 по-разному, в зависимости от результатов из DNS-разрешения и/или конфигурации хоста. Чтобы поведение было предсказыемым, в хостовой части используйте числовой адрес». Документация.

Подробнее об этом вы узнаете позже в разделе «Использование имён хостов». А пока достаточно понять, что при использовании имени хоста можно увидеть разные результаты в зависимости от того, чтó возвращается в процессе разрешения имён. Это может быть что угодно: при первом запуске приложения можно получить 10.1.2.3, а в следующий раз получится 192.168.0.1. Дальше может быть 172.16.7.8 и т. д.

В примере ниже подключения на сервере принимаются благодаря .listen(), а сам сервер становится «прослушиваемым» сокетом:

# echo-server.py

# ...

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.bind((HOST, PORT))
    s.listen()
    conn, addr = s.accept()
    # ...

У метода .listen() есть параметр backlog. Он указывает число непринятых подключений, которые система разрешит до отклонения новых подключений. С версии Python 3.5 он необязателен. Если его нет, выбирается значение backlog по умолчанию.

А если на сервере получается много одновременных запросов на подключение, значение backlog можно увеличить через установку максимальной длины очереди для отложенных подключений. Это предельное значение зависит от системы. Например, на Linux смотрите /proc/sys/net/core/somaxconn.

Методом .accept() выполнение блокируется, и ожидается входящее подключение. При подключении клиента возвращается новый объект сокета, который представляет собой подключение и кортеж с адресом клиента. В кортеже содержится (host, port) — для подключений IPv4 или (host, port, flowinfo, scopeid) — для IPv6. Подробнее о значениях кортежей рассказывается в справочном разделе «Семейства адресов сокетов».

Итак, теперь у вас есть новый объект сокета из .accept(). Это важно потому, что сокет будет использоваться для взаимодействия с клиентом. Он отличается от прослушиваемого, который применяется на сервере для приёма новых подключений:

# echo-server.py

# ...

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.bind((HOST, PORT))
    s.listen()
    conn, addr = s.accept()
    with conn:
        print(f"Connected by {addr}")
        while True:
            data = conn.recv(1024)
            if not data:
                break
            conn.sendall(data)

После того как в .accept() клиенту предоставляется объект сокета conn, для перебора блокирующих вызовов в conn.recv() используется бесконечный цикл while. Так любые отправляемые от клиента данные считываются и передаются обратно с помощью conn.sendall().

Если в conn.recv() возвращается пустой объект bytes и b'', значит, в клиенте подключение закрыто и цикл завершён. Чтобы автоматически закрыть сокет в конце блока, с conn применяется оператор with.

Эхо-клиент


Перейдём к клиенту:

# echo-client.py

import socket

HOST = "127.0.0.1"  # The server's hostname or IP address
PORT = 65432  # The port used by the server

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.connect((HOST, PORT))
    s.sendall(b"Hello, world")
    data = s.recv(1024)

print(f"Received {data!r}")

По сравнению с сервером клиент довольно прост. В нём создаётся объект сокета. Для подключения к серверу используется .connect(), и для отправки сообщения вызывается s.sendall(), s.recv() считывает ответ, а затем этот ответ выводится.

Запуск эхо-клиента и эхо-сервера


В этом разделе запускаем клиент и сервер, следим за их поведением и за происходящим.

> Если вам не удаётся запустить из командной строки примеры или собственный код, прочитайте How Do I Make My Own Command-Line Commands Using Python? или How to Run Your Python Scripts (англ.). Если у вас Windows, ознакомьтесь с Python Windows FAQ («Часто задаваемыми вопросами по Python для Windows»).

Откройте терминал или командную строку, перейдите в каталог со скриптами, убедитесь, что в переменной PATH у вас есть Python 3.6 или новее, а затем запустите сервер:

$ python echo-server.py

Терминал зависнет, потому что сервер заблокирован или находится в состоянии ожидания, в .accept():

# echo-server.py

# ...

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.bind((HOST, PORT))
    s.listen()
    conn, addr = s.accept()
    with conn:
        print(f"Connected by {addr}")
        while True:
            data = conn.recv(1024)
            if not data:
                break
            conn.sendall(data)

Ожидается подключение клиента. Затем откройте другое окно терминала или командную строку и запустите клиента:

$ python echo-client.py 
Received b'Hello, world'

В окне сервера вы должны заметить что-то такое:

$ python echo-server.py 
Connected by ('127.0.0.1', 64623)

Здесь на сервере выведен кортеж addr, возвращаемый из s.accept(). Это IP-адрес клиента и номер TCP-порта — 64623 (скорее всего, он будет другим, когда вы запустите сервер на своём компьютере).

Просмотр состояния сокета


Чтобы увидеть текущее состояние сокетов на хосте, используйте netstat. На macOS, Linux и Windows он доступен по умолчанию.

А ниже вывод netstat из macOS после запуска сервера:

$ netstat -an
Active Internet connections (including servers)
Proto Recv-Q Send-Q  Local Address          Foreign Address        (state)
tcp4       0      0  127.0.0.1.65432        *.*                    LISTEN

Обратите внимание: Local Address здесь 127.0.0.1.65432. Если бы в echo-server.py был HOST = "", а не HOST = "127.0.0.1", в netstat отображалось бы это:

$ netstat -an
Active Internet connections (including servers)
Proto Recv-Q Send-Q  Local Address          Foreign Address        (state)
tcp4       0      0  *.65432                *.*                    LISTEN

Local Address здесь *.65432. Это означает, что для приёма входящих подключений будут задействованы все поддерживающие семейство адресов доступные интерфейсы хоста. В этом примере в вызове socket() используется socket. AF_INET (IPv4) — смотрите tcp4 в столбце Proto.

Здесь показывается только вывод эхо-сервера. Скорее всего, полный вывод будет гораздо больше, это зависит вашей системы. Стóит обратить внимание на столбцы Proto, Local Address и (state). В последнем примере netstat показывает, что на эхо-сервере используется TCP-сокет IPv4 (tcp4) в порте 65432 на всех интерфейсах (*.65432) и он находится в состоянии прослушивания (LISTEN).

Другой способ получить к нему доступ (и дополнительную полезную информацию) — использовать программу lsof, которая выводит список открытых файлов. На macOS она доступна по умолчанию, а на Linux её можно установить пакетным менеджером:

$ lsof -i -n
COMMAND     PID   USER   FD   TYPE   DEVICE SIZE/OFF NODE NAME
Python    67982 nathan    3u  IPv4 0xecf272      0t0  TCP *:65432 (LISTEN)

Если lsof используется с параметром -i, в её выводе предоставляется COMMAND, PID (идентификатор процесса) и USER (идентификатор пользователя) открытых интернет-сокетов. Выше показан процесс эхо-сервера.

netstat и lsof различаются в зависимости от ОС, у них много опций. Загляните в их man или документацию, на них определённо стóит потратить немного времени. На macOS и Linux используйте man netstat и man lsof. На Windows — netstat /? .

При попытке подключения к порту без прослушиваемого сокета есть типичная ошибка:

$ python echo-client.py 
Traceback (most recent call last):
  File "./echo-client.py", line 9, in <module>
    s.connect((HOST, PORT))
ConnectionRefusedError: [Errno 61] Connection refused

Здесь либо указан неверный номер порта, либо не запускается сервер. Или, может быть, на пути стоит брандмауэр, которым подключение блокируется (об этом легко забыть). Также может быть сообщение об ошибке Connection timed out («Превышено время ожидания подключения»). Чтобы клиент подключался к TCP-порту, добавьте соответствующее правило брандмауэра!

Поможем разобраться в программировании, чтобы вы прокачали карьеру или стали востребованным профессионалом в IT:
Чтобы увидеть все курсы, нажмите на баннер:




Комментарии (0)