Blogs • Published on 2022-05-19
The built-in capabilities of Django are extended with Django Channels, allowing Django projects to handle protocols like WebSockets, MQTT (IoT), chatbots, radios, and other real-time applications in addition to HTTP. It is based on the ASGI Python specification.
$mkdir dhuni-django-channels && cd dhuni-django-channels
$ python -m venv venv
$ venv/Scripts/activate
(venv)$ pip install django==3.2.14
(venv)$ django-admin startproject dashboard
(venv)$ cd dashboard
(venv)$ python manage.py startapp connection
#Application definition
NSTALLED_APPS = [
'apos;django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'connection'
]
(env)$ pip install channels==3.0.5
NSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'connection',
'channels',
]
import os
import django
from channels.routing import get_default_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dashboard.settings')
django.setup()
application = get_default_application()
When Django accepts an HTTP request, it consults the root URLconf to lookup a view function, and then calls the view function to handle the request. Similarly, when Channels accepts a WebSocket connection, it consults the root routing configuration to lookup a consumer, and then calls various functions on the consumer to handle events from the connection. However, unlike Django views, consumers are long-running by default. A Django project can have multiple consumers that are combined using Channels routing (which we'll take a look at in the next section).Each consumer has its own scope, which is a set of details about a single incoming connection. They contain pieces of data like protocol type, path, headers, routing arguments, user agent, and more.
import json
from asgiref.sync import async_to_sync
from channels.generic.websocket import JsonWebsocketConsumer
from channels.layers import get_channel_layer
class SocketConsumer(JsonWebsocketConsumer):
def connect(self):
self.room_name = self.scope['url_route']['kwargs']['user_type'].replace('/', '_')
#converted into sync because channel are asynchronous
async_to_sync(self.channel_layer.group_add)(
self.room_name,
self.channel_name
)
async_to_sync(self.channel_layer.send)(
self.channel_name,
{
"type": 'chat_message',
"message": {"channel_id": self.channel_name}
}
)
self.accept()
def disconnect(self, close_code):
# Leave room group
async_to_sync(self.channel_layer.send)(
self.channel_name,
{
"type": 'chat_message',
"message": "disconnecting"
}
)
async_to_sync(self.channel_layer.group_discard)(
self.room_name,
self.channel_name
)
self.close()
#Receive message from WebSocket
def receive(self, text_data):
text_data_json = json.loads(text_data)
if "task" in text_data_json and text_data_json['task'] == 'send_message_to_group':
send_message_to_group(text_data_json)
#Receive message from room group
def chat_message(self, event):
message = event['message']
#Send message to WebSocket
self.send_json(content={
'message': message
})
def send_message_to_group(json_data):
try:
channel_layer = get_channel_layer()
g_name = str(json_data['group_name'])
print(json_data)
async_to_sync(channel_layer.group_send)(
g_name,
{
'type': 'chat.message',
'message': json_data
}
)
except Exception as e:
print(str(e))
The async_to_sync(...) wrapper is required because SocketConsumer is a synchronous JsonWebsocketConsumer but it is calling an asynchronous channel layer method. (All channel layer methods are asynchronous.)
Group names are restricted to ASCII alphanumerics, hyphens, and periods only. Since this code constructs a group name directly from the room name, it will fail if the room name contains any characters that aren't valid in a group name so we have to replace / with _
Next we have to create a root routing configuration for Channels. A Channels routing configuration is an ASGI application that is similar to a Django URLconf, in that it tells Channels what code to run when an HTTP request is received by the Channels server.
from django.urls import path,re_path
from channels.routing import ProtocolTypeRouter, URLRouter
from connection.consumer import SocketConsumer
websockets = URLRouter([
re_path(
r'^(?P<user_type>.*)$', SocketConsumer
)
])
from channels.routing import ProtocolTypeRouter, URLRouter
from connection.routing import websockets
application = ProtocolTypeRouter({
"websocket": websockets,
})
WSGI_APPLICATION = 'dashboard.wsgi.application'
ASGI_APPLICATION = 'dashboard.routing.application' # new
A channel layer is a kind of a communication system, which allows multiple parts of our application to exchange messages, without shuttling all the messages or events through the database.
We need a channel layer to give consumers (which we'll implement in the next step) the ability to talk to one another.
While we could use use the InMemoryChannelLayer layer since we're in development mode, we'll use a production-ready layer, RedisChannelLayer.
(env)$ docker run -p 6379:6379 -d redis:5
This command downloads the image and spins up a Redis Docker container on port 6379.
If you don't want to use Docker, feel free to download Redis directly from the official website.Alternatively you can create redis server in various cloud application like aws heroku
(env)$ pip install channels_redis
#dashboard/settings.py
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
"hosts": [('127.0.0.1', 6379)],
},
},
}
Here, we let channels_redis know where the Redis server is located.
(env)$python manage.py runserver