Что нового появилось в Django Channels?

С момента последнего поста про Django Channels прошло много времени, проект развивается и потихоньку идёт к стабильному релизу. В новых версиях пакета появляются интересные фишки о которых я сегодня расскажу вкратце:

Generic Consumers

Generic Consumers чем то напоминают Class Based Views в Django. Их задача сократить количество кода при написании обработчиков каналов, а также улучшить их структуру и внешний вид.

Базовым классом является BaseConsumer.

from channels.generic import BaseConsumer

class MyConsumer(BaseConsumer):

    method_mapping = {
        "your.channel.name": "method_name",
    }

    def method_name(self, message, **kwargs):
        pass

У класса есть атрибут-словарь method_mapping, где ключом является наименование канала, а значением — функция, обрабатывающая данный канал. В случае использования т.н. Class Based Consumers, в настройках роутов нужно прописывать:

from channels import route, route_class

channel_routing = [
    route_class(consumers.MyConsumer, path=r"^/myconsumer/"),
]

То есть нужно использовать метод route_class. В последней на данный момент версии (0.17.2), у классов появился метод .as_route()

В пакете также появился WebsocketConsumer, определяющий каркас для работы с WebSocket.

from channels.generic.websockets import WebsocketConsumer

class MyWebsocketConsumer(WebsocketConsumer):

    def connection_groups(self, **kwargs):
        """
        Возвращает список групп для подключения/удаления подключенных участников
        """
        return ['general_group']

    def connect(self, message, **kwargs):
        """
        Срабатывает при старте соединения по WebSocket
        """
        pass

    def receive(self, text=None, bytes=None, **kwargs):
        """
        Срабатывает, когда приходит сообщение в WebSocket
        """
        # Echo
        self.send(text=text, bytes=bytes)

    def disconnect(self, message, **kwargs):
        """
        Срабатывает при разрыве соединения
        """
        pass

Если в момент соединения есть необходимость добавлять участника в группу/группы, то переопределите метод connection_groups, и укажите список групп. При разрыве соединения участники будут автоматически удалятся из заданных групп.

Data binding

API data binding ещё до конца не устаканился и поэтому до стабильного релиза может изменяться. Используйте этот функционал аккуратно, и внимательно читайте Changelog при обновлениях django-channels. Главная задача data binding упростить жизнь при обновлениях в моделях Django. Функциональная часть построена на родных Django Signals и имеет ряд ограничений:

  • Если вы изменяете объект модели за пределами Django или используете метод .update() на объекте QuerySet, binding не сработает, так как сигнала не произойдёт.
  • В качестве сериализаторов моделей используются стандартные средства Django, но при SPA (Single Page Applications) очень часто требуется гибкость в передаваемых данных. Автор советует в таком случае использовать пакет сериализаторов из Django REST Framework.

Подробно с деталями можно почитать тут.

Роутинг с фильтрами

Веб-сокет, например, можно открыть по конкретному URL:

route("websocket.connect", consumers.ws_connect, path=r"^/chat/$")

Нужно фильтровать по входным данным для функции-consumer? Не проблема:

route("email.receive", comment_response, to_address=r".*@example.com$", subject="^reply")

Все именованные аргументы передаются в функцию-consumer, поэтому ловите их там через **kwargs.

