DEV Community

Cover image for How to Add Websockets to a Django App without Extra Dependencies
Jayden Windle
Jayden Windle

Posted on • Edited on

How to Add Websockets to a Django App without Extra Dependencies

Note: This post was originally published on my blog.

Now that Django 3.0 ships with ASGI support out of the box, adding Websockets to your Django app requires no extra dependencies. In this post, you'll learn how to handle Websockets with Django by extending the default ASGI application. We'll go over how to handle Websocket connections, send and receive data, and implement the business logic in a sample ASGI application.

Getting started

To start, you'll need Python >= 3.6 installed on your machine. Django 3.0 is only compatible with Python 3.6 and up because it makes use of the async and await keywords. Once you've got your Python version setup, create a project directory and cd into it. Then, install Django inside of a virtualenv and create a new Django app in your project directory:

$ mkdir django_websockets && cd django_websockets
$ python -m venv venv
$ source venv/bin/activate
$ pip install django
$ django-admin startproject websocket_app .
Enter fullscreen mode Exit fullscreen mode

Take a look in the websocket_app directory of your Django app. You should see a file called asgi.py. Its contents will look something like this:

import os

from django.core.asgi import get_asgi_application

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

application = get_asgi_application()
Enter fullscreen mode Exit fullscreen mode

This file provides the default Django ASGI setup, and exposes an ASGI application called application which can be run using an ASGI server such as uvicorn or daphne. Before we go much further, let's take a look at how ASGI applications are structured.

ASGI app structure

ASGI, or the Asynchronous Server Gateway Interface, is a specification for building asynchronous web services with Python. It's the spiritual successor to WSGI, which has been used by frameworks like Django and Flask for a long time. ASGI lets you use Python's native async/await functionality to build web services that support long-lived connections, such as Websockets and Server Sent Events.

An ASGI application is a single async function which takes in 3 parameters: scope (the context of the current request), receive (an async function that lets you listen for incoming events), and send (an async function that lets you send events to the client).

Inside of an ASGI application, you can route requests based on values in the scope dictionary. For example, you can check whether the request is an HTTP request or a Websocket request by checking the value of scope['type']. To listen for data from the client, you can await the receive function. When you're ready to send data to the client, you can await the send function, and pass in any data you want to send to the client. Let's take a look at how this works in a sample application.

Creating an ASGI app

In our asgi.py file, we're going to wrap Django's default ASGI application function with our own ASGI application in order to handle Websocket connections ourselves. To do this, we'll need to define an async function called application, that takes in the 3 ASGI parameters: scope, receive, and send. Rename the result of the get_asgi_application call to django_application, because we'll need it process HTTP requests. Inside of our application function we'll check the value of scope['type'] to determine the request type. If the request type is 'http', then the request is a normal HTTP request and we should let Django handle it. If the request type is 'websocket', then we'll want to handle the logic ourselves. The resulting asgi.py file should look something like this:

import os

from django.core.asgi import get_asgi_application

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

django_application = get_asgi_application()

async def application(scope, receive, send):
    if scope['type'] == 'http':
        # Let Django handle HTTP requests
        await django_application(scope, receive, send)
    elif scope['type'] == 'websocket':
        # We'll handle Websocket connections here
        pass
    else:
        raise NotImplementedError(f"Unknown scope type {scope['type']}")
Enter fullscreen mode Exit fullscreen mode

Now we need to create a function to handle websocket connections. Create a file called websocket.py in the same folder as your asgi.py file, and define an ASGI application function called websocket_application that takes in the 3 ASGI parameters. Next, we'll import websocket_application in our asgi.py file, and call it inside of our application function to handle Websocket requests, passing in the scope, receive, and send parameters. It should look something like this:

# asgi.py
import os

from django.core.asgi import get_asgi_application
from websocket_app.websocket import websocket_application

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

django_application = get_asgi_application()

async def application(scope, receive, send):
    if scope['type'] == 'http':
        await django_application(scope, receive, send)
    elif scope['type'] == 'websocket':
        await websocket_application(scope, receive, send)
    else:
        raise NotImplementedError(f"Unknown scope type {scope['type']}")

# websocket.py
async def websocket_applciation(scope, receive, send):
    pass
Enter fullscreen mode Exit fullscreen mode

Next, let's implement some logic for our websocket application. We're going to listen for all websocket connections, and when the client sends the string "ping", we'll respond with the string "pong!".

Inside of the websocket_application function, we're going to define an indefinite loop that will handle Websocket requests until the connection is closed. Inside that loop, we'll wait for any new events that the server receives from the client. Then we'll act on the contents of the event, and send the response to the client.

To start, let's handle connections. When a new Websocket client connects to the server, we'll receive a 'websocket.connect' event. In order to allow this connection, we'll send a 'websocket.accept' event in response. This will complete the Websocket handshake and establish a persistent connection with the client.

