WebSockets-Based APIs with Django(Real-Time communication made easy)
WebSockets?
WebSocket is a bidirectional communication protocol that can send the data from the client to the server or from the server to the client by reusing the established connection channel.
How websockets 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.
They are commonly used to build interactive and dynamic web applications. Here’s how WebSockets work:
- Handshake:
The WebSocket connection begins with a handshake. This is a standard HTTP request made by the client to the server, requesting an upgrade to the WebSocket protocol. The client sends an HTTP request that includes an “Upgrade” header to request the upgrade.
2. Server Acceptance:
The server, upon receiving the client’s handshake request, checks if it supports the WebSocket protocol. If it does, the server responds with an HTTP 101 (Switching Protocols) status code and accepts the upgrade request. The server includes the “Upgrade” and “Connection” headers in the response to indicate the protocol switch.
3. Persistent Connection:
Once the handshake is successful, both the client and the server switch to the WebSocket protocol. Unlike traditional HTTP requests, WebSocket connections are persistent, meaning they remain open as long as both parties want to communicate.
4. Full-Duplex Communication:
With a WebSocket connection established, both the client and server can send data to each other at any time. This enables full-duplex communication, where data can be sent and received simultaneously without the need for repeated HTTP requests.
5. Data Framing:
Data sent over a WebSocket connection is divided into small frames for efficient transmission. Each frame can carry text or binary data. The frames contain information about the data type, length, and other control bits.
6. Data Encoding:
WebSockets support text and binary data transmission. Text data is encoded using UTF-8, and binary data is sent as raw binary. This flexibility allows WebSockets to handle various types of information, including JSON, XML, and images.
7. Event-Driven:
WebSockets are event-driven, which means they use a callback mechanism to handle messages and events. Both the client and the server can register event listeners to respond to incoming data or events.
8. Security:
WebSocket connections can use the same security mechanisms as standard HTTP connections, such as TLS/SSL encryption. This ensures that data transmitted over WebSocket connections remains secure.
9. Close Connection:
Either party can initiate the closing of a WebSocket connection. The initiating party sends a close frame, and the other party responds with its own close frame. This graceful closure allows resources to be released properly.
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.
Creating a Chat Application with Django and WebSockets
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 fromAsyncWebsocketConsumer
. - We define three methods:
connect
,disconnect
, andreceive
. 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 blog, we’ve explored the world of WebSockets and how to use them in Django to create real-time communication between clients and servers.
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 .
References
- https://blog.heroku.com/in_deep_with_django_channels_the_future_of_real_time_apps_in_django
- https://www.geeksforgeeks.org/what-is-web-socket-and-how-it-is-different-from-the-http/#:~:text=WebSocket%20is%20a%20bidirectional%20communication,reusing%20the%20established%20connection%20channel.
- https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API
That’s it, I guess.
Cheers, Happy Coding!!!
🙏🙏🙏
Since you’ve made it this far, sharing this article on your favorite social media network would be highly appreciated. For feedback, please ping me on Twitter.