Подробнее тут.

  • Paul Winex

    Спасибо за статьи, весьма полезно. Есть вопрос: существует необходимость работать с каждым подключенным юзером индивидуально. Как это лучше организовать? Например какой-то юзер прислал запрос на получение данных и оставил, например, своё какое-то имя. Далее сервер пошел что-то считать и по завершению (минут через 5) нужно юзеру обратно отправить данные и только ему.

    • Я так понимаю, что речь про веб-сокет соединение. Channels даёт каждому подключению уникальный идентификатор по которому можно послать ответ позже. В вашем случае при подключении пользователя вы можете сохранять данные в виде ключ-значение (username: channel_id), позже отсылать данные по этому соединению.

      • Paul Winex

        Примерно так я и попробовал организовать, записывая данные в redis. Выглядит следующим образом:

        redis_con.sadd(‘users’, json.dumps(
        dict(username=username, channel=self.message.reply_channel.name)
        )

        Меня в данном подходе не устраивает то, что при поиске нужного юзера приходится постоянно парсить JSON. Если писать просто строку типа

        ‘%s%s’ % (username, channel)

        то это и места занимает меньше и достается в переменные простым split в 10 раз быстрей

        https://gist.github.com/paulwinex/e82da222888ce0f4e365e046719af91c

        Попробовал я сделать тоже самое но сохраняя непосредственно словарь в redis, split тоже победил!

        https://gist.github.com/paulwinex/abc51a3a447dc18d78675fad2747ff09

        Но сплит выглядит весьма костыльно. Есть ли более логичные способы? Кроме как каждому юзеру заводить отдельную именованную группу.

        • Не занимаетесь ли вы premature optimization ? 🙂
          А почему вы не записываете в качестве ключа само имя пользователя, а в качестве его значения ID соединения? Я так понимаю в коде вы ведь знаете кому отправлять (имя пользователя). {‘paul’: ‘my_channel_id’}

          • Paul Winex

            К серверу юзер может подключиться несколько раз одновременно. Например две странички сайта открыл, еще есть десктоп приложение, да еще и на двух компах. Поэтому юзер уникальным быть не может. Уникально только сочетание юзер+канал. Другое дело имя канала использовать как ключ, но я хотел бы выделить этих юзеров в отдельную группу в БД. Так как таких групп будет несколько. Поэтому и придумал такой выход. быть может я плохо знаю redis и его возможности организации данных.

          • Как вариант можно создавать в channels Group с именем пользователя и добавлять все коннекты от одного юзера туда.

          • Paul Winex

            Но будет ли правильным под одного юзера держать отдельную группу?

          • По сути Group делает то же самое, что вы делали напрямую с Redis, добавляя туда данные.

          • Paul Winex

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

          • Paul Winex

            Удалось сделать полностью отдельный класс отвечающий за юзеров и группы. Даже работает. Остается навести лоск и можно использовать. Возник вопрос, как же проверить валидность канала? Например я имею имя канала, могу создать экземпляр Channel и отправить сообщение, но даже если юзера давно нет по этому каналу, сообщение уйдет и ошибку не покажет. Как определить по имени канала что там еще кто-то жив?

          • Насколько я знаю определить «живучесть» невозможно. Как вариант можно определить метод disconnet, который срабатывает, когда соединение закрывается.

          • Paul Winex

            disconnect конечно же работает. Но я спросил для случая когда падает сервер, в этом случае сервер виновник отключения и никакие колбеки не срабатывают. При этом все соединения отключаются а записи в redis остаются. Тогда нужно придумать как очищать базу во время старта djangoили channels.

          • Paul Winex

            Идея такая есть.

            Во время нового подключения юзера отправлять пинг всем найденным у юзера сокетам и копировать в некий буфер (тоже в базе). И запустить колбек, через 10 сек например, который пройдется по буферу и удалит все сокеты что там есть. У клиентов будет 10 сек чтобы ответить и удалиться из буфера или умереть!
            Второй вариант, всегда от клиента ждать ответ с подтверждением получения данных. Этот вариант можно расширить и пробовать отправлять еще раз если какие то проблемы и через некоторое время считать соединение не рабочим и удалять. Но только если нужна гарантия доставки например. Если не нужна, то отрубать после первого фейла.

  • Paul Winex

    Здравствуйте. Последние несколько месяцев я писал приложение на channels. Всё прекрасно работает и в целом доволен. Но вот решил развернуть на внешнем виртуальном сервере. Столкнулся с проблемой что gunicorn не видит WS запросы. Нашел в сети некий модуль gunicorn-websocket
    https://github.com/CMGS/gunicorn-websocket
    но что-то не хочется пока что такие костыли сразу вставлять. Подскажите пожалуйста как лучше развернкть сервер? На каких технологиях и приложениях?

    Спасибо.

  • Pingback: Django Channels: работа с WebSocket и не только — Персональный блог Адиля Хаштамова()