What is Django Channels?

Introduction

Chat applications everywhere!

If you, like me, have done any amount of research to answer that question, you have discovered: you can build a chat application. There are 100's, if not more, tutorials out there that will help you build the next Slack. There are not a lot of tutorials, blogs, documentation or other resources out there to help you do much else with Django Channels.

I would like to discuss some of my discoveries into working with Django Channels and see if I can help explain some of the functionality of its parts, the methods and Classes it and some of its dependent libraries provide. And do so outside the scope of just another chat application.

If you haven't spent any time with Django Channels, though, by all means, build a chat application. The Django Channels documentation has one and there are many other tutorials much like it out there. That's a good place to start and that's where I got started. This is not one of those tutorials.

What is Django Channels?

Django Channels extends the built-in capabilities of Django beyond just HTTP by taking advantage of the "spiritual successor" to WSGI (Web Server Gateway Interface), ASGI (Asynchronous Server Gateway Interface). Where WSGI provided a synchronous standard for Python web applications, ASGI provides both synchronous and asynchronous standards.

That's Django channels in a nutshell. I will provide a link below, so you can read more about Django Channels, ASGI and WSGI. If you have not built a chat application using Django Channels, then I recommend doing so; because, this article presupposes that you have and, thus, you already have some working knowledge of the terminology and references: Django Channels Chat Tutorial

Beyond Chat Applications

After you've built a couple of chat applications, you will likely want to build something else. At Lofty Labs, we build a lot of dashboard applications for our clients to view and interact with their data in a meaningful way. Another tool for us to add to our arsenal: web sockets. Django Channels is the logical choice since we rely heavily on Django already.

I encountered a few obstacles going outside the scope of a chat application, where we really just want to push changes to state to multiple browsers, on a user triggered event, without reloading the page. What about triggering a web socket message based on a server side event?

Declaring Channel Layers

In order to trigger a message based on, say, an HTTP POST request. We need to access the channel layer.

Cool.

What's that?

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

Remember this guy, from the chat tutorial? This is where we define our channel layers, here we are just declaring a default layer that uses Redis to store our web socket messages. We can use Redis for this; because, we're not wanting to persist these messages indefinitely, but we do need a place to read and write to those messages. That is our channel layer. It is possible to setup more than one channel layer, but for now a default channel layer is all we need.

But how do we access that layer outside of a Django Channels Consumer?

Well, Django Channels provides a method to access a channel layer outside of a Consumer.

from channels.layers import get_channel_layer

Awesome! I can just call that from a rest_framework APIView, then, right? Well, I could not figure out how to get that to work with a standard Django Channels SyncConsumer. I tried a lot of things based on Django Channels Documentation, Django Channels' source code and Stack Overflow, but nothing was effective. And the super annoying thing about it was that it was silent.

No errors.

No new web socket messages reached the browser.

Awesome.

AsyncJsonWebsocketConsumer

After banging my head against that wall for hours, I swapped out my SyncConsumer--I was not trying to go fast and I did not think I would need anything fancier than a SyncConsumer just for a proof of concept. Once, I switched to an AsyncJsonWebsocketConsumer, I started making some progress.

class IndexConsumer(AsyncJsonWebsocketConsumer):
   async def connect(self):
       await self.accept()
       await self.channel_layer.group_add('index', self.channel_name)

   async def disconnect(self, code):
       print('disconnected')
       await self.channel_layer.group_discard('index', self.channel_name)
       print(f'removed {self.channel_name}')

   async def websocket_receive(self, message):
       return super().websocket_receive(message)

   async def websocket_ingest(self, event):
       await self.send_json(event)

There are some things happening in that consumer that are worth mentioning. For one thing, we have to be more explicit with our methods on this consumer.

In the connect method on the IndexConsumer class, after we explicitly accept new connections, we also add that connection to a new group, 'index'. This can be named anything you want. The general idea is as new connections occur, as, perhaps, more browsers are connecting to the server, they all have access to the same messages in this channel layer that are organized into this group.

Notice how our custom methods are prepended with websocket_. That is a reference to the route we declared in the channel router.

application = ProtocolTypeRouter({
   'websocket': AuthMiddlewareStack(URLRouter([
       path('index/', IndexConsumer)
   ]))
})

That seems to be the conventional naming method for handlers and how the router routes messages to their intended handlers.

So, now, coming full circle. How do we trigger/send an event outside of our declared consumer?

Accessing Channel Layers

