Using Django Channels and Channel Layers
Motivation
Getting started with Django channels came from my fourth portfolio project, BookShelf, written as part of my training with Code Institute. BookShelf is a blogging site where users can post, like, and comment on books they enjoy reading. As it stands, the user must refresh the page in order to update the list of displayed books. This is not currently an issue since the number of users is currently small. However, one can easily imagine that as the number of users grow, we would expect the list to be updated each time a post was made.
This is where Django channels (and channel layers) can help.
Django channels and channel layers are technologies that allow a client to send a message to the backend and for the backend to send signals to the client. The backend can then be programmed to respond to the clients signals and the client can respond to signals received from the backend.
Getting Setup
I have a basic Django project already set up. For this test project, I have allauth setup for authentication and a simple form for the user to make a post with a title and some text. What we want to do is alert the user whenever someone makes a new post.
- The first thing to do is install two packages: channels and daphne. Daphne is an asgi application server.
python -m pip install -U channels["daphne"]
- Add daphne to your INSTALLED_APPS list. Put it at the top. Also add the channels app.
INSTALLED_APPS = ( "daphne", "channels", ... ... )
- In your projects root app, there should be a file called asgi.py. Open it make sure it looks like the following. This will make our django backend an asgi instead of a wsgi server. An ASGI (Asynchronous Server Gateway Interface) application is a type of web application that utilizes asynchronous programming to handle incoming requests and generate responses. It is designed to work with web servers that support ASGI and provides a flexible and scalable approach to building web applications. ASGI applications can handle long-running tasks, streaming data, and large volumes of concurrent connections by using asynchronous functions or coroutines that perform non-blocking I/O operations. This allows for improved performance and scalability compared to traditional synchronous web frameworks.
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, })
- Add the following entry in settings.py. In your case, replace blog with the root app of your project.
ASGI_APPLICATION = "blog.asgi.application"
Workflow
So now that we have channels installed, how do we actually use them so that our front end and back end have a way to communicate with each other?
Here is the strategy:
- In whatever django app we have designated to handle the channel, we write a class to create something called a Consumer. A Consumer is very similar to a View in regular Django. A Consumer will process the data received from the channel and can send data through the channel.
- We need set up routes in a routing.py file. routing.py is similar to urls.py - in this file we tell django which Consumer handles a particular URL.
- In the html template, we will write a JavaScript script to open a channel with the backend using a url pattern we define ourselves. In this script, we can write methods to handle messages received from the backend and methods to send messages to the backend.
You can use any of your django apps to implement channels. However, I want to keep things separate so I created an app called "updater" since its task is to handle the channel communication and update the user.
Let us create the app and add it to our INSTALLED_APPS in settings.py.
$ python manage.py startapp updater
INSTALLED_APPS = ( "daphne", "channels", "updater", ... )
This will of course create the folder structure for the updater app. We will not be using models.py, urls.py, or views.py for this app.
Creating a Consumer
Create file called consumers.py and add the following contents. We will write a class called UpdateConsumer to handle websocket connections.
import json from asgiref.sync import async_to_sync from channels.generic.websocket import WebsocketConsumer class UpdateConsumer(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}))
This is a basic setup. Let's talk about each of these methods one at a time.
- The connect(self): method simply accepts a connection.
- Right now the disconnect(self, close_code) method is empty. We'll come back to that.
- The receive(self, text_data) method handles the incoming messages from the front end. Here, it is extracting the data from a json object sent from the front end with a key of "message". That means of course, we are programming the front end to send json data with a key of "message" along with a value. (We'll see how that works a bit later.) Right now, the backend sends the data right back to the client. We will deal with that later.
As it stands, this is a one to one communication between the front and back end. That means that when the backend receives data from the front end, it sends that data ONLY to that one client. Later, we will make the server broadcast the data to all clients because that is what we want to happen - when a new post is made, ALL clients are notified.
Create the Route
Create a file called routing.py and add the following:
from django.urls import path from . import consumers websocket_urlpatterns = [ path("ws/update_notice/", consumers.UpdateConsumer.as_asgi()), ]
This should look familiar - we have a path that will point to our UpdateConsumer class. The ws/ is not mandatory, but is custom.
Now we have to tell the asgi server about these routes. Go back to the asgi.py file in your root app and add the websocket entry.
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 import chat.routing import updater.routing os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'blog.settings') django_asgi_app = get_asgi_application() application = ProtocolTypeRouter({ "http": django_asgi_app, "websocket": AllowedHostsOriginValidator( AuthMiddlewareStack(URLRouter(updater.routing.websocket_urlpatterns)) ), })
- It creates an ASGI application that is used by Django Channels.
- It creates a ProtocolTypeRouter that can handle different types of connections:
- http: the normal HTTP server that Django channels runs on. This is used by the web interface.
- websocket: the websocket server that Django channels runs on.
- It wraps the websocket server with the AllowedHostsOriginValidator, which will only allow requests from the host specified in ALLOWED_HOSTS. This is a security measure to prevent other sites from connecting to your server.
- It wraps the websocket server with the AuthMiddlewareStack, which will allow the websocket server to use the authentication middleware. This is required to allow the client to connect to the websocket server.
- It wraps the websocket server with the URLRouter, which will allow the websocket server to use the URL patterns defined in updater.routing. updater.routing.websocket_urlpatterns is our list of urls mapping them to consumers. This is required to allow the client to connect to the websocket server.
Run the migrations:
python manage.py migrate
Connecting the front end
Great! But how do we connect the front end?
We need to execute some JavaScript. Now where you want to run this script is up to you. You put it in the template that needs to make a websocket connection. I have decided to put this in the base.html template.
<script> // Create a new socket object. Note how the url is built. The protocol is ws:// not http:// const socket = new WebSocket( 'ws://' + window.location.host + '/ws/update_notice' + '/' ); //This method executes when a new message is received from the backend. Right now, it is expecting data with a key of "new_id" socket.onmessage = function (e) { const data = JSON.parse(e.data); alert(data.new_id); }; //We could also write a method to send data to the backend. But we will skip that for now. </script>
As it stands, the backend will only notify the single user on the front end of a received message. This is not what we want. Whenever a new post is made, we want ALL users to be notified. To do that, we need to implement channel layers.
Channel Layers
Channel layers allow for different websocket connections to be bundled together so the server can broadcast to all of the connections that belong to the same group. According to the Django documentation, "A channel layer is a kind of communication system. It allows multiple consumer instances to talk with each other, and with other parts of Django." There is tremendous freedom here. We will keep it simple by putting all of the connections to the server in one group. We do this using group names.
Setting Up Channel Layers
The first thing is to install Redis. To quote them directly, "Redis is an open source (BSD licensed), in-memory data structure store used as a database, cache, message broker, and streaming engine." Redis basically is a lightweight server to handle things like messages, etc. You can find the instructions for installing redis here: Getting Started With Redis
Install the following package so Channels know how to interface with Redis.
pip install channels_redis
Add a CHANNEL_LAYERS setting to settings.py
CHANNEL_LAYERS = { "default": { "BACKEND": "channels_redis.core.RedisChannelLayer", "CONFIG": { "hosts": [("127.0.0.1", 6379)], }, }, }
Enhancing UpdateConsumer
Open the consumers.py file so we can adjust the methods one at a time.
def connect(self): self.group_name = "updater" # Join room group async_to_sync(self.channel_layer.group_add)( self.group_name, self.channel_name ) self.accept()
Here we are creating a group name and adding the incoming channel to it. The async_to_sync is a helper method to take an asynchronous function and make it synchronous. This makes these methods behave nicely withe synchronous nature of Django.
Let us write the disconnect method which removes the channel from the group.
def disconnect(self, close_code): # Leave group async_to_sync(self.channel_layer.group_discard)( self.group_name, self.channel_name )
Now for the good bit, broadcasting the data.
# Receive message from WebSocket def receive(self, text_data): text_data_json = json.loads(text_data) new_id = text_data_json["new_id"] # Send the data to the group. async_to_sync(self.channel_layer.group_send)( self.group_name, {"type": "update_notice", "new_id": new_id} )
This method extracts the value from the text_data using the "new_id" key. The method then broadcasts the data to the group. Now look at the {"type": "update_notice", "new_id": new_id} object. This looks like a context object in normal Django. The "type" is the method that is used to encapsulate the data and actually send it.
def update_notice(self, event): id = event["new_id"] self.send(text_data=json.dumps({"new_id": id}))
Triggering a Send
But now we want to broadcast a message to the group every time a user creates a new post. The message will just be the id of the newly created post. But how do we trigger it? We could send a message from the front end when the user saves a post. But that would be awkward. We would have to save the new post, get its id from the backend, and then send that id back to the backend so it can be broadcasted.
That seems like a lot of running around. Wouldn't it be easier to just broadcast the id when a new post is saved? Yes it would.
In my views.py file for the posts app (since I want to send the broadcast when a post is created), I add the following lines of code right when the post is saved but before the user is redirected.
# get the id for the newly created post newID = BlogPost.objects.create(title=title, body=content) # get a reference to the channel layer so you can send data over it. channel_layer = get_channel_layer() # Send the data to the group. Note the name - "update_notice_group" and the {"type": "update_notice", "new_id": newID.id} # Those should match the values in consumers.py async_to_sync(channel_layer.group_send)("update_notice_group", {"type": "update_notice", "new_id": newID.id}) return redirect("blog_list")
In other words, add this code to whatever method in views.py handles the saving of a new post.
That's it! Later, we'll look at updating the UI when the data is broadcast.