Django Channels: работа с WebSocket и не только

Эпоха перемен

За последнее время благодаря активному развитию технологий, веб значительно преобразился. Буквально один десяток лет назад, всё что у нас было это несложные динамические веб-страницы с перезагрузкой при каждом запросе к серверу. Позже пришел Ajax, принёсший немало головной боли для программистов и пользователей (в основном из-за слабого канала, разных браузеров и кривых рук самих программистов). Идут годы, а тем временем запросы пользователей растут, инструменты находятся в постоянном режиме совершенствования с целью удовлетворения растущих потребностей конечных пользователей. Сейчас диву даёшься — веб-сайты превращаются в полноценные интерактивные приложения, способные практически полностью заменять своих настольных собратьев (Microsoft Word против Google Docs, например), появляются 3D-игры, мощности браузерных движков растут как на дрожжах. Современный браузер способен определять геолокацию, работать со  звуком, камерой, 3D изображением и многое-многое другое. Не за горами время, когда единственным полезным приложением внутри операционной системы будет браузер.

Фреймворк Django появился в далёком 2003 году, то есть 13 лет назад. За эти годы многое изменилось. Сейчас Django является самым популярным веб-фреймворком в экосистеме Python. И это не просто так: поразительное качество документации, частота релизов ошеломляет, очень сильное сообщество разработчиков, активная поддержка со стороны коммерческих и некоммерческих организаций. Чтобы оставаться «на плаву» и быть актуальным инструментом, необходимо реагировать на вызовы рынка. И одним из таких вызовом для фреймворка является появление WebSocket и HTTP 2.

Задача WebSocket и HTTP 2 заключается в улучшении пользовательского взаимодействия (user experience) с веб-приложением путем сокращения задержек в передаче данных (уменьшение объема передаваемых данных, установка постоянного соединения с сервером). Django создавался по старой модели HTTP версии 1.1 «запрос-ответ»: браузер посылает запрос серверу, тот в свою очередь его анализирует и отправляет ответ назад. Каждый запрос содержит в себе всю необходимую информацию: заголовки, куки, данные и так далее. С ростом числа пользователей и обилия передаваемой информации, сокращение объёма передачи данных приносит колоссальную экономию ресурсов и одновременно увеличивает производительность систем. WebSocket существует уже достаточно длительное время, за этот период разработчики придумали массу способов работы с этим протоколом даже в тех ситуациях, когда браузеры клиентов вовсе его не поддерживали (Flash, Ajax). Для создания так называемых real-time веб-приложений на Python, предпочтения в таких случаях отдавали далеко не Django, а фреймворкам вроде Tornado, Twisted, построенным на асинхронных подходах (событийно-ориентированных). Но настал момент, когда используя Django можно работать с WebSocket. Виновником этого события является ещё одна батарейка для Django под названием Channels, созданная небезызвестным Andrew Godwin (к слову, автором пакета South).

Что такое Channels?

Django Channels привносит в привычную модель работы Django новый концепт, а именно ориентированность на события. Вместо оригинальной модели по типу запрос-ответ, фреймворк реагирует на ряд событий, попадающих в тот или иной канал, который «просматривается/прослушивается» обработчиками событий (проще говоря, процессами, вызывающими те или иные функции, workers). Раньше для того, чтобы изменить любой HTTP запрос «на лету», необходимо было вмешиваться в цепочку Django Middleware, сейчас же HTTP запрос от браузера это событие, попадающее в канал http.request. Достаточно «повесить» на него прослушку, тем самым изменив его поведение должным образом (немного позже я опишу этот процесс для наглядности).

Изнутри Channels это классическая очередь задач (вроде Celery), использующая Redis в качестве прослойки для коммуникации между теми кто создаёт события (producers) и теми, кто их выполняет (workers). Redis в данном случае является необязательным условием, можно написать и своё решение. Кстати, стоит отметить, что релиз Django 1.10 уже будет иметь в своей кодовой базе пакет Channels, он скорее всего попадёт в django.channels (релиз назначен на август 2016 года). Сейчас же Channels поддерживает работу с версиями Django >= 1.8.