I mentioned a method that is included with Django Channels to access the channel layer. Let us take a look at the method in action.

from rest_framework.views import APIView
from rest_framework.response import Response
from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync


def random_data():
   return [random.choice([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) for i in range(7)]


class IngestView(APIView):
   def post(self, request):
       channel_layer = get_channel_layer()
       text = random_data()

       async_to_sync(channel_layer.group_send)(
           'index', {
               'type': 'websocket.ingest',
               'text': text
               }
       )
       return Response([])

   def get(self, request):
       return Response([])

IngestView is wired up just as you would expect in urls.py, so there's nothing new there. I have written a simple function that will return a list of random integers whose values range from 1-10. We'll use that to simulate a week's worth of time series data.

We can use get_channel_layer from inside of a rest_framework APIView, but in order to do so we must use another magic function that is included with the asgiref library: async_to_sync. You can get a better understanding of the asgiref library in Django github repo: asgiref The asgiref.async_to_sync method allows us to interact with our asynchronous consumer inside of a synchronous rest_framework view. According to the asgiref source code on github: "Utility class which turns an awaitable that only works on the thread with the event loop into a synchronous callable that works in a subthread." That's from a docstring in the class that async_to_sync uses to declare an instance of the AsyncToSync class.

So, we declare a channel_layer object, passing in no arguments--we are just using the default channel layer we declared in our settings file, and passing that to the async_to_sync utility function. Now, the syntax gets a little weird. I do not know about you, but I have not seen something like this in Python. It looks like a Javascript IIFE (Immediately Invoked Function Expression). After looking at the source code, that seems to be what it is, perhaps, by taking advantage of overriding the __call__ method for the AsyncToSync class.

I am pointing this out; because, I wrestled with getting my POST method to trigger anything at this point and it was simply do to a couple of syntax errors.

Success!

I was holding my breathe at this point, I simply wanted to wire up an APIView, access it from the browsable API that comes with rest_framework, and be able to see a change in the UI without reloading the page. And then it WORKED! I have not been this happy to click a button in a webpage since my first AJAX call. Of course, that instant was also made possible by Vue. Vue is great for Django and I reach for it over React; because, it is possible to use a Vue object inside of a Django template without having to setup a separate Javascript project for the frontend--I would probably do so, but this is just a proof of concept. So, a quick look at the frontend code:

       var LineChart = Vue.component('line-chart', {
         extends: VueChartJs.Line,
         props: ['dataSet'],
         watch: {
           dataSet() {
             this.loadData()
           }
         },        
         mounted () {
           this.loadData()
         },
         methods: {
           loadData() {
             var self = this;
               this.renderChart({
                 labels: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'],
                 datasets: [{
                   label: 'Weekly Data',
                   backgroundColor: 'rgb(255, 99, 132)',
                   borderColor: 'rgb(255, 99, 132)',
                   data: self.dataSet
                 }]
             })
           }
         }    
       })      

       var app = new Vue({
         el: '#app',
         components: {LineChart},
         data: {
           message: 'Hello!',
           dataSet: [6, 3, 10, 2, 10, 1, 10],
           connected: false,
         },
         mounted() {
             console.log()
             var socket = new WebSocket(
               'ws://' + window.location.host +
               '/index/')

             socket.onopen = event => {
                 console.log('connected')
                 this.connected = true
                 socket.send({})
             }

             socket.onmessage = event => {
               json_data = JSON.parse(event.data)
               this.dataSet = json_data.text
               console.log(json_data.text)
             }

             socket.onclose = event => {
               this.connected = false
             }

           }          
       })

I am using a prebuilt Vue component that integrates nicely with Chart.js. In the main Vue object, I am setting up the client side web sockets inside of the mounted method. This allows us to update the DOM (Document Object Model) as data comes through a web socket. In this case we can re render the chart based on the new dataset that comes through and do so when it comes through without having to reload the entire page.

Conclusion

So, there we go! I hope that we have a better understanding of what we can accomplish with Django Channels. At the very least, I hope that we have an understanding of how we can trigger Web Socket messages from server side events. I look at Django Channels as, in some cases, an upgrade from the already useful, django.contrib.messages library. Flash messages are a great way to slide in a status message, like, the status of an HTML form submission when the page reloads, but the fact that flash messages are dependent on a page reload can be a pretty big limitation for today's responsive web applications.

Links

Django

Django Channels

ASGI

WebSockets

asgiref

Vue

Vue Chart.js

Chart.js

More from Lofty