Introduction
Real-time applications like chat apps, notifications, and live dashboards require instant communication between the server and client. Traditional HTTP requests are one-way and cannot push updates to clients immediately. This is where WebSockets and Django Channels come in.
Django Channels extends the Django framework to support asynchronous communication and WebSockets, allowing you to build interactive, real-time applications.
In this guide, we will build a fully functional real-time chat app using:
- Django
- Django Channels
- WebSockets
- Redis (as the channel layer backend)
By the end, you will understand how to structure a Channels project, manage WebSocket connections, and broadcast messages to multiple clients.
Step 1: Setting Up the Project
Start by creating a new Django project.
django-admin startproject chat_project
cd chat_project
python manage.py startapp chat
Install necessary dependencies:
pip install channels channels_redis
Add channels and your app to INSTALLED_APPS in settings.py:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'channels',
'chat',
]
Step 2: Configure ASGI
Unlike regular Django projects that use WSGI, Channels uses ASGI for asynchronous communication.
In chat_project/asgi.py:
import os
import django
from channels.routing import get_default_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'chat_project.settings')
django.setup()
application = get_default_application()
Update settings.py to define the ASGI application:
ASGI_APPLICATION = 'chat_project.asgi.application'
Step 3: Configure Redis Channel Layer
Django Channels requires a channel layer to handle communication between consumers. Redis is the most common backend.
Install Redis on your system or use Docker:
docker run -p 6379:6379 redis
Add channel layer settings in settings.py:
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [("127.0.0.1", 6379)],
},
},
}
Step 4: Creating the Chat Consumer
A consumer is analogous to a Django view but for WebSockets. Consumers handle events like connect, disconnect, and receive.
Create chat/consumers.py:
import json
from channels.generic.websocket import AsyncWebsocketConsumer
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}'
# Join room group
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
)
await self.accept()
async def disconnect(self, close_code):
# Leave room group
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name
)
# Receive message from WebSocket
async def receive(self, text_data):
text_data_json = json.loads(text_data)
message = text_data_json['message']
username = text_data_json['username']
# Send message to room group
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'chat_message',
'message': message,
'username': username
}
)
# Receive message from room group
async def chat_message(self, event):
message = event['message']
username = event['username']
# Send message to WebSocket
await self.send(text_data=json.dumps({
'message': message,
'username': username
}))
Step 5: Define WebSocket Routing
Create chat/routing.py to define WebSocket URLs:
from django.urls import re_path
from . import consumers
websocket_urlpatterns = [
re_path(r'ws/chat/(?P<room_name>\w+)/$', consumers.ChatConsumer.as_asgi()),
]
Then, update chat_project/routing.py:
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
import chat.routing
application = ProtocolTypeRouter({
'http': get_asgi_application(),
'websocket': AuthMiddlewareStack(
URLRouter(
chat.routing.websocket_urlpatterns
)
),
})
Step 6: Create Chat Templates
Create chat/templates/chat/room.html:
<!DOCTYPE html>
<html>
<head>
<title>Chat Room</title>
</head>
<body>
<h2>Room: {{ room_name }}</h2>
<div id="chat-log"></div>
<input id="chat-message-input" type="text" size="100">
<input id="chat-message-submit" type="button" value="Send">
<script>
const roomName = "{{ room_name }}";
const username = prompt("Enter your username:");
const chatSocket = new WebSocket(
'ws://' + window.location.host +
'/ws/chat/' + roomName + '/'
);
chatSocket.onmessage = function(e) {
const data = JSON.parse(e.data);
const chatLog = document.getElementById('chat-log');
chatLog.innerHTML += &lt;p&gt;&lt;b&gt;${data.username}:&lt;/b&gt; ${data.message}&lt;/p&gt;;
};
chatSocket.onclose = function(e) {
console.error('Chat socket closed unexpectedly');
};
document.getElementById('chat-message-submit').onclick = function(e) {
const messageInputDom = document.getElementById('chat-message-input');
const message = messageInputDom.value;
chatSocket.send(JSON.stringify({
'message': message,
'username': username
}));
messageInputDom.value = '';
};
</script>
</body>
</html>
Step 7: Create URL Patterns
In chat/urls.py:
from django.urls import path
from . import views
urlpatterns = [
path('<str:room_name>/', views.room, name='room'),
]
In chat/views.py:
from django.shortcuts import render
def room(request, room_name):
return render(request, 'chat/room.html', {
'room_name': room_name
})
Include the app URLs in chat_project/urls.py:
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('chat/', include('chat.urls')),
]
Step 8: Running the Application
Run Redis locally first (if using Docker):
docker run -p 6379:6379 redis
Then start the Django development server:
python manage.py runserver
Visit: http://127.0.0.1:8000/chat/room1/
Open the same URL in multiple tabs, enter usernames, and send messages — all tabs will update in real-time.
Step 9: Enhancing the Chat
1. Persisting Messages
To store chat history, create a Message model in chat/models.py:
from django.db import models
class Message(models.Model):
room_name = models.CharField(max_length=100)
username = models.CharField(max_length=50)
content = models.TextField()
timestamp = models.DateTimeField(auto_now_add=True)
Update the consumer to save messages:
from .models import Message
@receiver(order_created)
async def receive(self, text_data):
...
# Save message to database
Message.objects.create(
room_name=self.room_name,
username=username,
content=message
)
2. Display Chat History
In views.py, fetch previous messages:
from .models import Message
def room(request, room_name):
messages = Message.objects.filter(room_name=room_name)
return render(request, 'chat/room.html', {
'room_name': room_name,
'messages': messages
})
Update room.html to display historical messages:
<div id="chat-log">
{% for msg in messages %}
<p><b>{{ msg.username }}:</b> {{ msg.content }}</p>
{% endfor %}
</div>
3. Styling
Add simple CSS for better appearance:
<style>
#chat-log {
border: 1px solid black;
height: 300px;
overflow-y: scroll;
padding: 10px;
}
#chat-message-input {
width: 80%;
}
</style>
4. Authentication
You can integrate Django authentication so users automatically use their account usernames instead of entering them manually.
username = "{{ request.user.username }}"
Step 10: Production Considerations
- Use Redis as the production channel layer.
- Deploy with ASGI-compatible servers like Daphne or Uvicorn:
pip install daphne
daphne chat_project.asgi:application
- Secure WebSocket connections with HTTPS/WSS.
- Use Celery for asynchronous notifications or message processing if needed.
Step 11: Scaling
To scale the chat app:
- Add multiple worker nodes with Channels and Redis.
- Load balance using Nginx or HAProxy.
- Use Redis Pub/Sub for inter-process communication.
- Limit messages per room or paginate history to reduce memory usage.
Step 12: Testing
Unit Test Example for Consumer:
from channels.testing import WebsocketCommunicator
from chat_project.asgi import application
import pytest
import asyncio
@pytest.mark.asyncio
async def test_chat_consumer():
communicator = WebsocketCommunicator(application, "/ws/chat/testroom/")
connected, subprotocol = await communicator.connect()
assert connected
await communicator.send_json_to({"message": "Hello", "username": "Alice"})
response = await communicator.receive_json_from()
assert response['message'] == "Hello"
await communicator.disconnect()
This ensures your consumer behaves correctly.
Leave a Reply