WebSocket — протокол связи поверх TCP-соединения, предназначенный для обмена сообщениями между браузером и веб-сервером в режиме реального времени. — Википедия
Мы научимся настраивать собственный веб-сокет на Python, используя WebSockets API, который делает возможным двусторонний интерактивный сеанс связи между клиентом и сервером. С веб-сокетами вы можете отправлять и получать сообщения в режиме отслеживания событий без необходимости все время запрашивать данные у сервера, что уменьшает накладные расходы и позволяет передавать данные с сервера и на сервер в режиме реального времени.
Начинаем
Для работы с веб-сокетами требуется Python версии 3.6.1 и выше.
API в Python легко установить следующей командой:
pip install websockets
Я использую пример, где сервер синхронизирует все полученные им сообщения с подключенными клиентами.
Я не буду затрагивать тему безопасности, и весь код будет на Python. Этого достаточно для понимания всеми, кто немного знаком с языком или с программированием в целом. Таким образом, в дальнейшем вам будет проще написать consumer, producer или даже сервер на другом языке или для фронтенд приложения.
Я надеюсь, что приведенные примеры будут вам полезны, и призываю разработчиков попробовать веб-сокеты хотя бы раз в своей карьере — они потрясающие. Это больше, чем REST, знаете ли!
Простой 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 сообщений.

Для запуска этого простого consumer’а просто укажите имя хоста и порт, он запустится в постоянном режиме. Предельно просто. Не беспокойтесь, если нет цикла событий. asyncio
создаст новый цикл и установит в качестве текущего.
Этот пример кода будет принимать сообщения от ws://localhost:4000
. Если сервер не запущен, он выдаст ошибку 404 Not Found.
Простой producer
Приведу пример producer’а, выдающего только одно значение. И он даже проще, чем comsumer:

Код выше говорит сам за себя. Мы подключаемся к веб-сокету так же, как делали это ранее с 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 отправляет что-то. Затем он доставит сообщение всем подключенным клиентам.

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

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

Заключение
Вот несколько ключевых преимуществ веб-сокетов в сравнении с моделью длинного опроса HTTP:
- Информация может передаваться в обе стороны в любое время в течение срока действия веб-сокет соединения.
- Клиент и сервер постоянно соединены — данные могут отправляться клиентам все время без необходимости получать запрос.
- Довольно просто работать с веб-сокетами в Python. Пример с синхронизацией сообщений был реализован без написания большого количества кода. Выполнить эффективно ту же задачу с длинным опросом HTTP было бы значительно сложнее.
Читайте также:
- Элегантное ООП в Python
- Пишем интерфейсы командной строки в Python как профи
- Что такое *args и **kwargs в Python?
Перевод статьи Dieter Jordens: How To Create a WebSocket in Python