1. Introduction to Django Channels

Django Channels extends Django to handle asynchronous protocols like WebSockets, enabling real-time features in your applications. It allows you to build applications that can handle long-lived connections, such as chat applications or live notifications.

2. Installing Django Channels

To get started with Django Channels, you need to install it. You can do this using pip:

pip install channels[daphne]

3. Setting Up Channels in Django

After installing Channels, you need to add setup channel to your Django project.

3.1 Update your settings.py file:

INSTALLED_APPS = [
    ...
    'daphne', # Add Daphne for ASGI server
]

This will install the Daphne’s ASGI version of the runserver management command.

You can also add “channels” for Channel’s runworker command.

3.2 Create an asgi.py file in your project directory:

myproject/
    ├── myapp/
    ├── myproject/
    │   ├── __init__.py
    │   ├── settings.py
    │   ├── urls.py
    │   ├── asgi.py  # Create this file
    │   └── wsgi.py

asgi.py

import os

from channels.routing import ProtocolTypeRouter
from django.core.asgi import get_asgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')
# Initialize Django ASGI application early to ensure the AppRegistry
# is populated before importing code that may import ORM models.
django_asgi_app = get_asgi_application()

application = ProtocolTypeRouter({
    "http": django_asgi_app,
    # Just HTTP for now. (We can add other protocols later.)
})

And finally, set your ASGI_APPLICATION setting to point to that routing object as your root application:

settings.py

ASGI_APPLICATION = 'myproject.asgi.application'

4. Routing in Django Channels

Django Channels uses a routing system similar to Django’s URL routing. You define a routing configuration that maps WebSocket URLs to consumers.

4.1 Create a routing.py file in your app directory:

myapp/
    ├── routing.py  # Create this file

routing.py

from django.urls import path
from . import consumers
websocket_urlpatterns = [
    path('ws/some_path/', consumers.MyConsumer.as_asgi()),  # Replace 'some_path' with your WebSocket path
    # Add more WebSocket paths as needed
    path('ws/another_path/', consumers.AnotherConsumer.as_asgi()),  # Example of another WebSocket path
    # You can add more paths as needed
    path('ws/chat/<str:room_name>/', consumers.ChatConsumer.as_asgi()),  # Example for chat rooms

]

4.2 Update your asgi.py file to include the routing:

from channels.routing import ProtocolTypeRouter, URLRouter
import os
from django.core.asgi import get_asgi_application
from myapp.routing import websocket_urlpatterns  # Import your routing file
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
django_asgi_app = get_asgi_application()
application = ProtocolTypeRouter({
    "http": django_asgi_app,
    "websocket": URLRouter(
        websocket_urlpatterns  # Use the WebSocket URL patterns defined in your routing file
    ),
})

5. Creating Consumers

5.1 What is a Consumer?

A consumer is a Python class that handles WebSocket connections. It defines methods to handle events like connecting, disconnecting, and receiving messages.

5.2 Creating a Simple Consumer

Consmer is of different types like AsyncConsumer, SyncConsumer, WebsocketConsumer, etc. But we use generic WebsocketConsumer for most of the cases.for now we will be using AsyncJsonWebsocketConsumer which is a subclass of AsyncWebsocketConsumer that handles JSON messages.

5.2.1 Method of AsyncJsonWebsocketConsumer

  • connect(): Called when a WebSocket connection is established.
async def connect(self):
    # This method is called when the WebSocket connection is established.
    # You can perform actions like authentication or joining a group here.
    await self.accept()  # Accept the WebSocket connection
  • accept(): Accepts the WebSocket connection.

  • disconnect(): Called when the WebSocket connection is closed.

async def disconnect(self, close_code):
    # This method is called when the WebSocket connection is closed.
    # You can perform cleanup actions here.
    pass  # No specific action needed for now
  • receive_json(): Called when a JSON message is received from the WebSocket.
async def receive_json(self, content):
    # This method is called when a JSON message is received from the WebSocket.
    # You can process the received message here.
    print("Received message:", content)  # Print the received message
    await self.send_json({"message": "Message received!"})  # Send a response back to the client
  • send_json(): Sends a JSON message back to the WebSocket client.

  • close(): Closes the WebSocket connection.

5.3 Example of a Simple Consumer

from channels.generic.websocket import AsyncJsonWebsocketConsumer
class MyConsumer(AsyncJsonWebsocketConsumer):
    async def connect(self):
        await self.accept()  # Accept the WebSocket connection
        #await self.close()  # Close the connection immediately for demonstration purposes

    async def disconnect(self, close_code):
        pass  # No specific action needed for now

    async def receive_json(self, content):
        print("Received message:", content)  # Print the received message
        await self.send_json({"message": "Message received!"})  # Send a response back to the client

5.4 Channel Layers

Normally,We without channels layer we can communicate between the server and the client but for communication between different instance we need to use channel layers. Channel layers allow you to send messages between different instances of your application, enabling features like chat rooms .

Where channel layers are used:

  • TO send message between different tab of the same browser
  • TO make chat application

