Building a Real-Time Chat System with Django Channels and WebSockets
Last time we explored the power of Django Channels in our article, ‘Diving into Django Channels: Real-Time Web Applications Made Easy’. In this article, we will expand on understanding how to develop a real-time chat system utilizing Django Channels to create an oversimplified application that allows users to communicate in real-time.
Django Channels
Channels is a django project that takes Django and extends its abilities beyond HTTP to handle websockets, chat protocols like IOT protocols, and more. It’s built on a python specification called ASGI.
Building a Real-Time Chat System
In this project, we’ll use channels along with websockets to establish two-way communication and an open connection between our client and server. We’ll use websockets on the client side to initiate a connection and channels on the server side to receive and send requests back to the client.
With django channels, there are four key steps that we need to go through to set up our server and create a socket connection:
- Configure ASGI: first we’ll need to switch our django project to use asgi and complete some basic channels configuration after installation.
- Consumers: then we’ll need to create some consumers which are the channel’s version of django views.
- Routing: create some routing to handle the URL routing for these consumers.
- WebSockets: when the channel setup is complete we’ll use the built-in javascript websocket API on the client side to initiate the handshake and create an open connection between our client and server.
Getting started
To get things started, we’ll set up a simple application:
- From your terminal create a project called chatRoom.
- Change the current directory.
- Set up an app called chat.
- Choose any code editor of your own, I’ll be using Visual Studio Code.
$ django-admin startproject chatRoom
$ cd chatRoom/
$ python manage.py startapp chat
After opening the project in your editor, go into settings.py in the root app (ChatRoom) and configure the app we created inside the installed apps.
INSTALLED_APPS = [
...
'chat.apps.ChatConfig',
]
Adding templates
Let us jump into the chat folder and create a folder called templates, and in the templates folder, create another folder called chat.
It’s odd how Django designed this, since, for it to function, we will need to build a second folder with the same app name as ‘chat ’ in the templates folder.
Let us create a template and call it lobby.html, and here we will want to create some boilerplate code for our HTML. Change the title and add some text so we can see something when we first open up this website.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chat Room with Django Channels</title>
</head>
<body>
<h1>Chat Room</h1>
</body>
</html>
Let us jump back into the views.py file in the chat app and set up a view called lobby and return the template that we just created for this application.
from django.shortcuts import render
# Create your views here.
def lobby(request):
return render(request, 'chat/lobby.html')
URL routing
We will need some URL routing, so let’s create a urls.py file in the chat app, import the path function along with our views, and then create a url patterns list. In this list, use the path function to set a url route and return our lobby view.
from django.urls import path
from . import views
urlpatterns = [
path('', views.lobby)
]
The last thing we want to do is jump back into our root urls.py file (in the chatRoom folder). Import the include method and set the route to our app’s urls.py file using the include method.
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('chat.urls'))
]
Starting the server
To make sure all is working well let’s go back to the terminal and
use the run server command to start up our server, then open up the browser on the specified port that’s listed here in the terminal.
$ python manage.py runserver
When you open the port, if you see this response, then everything went correctly here and we are ready to move forward with installing and setting channels for our application.
Configuring channels
To install channels, open up your terminal and run pip install
channels.
$ pip install channels
Once channels are installed, let’s open up settings.py in the chatRoom app to add channels to our top list of installed apps.
INSTALLED_APPS = [
'channels',
...
'chat.apps.ChatConfig',
]
Now, it is time to integrate channels, so let’s start by creating the routing configuration.
A channel’s routing configuration is an ASGI application similar to a Django configuration in that it tells channels what code to run when a channel server receives an HTTP request.
In the asgi.py file in the root app (chatRoom):
- Import protocol type router from channels.routing.
- Change the value of the application to use the newly imported protocol type router.
- Add HTTP for now as the first value we’ll update this later when we add in other protocols.
import os
from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'chatRoom.settings')
application = ProtocolTypeRouter({
'http': get_asgi_application(),
})
We also need to point channels at the root routing configuration, so let’s jump back into the settings.py file in the chatRoom app and add in the ASGI_APPLICATION and set the value to point to chatRoom or whatever your project was named.
ASGI_APPLICATION = 'chatRoom.asgi.application'
In order to use django channels, you need to install the daphne ASGI server, as it is the recommended server to use with Channels.
$ pip install daphne
Once it is installed, you should be able to start the channel development server by running the following command in your project terminal:
$ daphne chatRoom.asgi:application
This command will start the Daphne server and point it to your project’s ASGI application, which is defined in the asgi.py file in the ChatRoom app. You should see the message that indicates that channels are running and ready to accept websocket connections and other asynchronous protocols.
2023-04-15 01:54:40,202 INFO Starting server at tcp:port=8000:interface=127.0.0.1
2023-04-15 01:54:40,202 INFO HTTP/2 support not enabled (install the http2 and tls Twisted extras)
2023-04-15 01:54:40,207 INFO Configuring endpoint tcp:port=8000:interface=127.0.0.1
2023-04-15 01:54:40,209 INFO Listening on TCP address 127.0.0.1:8000
Establish WebSocket Connection
Okay, so let’ us establish a websocket connection and jump back to the client side in our lobby.html file.
Let’s add some script tags so we can write some Javascript directly in our template.
To establish a websocket connection, we first need an endpoint to start the handshake. We don’t have this url yet, but we’ll create it in the next step, so for now, let’s just go ahead and set the value to HTTP. Then grab the root URL by getting the location host at the end of the URL.
Next, we’ll use the websocket object to set the chat socket variable and pass in the URL that we just set.
Once we have our chat socket object, we can use the message event to listen to messages from our server. This will fire off anytime our server sends a message from the backend. Just go ahead and parse the data and console out whatever message the server sends us.
<script>
let url = `ws://${window.location.host}/ws/socket-server/`;
const chatSocket = new WebSocket(url);
chatSocket.onmessage = function (e) {
let data = JSON.parse(e.data)
console.log('Data: ', data)
}
</script>
Consumers
We have no endpoint for this socket to connect to, so let’s go ahead and fix it by jumping back into our code and creating some routers and consumers to actually handle this connection.
Consumers are the channel’s version of Django views, except they do more than just respond to requests from the client. They can also initiate requests to the client, all while keeping an open connection.
Let’s go into our chat app and create a file called consumers.py
In this file, create your first consumer by calling it ChatConsumer and inheriting it from the websocket consumer object. It’s going to be responsible for receiving incoming messages from the client and broadcasting them out to anybody that has a connection to this consumer, all in real-time.
Consumers structure our code into a series of functions for example we have a connect method for the initial request that comes in from the client
Then we have a receive method for when we receive messages from the client, and we also have a disconnect method to handle what happens when a client disconnects from this consumer.
import json
from channels.generic.websocket import WebsocketConsumer
class ChatConsumer(WebsocketConsumer):
def connect(self):
self.accept()
self.send(text_data=json.dumps({
'type': 'connection_established',
'message': 'You are now Connected!'
}))
Routing
To use this consumer, we’re going to need to set up some routing. So in your chat app create a routing.py file
Go ahead and import the re_path method, then import our consumers into this file and start setting up the websocket URL pattern list.
from django.urls import re_path
from . import consumers
websocket_urlpatterns = [
re_path(r'ws/socket-server/', consumers.ChatConsumer.as_asgi())
]
Back to our asgi.py file, let’s go ahead and import the URLRouter method, AuthMiddleware, and your routing.py file. Inside the protocol type router, add websocket to the list of protocols and use the auth middleware stack method to wrap the URLRouter.
import os
from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
import chat.routing
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'chatRoom.settings')
application = ProtocolTypeRouter({
'http': get_asgi_application(),
'websocket': AuthMiddlewareStack(
URLRouter(
chat.routing.websocket_urlpatterns
)
)
})
Once this is all set, open up your terminal and run migrations to apply the database changes and start up your development server.
$ python manage.py migrate
Now if you jump back into your browser and view the console, you should see a message that was sent from our consumer on the initial connection.
Sending Messages
Let’s jump back into our lobby.html file and create a form where we can send messages, then query our form by using document.getElementById. Next, we’ll add an event handler to our form to handle form submissions. Grab the value of the form message and set this to the message variable, then stringify the message and send it to the chat room using the send method from our websocket object, which is stored inside of the chat socket variable.
New updated code for lobby.html:
<body>
<h1>Chat Room</h1>
<form id="form">
<input type="text" name="message" autocomplete="off" />
</form>
<script>
let url = `ws://${window.location.host}/ws/socket-server/`;
const chatSocket = new WebSocket(url);
chatSocket.onmessage = function (e) {
let data = JSON.parse(e.data)
console.log('Data: ', data)
}
let form = document.getElementById('form')
form.addEventListener('submit', (e) => {
e.preventDefault()
let message = e.target.message.value
chatSocket.send(JSON.stringify({
'message': message,
}))
form.reset()
})
</script>
</body>
Back to our ChatConsumer class in consumers.py, add a receive method that will listen for incoming messages from the client. So what we’ll want to do here is go ahead and parse the data and then handle the response and set the message variable.
New updated code for the ChatConsumer class:
class ChatConsumer(WebsocketConsumer):
def connect(self):
self.accept()
self.send(text_data=json.dumps({
'type': 'connection_established',
'message': 'You are now Connected!'
}))
def receive(self, text_data):
text_data_json = json.loads(text_data)
message = text_data_json['message']
self.send(text_data=json.dumps({
'type': 'chat',
'message': message
}))
In the front end let’s jump to our messages function and handle the
incoming messages from the server.
Let’s query the messages wrapper first, and then we’ll create a wrapper in the HTML; this will be a div with the id of messages and will remain empty until users begin to add messages on that form submission.
Newly updated code for the client side:
<body>
<h1>Chat Room</h1>
<form id="form">
<input type="text" name="message" autocomplete="off" />
</form>
<div id="messages"></div>
<script>
let url = `ws://${window.location.host}/ws/socket-server/`;
const chatSocket = new WebSocket(url);
chatSocket.onmessage = function (e) {
let data = JSON.parse(e.data)
console.log('Data: ', data)
if (data.type === 'chat') {
let messages = document.getElementById('messages')
messages.insertAdjacentHTML('beforeend', `<div>
<p>${data.message}</p>
</div>`
)
}
}
let form = document.getElementById('form')
form.addEventListener('submit', (e) => {
e.preventDefault()
let message = e.target.message.value
chatSocket.send(JSON.stringify({
'message': message,
}))
form.reset()
})
</script>
</body>
Channel Layers
Now we need to enable channel layers so two users are connected to the same websocket (broadcast the message for every single channel in the same group).
So to do this we need to enable channel layers in the root (chatRoom app) settings.py file
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels.layers.InMemoryChannelLayer'
}
}
Now we need to go back to the consumers.py file in the chat app. On the initial connection, we’ll first want to set a group name. Normally this would be a dynamic value from our url or whatever room the user joined, but in this case, because it’s an oversimplified application, we’re only going to have one group, which we’re going to call test, and all users will be added to this group.
Using the async to sync method we’ll access the channel layer and call
the group add method. To add a user’s channel to the group, we simply specify the group name and our user’s channel name. Now for the channel name, we don’t need to create it; it will be created for us automatically for each user.
In the receive method, use async to sync again. Now in this case we’ll use
the group sends a broadcast message to every specific user in this group. We’ll specify the group name that we want to send this message to, along with the message and the name of the function that we want to have handle this event. We haven’t created the chat message function yet, so for now, just go ahead and add in the type value of the chat message, and we’ll create the function next.
Below the receive method, let’s go ahead and create a function called chat_message. We’ll then pass in self and the event and use the event object to retrieve the message that was sent. Now, from this function, we can call the send method, and because this message was sent to a group, every user that has a channel in this group will receive this message, which will be broadcast in real-time.
New updated code for consumers.py:
import json
from channels.generic.websocket import WebsocketConsumer
from asgiref.sync import async_to_sync
class ChatConsumer(WebsocketConsumer):
def connect(self):
self.room_group_name = 'test'
async_to_sync(self.channel_layer.group_add)(
self.room_group_name,
self.channel_name
)
self.accept()
self.send(text_data=json.dumps({
'type': 'connection_established',
'message': 'You are now Connected!'
}))
def receive(self, text_data):
text_data_json = json.loads(text_data)
message = text_data_json['message']
async_to_sync(self.channel_layer.group_send)(
self.room_group_name,
{
'type': 'chat_message',
'message': message
}
)
def chat_message(self, event):
message = event['message']
self.send(text_data=json.dumps({
'type': 'chat',
'message': message
}))
We’ll open two tabs up and let’s test this out in the browser
Final Touch
Ok, so now I hope I gave a high-level overview of how Django channels and Javascript websockets work together to create an oversimplified chat app.
If we have a database of users, we can implement a function to distinguish between the sender and the receiver based on their names, but for now, let’s just change our code to determine whether a message is sent by the current user or received from another user. We can also add some simple styling to help the user distinguish between the sender and the receiver.
Let us jump back to the consumer.py file and modify the ChatConsumer function so it broadcasts the message to all clients in the same group as the sender.
New updated code:
import json
from channels.generic.websocket import WebsocketConsumer
from asgiref.sync import async_to_sync
class ChatConsumer(WebsocketConsumer):
def connect(self):
self.room_group_name = 'test'
async_to_sync(self.channel_layer.group_add)(
self.room_group_name,
self.channel_name
)
self.accept()
def receive(self, text_data):
text_data_json = json.loads(text_data)
message = text_data_json['message']
sender = text_data_json['sender']
async_to_sync(self.channel_layer.group_send)(
self.room_group_name,
{
'type': 'chat_message',
'message': message,
'sender': sender
}
)
def chat_message(self, event):
message = event['message']
sender = event['sender']
self.send(text_data=json.dumps({
'type': 'chat',
'message': message,
'sender': sender
}))
Back to the lobby.html file, add some internal style in the head tag, and modify the javascript to determine whether a message is sent by the current user or received from another user based on the sender field in the incoming message data.
New updated code:
<head>
<style>
.sender {
position: relative;
right: 0;
background-color: #e0f7fa;
margin: 10px 0;
width: fit-content;
}
.receiver {
position: relative;
left: 0;
background-color: #ffccbc;
margin: 10px 0;
width: fit-content;
}
</style>
</head>
<body>
<div>
<h1>Chat Room</h1>
</div>
<form id="form">
<input type="text" name="message" autocomplete="off" />
</form>
<div id="messages"></div>
<script>
let url = `ws://${window.location.host}/ws/socket-server/`;
const chatSocket = new WebSocket(url);
let senderName = localStorage.getItem('senderName');
if (!senderName) {
localStorage.setItem('senderName', senderName);
}
chatSocket.onmessage = function (e) {
let data = JSON.parse(e.data);
console.log('Data:', data);
if (data.type === 'chat') {
let messages = document.getElementById('messages')
let cssClass = data.sender === senderName? 'sender' : 'receiver';
messages.insertAdjacentHTML('beforeend', `<div class="${cssClass}">
<p>${data.message}</p>
</div>`)
}
}
let form = document.getElementById('form')
form.addEventListener('submit', (e) => {
e.preventDefault()
let message = e.target.message.value
chatSocket.send(JSON.stringify({
'message': message,
'sender': senderName
}))
form.reset()
})
</script>
</body>
Summary
In this article, we have built an oversimplified chat application using django channels and javascript built-in websocket server. We started by setting up the django views and URLs for handling websocket connections, and then we created a javascript websocket client to connect to the django server. We then built a simple chat interface that allows users to send and receive messages in real-time.
We used the Django Channels library to handle WebSocket connections on the server-side and the channels.generic.websocket class to define our WebSocket consumer for handling incoming messages. On the client side, we used the websocket API provided by modern browsers to connect to the Django server and send and receive messages.
All in all, this article provides a basic introduction to the fundamentals of building a chat system using channels and websockets. With some additional work, this app could be extended with some more features like user authentication, message persistence, and real-time updates.