50
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 withSyncConsumers
. For the purpose of this tutorial, we will be writingSyncConsumers
.
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
}))
-
WebsocketConsumer
which is available fromchannels.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 asSync-to-async
andasync-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 synchronousWebsocketConsumer
. That's why we need to wrap the channel layer methods in aasync-to-sync
function provided from theasgiref.sync
module. This wrapper function takes an async function and returns a sync function. - Any consumer has a
self.channel_layer
andself.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 inasync-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 specialtype
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 namechat_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 methodchat_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 toas_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’sAuthenticationMiddleware
populates therequest
object of aview
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.
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 👋
50