Were channel layers are not used:

  • For sending message between server and client

Key termenology:

  • Group: It is not appropriate to send message to individual user so in order to send message to multiple user we use group. It is like a room where multiple user can join and send message to each other.
  • message: It is the data that we send to the group.Message must be dict

5.5 Setting Up Channel Layers

We use Redis as the channel layer backend. for that we need to install the channels_redis package:

pip install channels_redis

To setup the channel layer, add the following configuration to your settings.py file:

CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels_redis.core.RedisChannelLayer",
        "CONFIG": {
            "hosts": [("127.0.0.1", 6379)],
        },
    },
}

5.6 Key Methods for Channel Layers

  • get_channel_layer(): This method retrives the default channel layer instance.
from channels.layers import get_channel_layer
channel_layer = get_channel_layer()
  • channel_layer: This attribute retrives the default channel layer instance.

  • channel_name: This attribute retrives the name of the channel that will help to reach the consumer.

The following requires channel layer then we can use the following methods to send and receive messages:

  • send(channel_name, message): Sends a message to the specified channel.
  • group_send(group_name, message): Sends a message to a group of channels.
  • group_add(group_name, channel_name): Adds a channel to a group.
  • group_discard(group_name, channel_name): Removes a channel from a group.

5.7. Some of the example of how to use is:

from channels.generic.websocket import AsyncJsonWebsocketConsumer

class ChatConsumer(AsyncJsonWebsocketConsumer):
    async def functionName(self):
        print(self.channel_name) # The name of the channel that will help to reach the consumer
        print(self.channel_layer) # The default channel layer instance

5.8 Simple Chat Consumer Example for single room

from channels.generic.websocket import AsyncJsonWebsocketConsumer
class ChatConsumer(AsyncJsonWebsocketConsumer):
    async def connect(self):
        self.room_name= "my_room"  # Replace with your room name
        self.room_group_name = f"chat_{self.room_name}"  # Create a unique group name for the room
        # Join the room group
        await self.channel_layer.group_add(
            self.room_group_name,
            self.channel_name
        )
        await self.channel_layer.group_send(  # Send a message to the group when a user joins
            self.room_group_name,
            {
                'type': 'chat.message',
                'message': f'{self.channel_name} has joined the room.'
            }
        )
        await self.send_json({                   # Send a welcome message to the user
            'message': f'You have joined the room: {self.room_name}'
        })
        await self.accept()  # Accept the WebSocket connection
    
    async def disconnect(self, close_code):
        # Leave the room group
        await self.channel_layer.group_discard(    # Remove the user from the room group
            self.room_group_name,
            self.channel_name
        )
        await self.channel_layer.group_send(      # Send a message to the group when a user leaves
            self.room_group_name,
            {
                'type': 'chat.message',
                'message': f'{self.channel_name} has left the room.'
            }
        )
        
      

    async def receive_json(self, content):
        message = content.get('message', '')
        if message:
            # Send the received message to the room group
            await self.channel_layer.group_send(
                self.room_group_name,
                {
                    'type': 'chat.message',
                    'message': message
                }
            )
    async def chat_message(self, event):
        message = event['message']
        # Send the message to the WebSocket
        await self.send_json({
            'message': message
        })

5.9 Getting The group name dynamically

#routing.py
from django.urls import path
from . import consumers
websocket_urlpatterns = [
    path('ws/chat/<str:room_name>/', consumers.ChatConsumer.as_asgi()),  # Use a dynamic room name
]

5.10 Example of a Chat Consumer with Dynamic Room Name

from channels.generic.websocket import AsyncJsonWebsocketConsumer

class ChatConsumer(AsyncJsonWebsocketConsumer):
    async def connect(self):
        self.room_name = self.scope['url_route']['kwargs']['room_name']  # Get the room name from the URL
        self.room_group_name = f"chat_{self.room_name}"  # Create a unique group name for the room

        # Join the room group
        await self.channel_layer.group_add(
            self.room_group_name,
            self.channel_name
        )
        await self.accept()  # Accept the WebSocket connection

    async def disconnect(self, close_code):
        # Leave the room group
        await self.channel_layer.group_discard(
            self.room_group_name,
            self.channel_name
        )

    async def receive_json(self, content):
        message = content.get('message', '')
        if message:
            # Send the received message to the room group
            await self.channel_layer.group_send(
                self.room_group_name,
                {
                    'type': 'chat.message',
                    'message': message
                }
            )

    async def chat_message(self, event):
        message = event['message']
        # Send the message to the WebSocket
        await self.send_json({
            'message': message
        })

How it works:

  • When a user connects to the WebSocket, the connect method retrieves the room name from the URL and creates a unique group name for that room. for example: When user connect to ws/chat/room1/, the room_name will be room1 and the room_group_name will be chat_room1.
  • The user is then added to the room group using group_add

Whena a user disconnects, the disconnect method removes the user from the room group using group_discard.

  • When a user sends a message, the receive_json method sends that message to the room group using group_send.

