Django

Websockets with Django Channels – From start to deployment

With all the AI buzz ongoing, you don’t care. You just wanna create your Django Channels websockets enabled endpoint, and do some real-time communication with your frontend application.

Don’t worry, I needed to include ‘AI’ to boost this article’s rankings (assuming that even works). Blame the game, not the players! 🤣

In this article, you’ll see how to

  • setup basic channels use for websockets
  • deploy that behind an nginx proxy

You’re ready?

Let’s start

The steps outlined in this article relates to. If you’re reading this in the future, things may have changed, so take note (by future, I mean 3 days after writing this artcle)

  • Django 5.0.1
  • Channels 4
  • Daphne 4

I’m not gonna tell you what Django or Channels are, or how to install them.

Well, to be on the same page, here’s how to install the channels: pip install 'channels[daphne]' This installs channels with the daphne protocol server for

Daphne is a HTTP, HTTP2 and WebSocket protocol server for ASGI and ASGI-HTTP, developed to power Django Channels.

Add daphne to installed apps

INSTALLED_APPS = (
    "daphne",
    "django.contrib.auth",

    ...,

    "channels"
)

Routing

Django has its routing, and with channels, you wanna also add your route at which incoming socket requests will hit and be processed accordingly. To do that, in your project directory (where the settings.py file is), create the asgi.py file if it doesn’t exist

# asgi.py
import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'your_project.settings')

import django
django.setup()

from django.core.asgi import get_asgi_application

from channels.routing import ProtocolTypeRouter, URLRouter

from channels.security.websocket import AllowedHostsOriginValidator
from api.socketmiddleware import TokenAuthMiddleware

from django.urls import path

websocket_urlpatterns = [
    path('ws/chat/', consumers.MyConsumer.as_asgi()),
]

application = ProtocolTypeRouter({
    "http": get_asgi_application(),
    "websocket": AllowedHostsOriginValidator(
        URLRouter(
            websocket_urlpatterns
        )
     )
})

The above code is English. It’s saying, if a request comes in and it’s of the type http, it should proceed the usual path of handling http requests via the get_asgi_application()

Otherwise, if websocket, then go the websocket path.

In the same directory as the settings.py, create a consumers.py file and include this in it

# consumers.py

import json
from channels.generic.websocket import AsyncWebsocketConsumer

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

    async def disconnect(self, close_code):
        pass

    async def receive(self, text_data):
        req = json.loads(text_data)
        print(req)
        await self.send(text_data=json.dumps({'message': 'Hello world!'}), close=True)

The last step is to replace the wsgi with asgi like so in the Django settings.py

# WSGI_APPLICATION = 'your_project.wsgi.application'
ASGI_APPLICATION = "your_project.asgi.application"

Run your Django application, and hopefully (without me holding my breath), all should work well.

Now, how do you connect via a frontend? Let’s try that with Angular.

Connect the socket

In Angular, with a service, it can look like this:

// websocket.service.ts

import { Injectable } from '@angular/core';
import { webSocket, WebSocketSubject } from 'rxjs/webSocket';


@Injectable({
  providedIn: 'root',
})
export class WebSocketService {
  private socket$: WebSocketSubject<any>;

  constructor(private auth: AuthService) {

    this.socket$ = webSocket({
      // use wss for https support
      'url': `ws://your-endpoint/ws/chat/`
    });
  }

  sendMessage(message: string): void {
    this.socket$.next({ message });
  }

  getMessage() {
    return this.socket$.asObservable();
  }
}

Then in your component (maybe chat.component.ts, do this:

import { Component } from '@angular/core';
import { Subscription } from 'rxjs';
import { WebSocketService } from '../../services/websocket.service';

@Component({
    selector: 'app-chat',
    templateUrl: './chat.component.html',
    styleUrls: ['./chat.component.scss']
})
export class ChatComponent {
    private subscription!: Subscription;

    constructor(
        private webSocketService: WebSocketService,
    ) { }

    sendMessage(message: string): void {
        this.webSocketService.sendMessage(message)

        this.subscription = this.webSocketService.getMessage().subscribe({
            next: (res: any) => {
                console.log(res);
            },
        });
    }
}

You should get a json response of { "message": "Hello world!" }

With that response in, your communication with the websocket is working fine.

Let’s deploy it!

Deployment

Create a systemd service like so, maybe call the file myproject.daphne.service

[Unit]
Description=daphne daemon for my project
After=network.target

[Service]
WorkingDirectory=/home/username/apps/your_project
ExecStart=/home/username/apps/your_project/venv/bin/daphne -b 0.0.0.0 -p 8000 your_project.asgi:application

[Install]
WantedBy=multi-user.target

Enable, and run the service

systemctl daemon-reload
systemctl enable myproject.daphne.service
systemctl status myproject.daphne

If all works well, you should see:

● myproject.daphne.service - daphne daemon for edu backend
     Loaded: loaded (/etc/systemd/system/myproject.daphne.service; enabled; vendor preset: enabled)
     Active: active (running) since Mon 2024-01-22 16:14:41 UTC; 2 days ago
   Main PID: 2669502 (daphne)
      Tasks: 5 (limit: 1101)
     Memory: 13.0M
        CPU: 1min 44.389s
     CGroup: /user.slice/user-1000.slice/user@1000.service/app.slice/myproject.daphne.service
             └─2669502 /home/username/apps/your_project/venv/bin/python3 /home/username/apps/your_project/venv/bin/daphne -b 0.0.0.0 -p 8000 your_project.asgi:application

Jan 23 18:31:48 ubuntu-verifine daphne[2669502]: Not Found: /

With the Django running daphne up and running, let’s proxy Nginx requests to it accordingly

Remember that, Daphne is a combined server software, capable of handling both normal HTTP requests, and that of websockets.

You do NOT need to proxy to two separate endpoints. In our example above, BOTH websockets and HTTP requests will all go to the SAME port of 8000, running daphne

upstream django-backend {
    server 127.0.0.1:8000;
}

server {
    server_name my.domain;
    client_max_body_size 10m;
    root /home/username/apps/your_project;

    error_log /var/log/nginx/mydomain_error.log;
    access_log /var/log/nginx/mydomain_access.log;

    location /media/  {
        alias /home/username/apps/your_project/media/;
    }

    location /static/ {
        alias /home/username/apps/your_project/static/;
    }

    location / {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;

        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header X-Real-IP $remote_addr;

        proxy_http_version 1.1;
        proxy_connect_timeout 1000s;
        proxy_read_timeout 1000s;
        proxy_send_timeout 1000s;
        proxy_set_header Connection "";
        proxy_redirect off;
        proxy_pass http://django-backend;
    }

    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/my.domain/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/my.domain/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}

server {
    if ($host = my.domain) {
        return 301 https://$host$request_uri;
    } # managed by Certbot

    server_name my.domain;
    listen 80;
    return 404; # managed by Certbot
}

const takeNote = “Remember, as long as you're using Nginx proxy that has HTTPS, your request to the websocket MUST be using the wss://... prefix, NOT ws://"

I repeat, takeNote

Conclusion

Lemme know in the comments what you think. Hope to catch you in the next one.

Related Articles

Back to top button