Django Channels is All You Need

Team Atomic Loops
Atomic Loops
Published in
11 min readAug 22, 2023

Since the beginning, humans have been trying to search for faster modes of communication. At first it was in the format of the written letters. Postmen used to come and deliver the long awaited letters. This took time. Days or sometimes even weeks. It was used for centuries, which now exists in the form of delivering official documents, bills and courier.

Then came the telegraph. Well, the advantage we got with using telegraph was, it was fast, blazingly fast. Messages across the globe would transfer within a few minutes if not seconds. But still, we needed something more.

Then started a rollercoaster ride of faster ways to transfer messages. Fax machines (essentially telegraphs but more user-friendly), Pagers (Beep-Bop), Emails, SMS came into the market as an even faster way to communicate. Then came instant messengers. Services like WhatsApp, Facebook Messenger, Insta DMs, Discord. These were even faster than traditional emails and SMS text messages. And now we live in an era where we already have instant messengers, but we don’t even need someone to send the messages, for that we have large AI models like ChatGPT and Bard.

I purposefully did not include invention of Telephone (or Walkie-talkies) in above mentioned technologies. Because there is a fundamental difference in the technologies.

Telephones are real time.

What is ”real time”?

There is a difference between a live video call, and sending and receiving pre-recorded videos, right?

Let’s see Google has to say:

Google‘s definition.

It is not incorrect, when we are on a call, we do get replies almost instantly. Your voice or video is transmitted instantly through the phone line or a digital network to the recipient, and you can have a live conversation with them. The back-and-forth communication occurs without significant delays, making it real-time. You need this “real-time” concept in such applications, or it will be like you sending voice notes back and forth instead of talking.

In real time systems, the communication channel stays alive or open, and you can communicate without interruptions or delays.

There are message transfer techniques like HTTP, TCP/IP. These will let you transfer messages very very fast.

But to implement these real time applications we need to consider the real time part of it too.

But to implement these real time applications we need to consider the real time part of it too.

It cannot be done using the HTTP request-response model.

Why not?

“HTTP is a stateless protocol.”

(HTTP — HyperText Transfer Protocol)

This means HTTP does not keep track of previous connections, requests and responses. Client requests a connection to the server. Server accepts and establishes the connection. Then the client sends a request to the server, the server processes the request and then returns a response, then the connection is broken by the server. Here, connection is established only for that one request. Every time a client wants to send data, a new connection will have to be made, and then data will have to be sent. This is slow, and computationally costly when you want to send large amounts of data in a short time, repeatedly. Imagine live streaming using HTTP request-response model. There won’t be anything “live” in it. Then what can we do?

For this, we need what is called a Channel.

Channel

A channel is a connection layer over the underlying TCP layer. It keeps the connection persistent, which means, till the client says, “Cut the connection!”, the server should not break the connection. Using the open connection, the client can send as much data as it wants, as many times as it wishes, without the headache of establishing the connection every time. This reduces the overhead on both the client and the server.

(WebSocket working)

WebSocket is a bidirectional communication protocol that can send the data from the client to the server or from the server to the client by reusing the established connection channel. Almost all real time applications and services use WebSocket.

A channel layer provides the following abstractions:

  • A channel is a mailbox where messages can be sent to. Each channel has a name. Anyone who has the name of a channel can send a message to the channel.
  • A group is a group of related channels. A group has a name. Anyone who has the name of a group can add/remove a channel to the group by name and send a message to all channels in the group. It is not possible to enumerate what channels are in a particular group.

There are many programming languages, technologies and libraries to implement this.

Here are some of them:

  1. Celery — Python
  2. ServerSocket — Java
  3. Tornado — Python
  4. WebSockets: A Python Library
  5. WebSocket — JavaScript

For the time being, we are going to go forward with a framework in Python called Django. Django is a free, open-source high level python framework for rapidly building backends of web-apps. It is based on Model-View-Template (MVT) architecture. Django is synchronous. It natively supports HTTP based models. Then how can we use that to implement a real time service, an asynchronous concept in it?

To add asynchronous behavior to it, we need Django Channels.

Django Channels

(Django Channels)

Django channels is a module in Django which enables Django to behave asynchronously while maintaining native Django’s simplicity and ease of use.

In this blog we will implement a basic chatting app.

The Implementation

