Приветствую, друзья!
Когда я впервые начал работать с Django, меня всё устраивало, за исключением одного момента: как сделать так, чтобы приложение могло общаться с пользователем в реальном времени? Веб-сокеты, уведомления, асинхронные запросы — казалось, это точно не про чистый Django. Но затем я наткнулся на Django Channels, и многое изменилось. Channels позволили мне сделать приложение асинхронным, добавить поддержку веб-сокетов и превратить его во что-то гораздо более крутое.
В этой статье я расскажу, как работать с Django Channels.
Установка
Первым делом установим необходимые пакеты:
pip install channels
Далее обновим settings.py
проекта:
# settings.py INSTALLED_APPS = [ # ... 'channels', # ... ] ASGI_APPLICATION = 'myproject.asgi.application'
Создадим файл asgi.py
в корневой директории проекта:
# asgi.py import os from channels.routing import ProtocolTypeRouter, URLRouter from django.core.asgi import get_asgi_application from channels.auth import AuthMiddlewareStack import chat.routing os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings') application = ProtocolTypeRouter({ "http": get_asgi_application(), "websocket": AuthMiddlewareStack( URLRouter( chat.routing.websocket_urlpatterns ) ), })
Немного про архитектуру Channels
ASGI
Вы, наверное, знакомы с WSGI — стандартом, связывающим веб-сервер с Django-приложением. Он отлично подходит для синхронных задач, но при создании чатов или уведомлений в реальном времени начинает показывать свои ограничения.
ASGI — это как WSGI, но с поддержкой асинхронности. Он позволяет Django обрабатывать несколько запросов одновременно, не блокируя основной поток. Используя asyncio
, await
и async def
, можно легко писать масштабируемый код, который легко справляется с реальным временем.
Consumers
В обычном Django представления отвечают за обработку HTTP-запросов и формирование ответов. Но с веб-сокетами и другими асинхронными протоколами возникает вопрос: «А как теперь?» Здесь на помощь приходят Consumers. Если провести аналогию, то Consumer — это представление для асинхронных соединений.
Consumers — это классы, обрабатывающие события жизненного цикла соединения: подключение, получение сообщений и отключение.
Типы Consumers:
-
WebSocketConsumer
: Для стандартных веб-сокетных соединений. -
AsyncWebsocketConsumer
: Асинхронная версия с использованиемasync/await
. -
JsonWebsocketConsumer
: Упрощает работу с JSON‑данными. -
Custom Consumers: Создаёте свои собственные Consumers под специфические задачи.
Основные методы Consumers:
-
connect()
: Вызывается при установке соединения. Здесь можно аутентифицировать пользователя. -
receive()
: Обрабатывает входящие сообщения от клиента. -
disconnect()
: Вызывается при разрыве соединения. Время попрощаться и очистить ресурсы.
Поначалу, конечно, непривычно работать с асинхронным кодом, но это того стоит.
Channel Layers
Теперь поговорим о Channel Layers — «нервной системе» самого приложения. Они позволяют различным частям приложения общаться друг с другом, независимо от серверов или процессов.
Channel Layer — это абстракция для передачи сообщений между Consumers. Состоит из двух основных компонентов:
-
Каналы: Уникальные адреса для отправки сообщений. Каждый Consumer имеет свой канал.
-
Группы: Коллекции каналов под общим именем. Позволяют отправлять сообщения сразу нескольким Consumers.
Бэкенды для Channel Layers:
-
In-Memory: Подходит для разработки и тестирования, но не для продакшена.
-
Redis: Наиболее популярный и высокопроизводительный вариант. Быстро, надёжно и масштабируемо.
-
RabbitMQ: Более сложный в настройке, но предоставляет дополнительные возможности и повышенную надёжность.
Для большинства проектов Redis будет идеальным выбором.
Как всё это взаимодействует
Представьте следующий сценарий:
-
Клиент открывает веб-сокетное соединение с вашим приложением.
-
ASGI‑сервер принимает соединение и передаёт его вашему Django‑приложению через интерфейс ASGI.
-
Ваш Consumer получает событие
connect
, аутентифицирует пользователя и устанавливает соединение. -
После подключения Consumer добавляет свой канал в одну или несколько групп через Channel Layer.
-
Клиент отправляет сообщение, которое обрабатывается методом
receive
и отправляется в группу. -
Channel Layer распространяет сообщение всем Consumers в группе.
-
Каждый Consumer отправляет сообщение своему клиенту, и все пользователи видят обновления в реальном времени.
Создаём приложение чата с веб-сокетами
Создадим простой чат, чтобы продемонстрировать возможности Channels.
python manage.py startapp chat
Добавляем его в INSTALLED_APPS
:
# settings.py INSTALLED_APPS = [ # ... 'chat', # ... ]
Создаём файл routing.py
в приложении chat
:
# chat/routing.py from django.urls import re_path from . import consumers websocket_urlpatterns = [ re_path(r'ws/chat/(?P<room_name>\w+)/$', consumers.ChatConsumer.as_asgi()), ]
Пишем consumer:
# chat/consumers.py from channels.generic.websocket import AsyncWebsocketConsumer import json class ChatConsumer(AsyncWebsocketConsumer): async def connect(self): self.room_name = self.scope['url_route']['kwargs']['room_name'] self.room_group_name = f'chat_{self.room_name}' # Присоединяемся к группе await self.channel_layer.group_add( self.room_group_name, self.channel_name ) await self.accept() async def disconnect(self, close_code): # Покидаем группу await self.channel_layer.group_discard( self.room_group_name, self.channel_name ) # Получаем сообщение от WebSocket async def receive(self, text_data): text_data_json = json.loads(text_data) message = text_data_json['message'] # Отправляем сообщение в группу await self.channel_layer.group_send( self.room_group_name, { 'type': 'chat_message', 'message': message } ) # Получаем сообщение от группы async def chat_message(self, event): message = event['message'] # Отправляем сообщение обратно клиенту await self.send(text_data=json.dumps({ 'message': message }))
Код может показаться длинным, но на самом деле всё довольно просто. Главное — понять, как работают группы и сообщения.
Создаём routing.py
в корневой директории проекта:
# myproject/routing.py from channels.auth import AuthMiddlewareStack from channels.routing import ProtocolTypeRouter, URLRouter from django.core.asgi import get_asgi_application import chat.routing application = ProtocolTypeRouter({ "http": get_asgi_application(), "websocket": AuthMiddlewareStack( URLRouter( chat.routing.websocket_urlpatterns ) ), })
Создадим шаблоны и представления:
Представления:
# chat/views.py from django.shortcuts import render def index(request): return render(request, 'chat/index.html') def room(request, room_name): return render(request, 'chat/room.html', { 'room_name': room_name })
Шаблон chat/index.html
:
<!DOCTYPE html> <html> <head> <title>Чат</title> </head> <body> <h1>Добро пожаловать в чат!</h1> <p>Введите имя комнаты и присоединяйтесь:</p> <form method="get" action="{% url 'room' room_name=room_name %}"> <input placeholder="Название комнаты" name="room_name" type="text" required> <button type="submit">Войти</button> </form> </body> </html>
Шаблон chat/room.html
:
<!-- chat/templates/chat/room.html --> <!DOCTYPE html> <html> <head> <title>Комната {{ room_name }}</title> </head> <body> <h2>Комната: {{ room_name }}</h2> <div id="chat-log"></div> <input placeholder="Введите сообщение..." id="chat-message-input" type="text" size="100"> <button id="chat-message-submit">Отправить</button> <script> const roomName = "{{ room_name }}"; const chatSocket = new WebSocket( 'ws://' + window.location.host + '/ws/chat/' + roomName + '/' ); chatSocket.onmessage = function(e) { const data = JSON.parse(e.data); const message = data['message']; document.querySelector('#chat-log').innerHTML += (message + '<br>'); // знаете, почему именно <br>? }; chatSocket.onclose = function(e) { console.error('Chat socket closed unexpectedly'); }; document.querySelector('#chat-message-submit').onclick = function(e) { const messageInputDom = document.querySelector('#chat-message-input'); const message = messageInputDom.value; chatSocket.send(JSON.stringify({ 'message': message })); messageInputDom.value = ''; }; </script> </body> </html>
Для простоты в шаблонах использован минимальный HTML и JavaScript.
Настройка URL-адресов:
# myproject/urls.py from django.urls import path from chat import views urlpatterns = [ path('', views.index, name='index'), path('chat/<str:room_name>/', views.room, name='room'), ]
Для взаимодействия между различными Consumers настроим каналный слой с Redis.
Установка Redis и зависимостей:
pip install channels_redis
Настройка settings.py
:
# settings.py CHANNEL_LAYERS = { 'default': { 'BACKEND': 'channels_redis.core.RedisChannelLayer', 'CONFIG': { 'hosts': [os.environ.get('REDIS_URL', ('127.0.0.1', 6379))], }, }, }
Запуск Redis:
Для Ubuntu:
sudo apt-get install redis-server sudo service redis-server start
Про Consumers
Типы Consumers
-
Synchronous Consumers: Наследуются от
channels.generic.websocket.WebsocketConsumer
. Используют синхронный код. -
Asynchronous Consumers: Наследуются от
channels.generic.websocket.AsyncWebsocketConsumer
. Используютasync/await
.
Пример синхронного Consumer:
# chat/consumers.py from channels.generic.websocket import AsyncWebsocketConsumer, WebsocketConsumer import json class ChatConsumer(AsyncWebsocketConsumer): async def connect(self): self.room_name = self.scope['url_route']['kwargs']['room_name'] self.room_group_name = f'chat_{self.room_name}' # Присоединяемся к группе await self.channel_layer.group_add( self.room_group_name, self.channel_name ) await self.accept() async def disconnect(self, close_code): # Покидаем группу await self.channel_layer.group_discard( self.room_group_name, self.channel_name ) # Получаем сообщение от WebSocket async def receive(self, text_data): text_data_json = json.loads(text_data) message = text_data_json['message'] # Отправляем сообщение в группу await self.channel_layer.group_send( self.room_group_name, { 'type': 'chat_message', 'message': message } ) # Получаем сообщение от группы async def chat_message(self, event): message = event['message'] # Отправляем сообщение обратно клиенту await self.send(text_data=json.dumps({ 'message': message })) # Пример синхронного Consumer class SyncChatConsumer(WebsocketConsumer): def connect(self): self.accept() self.send(text_data=json.dumps({ 'message': 'Привет от синхронного Consumer!' })) def receive(self, text_data): pass def disconnect(self, close_code): pass
Когда использовать синхронные Consumers?
Если код полностью синхронный и не использует асинхронные операции, можно использовать синхронные Consumers. Однако рекомендуется отдавать предпочтение асинхронным Consumers для лучшей производительности и масштабируемости.
Аутентификация и доступ
AuthMiddlewareStack позволяет получать доступ к пользователю через self.scope
.
Пример доступа к пользователю:
class ChatConsumer(AsyncWebsocketConsumer): async def connect(self): user = self.scope["user"] if user.is_authenticated: await self.accept() else: await self.close()
Можно проверять права пользователя и предоставлять или ограничивать доступ к определённым комнатам или действиям:
if not user.has_perm('chat.view_room'): await self.close()
Middleware в Channels
Channels поддерживает middleware для ASGI-приложений. Можно создавать свои собственные middleware для обработки входящих соединений.
Пример создания middleware:
class CustomAuthMiddleware: def __init__(self, inner): self.inner = inner async def __call__(self, scope, receive, send): # Здесь можно изменить scope или выполнить другие действия return await self.inner(scope, receive, send)
Подключение middleware:
from channels.routing import ProtocolTypeRouter, URLRouter from channels.middleware import BaseMiddleware import chat.routing application = ProtocolTypeRouter({ "websocket": CustomAuthMiddleware( URLRouter(chat.routing.websocket_urlpatterns) ), })
Развертывание Channels-приложения
Для запуска Channels-приложения рекомендуется использовать Daphne — ASGI-сервер.
Установка Daphne:
pip install daphne
Запуск приложения:
daphne -b 0.0.0.0 -p 8000 myproject.asgi:application
Daphne отлично подходит для небольших проектов, но для продакшена лучше использовать комбинацию с Gunicorn.
Комбинирование Gunicorn с Uvicorn Worker:
pip install uvicorn gunicorn
Запуск Gunicorn:
gunicorn myproject.asgi:application -k uvicorn.workers.UvicornWorker
Интеграция с существующими Django-приложениями
Channels прекрасно сочетается с существующими Django-приложениями. Можно потихоньку добавлять асинхронные возможности, не переписывая весь код.
Допустим, есть модель Order
, и хочется уведомлять пользователей в реальном времени о статусе заказа.
Модель и сигнал:
# orders/models.py from django.db.models.signals import post_save from django.dispatch import receiver from asgiref.sync import async_to_sync from channels.layers import get_channel_layer from .models import Order @receiver(post_save, sender=Order) def order_status_changed(sender, instance, **kwargs): channel_layer = get_channel_layer() async_to_sync(channel_layer.group_send)( f"user_{instance.user.id}", { 'type': 'order_status', 'status': instance.status, 'order_id': instance.id, } )
Consumer для получения уведомлений:
# notifications/consumers.py from channels.generic.websocket import AsyncWebsocketConsumer import json class NotificationConsumer(AsyncWebsocketConsumer): async def connect(self): self.user = self.scope['user'] if self.user.is_authenticated: self.group_name = f"user_{self.user.id}" await self.channel_layer.group_add( self.group_name, self.channel_name ) await self.accept() else: await self.close() async def disconnect(self, close_code): await self.channel_layer.group_discard( self.group_name, self.channel_name ) async def order_status(self, event): await self.send(text_data=json.dumps({ 'order_id': event['order_id'], 'status': event['status'], }))
Работа с сессиями
В Channels вы также можете получать доступ к сессиям пользователя.
Подключение SessionMiddlewareStack:
from channels.sessions import SessionMiddlewareStack from channels.routing import ProtocolTypeRouter, URLRouter import chat.routing application = ProtocolTypeRouter({ "websocket": SessionMiddlewareStack( AuthMiddlewareStack( URLRouter(chat.routing.websocket_urlpatterns) ) ), })
Доступ к сессии:
class ChatConsumer(AsyncWebsocketConsumer): async def connect(self): session_key = self.scope['session'].session_key # Используйте сессию по своему усмотрению
Часто сесси юзают для для хранения временных данных или состояния пользователя.
Производительность
Channels поддерживает запуск нескольких воркеров для обработки нагрузки:
daphne myproject.asgi:application & python manage.py runworker & python manage.py runworker &
Помимо этого при развертывании в облаке можно настроить автоскейлинг воркеров в зависимости от нагрузки.
Также юзайте кэширование для хранения часто используемых данных и уменьшения нагрузки на базу данных. Тот же Redis отлично подходит для кэширования.
Немного советов
Держите Consumers простыми: Разделяйте логику на отдельные функции или классы для лучшей читаемости и поддержки.
Для обработки большой нагрузки используйте очереди сообщений, например RabbitMQ, если Redis не справляется.
Всегда проверяйте входные данные и права доступа пользователей.
Ознакомьтесь с официальной документацией Channels для получения более подробной информации.
В заключение напомню про открытый урок «Patroni и его применение с Postgres» — на нем можно получить практические навыки мониторинга и управления высокодоступными кластерами PostgreSQL с помощью Patroni. Урок пройдет 24 октября. Если интересно, записывайтесь по ссылке.
ссылка на оригинал статьи https://habr.com/ru/articles/852510/
Добавить комментарий