Real-Time Communication Made Easy: How to Create Custom WebSockets-Based APIs with Django

Aditya Sharma
7 min readMay 14, 2023

Introduction to WebSockets

Remember that episode of Friends where Chandler and Joey were playing video games online and they kept getting interrupted by Ross and Rachel? If only they had a real-time chat app, they could have just talked to each other instead of yelling at each other across the apartment.

Before we dive into the world of Django and WebSockets, let’s first take a look at what WebSockets are and how they work.

WebSockets are a protocol that allows for bidirectional communication between a client and a server over a single, long-lived connection. This is in contrast to the traditional HTTP request-response model, which involves the client making a request to the server and the server responding with a single, static response.

To understand the difference between HTTP and WebSockets, let’s use an example from Friends. In one episode, Ross is trying to move a couch up a narrow staircase, and he’s having a difficult time getting it around a corner. He’s able to eventually get it around the corner by pivoting it, but he has to make multiple trips up and down the stairs to do so.

This is similar to the HTTP model. Each time the client (Ross) makes a request to the server (the couch), the server sends back a static response (the couch’s position). Ross has to keep making requests (trips up and down the stairs) to get the information he needs to move the couch.

In contrast, WebSockets are like having a friend (let’s call her Phoebe) who is standing at the top of the stairs and can see everything that’s happening. Ross can communicate with Phoebe in real-time, and she can give him updates on the couch’s position as he moves it. This makes the process much more efficient and effective.

Django and WebSockets

Now that we have a basic understanding of WebSockets, let’s take a look at how to use them in Django.

Django is a popular web framework for building web applications in Python. While Django doesn’t have built-in support for WebSockets, there are several third-party libraries available that make it easy to work with WebSockets in Django.

One such library is Channels, which provides a layer of abstraction over WebSockets and allows for real-time communication between clients and servers. Channels is built on top of the ASGI (Asynchronous Server Gateway Interface) specification, which is designed to handle asynchronous communication between clients and servers.

To continue our Friends analogy, Channels is like having Joey there to help Ross move the couch. Joey is an expert at moving furniture, and he knows all the best techniques for getting a couch up a narrow staircase. With Joey’s help, Ross is able to move the couch much more efficiently and effectively.

Creating a Chat Application with Django and WebSockets

Now that we understand the basics of Django and WebSockets, let’s create a simple chat application using Django and Channels.

In our chat application, users will be able to join a chat room and send messages in real-time to other users in the same chat room.

Setting Up Our Project

To get started, we’ll need to create a new Django project and install Channels:

$ django-admin startproject websocket_demo
$ cd websocket_demo
$ python -m venv venv
$ source venv/bin/activate
$ pip install channels

Creating the WebSocket Consumer

Next, we’ll create a new file called consumers.py in our project's root directory. This file will contain the code for our WebSocket consumer, which will handle incoming WebSocket connections and messages.

import json
from channels.generic.websocket import AsyncWebsocketConsumer

class ChatConsumer(AsyncWebsocketConsumer):
async def connect(self):
await self.accept()

async def disconnect(self, close_code):
pass

async def receive(self, text_data):
text_data_json = json.loads(text_data)
message = text_data_json['message']

await self.send(text_data=json.dumps({
'message': message
}))

Let’s break down what this code is doing:

  • We import the AsyncWebsocketConsumer class from the Channels library.
  • We create a new class called ChatConsumer, which inherits from AsyncWebsocketConsumer.
  • We define three methods: connect, disconnect, and receive. These methods handle WebSocket connections, disconnections, and incoming messages, respectively.
  • In the connect method, we accept the WebSocket connection.
  • In the disconnect method, we do nothing (for now).
  • In the receive method, we parse the incoming message as JSON, extract the message text, and then send the message back to the client.

Routing WebSocket Requests

Next, we need to define a URL route for our WebSocket consumer. We’ll create a new file called routing.py in our project's root directory:

from django.urls import re_path

from . import consumers

websocket_urlpatterns = [
re_path(r'ws/chat/$', consumers.ChatConsumer.as_asgi()),
]

This file defines a URL route for WebSocket requests to /ws/chat/. When a WebSocket request is made to this URL, it will be handled by our ChatConsumer.

Updating Django Settings

Finally, we need to update our Django settings to include Channels:

INSTALLED_APPS = [
# ...
'channels',
]

ASGI_APPLICATION = 'websocket_demo.routing.application'

We add 'channels' to our INSTALLED_APPS, and we specify the ASGI_APPLICATION as websocket_demo.routing.application.

Testing Our Application

Now that our application is set up, let’s test it out! First, we’ll start our Django development server:

$ python manage.py runserver

Next, we’ll open up a browser and navigate to http://localhost:8000/ws/chat/. This will initiate a WebSocket connection to our ChatConsumer.