Assuming you have Python 3.x, Django 4.x installed, let’s begin. (If you are not familiar with Django, visit https://docs.djangoproject.com/en/4.2/)

Installation and Setup:

Start Django Project:

django-admin startproject mychatapp .

This will create a Django project with name mychatapp in the currently open directory.

Create app chat:

python manage.py startapp chat

This will create an app with name chat in currently open directory.

Install Django Channels:

python -m pip install -U 'channels[daphne]'

Daphne is a HTTP and WebSockets protocol server for ASGI to power Django-Channels.

Now in asgi.py add following code snippet:

import os

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

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mychatapp.settings")

application = ProtocolTypeRouter(
{
"http": get_asgi_application(),
}
)

We are telling Django that this is going to use ASGI.

Add the app chat and daphne in your settings.py file:

INSTALLED_APPS = [
"daphne",
"chat",
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
]

Adding Views:

Create index.html in chat directory structured like:

chat/
└───templates/
└───chat/
└───index.html

and add following code in it:

<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8" />
<title>Chat Rooms</title>
</head>

<body>
What chat room would you like to enter?<br>
<input id="room-name-input" type="text" size="100"><br>
<input id="room-name-submit" type="button" value="Enter">

<script>
document.querySelector('#room-name-input').focus();
document.querySelector('#room-name-input').onkeyup = function (e) {
if (e.key === 'Enter') { // enter, return
document.querySelector('#room-name-submit').click();
}
};

document.querySelector('#room-name-submit').onclick = function (e) {
var roomName = document.querySelector('#room-name-input').value;
window.location.pathname = '/chat/' + roomName + '/';
};
</script>
</body>

</html>

This code is just for creating a chat room lobby.

(Output)

In chat/views.py file include the following:

from django.shortcuts import render

def index(request):
return render(request, "chat/index.html")

This will process the request made by the client to load the lobby page.

In chat/urls.py file include the following:

from django.urls import path
from . import views

urlpatterns = [
path("", views.index, name="index"),
]

This is just to map the URLs. This tells Django server what to do when some user goes to some URL. urls.py file in “chat/” tells chat app what to do.

Then in mychatapp/urls.py, add the following code:

from django.contrib import admin
from django.urls import include, path

urlpatterns = [
path("chat/", include("chat.urls")),
path("admin/", admin.site.urls),
]

This maps the base URL “chat/” to the urls.py in the chat app we created earlier. There can be multiple apps inside one Django project. To connect all those apps to one single point, we need this mychatapp/urls.py file.

Now go to settings.py and add following line at the bottom:

ASGI_APPLICATION = "mychatapp.asgi.application"

Now go ahead and run start the server:

python manage.py runserver

Now go to http://127.0.0.1:8000/chat/ and you should still see the index page that we created before. Nothing happens, right? Well, we are halfway there!

Now create file room.html in the same directory as index.html. Paste the following code in it.

<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8" />
<title>Chat Room</title>
</head>

<body>
<textarea id="chat-log" cols="100" rows="20"></textarea><br>
<input id="chat-message-input" type="text" size="100"><br>
<input id="chat-message-submit" type="button" value="Send">
{{ room_name|json_script:"room_name" }}
<script>
const roomName = JSON.parse(document.getElementById('room_name').textContent);

const chatSocket = new WebSocket(
'ws://'
+ window.location.host
+ '/ws/chat/'
+ roomName
+ '/'
);

chatSocket.onmessage = function (e) {
const data = JSON.parse(e.data);
document.querySelector('#chat-log').value += (data.message + '\n');
};

chatSocket.onclose = function (e) {
console.error('Chat socket closed unexpectedly');
};

document.querySelector('#chat-message-input').focus();
document.querySelector('#chat-message-input').onkeyup = function (e) {
if (e.key === 'Enter') { // enter, return
document.querySelector('#chat-message-submit').click();
}
};

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>

This will implement a simple chat room with a chat log, a message input field and a send button. Nothing too fancy. You can always add styling and make it responsive. But our focus right now is to develop a minimalistic chatting application.

(room.html output)

Add following function in chat/views.py below the index function:

def room(request, room_name):
return render(request, "chat/room.html", {"room_name": room_name})

This tells the server to render room.html when the user enters into a chat room (i.e. type name in “What chat room would you like to enter?” text field).

Add route to the room view:

from django.urls import path

from . import views

urlpatterns = [
path("", views.index, name="index"),
path("<str:room_name>/", views.room, name="room"),

This tells our server to call room function when chat/<str:room_name>/. For example, if “chat/abc” is typed, the user will enter the chat room abc.

Now refresh the server!

You should be able to see the chat room correctly now. Type the message “hello” and press enter. Nothing happens. In particular the message does not appear in the chat log. Why?

That’s because we haven’t written a Consumer.

What is a Consumer?

Similar to Django’s URLs configuration in urls.py, Channels has a guidebook called root routing configuration. This guidebook tells Channels which part of your code should take care of new chat connection. Instead of functions, Channels uses something called consumers. These consumers are like friendly assistants that know how to handle different types of real-time communication events, such as receiving a new message in the chat.

Create new file consumers.py and paste the below code in it:

import json

from channels.generic.websocket import WebsocketConsumer


class ChatConsumer(WebsocketConsumer):
def connect(self):
self.accept()

def disconnect(self, close_code):
pass

def receive(self, text_data):
text_data_json = json.loads(text_data)
message = text_data_json["message"]

self.send(text_data=json.dumps({"message": message}))

We need to create a routing configuration for the chat app that has a route to the consumer. Create a new file chat/routing.py. Your app directory should now look like:

In this file routing.py, paste the below code in it:

from django.urls import re_path

from . import consumers

websocket_urlpatterns = [
re_path(r"ws/chat/(?P<room_name>\w+)/$", consumers.ChatConsumer.as_asgi()),
]

Configure ASGI at the chat.routing module. Replace previous ASGI code with following:

import os

from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.security.websocket import AllowedHostsOriginValidator
from django.core.asgi import get_asgi_application

from chat.routing import websocket_urlpatterns

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()

import chat.routing

application = ProtocolTypeRouter(
{
"http": django_asgi_app,
"websocket": AllowedHostsOriginValidator(
AuthMiddlewareStack(URLRouter(websocket_urlpatterns))
),
}
)

This root routing configuration specifies that when a connection is made to the Channels development server, the ProtocolTypeRouter will first inspect the type of connection. If it is a WebSocket connection (ws:// or wss://), the connection will be given to the AuthMiddlewareStack .

Enabling Channels Layer:

We will implement a Channels layer that uses Redis as its backing store, thus we need Redis server running. Now, we can download it, or better yet, we can use Docker image for it. If you don’t have Docker Desktop installed, go get it.

https://docs.docker.com/get-docker/

Follow the guide, and go through the tutorial after installing the software.

Then, after all said and done, start Docker Desktop and run the command below:

docker run --rm -p 6379:6379 redis:7

Redis server will start on port 6379.

To connect channels and Redis, we must install channels_redis library:

python -m pip install channels_redis

Add the following setting to settings.py file, below ASGI setting:

This will tell Django that we are using Redis.

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

Let’s make sure that the channel layer can communicate with Redis.

Open a Django shell and run the following commands:

$ python3 manage.py shell
>>> import channels.layers
>>> channel_layer = channels.layers.get_channel_layer()
>>> from asgiref.sync import async_to_sync
>>> async_to_sync(channel_layer.send)('test_channel', {'type': 'hello'})
>>> async_to_sync(channel_layer.receive)('test_channel')
{'type': 'hello'}

Replace old code in consumers.py with following code:

import json

from asgiref.sync import async_to_sync
from channels.generic.websocket import WebsocketConsumer


class ChatConsumer(WebsocketConsumer):
def connect(self):
self.room_name = self.scope["url_route"]["kwargs"]["room_name"]
self.room_group_name = f"chat_{self.room_name}"

async_to_sync(self.channel_layer.group_add)(
self.room_group_name, self.channel_name
)

self.accept()

def disconnect(self, close_code):
async_to_sync(self.channel_layer.group_discard)(
self.room_group_name, self.channel_name
)

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({"message": message}))

In the second browser tab, type the message “hello” and press enter. You should now see “hello” echoed in the chat log in both the second browser tab and in the first browser tab. But…

Let’s add async functionality:

Rewrite the ChatConsumer class as follows:

This will improve performance using async functionalities.

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}"

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)

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

You now have a basic fully-functional chatting server!

Open the same URL (http://127.0.0.1:8000/chat/) and the same chat room in two tabs, you will be able to chat with each other!

That was fast, wasn’t it?

What Next?

Go ahead and implement a chat bot for Discord, or implement a social media app which will be customized just for your friends. There are infinite possibilities starting from this base. Django Channels can be used to implement a live streaming app, your own Twitch!

Conclusion:

As I said earlier, we have come from letter writing to Django Channels, and Ibelieve that there will be some new way, some better technology of communication in future. That’s because we humans are social creatures and have a desire to communicate with our peers. Our constant need to improve our standard of living makes us invent such amazing technologies, and this is the quality we need to foster for a better future.

The full code for this blog is available at:

https://github.com/NotShrirang/django-channels-chatting-app

Contributed by:

Shrirang Mahajan

--

--