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

Установка

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

Python

pip install channels

pip install channels-redis

pip install daphne

Если при установке daphne пип выдает ошибку про C++, то скачиваем BuildTools и во время установки выбираем "Разработка классических приложений на C++". Ребутаем комп и снова запускаем pip install.

Шаг №1

В файл settings.py добавляем загруженные пакеты (Причем исходя из документации - daphne надо добавить обязательно в начало списка):

INSTALLED_APPS = [
  'daphne',
  'channels',
  ...
]

В этом же файле изменяем:

WSGI_APPLICATION = 'yourapp.wsgi.application'

на:

ASGI_APPLICATION = 'yourapp.asgi.application'

Добавляем данные Channel layers в settings.py, конфигурация зависит от того, где у вас стоит Redis, подробнее тут. Для тестов я использовал такой сетап:

CHANNELS_LAYERS = {
  'default': {
    'BACKEND': 'channels.layers.InMemoryChannelLayer'
  }
}

Шаг №2

Создаем файл consumers.py в директории нашего приложения. Если вы работали с Class Based Views в Django то Consumers вам покажется знакомым.

Создаем свой обработчик:

from channels.consumer import AsyncConsumer


YourConsumer(AsyncConsumer):

    async def websocket_connect(self, event):
        await self.send({"type": "websocket.accept"})

    async def websocket_recieve(self, text_data):
        await self.send({
            "type": "websocket.send",
            "text": "Hello from Django socket"
        })

    async def websocket_disconnect(self, event):
        pass

Каждый метод класса YourConsumer отвечает за свой тип запросов через сокеты. Очень похоже на CBV

Шаг №3

Изменяем данные файла asgi.py в корневой директории проекта:

from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter
from channels.routing import URLRouter

from django.core.asgi import get_asgi_application
from django.urls import path


import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'yourapp.settings')


from yourapp.consumers import YourConsumer


django_asgi_app = get_asgi_application()

application = ProtocolTypeRouter({
    'http': django_asgi_app,
    'websocket': AuthMiddlewareStack(
        URLRouter([
            path('ws', YourConsumer.as_asgi())
        ])
    )
})

В URLRouter мы прописываем путь, по которому будет доступен Consumer. Например в данном случае для 127.0.0.1 это - ws://127.0.0.1:8000/ws. Это очень похоже на концепцию urls.py, кстати при желании можно вынести все пути в отдельный файл routers.py и импортировать их потом в корневую директорию.

Тестируем

Запускаем локалку и отправляем запросы к сокету. Например через Js:

const socket = new WebSocket('ws://127.0.0.1:8000/ws');

socket.onopen = function(e) {
  socket.send(JSON.stringify({
    message: 'Hello from Js client'
  }));
};

socket.onmessage = function(event) {
  try {
    console.log(event);
  } catch (e) {
    console.log('Error:', e.message);
  }
};
Запускаем запрос и видим ответ который мы прописывали в consumers.py
Запускаем запрос и видим ответ который мы прописывали в consumers.py

Ссылки

Документация

Гитхаб

Мой тг

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


  1. baldr
    17.11.2022 23:58
    -1

    Не используйте channels - возьмите что-нибудь вроде socket.io - там и клиентская часть есть, и движок с автореконнектом и может откатиться на long polling если websockets недоступен по какой-то причине. Автор питоновского движка довольно оперативно отвечает на баги и на SO.


    1. MatveySss Автор
      18.11.2022 04:14

      Мне нужен был минимальный функционал, потому что в качестве клиента выступает расширение для браузера. Взял самое простое и популярное решение.


    1. Yuribtr
      18.11.2022 08:37
      +4

      При перечисленных вами плюсах, у SocketIO есть помимо описанных плюсов некоторые минусы:

      • Протокол общения SocketIO не совместим с вебсокетами, это связано с тем, что SocketIO работает не только на вебсокетах

      • SocketIO генерирует заметно больше трафика при подключении.

      • Для запуска SocketIO нужно подгрузить JS библиотеку в браузер, в то время как вебсокеты работают «из коробки»

      • У библиотек SocketIO, которая используется на фронте (Javascript), и библиотеки SocketIO на сервере (python-socketio) должны совпадать мажорные версии. Там даже есть целая таблица совместимостей, то есть апгрейд на мажорную версию возможен только одновременно обоих движков. Например фронтовый движок версии 1,2 совместим с протоколом версии 3,4 который поддерживает движок версии 3. В крупных компаниях это неудобно делать.

      • У бэковой части SocketIO (EngineIO) проблема с распараллеливанием при установленном параметре workers в gunicorn больше единицы - ранее открытые вебсокет соединения теряются при открытии новых. Разработчик предписывает применять горизонтальное масштабирование без конкурентного выполнения.

      • неудобна концепция обработчиков на бэке - EngineIO крутится в loop параллельно основному приложению и путь прописывается у каждого экземпляра ASGIApp, то например сделать привычное версионирование API уже проблема. Плюс надо заранее закладывать имена всех событий которые могут прийти с фронта иначе просто не будет обработки такого сообщения.

      • Перехват событий коннекта и дисконнекта возможен только для всего AsyncServer а не для каждой версии API (ASGIApp) отдельно, а именно в них обычно производится авторизация.


      1. baldr
        18.11.2022 08:46
        -1

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

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

        Поддержка чистых вебсокетов есть уже почти в каждом http-фреймворке - и в httpx, и в aiohttp.


  1. vagon333
    18.11.2022 09:20

    У меня проект на .Net
    Тестировал SocketIO и др. решения для фонового обновления клиентского кеша (IndexedDB).
    В конечном итоге использую SignalR:

    • не нужен отдельный порт для ws

    • простая реализация на клиентской и серверной стороне

    • интеграция в сущестующее веб-приложение, без необходимости в отдельном сервере


    1. baldr
      18.11.2022 13:28

      Ох, сэр.. Я сталкивался с SignalR когда мне нужно было получать какие-то данные по этому протоколу как клиенту. У меня основной код был на Python, поэтому пришлось искать библиотеку на нем. Нашел, конечно.. Конечно же, сразу не завелось. И пришлось поразбираться в исходниках, как обычно..

      И если SocketIO не очень совместим с вебсокетами, то вот SignalR - это здоровенный комбайн с абстракциями и интерфейсами и полупроприетарным протоколом. Я уже не помню что у меня там не работало - что-то с сертификатами или шифрованием, кажется. Прикрутить это, вроде, можно было, но каким-то образом задача отменилась и я, с облегчением, все выкинул.

      Я не могу сказать что он плохой - если у кого-то архитектура на .NET и все клиенты - это только веб-фронт, то почему нет, поскользу он ближе этой среде. Но брать как протокол для других языков - ох нет.


      1. vagon333
        18.11.2022 16:25

        Верно, для других языков SignalR не подойдет.
        Как вы написали выше - в SignalR под капотом масса логики, включая авто-переход на long polling, если websockets по какой-то причине не поддерживается.
        А на поверхности все просто.