note:self.scope['url_route']['kwargs']['room_name'] is used to get the room name from the URL. This allows you to create dynamic chat rooms based on the URL path.

6. Database Integration with Django Channels

Since We are using AsyncJsonWebsocketConsumer, we use database_sync_to_async to interact with the database in an asynchronous manner. This decorator allows you to call synchronous Django ORM methods from an asynchronous context.

To Import it we use

from channels.db import database_sync_to_async

To use it

await database_sync_to_async(query)()

Example

from channels.generic.websocket import AsyncJsonWebsocketConsumer
from channels.db import database_sync_to_async
from .models import Message  # Import your Django model
class ChatConsumer(AsyncJsonWebsocketConsumer):
    async def receive_json(self, content):
        message = content.get('message', '')
        if message:
            # Save the message to the database
            await  database_sync_to_async(Message.objects.create(content=message, room_name=self.room_name))()
            # Send the received message to the room group
            await self.channel_layer.group_send(
                self.room_group_name,
                {
                    'type': 'chat.message',
                    'message': message
                }
            )

7.Authentication in Django Channels

Authentication in Django Channels is similar to Django’s authentication system, but it requires some additional steps to ensure that the user is authenticated before allowing them to connect to a WebSocket.

7.1 Middleware for Authentication

There is a middleware called AuthMiddlewareStack that can be used to handle authentication in Django Channels. It wraps the ASGI application and adds the user to the scope.

7.2 Setting Up Authentication Middleware

In your asgi.py file, you can add the AuthMiddlewareStack to your application:

from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application
from myapp.routing import websocket_urlpatterns  # Import your routing file
import os

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
django_asgi_app = get_asgi_application()
application = ProtocolTypeRouter({
    "http": django_asgi_app,
    "websocket": AuthMiddlewareStack(  # Use AuthMiddlewareStack for WebSocket connections
        URLRouter(
            websocket_urlpatterns  # Use the WebSocket URL patterns defined in your routing file
        )
    ),
})

7.3 Accessing the User in the Consumer

In your consumer, you can access the authenticated user from the scope:

from channels.generic.websocket import AsyncJsonWebsocketConsumer
class ChatConsumer(AsyncJsonWebsocketConsumer):
    async def connect(self):
        self.user = self.scope['user']  # Access the authenticated user
        if self.user.is_authenticated:
            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()
        else:
            await self.close()  # Close the connection if the user is not authenticated

8. Session in Django Channels

In Django Channels, you can access the session data in your consumers. The session data is stored in the scope of the WebSocket connection, allowing you to retrieve and manipulate session data just like you would in a regular Django view. for this you need to use SessionMiddlewareStack in your asgi.py file:

from channels.sessions import SessionMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application
import os
from myapp.routing import websocket_urlpatterns  # Import your routing file
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
django_asgi_app = get_asgi_application()
application = ProtocolTypeRouter({
    "http": django_asgi_app,
    "websocket": SessionMiddlewareStack(  # Use SessionMiddlewareStack for WebSocket connections
        URLRouter(
            websocket_urlpatterns  # Use the WebSocket URL patterns defined in your routing file
        )
    ),
})

8.1 Accessing Session Data in the Consumer

In your consumer, you can access the session data from the scope:

from channels.generic.websocket import AsyncJsonWebsocketConsumer
class ChatConsumer(AsyncJsonWebsocketConsumer):
    async def connect(self):
        self.session = self.scope['session']  # Access the session data

10. Send data to specific user

To send data from one user to specific user can use send method of the channel layer and channel_name of the user.

self.channel_layer.send(channel_name,message)

10.1 Example of Sending Data to a Specific User

from channels.generic.websocket import AsyncJsonWebsocketConsumer
class NotificationConsumer(AsyncJsonWebsocketConsumer):
    async def connect(self):
        await self.accept()  # Accept the WebSocket connection
        await self.channel_layer.send(
            channel_name,  # The channel name of the specific user
            {
                                'message': 'You have a new notification!'
            }
        )

11. Sending Data from outside the consumer

To send data from outside the consumer, you can use the channel_layer to send messages to a specific channel or group. This is useful for sending notifications or updates to users without them having to be connected to a WebSocket.

11.1 Example of Sending Data from Outside the Consumer

from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync

class NotificationService:
    def __init__(self):
        self.channel_layer = get_channel_layer()  # Get the channel layer instance

    def send_notification(self, channel_name, message):
        async_to_sync(self.channel_layer.send)(channel_name, {  
            'type': 'notification.message',  # Custom type for handling the message
            'message': message
        })

11.2 Example of Handling the Notification in the Consumer

from channels.generic.websocket import AsyncJsonWebsocketConsumer
class NotificationConsumer(AsyncJsonWebsocketConsumer):
    async def connect(self):
        await self.accept()  # Accept the WebSocket connection

    async def disconnect(self, close_code):
        pass  # Handle disconnection if needed

    async def notification_message(self, event):
        message = event['message']  # Get the message from the event
        await self.send_json({'message': message})  # Send the message to the WebSocket client