Ввиду того, что фреймворк работает в синхронном стиле, задача по его переписыванию на асинхронный лад является практически невозможной. В связи с этим, дабы упростить процесс написания кода (к слову, код пишется всё в том же привычном синхронном стиле), а также обработку long-polling соединений, появилась необходимость разделить привычный механизм запрос-ответ на 3 уровня.

  • Уровень интерфейса. Это обработчики привычных нам протоколов взаимодействия между приложением и сервером, например WSGI, WebSocket.
  • Уровень канала: проще говоря, брокер. В качестве данного уровня могут выступать Redis, SQL база данных, область памяти или любой собственный велосипед.
  • Уровень обработчиков: процессы, следящие за поступлением сообщений в канал (очередь) и реагирующие на них тем или иным образом (обычно вызовом соответствующих функций-обработчиков).

Вот как Jacob Kaplan-Moss проиллюстрировал эту схему:

django-wsgi

Модель работы сильно напоминает Celery. И это действительно так, Channels способен заменить Celery, но с некоторыми оговорками о которых речь пойдёт в конце статьи.

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

Установка Channels

В качестве быстрой демонстрации возможностей работы с Channels, приведу пример перехватчика HTTP запросов. Но для начала нам необходимо поставить пакет.

Тут у нас всё просто.

$ pip install channels

Далее в новом Django проекте прописываем пакет в настройках:

INSTALLED_APPS = [
    ...
    'channels',
]

...

CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'asgiref.inmemory.ChannelLayer',
        'ROUTING': 'myproj.routing.channel_routing',
    },
}

Рядом с settings.py создаём routing.py со следующим содержимым:

from channels.routing import route


channel_routing = [
    route('http.request', 'myapp.consumers.http_request_consumer')
]

Настройки routing.py чем то напоминают CELERY_ROUTES, где мы указываем очередь в которую должна падать задача (task). Здесь же мы указываем обработчика заданного канала (http.request обрабатывается функцией http_request_consumer).

myapp/consumers.py:

from django.http import HttpResponse
from channels.handler import AsgiHandler


def http_request_consumer(message):
    response = HttpResponse('Hello world! You asked for %s' % message.content['path'])
    for chunk in AsgiHandler.encode_response(response):
        message.reply_channel.send(chunk)

Запускаем приложение:

$ python manage.py runserver

При переходе на любую ссылку, видим перехваченный запрос. Обработчики сообщений (consumers) принимают первым аргументом message (channels.message.Message). Не вдаваясь в подробности, это структура, хранящая в себе всю необходимую информацию об отправителе, канале и данных, пришедших к нам.

  • content: содержимое сообщения (в нашем случае информация о запросе) типа dict.
  • reply_channel: структура типа Channel. Содержит в себе «адресат отправителя» по которому нужно доставить ответ, либо None, если таковой отсутствует.

Любопытства ради, советую подключить отладчик и изучить содержимое message. Если внимательно приглядеться к коду, то можно заметить класс AsgiHandler, помимо него также существует AsgiRequest. Если мы хотим получить из message стандартный django request, то выполняем:

request = AsgiRequest(message)

AsgiHandler же нужен для обратной конвертации в message. Более подробную информацию можно получить, изучив спецификацию ASGI (Asynchronous Server Gateway Interface). Ссылка находится внизу.

Пишем real-time Django app

Простейший вариант, основанный на перехвате события http.request, мы изучили, пора бы теперь применить знания во благо. Самое простое, что приходит в голову при фразе «приложение реального времени» это чат (да-да, звучит банально). Готовый код приложения вы можете как всегда найти в моём репозитории на гитхаб.

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

chat/consumers.py

import json
from channels.channel import Group

def ws_connect(message):
    Group('chat').add(message.reply_channel)

def ws_message(message):
    Group('chat').send({'text': json.dumps({'message': message.content['text'],
                                            'sender': message.reply_channel.name})})

def ws_disconnect(message):
    Group('chat').discard(message.reply_channel)

blog_channels/routing.py

from channels.routing import route

channel_routing = {
    'websocket.connect': 'chat.consumers.ws_connect',
    'websocket.receive': 'chat.consumers.ws_message',
    'websocket.disconnect': 'chat.consumers.ws_disconnect',
}

websocket.* — стандартные события Channels при использовании WebSocket соединения. Обратите внимание на Group, это простейшая структура данных, хранящая в себе адреса получателей/клиентов.

Вкратце алгоритм таков:

  1. Когда происходит соединение по веб-сокету, мы добавляем нашего клиента в группу chat.
  2. Когда один из участников группы посылает данные по веб-сокету (сообщение), мы транслируем сообщение по всей группе.
  3. Когда соединение по веб-сокету прерывается, то участник удаляется из группы.