We'll also need to handle disconnection events when a client terminates their connection to the server. To do that, we'll listen for a 'websocket.disconnect' event. When a client disconnects, we'll break out of our indefinite loop.

Finally, we need to handle requests from the client. To do that, we'll listen for a 'websocket.receive' event. When we receive a 'websocket.receive' event from the client, we'll check and see if the value of event['text'] is 'ping'. If it is, we'll send a 'websocket.send' event, with a text value of 'pong!'

After setting up the Websocket logic, our websocket.py file should look something like this:

# websocket.py
async def websocket_applciation(scope, receive, send):
    while True:
        event = await receive()

        if event['type'] == 'websocket.connect':
            await send({
                'type': 'websocket.accept'
            })

        if event['type'] == 'websocket.disconnect':
            break

        if event['type'] == 'websocket.receive':
            if event['text'] == 'ping':
                await send({
                    'type': 'websocket.send',
                    'text': 'pong!'
                })
Enter fullscreen mode Exit fullscreen mode

Testing it out

Now our ASGI application is set up to handle Websocket connections and we've implemented our Websocket server logic, let's test it out. Right now, the Django development server doesn't use the asgi.py file, so you won't be able to test your connections using ./manage.py runserver. Instead, you'll need to run the app with an ASGI server such as uvicorn. Let's install it:

$ pip install uvicorn
Enter fullscreen mode Exit fullscreen mode

Once uvicorn is installed, we can run our ASGI application using the following command:

$ uvicorn websocket_app.asgi:application
INFO:     Started server process [25557]
INFO:     Waiting for application startup.
INFO:     ASGI 'lifespan' protocol appears unsupported.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
Enter fullscreen mode Exit fullscreen mode

To test the Websocket connection, open up your browser's development tools in a new tab. In the console, create a new Websocket instance called ws pointed to ws://localhost:8000/. Then attach an onmessage handler to ws that logs event.data to the console. Finally, call ws.send('ping') to send the message to the server. You should see the value "pong!" logged to the console.

> ws = new WebSocket('ws://localhost:8000/')
  WebSocket {url: "ws://localhost:8000/", readyState: 0, bufferedAmount: 0, onopen: null, onerror: null,ย โ€ฆ}
> ws.onmessage = event => console.log(event.data)
  event => console.log(event.data)
> ws.send("ping")
  undefined
  pong!
Enter fullscreen mode Exit fullscreen mode

Congrats! Now you know how to add Websocket support to your Django application using ASGI. Go build something awesome with it ๐Ÿ˜Ž

๐Ÿ‘‹ Hi, I'm Jayden. I love building apps and teaching others how to build apps. For more posts about building apps with Django, React, and GraphQL, follow me on Twitter or subscribe to my newsletter at jaydenwindle.com.

Top comments (15)

Collapse
 
mehdimortaz profile image
Parsezan

Thank you for this post. I have a question, how possible send a message to WebSocket from Django views? by above I just can do something just when a client sends me a message.

Collapse
 
lautaronavarro profile image
Lautaro Navarro

I have an implementation for this, maybe you would like to take a look at it!

Basically, the queue can be change by any other type of queue which allows you to exchange data between routines and threads (Each requests to django_application are new threads, you can check that, requests to websocket_application share the same thread)

Collapse
 
htrampe profile image
Holger Trampe • Edited

Did someone found a good solution? The above information is great, but i can do something like that with ajax and json-response. A way from view to the client-websockets is much more interesting. THX!

Collapse
 
lautaronavarro profile image
Lautaro Navarro

That's an excellent question, and I'd like to know the answer, let me know if you find out a way

Collapse
 
piexpie profile image
Piyush • Edited

Hi, I am encountering errrors when creating a websocket connection,

ws = new WebSocket('ws://localhost:8000/')
WebSocketย {url: "ws://localhost:8000/", readyState: 0, bufferedAmount: 0, onopen: null, onerror: null,ย โ€ฆ}
VM50:1 WebSocket connection to 'ws://localhost:8000/' failed: Error during WebSocket handshake: Unexpected response code: 400
Enter fullscreen mode Exit fullscreen mode

And the uvicorn request says: "WARNING: Unsupported upgrade request."

How do I fix this issue?

Thanks.

And also this

ws = new WebSocket('wss://localhost:8000/')
WebSocketย {url: "wss://localhost:8000/", readyState: 0, bufferedAmount: 0, onopen: null, onerror: null,ย โ€ฆ}
ws = new WebSocket('ws://localhost:8000/')
WebSocketย {url: "ws://localhost:8000/", readyState: 0, bufferedAmount: 0, onopen: null, onerror: null,ย โ€ฆ}
ws.onmessage = event => console.log(event.data)
event => console.log(event.data)
ws.send("ping")
VM92:1 Uncaught DOMException: Failed to execute 'send' on 'WebSocket': Still in CONNECTING state.
    at <anonymous>:1:4
