Everything you wanted to know about Django Channels

Greetings, friends!

When I first started working with Django, I was happy with everything, except for one thing: how to make the application communicate with the user in real time? Web sockets, notifications, asynchronous requests – it seemed that this was definitely not about pure Django. But then I came across Django Channelsand a lot has changed. Channels allowed me to make the application asynchronous, add websocket support, and turn it into something much cooler.

In this article I will tell you how to work with Django Channels.

Installation

First of all, let's install the necessary packages:

pip install channels

Next we will update settings.py project:

# settings.py

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

ASGI_APPLICATION = 'myproject.asgi.application'

Let's create a file asgi.py in the project root directory:

# 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
        )
    ),
})

A little about Channels architecture

ASGI

You're probably familiar with WSGI, the standard that connects a web server to a Django application. It's great for synchronous tasks, but when creating chats or real-time notifications it starts to show its limitations.

ASGI – it's like WSGI, but with asynchrony support. It allows Django to process multiple requests simultaneously without blocking the main thread. Using asyncio, await And async defyou can easily write scalable code that handles real-time easily.

Consumers

In regular Django, views are responsible for processing HTTP requests and generating responses. But with websockets and other asynchronous protocols, the question arises: “What now?” This is where they come to the rescue Consumers. To make an analogy, Consumer is a representation for asynchronous connections.

Consumers are classes that handle connection lifecycle events: connecting, receiving messages, and disconnecting.

Types of Consumers:

  • WebSocketConsumer: For standard web socket connections.

  • AsyncWebsocketConsumer: Asynchronous version using async/await.

  • JsonWebsocketConsumer: Makes it easier to work with JSON data.

  • Custom Consumers: Create your own Consumers for specific purposes.

Basic methods of Consumers:

  • connect(): Called when a connection is established. Here you can authenticate the user.

  • receive(): Processes incoming messages from the client.

  • disconnect(): Called when the connection is lost. Time to say goodbye and clear resources.

At first, of course, it’s unusual to work with asynchronous code, but it’s worth it.

Channel Layers

Now let's talk about Channel Layers — the “nervous system” of the application itself. They allow different parts of an application to communicate with each other, independent of servers or processes.

Channel Layer is an abstraction for passing messages between Consumers. Consists of two main components:

  • Channels: Unique addresses for sending messages. Each Consumer has its own channel.

  • Groups: Collections of channels under a common name. Allows you to send messages to several Consumers at once.

Backends for Channel Layers:

  • In-Memory: Suitable for development and testing, but not for production.

  • Redis: The most popular and high-performance option. Fast, reliable and scalable.

  • RabbitMQ: More complex to set up, but provides additional features and increased reliability.

For most projects, Redis will be the ideal choice.

How does it all interact?

Imagine the following scenario:

  1. The client opens a websocket connection to your application.

  2. The ASGI server accepts the connection and passes it to your Django application via the ASGI interface.

  3. Your Consumer receives an event connectauthenticates the user and establishes a connection.

  4. Once connected, the Consumer adds its channel to one or more groups via the Channel Layer.

  5. The client sends a message, which is processed by the method receive and goes to the group.

  6. Channel Layer distributes the message to all Consumers in the group.

  7. Each Consumer sends a message to its client, and all users see real-time updates.

Building a chat application with websockets

Let's create a simple chat to demonstrate the capabilities of Channels.

python manage.py startapp chat

Add it to INSTALLED_APPS:

# settings.py

INSTALLED_APPS = [
    # ...
    'chat',
    # ...
]

Create a file routing.py in the application 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()),
]

We write 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
        }))

The code may seem long, but it's actually quite simple. The main thing is to understand how groups and messages work.

We create routing.py in the project root directory:

# 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
        )
    ),
})

Let's create templates and views:

Views:

# 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
    })

Sample 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>

Sample 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>

For simplicity, the templates use minimal HTML and JavaScript.

Setting up URLs:

# 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"),
]

For interaction between different Consumers set up a channel layer with Redis.

Installing Redis and dependencies:

pip install channels_redis

Settings settings.py:

# settings.py

CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'channels_redis.core.RedisChannelLayer',
        'CONFIG': {
            'hosts': [os.environ.get('REDIS_URL', ('127.0.0.1', 6379))],
        },
    },
}

Starting Redis:

For Ubuntu:

sudo apt-get install redis-server
sudo service redis-server start

About Consumers

Types of Consumers

  • Synchronous Consumers: Inherited from channels.generic.websocket.WebsocketConsumer. Use synchronous code.

  • Asynchronous Consumers: Inherited from channels.generic.websocket.AsyncWebsocketConsumer. Use async/await.

Example of a synchronous 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

When to use synchronous Consumers?

If your code is completely synchronous and doesn't use asynchronous operations, you can use synchronous Consumers. However, it is recommended to prefer asynchronous Consumers for better performance and scalability.

Authentication and Access

AuthMiddlewareStack allows you to access the user via self.scope.

Example of user access:

class ChatConsumer(AsyncWebsocketConsumer):
    async def connect(self):
        user = self.scope["user"]
        if user.is_authenticated:
            await self.accept()
        else:
            await self.close()

You can check user rights and grant or restrict access to certain rooms or actions:

if not user.has_perm('chat.view_room'):
    await self.close()

Middleware in Channels

Channels supports middleware for ASGI applications. You can create your own middleware to handle incoming connections.

Example of creating 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)

Connecting middleware:

from channels.routing import ProtocolTypeRouter, URLRouter
from channels.middleware import BaseMiddleware
import chat.routing

application = ProtocolTypeRouter({
    "websocket": CustomAuthMiddleware(
        URLRouter(chat.routing.websocket_urlpatterns)
    ),
})

Deploying a Channels application

To launch the Channels application, it is recommended to use Daphne – ASGI server.

Daphne installation:

pip install daphne

Launching the application:

daphne -b 0.0.0.0 -p 8000 myproject.asgi:application

Daphne is great for small projects, but for production it is better to use a combination with Gunicorn.

Combining Gunicorn with Uvicorn Worker:

pip install uvicorn gunicorn

Launching Gunicorn:

gunicorn myproject.asgi:application -k uvicorn.workers.UvicornWorker

Integration with existing Django applications

Channels fits perfectly into existing Django applications. You can slowly add asynchronous features without rewriting all the code.

Let's say there is a model Orderand I want to notify users in real time about the status of the order.

Model and signal:

# 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 to receive notifications:

# 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'],
        }))

Working with sessions

In Channels you can also access user sessions.

Connecting 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)
        )
    ),
})

Session access:

class ChatConsumer(AsyncWebsocketConsumer):
    async def connect(self):
        session_key = self.scope['session'].session_key
        # Используйте сессию по своему усмотрению

Sessions are often used to store temporary data or user state.

Performance

Channels supports launching multiple workers to handle the load:

daphne myproject.asgi:application &
python manage.py runworker &
python manage.py runworker &

In addition, when deploying in the cloud, you can configure autoscaling of workers depending on the load.

Also use caching to store frequently used data and reduce the load on the database. Redis is great for caching.


Some advice

Keep Consumers Simple: Separate logic into separate functions or classes for better readability and maintainability.

Use queues to handle heavy loads messages, for example RabbitMQ, if Redis fails.

Always check user input and permissions.

Check out official Channels documentation for more details.

In conclusion, let me remind you about the open lesson “Patroni and its use with Postgres” – in it you can gain practical skills in monitoring and managing highly available PostgreSQL clusters using Patroni. The lesson will take place on October 24. If interested, sign up via the link.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *