Loading

Django Channels PyStPete

Dairon Medina Caro

This is a live streamed presentation. You will automatically follow the presenter and see the slide they're currently on.

PyStPete

Django Channels

Who i am?

Dairon Medina

@codeadict

www.dairon.org

 

Developer with Python and other daemons at

Django, HTTP 1 & WSGI

Based on a request/response model, was not evolving with the modern web.

Traditional Django Architecture

Presenting Django Channels

 

 

 

 

 

 

Mozilla Foundation helped with $150,000 in funds  on December 2015.

Announced as an official Django project on September 2016, will be merged into the Django core, hopefully soon.

https://www.djangoproject.com/weblog/2016/sep/09/channels-adopted-official-django-project/
https://github.com/django/channels

What is Channels?

Channels adds a new layer to Django, allowing some important improvements for real time web and message passing. Can be used for:

  • Web Sockets
  • Distributed tasks(Celery replacement?)
  • HTTP2 Server push
  • Can be extended to other protocols or event types like WebRTC, SMS...

Are there other Django websocket solutions?

 

  • Yes, there are some.
  • Nope, nothing is standard or plays well with the framework. 

 

 

  • AutobahnPython - WebSocket & WAMP for Python on Twisted and asyncio.
  • Crossbar - Open-source Unified Application Router (Websocket & WAMP for Python on Autobahn).
  • django-socketio - WebSockets for Django.
  • WebSocket-for-Python - WebSocket client and server library for Python 2 and 3 as well as PyPy.

Looks like sexi stuff. How this works?

​Components:

  • Channel Backend: Responsible for messages transport, can be in-memory, redis, POSIX IPC(shared memory segments).
  • ASGI Asynchronous Gateway  Interface Server: Allows handling multiple protocols(HTTP, HTTP2, Websockets). WSGI compatible but decouples normal HTTP from other protocols, in draft spec probably will be sent as a PEP sometime.
  • Workers.

Concepts:

  • Channel: It's data structure that acts like a FIFO queue with message expiration and guarantees at most once delivery. Lots of producers can write to the same channel.
  • Group: Its a named set containing channels used to broadcast messages  to multiple clients. Groups have add, remove and send methods.
  • Messages: Represents HTTP or Websocket messages that are interpreted by ASGI. HTTP messages are rerouted to normal HTTP request/response lifecycle and Websocket ones are send to consumers using routes. Messages are JSON serializable or you can get fancy and use some sort of smaller binary protocol.

Lets get hands into the code

Adding channels to our django project

$ pip install channels

Then add channels to INSTALLED_APPS on your project settings.py

$ ./manage.py runserver

Channels replaces the typical WSGI interface with ASGI

Still works for local development, no need for extra worker or scary setups. On production you will need to run it using a ASGI server like Daphne and run the websockets on different workers using: 

$ ./manage.py runworker
# Define channel layers
CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'asgi_redis.RedisChannelLayer',
        'CONFIG': {
            'hosts': [os.environ.get('REDIS_URL', 'redis://localhost:6379')],
        },
        'ROUTING': 'core.urls.channel_routing',
    },
}

Setting up CHANNELS_LAYERS

Defines channels storage backend and how to route messages to it.

from channels import route

from .consumers import (
    ws_connect, ws_receive, ws_disconnect
)

websocket_routing = [
    route('websocket.connect', ws_connect),
    route('websocket.receive', ws_receive),
    route('websocket.disconnect', ws_disconnect),
]

Routing: pretty much like Django urls

When a message comes through a channel it does pattern matching on route to send the message to a consumer function.

route("websocket.connect", connect_light, path=r'^/lights/(?P<slug>[^/]+)/stream/$')

Consumer(consumers.py): similar to Django views, is where the messages are routed to be processed.

Can be class-based:

class LightsConsumer(WebsocketConsumer):

    # Set to True to automatically port users from HTTP cookies
    http_user = True

    # Set to True if you want it, else leave it out
    strict_ordering = False

    def connection_groups(self, **kwargs):
        """
        Add this connection to the Group lights.
        """
        return ["lights.receive"]

    def connect(self, message, **kwargs):
        """
        Do things on connection start
        """
        # Accept the connection
        self.message.reply_channel.send({"accept": True})

    def receive(self, text=None, **kwargs):
        """
        Called when a message is received with either text or bytes
        filled out.
        """
        payload = json.loads(text)
        self.send(text=payload)

    def disconnect(self, message, **kwargs):
        """
        Perform things on connection close
        """
        pass

Or functions:

import json

from channels import Channel, Group


def ws_connect(message):
    message.reply_channel.send({'accept': True})
    Group('lights').add(message.reply_channel)


def ws_receive(message):
    payload = json.loads(message['text'])
    payload['reply_channel'] = message.content['reply_channel']
    Channel('lights.receive').send(payload)


def ws_disconnect(message):
    Group('lights').discard(message.reply_channel)

Publishing can be made from any part of your Django application, e.g: model save, signals or a view. It can be also sent by a remote app using the ASGI protocol and sharing the same channels layer and routes.

    def send_status_update(self):
        """
        Sends a status updates to subscribed channels.
        """
        command = {
            "light_id": self.id,
            "status": self.status,
            "created": self.created.strftime("%a %d %b %Y %H:%M"),
        }
        Group('lights').send({
            # WebSocket text frame, with JSON content
            "text": json.dumps(command),
        })

    def save(self, *args, **kwargs):
        """
        Hooking send_notification into the save of the object.
        """
        result = super().save(*args, **kwargs)
        self.send_status_update()
        return result

Next write some views and Javascript to update the DOM in real time.


        $(function () {
            var ws_scheme = window.location.protocol == 'https:' ? 'wss' : 'ws';
            var ws_path = ws_scheme + '://' + window.location.host + '/lights/stream/';
            var socket = new ReconnectingWebSocket(ws_path);
            // Handle incoming messages
            socket.onmessage = function (message) {
                // Decode the JSON
                var data = JSON.parse(message.data);
                if (data.error) {
                    alert(data.error);
                    return;
                }

                if (data.light_id == {{ object.id }}) {
                    if (data.status === 1) {
                        $('body').removeClass('lightbulb--is-on').addClass('lightbulb--is-on');
                    } else {
                        $('body').removeClass('lightbulb--is-on');
                    }
                }
            };
        });

Everything is so easy, just like your normal Django HTTP code. Thinking asynchronous is very hard, write normal sync code, and forget about threads, greenlets, asyncio or third parties. Still can use asyncio if you want :)

Demo Project: LIGHT BULBS

  • User open a WebSocket when they access the page.
  • The WebSocket is added to a group based on the URL.
  • When the LightBulb.status changes we send a message to that group over WebSockets to turn on or off the light.
  • The Javascript side updates the DOM simulating light on or off.

https://github.com/codeadict/ChannelsLightsControl

Muchas Gracias!

Made with Slides.com