What is Django Channels?

Introduction

Chat apps are everywhere!

If, like me, you've done any research to answer this question, you've found that you can build a chat app. There are 100 if not more guides out there to help you build your next Slack. There aren't many tutorials, blogs, documentation, or other resources to help you do much more with Django Channels.

I'd like to share some of my findings with Django Channels and see if I can help explain some of the functionality of its parts, the methods and classes that it and some of its dependent libraries provide. And do it outside of a simple chat app.

If you haven't spent time with Django Channels yet, then by all means create a chat app. There is one such application in the Django Channels documentation, and there are many other similar tutorials. It's a good place to start, and that's where I started. This is not one of those tutorials.

What are Django Channels?

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

In a nutshell, these are Django channels. I will link below so you can read more about Django Channels, ASGI and WSGI. If you haven't created a chat app using Django Channels yet, then I recommend doing so; because this article assumes you've done it, and therefore you already have some working knowledge of terminology and references: Django Channels Chat Tutorial

Outside chat apps

After you've built a few chat apps, you'll likely want to create something else. At Lofty Labs, we create many dashboard apps to enable our clients to view and interact with their data in a meaningful way. Another tool we are adding to our arsenal: web sockets. Django Channels is a logical choice as we already rely heavily on Django.

I've run into a few hurdles outside of the chat application where we really want to just pass state changes to multiple browsers on a user event without reloading the page. How about triggering a websocket message based on a server side event?

Declaring channel layers

To trigger a message based on, say, an HTTP POST request. We need to access the link layer.

What is this?


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

Remember that guy from the chat tutorial? Here we define channel layers, here we just declare the default layer that Redis uses to store websocket messages. We can use Redis for this because we don't want to store these messages indefinitely, but we need a place to read and write these messages. This is our channel layer. It is possible to configure more than one link layer, but for now we only need the default link layer.

But how do we access this layer outside of the Django Channels Consumer?

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


from channels.layers import get_channel_layer

Wonderful! Then I can just call this from the rest_framework APIView, right? But I haven't been able to figure out how to get this to work with the default Django Channels SyncConsumer. I've tried many ways based on the Django Channels documentation, Django Channels source code, and Stack Overflow, but nothing worked. And the most annoying thing was that it was silent.

No errors.

No new web socket message has reached the browser.

AsyncJsonWebsocketConsumer

After banging my head against this wall for a few hours, I replaced my SyncConsumer - I wasn't looking to be fast and didn't think I'd need anything more complex than a SyncConsumer just to test the concept. Once I switched to AsyncJsonWebsocketConsumer I started to make 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 going on in this consumer that are worth mentioning. First, we need to be more explicit with our methods on this consumer.

In the connect method of the IndexConsumer class, after we explicitly accept new connections, we also add that connection to a new group, 'index'. It can be named anything. The general idea is that as new connections come in, as perhaps more browsers connect to the server, they all get access to the same messages in that channel layer, which are organized into that group.

Note that our custom methods are padded with the websocket_ symbol. This is a link to the route we declared in the channel router.


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

This appears to be the usual method of naming handlers and how the router directs messages to their intended handlers.

So, let's go in a circle. How can we trigger/dispatch an event outside of our declared consumer?

Apply channel layers

I mentioned the method that comes with Django Channels to access the channel layer. Let's see this 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([])

The

IngestView is connected as you would expect in urls.py, so there is nothing new there. I wrote a simple function that will return a list of random integers whose values ​​range from 1 to 10. We will use it to simulate a weekly time series of data.

We can use get_channel_layer from within the rest_framework APIView, but to do this we must use another magic function that is included in 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 async consumer inside a synchronous rest_framework views. According to the asgiref source code on github: "A utility class that turns an awaitable that runs only on an event loop thread into a synchronous callable that runs on a subthread." This is from the docstring in the class that async_to_sync uses to declare an instance of the AsyncToSync class.

So we declare the channel_layer object without passing any arguments - we just use the default channel layer we declared in our settings file and pass it to the async_to_sync utility function. Now the syntax is getting a little weird. I don't know about you, but I haven't seen anything like this in Python. It is similar to Javascript IIFE (Immediately Invoked Function Expression). After looking at the source code, this appears to be the case, perhaps due to the benefit of overriding the __call__ method for the AsyncToSync class.

I'm pointing this out because I was struggling with my POST method calling anything at this point, and it was just a couple of syntax errors.

Success!

I'm holding my breath at this point, I just wanted to attach an APIView, access it from the viewable API that comes with rest_framework, and be able to see changes in the UI without reloading the page. And then it worked! I haven't been this happy to click on a button on a web page since my first AJAX call. Of course, this moment is made possible by Vue. Vue is great for Django and I prefer it to React; because it's possible to use a Vue object inside a Django template without having to create a separate Javascript project for the frontend - I'd probably do that, but it's 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'm using a prebuilt Vue component that integrates well with Chart.js. In the main Vue object, I'm setting up client-side web sockets inside the mounted method. This allows us to update the DOM (Document Object Model) as data arrives via the web socket. In this case, we can redraw the chart based on the new dataset that comes through the socket, and do it when it arrives, without reloading the whole page.

Conclusion

So, that's it! I hope we have a better understanding of what we can achieve with Django Channels. At least I hope we have an understanding of how we can call Web Socket messages from server side events. I see Django Channels as, in some cases, an improvement on an already useful library django.contrib.messages . Flash messages are a great way to insert a status message, such as the submission status of an HTML form on page reload, but the fact that flash messages depend on page reload can be quite a limitation for modern responsive web applications

Back to Top