Django Channels - A Simple Chat App Part 2

Hello guys. In the last part of the tutorial we created the index view that allows users to type in the chat room they want to join. Buckle up since we are going to continue from where we left off.

Channel Layer

The basic feature of a modern chat app is that messages are sent and received in real-time. Imagine what a bad UX and storage unfriendly it would be to store these messages in a database before sending them to the other client! Querying the database every time you need to get your messages is inefficient. Real-time chat apps don't do this, but this doesn't mean they don't use any kind of transport mechanism to pass messages from senders to receivers.

  • Django Channels use something called a channel layer.

Channel layers allow you to talk between different instances of an application. They’re a useful part of making a distributed real-time application if you don’t want to have to shuttle all of your messages or events through a database.

This means that channel layers are the middle man that passes messages from senders to receivers. To achieve this functionality channels provide two options to use as layers; channels_redis and InMemoryChannelLayer.

Even though I'm going to use the InMemoryChannelLayer as a channel layer for the chat app, I'll show you how you can set up channels_redis as well.

Configuration Using channels_redis

channels_redis is an officially maintained channel layer that uses Redis as its backing store.

For those of you who don't know what Redis is let me explain it a bit here.

Redis

  • Redis is an open source, in-memory data structure store, used as a database, cache, and message broker.
  • Redis stores data in memory for high performance data retrieval and storage purposes.
  • Retrieving data from a database upon request might take some time leading to a bad UX. Redis comes into play to solve this problem. Instead of querying from the database directly, we can store data inside of a redis cache instance and make retrieval directly from memory of a server that's running the redis service.
  • Using Redis we can save chat messages in a queue before sending them to the receiver.
  • channels_redis uses Redis as its backing store.

How can we set this up?

First you need to install Redis. There are different ways to do this depending on the operating system you are using. For this you can check out the official documentation and google your way through it.

Next, install the channels_redis package in the chat app so that Django channels knows how to interface with redis.

pip install channels_redis

Next, channel layers are configured via the CHANNEL_LAYERS Django setting so head over to the chat app's settings and add the following.

settings.py

CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels_redis.core.RedisChannelLayer",
        "CONFIG": {
            "hosts": [("127.0.0.1", 6379)],
        },
    },
}
  • Redis is running on localhost (127.0.0.1) port 6379:

Configuration Using InMemoryChannelLayer

For testing and in local development, we can alternatively use the channels in-memory layer, but you shouldn't use this layer in production because:

In-memory channel layers operate with each process as a separate layer. This means that no cross-process messaging is possible. As the core value of channel layers is to provide distributed messaging, in-memory usage will result in sub-optimal performance, and ultimately data-loss in a multi-instance environment.

How can we set this up?

settings.py

CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels.layers.InMemoryChannelLayer"
    }
}
  • Since we are on development phase and we are building a very simple chat app, am going to use the second method which doesn't require Redis.

Alright, now that we are done setting the channel layer let's proceed to the next part.

Creating the room view

As we talked about in the last part, the room view is the view that lets users in the same connection to see messages posted in that chat room. It uses a WebSocket to communicate with the Django server and listen for any messages that are posted.

Alright, let's first create the route for this view.

chat/urls.py

from django.urls import path

from . import views

urlpatterns = [
    path('<str:room_name>/', views.room, name='room'),
]
  • Since we want part of the URL which is the room_name to be used in a view function, we captured it as you can see from the above code. The url parameter (room_name) is only captured if the provided value matches the given path converter which is str for string.

We haven't created the room view yet, so let's go ahead and do that to handle the logic.

views.py

from django.shortcuts import render


def room(request, room_name):
    return render(request, 'chat/room.html', {
        'room_name': room_name
    })
  • In the above view function, we added an additional parameter called room_name which is the parameter passed by the url. It's important to note that the names of url parameters must match the names of the view method arguments.
  • Now that the view method has access to the url parameter - room_name, it will pass it to the room template for presentation.

Therefore, within the templates directory of our chat app, create a file named room.html and add the following.

room.html

<!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
            + '/'
        );

        // onmessage - An event listener to be called when a message is received from the server.
        chatSocket.onmessage = function(e) {
            // JSON.parse() converts the JSON object back into the original object,
            // then examine and act upon its contents.
            const data = JSON.parse(e.data);
            document.querySelector('#chat-log').value += (data.message + '\n');
        };

        // onclose - An event listener to be called when the connection is closed.
        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.keyCode === 13) {  // 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;

            // Send the msg object as a JSON-formatted string.
            chatSocket.send(JSON.stringify({
                'message': message
            }));

            // Blank the text input element, ready to receive the next line of text from the user.
            messageInputDom.value = '';
        };
    </script>
</body>
</html>
  • Let me explain what's going on in the above code. First the template captures the room_name that is passed to it by the view. The template filter json_script outputs the Python object (room_name) as JSON inside of our script.
  • Next we parsed the JSON object room-name and created a WebSocket connection within that room ws://127.0.0.1:8000/ws/chat/room-name/.
  • Then we attached different even listeners on the WebSocket object. We have seen this in my last article and I've explained them in comments too.

Time too run the development server.

python manage.py runserver

Go to http://127.0.0.1:8000/chat/ in your browser. You will be presented with the index page we created in the previous part of the series. Type in the room and press enter.

You will be redirected to http://127.0.0.1:8000/chat/hannah/

Type any message you want and press enter.

Nothing happens right? But don't freak out. The room view opened a web socket connection but we haven’t created a consumer that accepts WebSocket connections yet which is why nothing is displayed. If you open the JavaScript console, you will see the error.

That being said, to fix this issue we will write a consumer that will accept WebSocket connection, but that's to be done in an upcoming tutorial, so stay tuned!

Till next time 👋

30