Найти в Дзене
Nuances of programming

Как создавать веб-сокеты в Python

Оглавление

Источник: Nuances of Programming

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

Мы научимся настраивать собственный веб-сокет на Python, используя WebSockets API, который делает возможным двусторонний интерактивный сеанс связи между клиентом и сервером. С веб-сокетами вы можете отправлять и получать сообщения в режиме отслеживания событий без необходимости все время запрашивать данные у сервера, что уменьшает накладные расходы и позволяет передавать данные с сервера и на сервер в режиме реального времени. 

Начинаем

Для работы с веб-сокетами требуется Python версии 3.6.1 и выше.

API в Python легко установить следующей командой: 

pip install websockets

Я использую пример, где сервер синхронизирует все полученные им сообщения с подключенными клиентами. 

Я не буду затрагивать тему безопасности, и весь код будет на Python. Этого достаточно для понимания всеми, кто немного знаком с языком или с программированием в целом. Таким образом, в дальнейшем вам будет проще написать consumer, producer или даже сервер на другом языке или для фронтенд приложения.

Я надеюсь, что приведенные примеры будут вам полезны, и призываю разработчиков попробовать веб-сокеты хотя бы раз в своей карьере — они потрясающие. Это больше, чем REST, знаете ли! 

Простой consumer сообщений

Простой consumer сообщений
Простой consumer сообщений

Итак, начнем с сопрограммы consumer, представленной выше. Я объясню каждую строку кода, чтобы вы смогли полностью понять, что происходит. Коротко: мы подключаемся к веб-сокету, указанному конкретным URL. Каждое сообщение, созданное сервером веб-сокета, логируется. 

Три самые важные строки я объясню детально, но, если вас не интересует синтаксис, можете смело пропустить эту часть.

async/await — это просто специальный синтаксис для комфортной работы с промисами. Промис — не более чем объект, представляющий собой возможную передачу или сбой асинхронной операции. 

Вместо передачи обратных вызовов в функцию вы можете присоединить обратные вызовы к этому возвращенному объекту.

В Python async гарантирует, что функция вернет промис и обернет в него не-промисы. В процессе вызова await может выполняться другой, не имеющий отношения к процессу, код. 

websocket_resource_url = f"ws://{host}:{port}"

URL ресурса веб-сокета использует собственную схему, начинающуюся с ws (или wss для безопасного подключения). Далее следует имя хоста и номер порта (например, ws://websocket.example.com:8400). Здесь я использовал f-строку для создания URL ресурса. Синтаксис такой же, как при использовании str.format(), но f-строка по умолчанию добавлена в Python 3.6 и делает форматирование строкового литерала менее многословным.

async with websockets.connect(websocket_resource_url) as ws:

Следующая строка открывает соединение с веб-сокетом, используя websockets.connect. Ожидание соединения вызывает WebSocketClientProtocol, который затем используется для отправки и получения сообщений. Эта строка использует async with, который работает с асинхронным контекстным менеджером. Соединение закрывается при выходе из контекста. 

Имейте в виду: иногда я использую аббревиатуру для веб-сокетов (ws), чтобы сделать код более читаемым в статье, но всегда пишу имя полностью в рабочем коде (Например, ws можно прочесть как веб-сайт или веб-сервер. Этого следует избегать хорошему разработчику. В конце концов код должен читаться как хорошая книга).

async for message in websocket:

async for — это что-то вроде синхронного цикла for, позволяющего асинхронное восприятие. 

Асинхронный IO позволяет выполнять перебор асинхронного итератора: вы можете вызывать асинхронный код на любом этапе перебора, в то время как обычный цикл for этого не позволяет. В этой строке кода веб-сокет — producer сообщений. 

-3

Для запуска этого простого consumer’а просто укажите имя хоста и порт, он запустится в постоянном режиме. Предельно просто. Не беспокойтесь, если нет цикла событий. asyncio создаст новый цикл и установит в качестве текущего. 

Этот пример кода будет принимать сообщения от ws://localhost:4000. Если сервер не запущен, он выдаст ошибку 404 Not Found.

Простой producer

Приведу пример producer’а, выдающего только одно значение. И он даже проще, чем comsumer:

Простой producer
Простой producer

Код выше говорит сам за себя. Мы подключаемся к веб-сокету так же, как делали это ранее с consumer’ом. Получаем сообщение от сервера и ждем ответ. Когда мы получаем сообщение от сервера, узнаем, что сообщение было доставлено. 

Теперь нам нужен только способ выполнить эту сопрограмму-отправитель только один раз. 

loop = asyncio.get_event_loop()
loop.run_until_complete(produce(message='hi', host='localhost', port=4000))

Конечно, у Python есть решение. Мы можем просто использовать цикл обработки событий так же, как мы делали это с consumer. Единственное отличие будет в том, что он будет запущен, пока мы не получим ответ от сервера. После получения ответа задача завершится. 

В Python 3.7 стало еще лучше — теперь можно использовать функцию run для выполнения сопрограмм. 

asyncio.run(produce(message='hi', host='localhost', port=4000))

Сервер: последний кусочек пазла 

Для этого случая я написал серверный класс, который объединяет весь функционал сервера. Этот сервер рассылает сообщения, отправленные producer’ом, всем слушающим consumer’ам. 

Сервер создается и определяет сопрограмму обработчика веб-сокета. Функция веб-сокета serve — это обёртка вокруг метода create_server() цикла обработки событий. Он создает и запускает сервер с create_server() и принимает обработчик веб-сокета в качестве аргумента. 

Когда бы ни подключался клиент, сервер принимает соединение, создает WebSocketServerProtocol, осуществляет открывающее “рукопожатие” и передает обработчику соединения, определенному обработчиком веб-сокета. Как только обработчик заканчивает работу, нормально или с исключением, сервер выполняет закрывающее “рукопожатие” и закрывает соединение. 

Все готово! Как только мы запустим сервер, он просто будет выполнять сопрограмму ws_handler, определенную в классе сервера (об этом выше) каждый раз, когда producer отправляет что-то. Затем он доставит сообщение всем подключенным клиентам. 

-5

Последняя часть кода — самая длинная, оставайтесь с нами, мы почти закончили.

Класс сервера, синхронизирующий сообщения с подключенными клиентами
Класс сервера, синхронизирующий сообщения с подключенными клиентами

ws_handler регистрирует клиентa, отправляет сообщение подключенным клиентам и, наконец, закрывает соединение. Consumer остается подключенным, в то время как producer отменяет собственную регистрацию. Сопрограмма distribute будет отправлять каждое сообщение в веб-сокете всем клиентам в списке подключенных клиентов. 

Если существует несколько подключенных клиентов, выполнится следующий фрагмент кода. asyncio.wait гарантирует, что мы продолжим только после того, как каждому клиенту будет отправлено сообщение. 

-7

Заключение

Вот несколько ключевых преимуществ веб-сокетов в сравнении с моделью длинного опроса HTTP:

  • Информация может передаваться в обе стороны в любое время в течение срока действия веб-сокет соединения.
  • Клиент и сервер постоянно соединены — данные могут отправляться клиентам все время без необходимости получать запрос. 
  • Довольно просто работать с веб-сокетами в Python. Пример с синхронизацией сообщений был реализован без написания большого количества кода. Выполнить эффективно ту же задачу с длинным опросом HTTP было бы значительно сложнее. 

Читайте также:

Читайте нас в телеграмме и vk

Перевод статьи Dieter Jordens: How To Create a WebSocket in Python