Plug Websockets into Djangos ASGI-Consumers

This is a recap of an POC how to plug websockets into a django backend and allow communication between the consumer instances using channels. There will not a lot of documentation found here, please go to the Django docs if you’re interested into details

Installation

Assumptions:

  • (Django >= 3) is installed and a project is created.
  • channels is installed.
  • For testing websocket connections node-ws is installed (apt install node-ws)

Note: For the sake of simplicity no app is created, everything just lives in the project-app.

# asgi.py
import os
from channels.routing import ProtocolTypeRouter
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'poc_channels_websockets_redis.settings')

application = ProtocolTypeRouter({
    "http": get_asgi_application(),
    # Just HTTP for now. (We can add other protocols later.)
})
# settings.py
...
INSTALLED_APPS = [ 
    ...
    'channels',
]
ASGI_APPLICATION = 'myapp.asgi.application'
CHANNEL_LAYERS = {
    "default": {
        # for local testing we can just use the in unsecure in memory channel
        "BACKEND": "channels.layers.InMemoryChannelLayer"  
    }
}

Ping Pong Consumer

# .consumers.py

from channels.generic.websocket import AsyncWebsocketConsumer

from uuid import uuid4

class PingConsumer(AsyncWebsocketConsumer):
    groups = ["pingpong"]

    async def connect(self):
        await self.accept()

    async def receive(self, text_data=None, bytes_data=None):
        await self.send(text_data="Pong")

Register the consumer

# asgi.py
...
from .consumers import PingConsumer

application = ProtocolTypeRouter({
    "http": get_asgi_application(),
    "websocket": URLRouter([
        url("ping/", PingConsumer.as_asgi()),
    ])
})

Testing the connection

wscat -c "ws://localhost:8000/ping/"
# Connected (press CTRL+C to quit)
> [Enter]
# < Pong

Django does respond to the websocket call. The websockets are integrated into Djangos asgi-server using the consumer above. Yay!

Ping Pong Consumer broadcasting Ping Events

For the communication between the consumers, we can send data through the channel layers.

# .consumers

from channels.generic.websocket import AsyncWebsocketConsumer

from uuid import uuid4

class PingConsumer(AsyncWebsocketConsumer):
    groups = ["pingpong"]  

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # each consumer has it's own ident
        # We use that to identify the scope on the broadcast
        self.ident = str(uuid4())  

    async def connect(self):
        await self.accept()

    async def receive(self, text_data=None, bytes_data=None):
        # sending down the channel
        await self.channel_layer.group_send(self.groups[0], {
            # maps to method ping_requested
            'type': 'ping.requested',  
            # Injecting the callers id
            'consumer_id': self.ident  
        })
        await self.send(text_data="Pong") 

    
    async def ping_requested(self, event):
        # The method awaits channel calls with type `ping.requested` 
        
        # send string data to the websocket client
        await self.send(text_data=f"Pong requested during {event=}")

Testing

 wscat -c "ws://localhost:8000/ping/"                                   git:master* 
Connected (press CTRL+C to quit)
>
< Pong
< Pong requested during event={'type': 'ping.requested', 'consumer_id': '62422280-9752-485a-90d7-0c9c5339a4c2'}    
> %  
# a second client calls ping in the background
< Pong requested during event={'type': 'ping.requested', 'consumer_id': 'd4605a01-40c1-42f5-ac1a-a7f3c0c57984'}   

The websocket sessions successfully communicating between each another. The system Works!