Django : WebSockets and Channels

Sarthak Kumar
5 min readSep 24, 2019

--

WebSockets is a technology that allows for opening an interactive communications session between a user’s browser and a server. With this technology, a user can send messages to a server and receive event-driven responses without requiring long-polling, i.e. without having to constantly check the server for a reply. Think about when you are replying to an email in Gmail, and at the bottom of your screen you see an alert pop up saying “1 unread message from [insert some email address here]” coming from the person you were just responding to. That kind of real-time feedback is due to technologies like WebSockets!

Why WebSockets ?

Websockets allow a long-held single TCP (transmission control protocol) socket connection to be established between the client and server, allowing for bi-directional, full duplex, messages to be instantly distributed. This is done with minimal overhead resulting in a low latency connection.

The real-world applications for WebSockets are endless, including chatting apps, internet of things, online multiplayer games, and really just any real-time application.

What is Channels ?

Channels is a project that takes Django and extends its abilities beyond HTTP — to handle WebSockets, chat protocols, IoT protocols, and more. It’s built on a Python specification called ASGI.

It does this by taking the core of Django and layering a fully asynchronous layer underneath, running Django itself in a synchronous mode but handling connections and sockets asynchronously, and giving you the choice to write in either style.

How To Integrate Channels ?

In this blog i will be showing an example of a case when you might need to constantly send data from the server to the client.

Setup

  • First create and activate the virtual environment, i’d recommend using Pipenv.
  • Install Django using pipenv install django in your virtualenv.
  • Install Channels and channels-redis in your virtualenv like:
    - pipenv install channels -pipenv install channels-redis

If you have a modern Django project layout like:

- my_proj/
- manage.py
- game/
- my_proj/
- __init__.py
- settings.py
- urls.py

STEP 1:

File : my_proj/my_proj/settings.py

# Add Channels to installed AppsINSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'game',
'channels',
.
.
.
.
# Channels settings
ASGI_APPLICATION = "my_proj.routing.application"
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
"hosts": [CHANNEL_REDIS_HOST],
"symmetric_encryption_keys": [SECRET_KEY],
},
},
}

STEP 2:

Channels provides you with Consumers, a rich abstraction that allows you to make ASGI applications easily. Consumers do a couple of things in particular: Structures your code as a series of functions to be called whenever an event happens, rather than making you write an event loop.

A consumer is a class whose methods you may choose to write either as normal Python functions (synchronous) or as awaitables (asynchronous). Asynchronous code should not mix with synchronous code. So there are conversion functions to convert from async to sync and back. Remember that the Django parts are synchronous. A consumer in fact a valid ASGI application.

So, the next step would be to make the my_proj/game/consumer.py file.

# Built in imports.
import json
# Third Party imports.
from channels.exceptions import DenyConnection
from channels.generic.websocket import AsyncWebsocketConsumer
# Django imports.
from django.core.exceptions import ObjectDoesNotExist
from django.contrib.auth.models import AnonymousUser
# Local imports.
from my_proj.game.models import Game
from my_proj.game.utils import get_live_score_for_game
class LiveScoreConsumer(AsyncWebsocketConsumer):async def connect(self):
self.room_name = self.scope['url_route']['kwargs']['game_id']
self.room_group_name = f'Game_{self.room_name}'
if self.scope['user'] == AnonymousUser():
raise DenyConnection("Invalid User")
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
)
# If invalid game id then deny the connection.
try:
self.game = Game.objects.get(pk=self.room_name)
except ObjectDoesNotExist:
raise DenyConnection("Invalid Game Id")
await self.accept()
async def receive(self, text_data):
game_city = json.loads(text_data).get('game_city')
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'live_score',
'game_id': self.room_name,
'game_city': game_city
}
)
async def live_score(self, event):
city = event['game_city']
# Here helper function fetches live score from DB.
await self.send(text_data=json.dumps({
'score': get_live_score_for_game(self.game, city)
}))
async def websocket_disconnect(self, message):
# Leave room group
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name
)

STEP 3:

Next step is to setup the routers for the consumer, the route we set in the routing.py files is what is used by frontend to actually access the consumer.

Once a WebSocket connection is established, a browser can send or receive messages. A sent message reaches the Protocol type router that determines the next routing handler based on its transport protocol. Hence you can define a router for HTTP and another for WebSocket messages.

Path :~ my_proj/game/routing.py

from django.urls import pathfrom channels.routing import ProtocolTypeRouter, URLRouterfrom .consumers import LiveScoreConsumerwebsockets = URLRouter([
path(
"ws/live-score/<int:game_id>", LiveScoreConsumer,
name="live-score",
),
])

Path :~ my_proj/routing.py

from channels.routing import ProtocolTypeRouter, URLRouter
from my_proj.game.routing import websockets
application = ProtocolTypeRouter({
"websocket": websockets,
})

Step 4:

You can also add middlewares to your sockets for Authorization checks or other cases.

Path :~ my_proj/game/middlewares.py

# Third party imports
from urllib.parse import urlparse, parse_qs
from channels.auth import AuthMiddlewareStack
# DRF imports.
from rest_framework.authtoken.models import Token
from rest_framework.exceptions import AuthenticationFailed
# Django imports.
from django.contrib.auth.models import AnonymousUser
from django.db import close_old_connections
class TokenAuthMiddleware:
"""
Token authorization middleware for channels
"""
def __init__(self, inner):
self.inner = inner
def __call__(self, scope):
query_string = scope['query_string']
if query_string:
try:
parsed_query = parse_qs(query_string)
token_key = parsed_query[b'token'][0].decode()
token_name = 'token'
if token_name == 'token':
user, _ = # Your Authentication Code
scope['user'] = user
close_old_connections()
except AuthenticationFailed:
scope['user'] = AnonymousUser()
else:
scope['user'] = AnonymousUser()
return self.inner(scope)
def TokenAuthMiddlewareStack(inner): return TokenAuthMiddleware(
AuthMiddlewareStack(inner))

If you apply this middleware then your my_proj/routing.py file would be :~

from channels.routing import ProtocolTypeRouter, URLRouter
from my_proj.game.routing import websockets
from my_proj.game.middlewares import TokenAuthMiddlewareStackapplication = ProtocolTypeRouter({
"websocket": TokenAuthMiddlewareStack(websockets),
})

Testing

Now that you have successfully integrated Channels to your project, if you want to test it then you can just start the server and open the Javascript console by going to your chrome browser and selecting inspect.

lo = new WebSocket("ws://localhost:8000/ws/live-score/1?token=[YOUR TOKEN]");
lo.onmessage = (data) => console.log(data);
lo.onopen = () => {
console.log("sending city");
lo.send(JSON.stringify({"game_city": 1}));
}

Note :- Since in the above example we are calling a helper function to send the live score, once the connection has been established the frontend will need to send the request again everytime it want’s the live score at that moment.

--

--

Sarthak Kumar

I’m a Software Engineer(Backend) who blogs sometimes and loves to learn and try new tools & technologies. My corner of the internet : https://sarthakkumar.xyz