Please wait...

How to implement WebSocket in Django using Channels and Stream WebSocket data

by in Django

Django Websocket

WebSocket is a technology that makes the two-way connection between a client browser and a server where a client can send and receive messages from the server without waiting for each other responses. There are many ways to create WebSocket.

WebSocket in different languages/frameworks/libraries As you can find all here 

Today, I'll give you an example using the Django framework and the Django Channel library.

Let's start by creating WebSocket in the Django framework

Installation:

First, install the libraries in your virtual environment.

pip install Django-channels 
pip install psutil         # this library retrieving information on running processes and system utilization (CPU, RAM, etc.)
pip install channels-redis # Django Channel Layer that uses Redis as Backend.

add channels app in INSTALLED_APPS of settings.py file

Create Django Project  

Note: Before create project, You should aware about Python virtual environment and activate it. follow here

We need to first create a Django project then need to create an app in a project that will handle WebSocket related configuration and connection.

I'm creating a project name sample using command

django-admin.py startproject sample

then creating an app system using command

python manage.py startapp system

add system app in INSTALLED_APPS of settings.py file

I create a Django project structure like the below image.

Django Directory Structure

I created this app system because later I'll make a WebSocket that will show the graph of CPU and RAM uses of your machine.

then set up Redis in your system, Redis is useful for creating layers in Django channels that will provide a way to send messages through channels. read here about installation in your system.

Now update settings.py file project after installation of Redis

settings.py

CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels_redis.core.RedisChannelLayer",
        "CONFIG": {
            "hosts": [("127.0.0.1", 6379)],
        },
    },
}

Here we configure channels_redis.core.RedisChannelLayer  for store Channel Layer in this.

Now add Group Name for Django channels that will use default group for send and receive the message in Django project

Add below line in settings.py

STREAM_SOCKET_GROUP_NAME = 'system_detail' # you can define name according to your requirements
ASGI_APPLICATION = 'sample.asgi.application'

ASGI_APPLICATION need to connect ASGI handler on runserver so this is required to update in settings.py file

Now update consumers.py file in the system app

consumers.py

import json

from channels.generic.websocket import AsyncWebsocketConsumer
from django.conf import settings


class SystemConsumer(AsyncWebsocketConsumer):
    group_name = settings.STREAM_SOCKET_GROUP_NAME

    async def connect(self):
        # Joining group
        await self.channel_layer.group_add(
            self.group_name,
            self.channel_name
        )
        await self.accept()

    async def disconnect(self, close_code):
        # Leave group
        await self.channel_layer.group_discard(
            self.group_name,
            self.channel_name
        )

    async def receive(self, text_data):
        # Receive data from WebSocket
        text_data_json = json.loads(text_data)
        message = text_data_json['message']
        print(message)
        # Print message that receive from Websocket

        # Send data to group
        await self.channel_layer.group_send(
            self.group_name,
            {
                'type': 'system_load',
                'data': {
                    'cpu_percent': 0,  # initial value for cpu and ram set to 0
                    'ram_percent': 0
                }
            }
        )

    async def system_load(self, event):
        # Receive data from group
        await self.send(text_data=json.dumps(event['data']))

Here we define a Class SystemConsumer that inherit AsyncWebsocketConsumer

AsyncWebsocketConsumer and async/await,  We use class and Asynchronous I/O, because Django Channel Library has functions send(), group_send(), group_add() that is async functions, so we are using these functions in

method of our class so we Inherit AsyncWebsocketConsumer class and async/await I/O.

1 Connect method call from a client and when client first time makes the connection with Websocket.

2. Disconnect method will call client close the tab and maybe client disconnect website using some event from browser.

3. Receive method call when client sends some message to the server through WebSocket.

4. System Load method is an event method that calls when group_send method of channel layer uses type as "system_load" method name.

5. System load method sends data to client and the client uses that data to represent on browser according to requirements.

6. According to the above code, When the client send a message to the server using WebSocket then Websocket return system default uses of RAM and CPU as 0

Now we will update the routing.py file 

routing.py

from django.urls import re_path

from . import consumers

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

Here consumers.SystemConsumer calling consumer.py Class and creating a WebSocket URL "ws/system/"

We need to connect the Routing with the Asgi file that will recognize WebSocket URL and connect according to the protocol type.

Let update the asgi.py file that explains the connectivity of simple HTTP and WebSocket connections.

asgi.py

import os

from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application

from system import routing

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'sample.settings')

application = ProtocolTypeRouter({
    "http": get_asgi_application(),     # For Http Connection
    "websocket": AuthMiddlewareStack(   # For Websocket Connection
        URLRouter(
            routing.websocket_urlpatterns
        )
    ),
})

In this asgi.py we define HTTP and WebSocket protocol,  ProtocolTypeRouter connect HTTP connect to AsgiHandler, and Websocket handled by Django channels

So here we mostly finish Django channel integration and created WebSocket that will connect to the client.

Now let me create an HTML file that will show, How we can connect to the socket using Javascript.

In the below HTML I'm using JustGage Js library that is useful for representing Gauges meter easily, I used to show CPU and RAM uses on HTML

System.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8"/>
    <title>System Monitor</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/raphael/2.1.0/raphael-min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/justgage/1.1.0/justgage.js"></script>
    <style>
        .column {
            float: left;
            width: 50%;
        }

        .row {
            margin: 0 -5px;
        }

        body, .row {
            height: 100vh;
        }

        .row {
            display: flex;
            align-items: center;
            justify-content: center;
        }

        .gage {
            width: 45%;
            height: 200px;
        }

    </style>