(anonymous) @ VM92:1
ws.send("ping")
VM97:1 Uncaught DOMException: Failed to execute 'send' on 'WebSocket': Still in CONNECTING state.
    at <anonymous>:1:4
(anonymous) @ VM97:1
VM75:1 WebSocket connection to 'wss://localhost:8000/' failed: WebSocket opening handshake timed out
(anonymous) @ VM75:1
VM82:1 WebSocket connection to 'ws://localhost:8000/' failed: Error during WebSocket handshake: Unexpected response code: 400
Enter fullscreen mode Exit fullscreen mode
Collapse
 
arjhe profile image
ARJhe • Edited

pip install uvicorn
did not deal with WebSocket configuration.

# h11_impl.py L:91
self.ws_protocol_class = config.ws_protocol_class
Enter fullscreen mode Exit fullscreen mode

config.ws_protocol_class is None
so in L:264 you will get a warning.

# h11_impl.py L:264
        if upgrade_value != b"websocket" or self.ws_protocol_class is None:
            msg = "Unsupported upgrade request."
            self.logger.warning(msg)
Enter fullscreen mode Exit fullscreen mode

try: pip install uvicorn[standard]
See ref

Collapse
 
jesuszerpa profile image
Jesรบs Abraham Zerpa Maldonado • Edited

Thanks for your post, i have a question, how i can use with nginx, i am trying deploy my project and i have the next case, the event['type'] is http.request i need websocket.connect, i belive is by my nginx configuration but i am not sure, my configuration is

location /ws/ {
                try_files $uri @proxy_to_app;
        }
location / {
           #include proxy_params;
           #proxy_pass https://unix:/run/gunicorn.sock;
           proxy_pass http://0.0.0.0:8000;

           #websocket support
           proxy_http_version 1.1;
           proxy_set_header Upgrade $http_upgrade;
           #proxy_set_header Connection "upgrade";
           proxy_set_header Host $host;
           #proxy_redirect off;
#proxy_set_header X-Real-IP $remote_addr;
           proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
           #proxy_set_header X-Forwarded-Host $server_name;

           #timeout
           proxy_read_timeout 18000;
           proxy_connect_timeout 18000;
           proxy_send_timeout 18000;
           send_timeout 18000;
           #try_files $uri @proxy_to_app;
        }

location @proxy_to_app {

           #proxy_pass http://channels-backend;
           proxy_pass http://0.0.0.0:8000;
           proxy_http_version 1.1;
           proxy_set_header Upgrade $http_upgrade;
           proxy_set_header Connection "upgrade";

           #proxy_redirect off;
           proxy_set_header Host $host;
           #proxy_set_header X-Real-IP $remote_addr;
  #proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
           #proxy_set_header X-Forwarded-Host $server_name;
    }
Enter fullscreen mode Exit fullscreen mode
Collapse
 
lautaronavarro profile image
Lautaro Navarro

Hi Jayden!
First of all I'd like to thank you for this amazing post!
I've been following this post step by step and I'm getting an error when I'm trying to start up de Uvicorn server, which I guess it's related to a breaking change on a dependency.
The error is the following
"module 'websockets' has no attribute 'WebSocketServerProtocol'"
Do you know which is the path to continue?

Collapse
 
lautaronavarro profile image
Lautaro Navarro

I realized that running it from the same directory that the asgi.py file it works, so it looks like I'm doing something wrong with the path :/
Running "uvicorn asgi:application" from the asgi file directory works
Running "uvicorn websockets.asgi:application" from the project's directory does not work (my project is called websockets)

Collapse
 
lautaronavarro profile image
Lautaro Navarro

Well I was able to find the problem, your project must not be called websockets.

Collapse
 
rehmatworks profile image
Rehmat Alam

Thank you so much for sharing this great piece of knowledge. I've been struggling with Websockets and now I feel that I've found a path. And I'm really glad to see this path through Django that I love a lot.

Collapse
 
valentinogagliardi profile image
Valentino Gagliardi • Edited

Thanks for this writeup Jayden. However, I'm wondering if this the right approach, and how maintainable is it. I feel I'd wait for things to settle a bit instead of reinventing the wheel. See github.com/django/deps/blob/master...

Collapse
 
curt114 profile image
curt114

This is a fantastic article. Thank you. How would you use a django database call with this? For example, User.objects.get( pk=1 )?

Collapse
 
pystar profile image
Pystar

This tutorial doesnt work for me. I am getting this traceback:

"TypeError: asgi_send() missing 1 required positional argument: 'message'"

Collapse
 
rohitba12937568 profile image
Rohit Bansal

Hi Jayden!
How can we use same websocket for multiple clients so that multiple users could chat to each other?