Вот как выглядит JavaScript код для работы чата (полный код шаблона можно посмотреть в репозитории):

templates/chat.html

 
$(document).ready(function(){
    var msgArea = $('#msgArea')
    var elementMessage = $('#message')
    var webSocket = new WebSocket('ws://' + window.location.host + '/chat/index');

    webSocket.onmessage = function(message) {
        var data = JSON.parse(message.data)
        msgArea.append('<p><strong>'+ data.sender + '</strong>: ' + data.message + '</p>')
    }
    $('#btnSubmit').click(function(e) {
        webSocket.send(elementMessage.val())
    })
})

Вот как выглядит общение через 2 открытых браузера.

Django Websocket

Я не стал заморачиваться с никами 😉 и поэтому в качестве «имён» отправителей указываю их так называемый адресат (reply_channel).

Убийца Celery?!

Channels можно использовать в качестве системы очередей задач, более того, работать это скорее всего будет быстро, но есть одно большое НО… Channels не гарантирует выполнение задачи, так как отсутствует failover/retry механизм. Вот что говорит по этому поводу автор:

Channels’ design is such that anything is allowed to fail — a consumer can error and not send replies, the channel layer can restart and drop a few messages, a dogpile can happen and a few incoming clients get rejected.

Существует компромисс между скоростью и надёжностью. Channels выбрал скорость. Если есть необходимость в надёжности, то можно «докрутить» гайки поверх Channels или использовать проверенные инструменты вроде Celery (привет, RabbitMQ!).

Вкратце продемонстрирую как выполнять задачи на примере отправки письма (полный код в том же репозитории):
Дабы не заморачиваться с SMTP сервером, поменяем Django email backend на консольный (все «письма» будут сыпаться в stdout). Для этого необходимо задать настройки в settings.py:

EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

chat/consumers.py:

def send_email_consumer(message):
    payload = message.content['payload']
    send_mail(payload['subject'], payload['body'], 'root@localhost', [payload['email']],
              fail_silently=False)

blog_chat/routing.py:

channel_routing = {
    'websocket.connect': 'chat.consumers.ws_connect',
    'websocket.receive': 'chat.consumers.ws_message',
    'websocket.disconnect': 'chat.consumers.ws_disconnect',
    'send_email': 'chat.consumers.send_email_consumer'
}

И наша вьюшка с формой из которой будут «улетать» письма счастья:

chat/views.py:

class MailMeView(View):
    def get(self, request):
        return render(request, 'mailme.html', {'form': MailMeForm()})

    def post(self, request):
        form = MailMeForm(request.POST)

        if form.is_valid():
            Channel('send_email').send({'payload': form.cleaned_data})
            return HttpResponse('Good job!')
        else:
            return render(request, 'mailme.html', {'form': form})

chat/forms.py:

from django import forms

class MailMeForm(forms.Form):
    subject = forms.CharField(max_length=100)
    body = forms.CharField(widget=forms.Textarea)
    email = forms.EmailField()

Проходим по адресу http://localhost:8000/chat/email, отправляем сообщение и видим в консоли:

Django Channels Async Task :)

Чтобы убедиться, что основной процесс обрабатывающий HTTP запрос не блокируется при отправке письма, достаточно добавить длительный sleep в функцию-consumer send_email_consumer.

Deploy, scale and conquer

В настройках приложения в качестве брокера я задал память процесса.

CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'asgiref.inmemory.ChannelLayer',
        'ROUTING': 'blog_channels.routing.channel_routing',
    },
}

Этот вариант неплох для тестов на локальной машине, но совсем не годится, когда мы идём в продакшен, более того такой подход является немасштабируемым. Продакшен решением является использование Redis, но для этого предварительно необходимо поставить пакет asgi_redis.

CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'asgi_redis.RedisChannelLayer',
        'CONFIG': {
            'hosts': [('localhost', 6379)],
        },
        'ROUTING': 'blog_channels.routing.channel_routing',
    },
}

