Django Channels - A Simple Chat App Part 3
Tags: python, django, webdev, tutorial
Published: 2021-10-11 08:03:54

Hello everyone :wave: 

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](https://dev.to/earthcomfy/django-channels-a-simple-chat-app-part-2-eh9) 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
```python
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
        }))

```
<center><u>**LET'S BREAKDOWN THE CODE**</u></center><br/>

- `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**](https://channels.readthedocs.io/en/stable/topics/consumers.html#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
```python
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
```python
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.

![Congrats](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jmuukyginp6zkzudqphk.gif)


*Reference - https://channels.readthedocs.io/en/stable/*


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 👋