Together with my wife, we run a small digital agency. We use Django as a primary web development framework and love simplicity.

In this post, I will guide you on how to enable WebSockets in your Django application without installing third-party apps.

Django has introduced the ASGI interface since version 3.0 and async views in 3.1. Our solution will be based on async views. In this tutorial, we will use Python 3.7 and Django 3.1.

Introduction into WebSockets ASGI interface

So, how does WebSocket work in this context? Let us find it!

The communication between WebSocket clients and your application is event-based. The ASGI specification defines two types of events: send and receive.

Receive events. These are events that clients send to your application. Let's look at them:

  1. websocket.connect is sent when the client tries to establish a connection with our application
  2. websocket.receive is sent when the client sends data to our app
  3. websocket.disconnect tells us that the client has disconnected.

Send events are emitted by our application to a client (e.g. a browser). Here is a list of them:

  1. websocket.accept — we send this event back to the client if we want to allow the connection
  2. websocket.send — with this event, we push data to the client
  3. websocket.close is emitted by the application when we want to abort the connection.

Now, as we know all participants of that party, it is the time to speak about their order.

When a browser opens a connection, the ASGI protocol server (we will talk about this later) sends us websocket.connect event. Our application must respond to it with either websocket.accept or websocket.close according to our logic. It is simple: emit websocket.accept if you allow the connection or emit websocket.close to cancel the connection. You may want to cancel the connection, for example, if the user has no permissions to connect or is not logged in. I will assume that you allow the connection in the next steps.

After you accepted the connection, the application is ready to send and receive data via that socket using websocket.send and websocket.receive events.

Finally, when the browser leaves your page or refreshes it, a websocket.disconnect is sent to the application. As a developer, you still have control over the connection and can abort the connection by sending websocket.close event at any time you wish.

This was a brief description of how ASGI processes WebSockets. It is not scoped to Django, it works for any other ASGI compatible web framework like Starlette or FastAPI.

Setting up Django apps

First, we have to create a new Django application. This app will keep the custom URL pattern function, ASGI middleware, and WebSocket connection class.

Let's make a new app using this command:

django-admin startapp websocket

Okay, now let's make a new little helper function for developer convenience. This function will be a simple alias for path function at the moment.

Add urls.py to the websocket app with this content:

from django.urls import pathwebsocket = path

Now you can configure WebSocket URLs in a distinct way. Time to create your first WebSocket view! To keep things simple and reusable we will make another Django app named, say, `users`. Don’t forget to enable both applications in the INSTALLED_APPS setting!

django-admin startapp users

Implementing ASGI middleware

Create a new websocket/middleware.py file and put the code in it:

websocket/middleware.py

Every ASGI middleware is a callable that accepts another callable. In the middleware, we test if the request type is websocket , and if so, we call Django’s URL resolver for a dispatchable view function. By the way, a 404 error will be raised if the resolver fails to find a view matching the URL.

Now, open project_name/asgi.py file and wrap default application with this middleware:

from django.core.asgi import get_asgi_application
from websocket.middleware import websockets
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project_name.settings")django.setup()
application = get_asgi_application()
application = websockets(application)

Since that moment, every request made will be caught by our middleware and tested for its type. If the type is websocket then the middleware will try to resolve and call a view function.

Don’t mind at the moment about missing import from the .connection module. We are about to make it in a minute.

Add WebSocket connection

Create websocket/connection.py with the contents from the gist below. To make life easier we will also enumerate all possible WebSocket events in classes, add Headers class to access request headers, and QueryParams to get variables from a query string.

Add your first WebSocket view

# users/views.pyfrom django.views.generic.base import TemplateViewclass IndexView(TemplateView):
template_name = "index.html"
async def websocket_view(socket):
await socket.accept()
await socket.send_text('hello')
await socket.close()

Mount both views in the root urls.py

# project_name/urls.pyfrom django.urls import path
from websocket.urls import websocket
from users import views
urlpatterns = [
path("", views.IndexView.as_view()),
websocket("ws/", views.websocket_view),
]

users/templates/index.html should contain this script:

<script>
new WebSocket('ws://localhost:8000/ws/');
</script>

This is a bare minimum to establish a WebSocket connection.

Start the development server

pip install uvicorn

Once installed start the server passing ASGI application as the first positional argument:

uvicorn project_name.asgi:application --reload --debug

Navigate to http://localhost:8000/test/, open browser’s console, switch to Network tab and observe the WebSockets working.

Image for post
Image for post
The network tab demonstrates “hello” message sent by server.

Echo server

Replace websocket_view in users/views.py with this code:

async def websocket_view(socket: WebSocket):
await socket.accept()
while True:
message = await socket.receive_text()
await socket.send_text(message)

and replace contents of users/templates/index.html with this:

<script>
let socket = new WebSocket('ws://localhost:8000/ws/');
let timer = null;
socket.onopen = () => {
timer = setInterval(() => {
socket.send('hello');
}, 1000);
};
socket.onclose = socket.onerror = () => {
clearInterval(timer);
};
</script>

The updated code will send hello text to our application every one second and our application will respond to it with the same message.

Image for post
Image for post

Conclusion

Written by

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store