При установке channels, стандартная команда runserver немного модифицируется. Как я ранее уже упоминал, channels вносит изменения в привычную модель работы Django, вводя разделение между обработчиками запросов и обработчиками событий в очередях (более того, коммуникация теперь происходит через ASGI). В связи с этим деплой приложения немного отличается от привычного. Для обработки HTTP/WebSocket необходим ASGI совместимый веб-сервер. На данный момент с Channels поставляется веб-сервер Daphne, написанный всё тем же Andrew Godwin. Daphne основан на сетевом фреймворке Twisted и имеет не так много кода (слава небесам). Помимо веб-сервера, нам необходимо также запускать воркеров (их количество уже зависит от нагрузки на сайт и настраивается индивидуально), ответственных за обработку событий (http.request, websocket.connect и так далее).

Добавляем вот такой файл с именем asgi.py в проект:

blog_channels/asgi.py:

import os
import channels.asgi

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'blog_channels.settings')
channel_layer = channels.asgi.get_channel_layer()

В версии Django 1.10 он скорее всего будет создаваться автоматически при инициализации django-проекта.

Стартуем веб-сервер в консоли:

$ daphne blog_channels.asgi:channel_layer --port 8000

и запуск воркера:

$ python manage.py runworker

Заключение

Стоит признать, что в текущем виде Channels ещё не готов для развёртывания на продакшене, но к моменту выхода Django 1.10 все проблемы должны быть решены (включая возможное появление новых ASGI серверов). Нативная поддержка WebSocket и в скором времени HTTP2 это серьёзный шаг в развитии фреймворка. Более того, архитектура, выбранная автором пакета, также позволяет использовать Channels в качестве инструмента исполнения фоновых задач и не только. Я описал лишь небольшую часть возможностей Channels, остальное же оставлю вам для самостоятельного изучения в свободное время.

P.S. 25/08/2016: Написал небольшое описание нововведений в Django Channels.

UPDATE: В Telegram не так давно я открыл канал для разработчиков, где стараюсь делиться интересным и полезным материалом на тему программирования, методологий и разработки программного обеспечения. Подписывайтесь, буду рад всем!