</head>
<body>
<div>
    <div class="row" style="position: relative">
        <div class="column">
            <div id="cpu" class="gage" style="float: right"></div>
        </div>
        <div class="column">
            <div id="ram" class="gage"></div>
        </div>
    </div>
</div>

<script>
    var cpu = new JustGage({
        id: "cpu",
        value: 0,
        min: 0,
        max: 100,
        decimals: 1,
        title: "CPU Load %"
    });
    var ram = new JustGage({
        id: "ram",
        value: 0,
        min: 0,
        max: 100,
        decimals: 1,
        title: "RAM Load %"
    });
    const systemSocket = new WebSocket(`ws://${window.location.host}/ws/system/`);
    systemSocket.onopen = function (e) {
        systemSocket.send(JSON.stringify({"message": 'Sending message to server'}));
    };
    systemSocket.onmessage = function (e) {
        const data = JSON.parse(e.data);
        cpu.refresh(data.cpu_percent);
        ram.refresh(data.ram_percent);
    };
    systemSocket.onclose = function (e) {
        console.error('Chat socket closed');
    };

</script>
</body>
</html>

Here WebSocket is the default Class in Javascript and it provides some useful method for send, receive messages, and open, close WebSocket 

We need to render this HTML page using Django views so we will update urls.py and views that will render the above HTML code.

system/urls.py 

from django.urls import path

from . import views

urlpatterns = [
    path('', views.index, name='index'),
]

sample/urls.py

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('system/', include('system.urls')),
]

views.py 

from django.shortcuts import render


def index(request):
    return render(request, 'system.html')

Now we will run the command runserver of Django

 python manage.py runserver

Output after Runserver command
 

Django Runserver Output
 

As you can see in the output of runserver command, these showing ASGI/Channel starting on runserver command not WSGI

Starting ASGI/Channels version 3.0.3 development server at http://127.0.0.1:8000/

Now open URL 127.0.0.1:8000/system/  and then the output of the above HTML will look like as below when we load this HTML
Output of CPU and RAM uses

 

But after when HTML WebSocket class execute then as you can check runserver logs as below, WebSocket

HTTP GET /system/ 200 [0.08, 127.0.0.1:59616]
WebSocket HANDSHAKING /ws/system/ [127.0.0.1:59617]
WebSocket CONNECT /ws/system/ [127.0.0.1:59617]
Sending message to server

WebSocket HANDSHAKING: The client requested to connect with WebSocket and after on Server when Server accepts the connection then it shows WebSocket CONNECT means now both are connected with WebSocket and can send, receive data anytime before disconnect.

In HTML, systemSocket send JSON data with stringify to the server, and the server receives this message on receive method

systemSocket.send(JSON.stringify({"message": 'Sending message to server'}));

After receive we connect that text data into JSON format printing message that the client sends to the server.

async def receive(self, text_data):
    # Receive data from WebSocket
    text_data_json = json.loads(text_data)
    message = text_data_json['message']
    print(message)
    # Print message that receive from Websocket


Now we send the default 0 value of CPU and RAM to show the graph initial in the meter at 0 points.

await self.channel_layer.group_send(
            self.group_name,
            {
                'type': 'system_load',
                'data': {
                    'cpu_percent': 0,  # initial value for cpu and ram set to 0
                    'ram_percent': 0
                }
            }
        )

The above method call system_load method and pass the event as an argument in the system_load method, System load method send data to the client for representation on the browser.

Here now we have a problem this will happened only once, the Only server send data to WebSocket when client message to Server for retrieve System but we are passing default 0 value on receive

So How we can provide Stream data to the client without request to the server?

For providing continuous stream data to the client we need to create a management command that we execute using cron job from system every 1 minute.
Read docs about management command if want to learn more https://docs.djangoproject.com/en/3.1/howto/custom-management-commands/
Here you can management command structure in the above image, so we need to follow this default structure of Django to make our custom command.

Now we will update stream_socket.py file.

stream_socket.py

from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
from django.conf import settings
from django.core.management import BaseCommand
import psutil


class Command(BaseCommand):
    help = 'Command to start stream socket data if any socket open'

    def handle(self, *args, **options):
        group_name = settings.STREAM_SOCKET_GROUP_NAME
        channel_layer = get_channel_layer()
        cpu_percent = psutil.cpu_percent()
        ram_percent = psutil.virtual_memory().percent
        async_to_sync(channel_layer.group_send)(
            group_name,
            {
                'type': 'system_load',
                'data': {
                    'cpu_percent': cpu_percent,
                    'ram_percent': ram_percent
                }
            }
        )

In this file we used psutil library for the retrieve system uses the percent of CPU and RAM.

Here one this, async_to_sync this function we use because group_send method in the async method so async_to_sync this function convert async to sync method.

in group_send we passed same group_name and type "system_load" for call consumer.py system_load method and pass current CPU and RAM uses percent in data key.

this data will receive in system_load method of consumer.py file and it'll send to client to show CPU and RAM percent uses.

Now we need to execute this file as a Django command 

python manage.py stream_socket

when we execute this command it'll update our gauges meter with current uses on our browser 

so the output will look like

CPU and RAM uses

Now we need to stream this data on the browser continuous so we need to execute cron job in our system,

cron job works in Mac OS and Linux operating system, Windows have scheduling so we can use that for run stream_socket command

run command as below

crontab -e

if it asks you to choose default editor, choose whatever you're comfortable 

and write code as below

* * * * * <path of python executable file from virtualenv> <path to project manage.py> stream_socket

# Example
* * * * * venv/bin/python /home/ubuntu/sample/sample/manage.py stream_socket

here 5 star means every minute it'll execute stream_socket command and send CPU and RAM uses to the client browser.

WebSocket data uses stream

Now we can see stream output of CPU and RAM uses using cron job with execution of custom management command.