Django Channels - A Simple Chat App Part 3

Hello everyone đź‘‹

Apologies for the delay of this article. This is the final part of the series. By the end of this tutorial, we will have a simple chat server in our hand.

In the last part of this series, we opened a WebSocket connection but nothing happened, because Channels doesn't know where to look up to handle/accept the connection. Similar to views in Django which accept/handle HTTP request, we have something called consumers in Channels.

There are two types of WebSocket consumers. AsyncConsumer and SyncConsumer. The former let us write async-capable code, while the latter is used to write synchronous code.

What to use when?

  • Use AsyncConsumers for greater performance and when working with independent tasks that could be done in parallel, otherwise stick with SyncConsumers. For the purpose of this tutorial, we will be writing SyncConsumers.

However, writing consumers alone isn't enough. We want multiple instances of consumers to be able to talk with each other. This is done using channel layers. In my last article I've talked about channel layers and how to set it up so, if you haven't already, go ahead and check that out.

A channel layer has the following:

1) 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.

  • But in cases where we want to send to multiple channels/consumers at once, we need some kind of broadcast system. That's why we have something called Groups.

2) 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.

We can use groups by adding a channel to them during connection, and removing it during disconnection. Like this: channel_layer.group_add and channel_layer.group_discard. Don't worry as we are going to see all of these concepts in action.

Since this series is all about getting started with Django Channels, We are going to create a basic consumer that listens to messages in a WebSocket connection and echoes it back to the same connection.

Create a new file called consumers.py inside the chat app, and put the following.

chat/consumers.py

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 = 'chat_%s' % self.room_name

        # Join room group
        async_to_sync(self.channel_layer.group_add)(
            self.room_group_name,
            self.channel_name
        )

        self.accept()

    def disconnect(self, close_code):
        # Leave room group
        async_to_sync(self.channel_layer.group_discard)(
            self.room_group_name,
            self.channel_name
        )

    # Receive message from WebSocket
    def receive(self, text_data):
        text_data_json = json.loads(text_data)
        message = text_data_json['message']

        # Send message to room group
        async_to_sync(self.channel_layer.group_send)(
            self.room_group_name,
            {
                'type': 'chat_message',
                'message': message
            }
        )

    # Receive message from room group
    def chat_message(self, event):
        message = event['message']

        # Send message to WebSocket
        self.send(text_data=json.dumps({
            'message': message
        }))
LET'S BREAKDOWN THE CODE
  • WebsocketConsumer which is available from channels.generic.websocket is the base WebSocket consumer. It is synchronous and provides a general encapsulation for the WebSocket handling model that other applications can build on.
  • The asgiref package that includes ASGI base libraries such as Sync-to-async and async-to-sync function wrappers is automatically installed as a dependency when you install Django with pip. How is this going to be useful? Glad you asked.
  • All channel layer methods are asynchronous, but our consumer, which we named ChatConsumer, is a synchronous WebsocketConsumer. That's why we need to wrap the channel layer methods in a async-to-sync function provided from the asgiref.sync module. This wrapper function takes an async function and returns a sync function.
  • Any consumer has a self.channel_layer and self.channel_name attribute, which contains a pointer to the channel layer instance and the channel name respectively.
  • Our very own ChatConsumer inherits from the base class, WebsocketConsumer and use its class methods to do the following:

1) connect() method => as the name implies it's called when a WebSocket connection is opened. Inside this method, we first obtained the room_name parameter from the URL route in chat/routing.py that opened the WebSocket connection to the consumer. This parameter is available from the scope of of the consumer instance.

  • Scope contains a lot of the information related to the connection that the consumer instance is currently on. Think of it as the kind of information you’d find on the request object in a Django view. It's from this scope that we are able to fetch the keyword argument (room_name) from the URL route.
  • self.room_group_name = 'chat_%s' % self.room_name what this does is construct a Channels group name directly from the user-specified room name.
  • async_to_sync(self.channel_layer.group_add)(...) joins a room group. For reasons I mentioned earlier, this function is wrapped in async-to-sync function.
  • self.accept() accepts an incoming socket.

2) disconnect() method => is called when a WebSocket connection is closed.

  • async_to_sync(self.channel_layer.group_discard)(...) leaves the group.

3) receive() method => is called with a decoded WebSocket frame. We can send either text_data or bytes_data for each frame.

  • json.loads(text_data) parses the JSON string (the user's input message) that is sent over the connection from room.html, and converts it into a Python Dictionary.
  • text_data_json['message'] captures that message.
  • async_to_sync(self.channel_layer.group_send) = sends events over the channel layer. An event has a special type key, which corresponds to the name of the method that should be invoked on consumers that receive the event. We set this type key as 'type': 'chat_message', therefore we are expected to declare a handling function with the name chat_message that will receive those events and turn them into WebSocket frames.

Note: The name of the method will be the type of the event with periods replaced by underscores - so, for example, an event coming in over the channel layer with a type of chat.join will be handled by the method chat_join.

4) chat_message() method => Handles the chat.message event when it's sent to us. It receives messages from the room group through event['message'] and send it over to the client.

That's consumers explained in a nutshell. Refer to the documentation for more.

Similar to how Django has urls.py that has a route to the views.py, Channels has routing.py that has a route to the consumers.py. Therefore, go ahead and create routing.py inside the chat app and put the following code.

routing.py

from django.urls import re_path

from . import consumers

websocket_urlpatterns = [
    re_path(r'ws/chat/(?P<room_name>\w+)/$', consumers.ChatConsumer.as_asgi()),
]
  • as_asgi() method is similar to as_view() method which we call when using class based views. What it does is return an ASGI wrapper application that will instantiate a new consumer instance for each connection or scope.

Next, head over to asgi.py and modify it as follows.

asgi.py

import os

from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application
import chat.routing

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

application = ProtocolTypeRouter({
  "http": get_asgi_application(),
  "websocket": AuthMiddlewareStack(
        URLRouter(
            chat.routing.websocket_urlpatterns
        )
    ),
})
  • The above code points the root routing configuration to the chat.routing module. This means when a connection is made to the development server (channel's development server), ProtocolTypeRouter checks whether it's a normal HTTP request or a WebSocket.
  • If it's a WebSocket, AuthMiddlewareStack will take it from there and will populate the connection’s scope with a reference to the currently authenticated user, similar to how Django’s AuthenticationMiddleware populates the request object of a view function with the currently authenticated user.
  • Next, URLRouter will route the connection to a particular consumer based on the provided url patterns.

Alright, run the development server,

python manage.py runserver

Open a browser tab, type in the name of the room you would like to join and open it in another tab as well.

In one of the tabs, type in any message you want, press enter, and go to the other tab. You should see the message broadcasted in both of the tabs. That brings us to the end of this series.

I hope you were able to get the basic picture of how all this works and use it in your next project. Thanks for your time and feedback đź‘‹

50