Полезные ссылки

  • Отличная статья!
    А есть возможность запустить все это под gunicorn или apache?

    • Рахмет!

    • Нет, сейчас нет такой возможности. Тому же gunicorn необходимо на своей стороне реализовать ASGI

    • Andrey Uminov

      Если очень надо, то на гитхабе каналов сейчас висит PR в котором обсуждают теоретическую возможность запуска на uWSGI.

      • Я думаю до релиза Django 1.11 (именно туда будут встроены каналы), поддержка ASGI будет в Gunicorn/uWSGI. С другой стороны,Daphne уже используют в Production (код там полностью базируется на Twisted).

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

  • Павел

    Спасибо! Познакомился с channels благодаря вам!

    • Пожалуйста!) Рад, что помог вам!

  • Andrey Uminov

    Спасибо!!!

    Ваша статья очень помогла разобраться! Три дня мучений и я таки запустил ваш пример ) Не представляю, что бы я делал, если бы вы его не опубликовали на гитхабе. Наверное, повесился бы.

    У меня остался небольшой вопрос, связанный, скорее всего, с недопониманием архитектуры в целом…
    Правильно ли я понимаю, что в варианте с ‘BACKEND’: ‘asgiref.inmemory.ChannelLayer’ использование websocket не возможно? И для разработки теперь нужно таскать с собой дафни?

    • Andrey Uminov

      Вопрос отпал. Проверил: все работает и без дафни.

      Еще раз спасибо за статью!

      • Пожалуйста!

  • Vladleelee Lee

    Добрый день у меня проблема есть одна python2.7
    CHANNEL_LAYERS = {
    ‘default’: {
    ‘BACKEND’: ‘asgi_redis.RedisChannelLayer’,
    ‘CONFIG’: {
    ‘hosts’: [(‘localhost’, 6379)],
    },
    ‘ROUTING’: ‘blog_channels.routing.channel_routing’,
    },
    }

    asgi_layer = backend_class(**self.configs[name].get(«CONFIG», {}))
    TypeError: __init__() got an unexpected keyword argument ‘hosts’

    • Добрый день.

      backend_class что из себя представляет?

      • Vladleelee Lee

        запускаю через virtualenv

      • Vladleelee Lee

        Разобрался, конфликт проектов

  • Vladleelee Lee

    скажите пожалуйста как можно подключиться из вне (с другого сайта) к данным сокетам, просто указать ip и порт?
    Спасибо

    • сервер нужно запустить на внешку, например — python manage.py runserver 0.0.0.0:8000

      • Vladleelee Lee

        Спасибо огромное

  • Vladleelee Lee

    Пытаюсь подключиться с другого домена пишет js
    консоль error 400

    • Что значит с другого домена?

  • Vladleelee Lee

    Прошу прощения что еще раз пишу глупый вопрос, у меня проблема с отправкой письма

    File «/home/modx/test_sockets/test_s/local/lib/python2.7/site-packages/django/core/handlers/exception.py», line 39, in inner
    response = get_response(request)
    File «/home/modx/test_sockets/test_s/local/lib/python2.7/site-packages/django/core/handlers/base.py», line 249, in _legacy_get_response
    response = self._get_response(request)
    File «/home/modx/test_sockets/test_s/local/lib/python2.7/site-packages/django/core/handlers/base.py», line 178, in _get_response
    response = middleware_method(request, callback, callback_args, callback_kwargs)
    File «/home/modx/test_sockets/test_s/local/lib/python2.7/site-packages/django/middleware/csrf.py», line 260, in process_view
    request_csrf_token = request.POST.get(‘csrfmiddlewaretoken’, »)
    File «/home/modx/test_sockets/test_s/local/lib/python2.7/site-packages/channels/handler.py», line 144, in _get_post
    self._load_post_and_files()
    File «/home/modx/test_sockets/test_s/local/lib/python2.7/site-packages/django/http/request.py», line 291, in _load_post_and_files
    if self.content_type == ‘multipart/form-data’:
    AttributeError: ‘AsgiRequest’ object has no attribute ‘content_type’
    2016-09-22 12:59:31,368 — ERROR — exception — Internal Server Error: /chat/email
    Traceback (most recent call last):
    File «/home/modx/test_sockets/test_s/local/lib/python2.7/site-packages/django/core/handlers/exception.py», line 39, in inner
    response = get_response(request)
    File «/home/modx/test_sockets/test_s/local/lib/python2.7/site-packages/django/core/handlers/base.py», line 249, in _legacy_get_response
    response = self._get_response(request)
    File «/home/modx/test_sockets/test_s/local/lib/python2.7/site-packages/django/core/handlers/base.py», line 178, in _get_response
    response = middleware_method(request, callback, callback_args, callback_kwargs)
    File «/home/modx/test_sockets/test_s/local/lib/python2.7/site-packages/django/middleware/csrf.py», line 260, in process_view
    request_csrf_token = request.POST.get(‘csrfmiddlewaretoken’, »)
    File «/home/modx/test_sockets/test_s/local/lib/python2.7/site-packages/channels/handler.py», line 144, in _get_post
    self._load_post_and_files()
    File «/home/modx/test_sockets/test_s/local/lib/python2.7/site-packages/django/http/request.py», line 291, in _load_post_and_files
    if self.content_type == ‘multipart/form-data’:
    AttributeError: ‘AsgiRequest’ object has no attribute ‘content_type’
    2016-09-22 12:59:31,385 — ERROR — worker — Error processing message with consumer channels.staticfiles.StaticFilesConsumer:
    Traceback (most recent call last):
    File «/home/modx/test_sockets/test_s/local/lib/python2.7/site-packages/channels/worker.py», line 78, in run
    consumer(message, **kwargs)
    File «/home/modx/test_sockets/test_s/local/lib/python2.7/site-packages/channels/handler.py», line 330, in __call__
    for reply_message in self.handler(message):
    File «/home/modx/test_sockets/test_s/local/lib/python2.7/site-packages/channels/handler.py», line 203, in __call__
    response = self.get_response(request)
    File «/home/modx/test_sockets/test_s/local/lib/python2.7/site-packages/channels/staticfiles.py», line 57, in get_response
    return super(StaticFilesHandler, self).get_response(request)
    File «/home/modx/test_sockets/test_s/local/lib/python2.7/site-packages/django/core/handlers/base.py», line 124, in get_response
    response = self._middleware_chain(request)
    File «/home/modx/test_sockets/test_s/local/lib/python2.7/site-packages/django/core/handlers/exception.py», line 41, in inner
    response = response_for_exception(request, exc)
    File «/home/modx/test_sockets/test_s/local/lib/python2.7/site-packages/django/core/handlers/exception.py», line 86, in response_for_exception
    response = handle_uncaught_exception(request, get_resolver(get_urlconf()), sys.exc_info())
    File «/home/modx/test_sockets/test_s/local/lib/python2.7/site-packages/django/core/handlers/exception.py», line 128, in handle_uncaught_exception
    return debug.technical_500_response(request, *exc_info)
    File «/home/modx/test_sockets/test_s/local/lib/python2.7/site-packages/django/views/debug.py», line 84, in technical_500_response
    html = reporter.get_traceback_html()
    File «/home/modx/test_sockets/test_s/local/lib/python2.7/site-packages/django/views/debug.py», line 316, in get_traceback_html
    c = Context(self.get_traceback_data(), use_l10n=False)
    File «/home/modx/test_sockets/test_s/local/lib/python2.7/site-packages/django/views/debug.py», line 293, in get_traceback_data
    ‘filtered_POST’: self.filter.get_post_parameters(self.request),
    File «/home/modx/test_sockets/test_s/local/lib/python2.7/site-packages/django/views/debug.py», line 167, in get_post_parameters
    return request.POST
    File «/home/modx/test_sockets/test_s/local/lib/python2.7/site-packages/channels/handler.py», line 144, in _get_post
    self._load_post_and_files()
    File «/home/modx/test_sockets/test_s/local/lib/python2.7/site-packages/django/http/request.py», line 291, in _load_post_and_files
    if self.content_type == ‘multipart/form-data’:
    AttributeError: ‘AsgiRequest’ object has no attribute ‘content_type’

  • Salavat Sharapov

    Вечер добрый! Как я понял, daphne не работает по https? Возникает проблема при проксировании https c nginx.

    • Добрый. А в чем именно проблема? Покажите конфигурацию nginx

  • Pingback: Блогу исполнилось ровно год! — Персональный блог Адиля Хаштамова()

  • Евгений Юрченко

    Делал на основании примера, но после подключения по ws, через 5 секунд происходит автоматический дисконнект. Где искать причину?

    Запускаю через Daphne, вот такие логи получаю, после перехода на страницу с чатом:
    127.0.0.1:51762 — — [12/Jan/2017:12:03:53] «WSCONNECTING /» — —
    127.0.0.1:51762 — — [12/Jan/2017:12:03:58] «WSDISCONNECT /» — —

    • Вышел релиз новой версии channels — стабильной, 1.0. Думаю стоит обновиться. Также стоит обратить внимание на зависимость Twisted, возможно также нужно обновить, т.к. была похожая проблема у одного из читателей блога.

      • Евгений Юрченко

        Изначально ставил версии:
        asgi-redis 1.0.0
        channels 1.0.0
        twisted 16.6.0

        • Попробуйте twisted 16.2

          • Евгений Юрченко

            Попробовал, проблема осталась.

            Даже скачал проект с репозитория, чтобы исключить какие-то ошибки у себя… но точно такая же ошибка.

          • В JS срабатывает onclose? Попробуйте другие браузеры, как там ведёт себя соединение. В onclose можно восстановить соединение.

          • Евгений Юрченко

            Сделал переподключение, проверил в FF, до этого пробовал в Chrome
            FF log:
            connection
            InvalidStateError: An attempt was made to use an object that is not, or is no longer, usable
            Firefox can’t establish a connection to the server at ws://127.0.0.1:8000/chat/index.
            reconnect
            connection
            InvalidStateError: An attempt was made to use an object that is not, or is no longer, usable
            Firefox can’t establish a connection to the server at ws://127.0.0.1:8000/chat/index.

          • Евгений Юрченко

            Предыдущий ответ куда-то пропал -(

          • Евгений Юрченко

            В общем, насколько я понял webSocket-соединение вообще не устанавливается, т.к. на стороне клиента(браузера) не срабатывает метод:
            webSocket.onopen = function () {
            console.log(«ws open»);
            };

            Redis сервер настраивал самостоятельно, еще до попыток «поковырять» djang0-channels по мануалу.

            Вопрос остается открытым: Куда копать?

          • Евгений Юрченко

            Изменил настройки на:
            CHANNEL_LAYERS = {
            ‘default’: {
            ‘BACKEND’: ‘asgiref.inmemory.ChannelLayer’,

            # ‘BACKEND’: ‘asgi_redis.RedisChannelLayer’,
            # ‘CONFIG’: {
            # ‘hosts’: [(‘localhost’, 6379)],
            # },
            ‘ROUTING’: ‘blog_channels.routing.channel_routing’,
            },
            }
            Запустил через django-server вместо daphne, та же самая проблема.