We can now open up the browser console and execute the following JavaScript code to send a message to the server:

const chatSocket = new WebSocket('ws://localhost:8000/ws/chat/');

chatSocket.onopen = function() {
console.log('WebSocket connection established.');
const message = {
'message': 'Hello, world!'
};
chatSocket.send(JSON.stringify(message));
};

chatSocket.onmessage = function(event) {
const message = JSON.parse(event.data);
console.log('Received message:', message);
};

When we execute this code, we should see a message printed to the console that says “Received message: { message: ‘Hello, world!’ }”. This confirms that our WebSocket connection is working and that we’re able to send and receive messages in real-time!

Advanced Features of Channels

Channels is a powerful library that provides many advanced features for building real-time applications with WebSockets. In this section, we’ll explore some of the most useful features of Channels and how to use them in Django.

Group Communication

One of the most useful features of Channels is the ability to group WebSocket connections together. This allows you to send messages to multiple clients at once, rather than sending messages to each client individually.

To use group communication in Channels, you’ll first need to import the ChannelLayer object:

from channels.layers import get_channel_layer
channel_layer = get_channel_layer()

Once you have the ChannelLayer object, you can use the group_add, group_discard, and group_send methods to manage groups and send messages to group members. Here's an example of how to use these methods:

async def connect(self):
self.room_name = self.scope['url_route']['kwargs']['room_name']
self.room_group_name = 'chat_%s' % self.room_name

await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
)

await self.accept()

async def disconnect(self, close_code):
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name
)

async def receive(self, text_data):
text_data_json = json.loads(text_data)
message = text_data_json['message']

await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'chat_message',
'message': message
}
)

async def chat_message(self, event):
message = event['message']

await self.send(text_data=json.dumps({
'message': message
}))

In this example, we’re using group communication to create a chat room where multiple clients can send messages to each other. When a client connects to the WebSocket, we add their channel to a group that corresponds to the chat room they’ve joined. When a client sends a message, we use the group_send method to send the message to all clients in the chat room. Finally, we define a chat_message method that is called when a message is received by a group member, which sends the message back to the client.

Handling Binary Data

By default, Channels only handles text data in WebSocket messages. However, you can also send binary data, such as images or audio, by using the bytes type instead of str.

To send binary data, you’ll need to use the bytes method instead of json.dumps to serialize the data. Here's an example of how to send a binary image:

import base64

async def receive(self, text_data):
bytes_data = base64.b64decode(text_data)
# handle binary data here

In this example, we’re decoding a base64-encoded string into binary data, which we can then handle as needed.

Background Tasks

Sometimes, you’ll need to perform long-running or asynchronous tasks in response to a WebSocket message. For example, you might need to process a large dataset, send an email, or make an API request.

Channels provides a way to run background tasks using async_to_sync and SyncConsumer. Here's an example of how to run a background task in response to a WebSocket message:

from asgiref.sync import async_to_sync
from channels.generic.sync import SyncConsumer

class TaskConsumer(SyncConsumer):
def task(self, message):
# long-running or asynchronous task

return 'Task complete!'

async def receive(self, text_data):
message = json.loads(text_data)
task_type = message['task_type']
task_args = message['task_args']

response = await async_to_sync(TaskConsumer().task)(task_args)

await self.send(text_data=json.dumps({
'response': response
}))

In this example, we’re defining a TaskConsumer that runs a long-running or asynchronous task when the task method is called. We're then using async_to_sync to run the task method in a background thread.

In the receive method, we're parsing a message that includes the type and arguments of the task to run. We're then using async_to_sync to call the TaskConsumer with the specified arguments and return the result. Finally, we're sending the result back to the client using the WebSocket connection.

Conclusion

In this article, we’ve explored the world of WebSockets and how to use them in Django to create real-time communication between clients and servers. We’ve used the Friends TV show as a fun analogy to explain the basics of WebSockets and Channels, and we’ve created a simple chat application to demonstrate how to use WebSockets in Django.

With WebSockets and Django, the possibilities for real-time communication and interaction in applications are endless. Whether you’re building a chat app, a collaborative editing platform, or a real-time dashboard, WebSockets provide a powerful and efficient way to handle real-time data transfer between clients and servers.

I hope this article has been informative and helpful in getting you started with WebSockets in Django. For further reading, we recommend checking out the official Channels documentation and the Django documentation on asynchronous views . And if you’re a fan of Friends, we encourage you to go back and re-watch some of the classic episodes to see how real-time communication plays a role in the lives of Ross, Rachel, Chandler, Monica, Joey, and Phoebe!

--

--

Aditya Sharma

#Tech is what I talk about - Just a Tech Enthusiast