Django Channels

From Code Self Study Wiki
Jump to: navigation, search

This page contains notes about Django Channels.

Types of Channels

  • normal channels
  • response channels. They have an exclamation point in the names, like websocket.send!otGzrJjU.

Anatomy of Channels

Message

  • content -- see the ASGI spec
  • channel -- Channel object
  • reply_channel -- Channel object or None
  • channel_layer ChannelLayer object
# Anatomy of a channels message
{
    'reply_channel': < channels.channel.Channel object at 0x7f118630e898 >,
    'content': {
        'path': '/chat/',
        'query_string': '',
        'headers': [
            (b 'accept-encoding', b 'gzip,deflate,lzma,sdch'),
            (b 'cookie', b 'csrftoken=JIh2Gxdd62DsBDCk7A0gHZZZB2UvVcIJDblNyJgmHeBwEkWxMt7JbyoIEF4Dv5D8; sessionid=yi6rySnQi53qv2b1rnn6aykmlhh77ehu'),
            (b 'accept', b 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8'),
            (b 'pragma', b 'no-cache'),
            (b 'upgrade', b 'websocket'),
            (b 'sec-websocket-key', b 'mX4tQzRNKmitRWNMHYL7uw=='),
            (b 'sec-websocket-extensions', b 'permessage-deflate'),
            (b 'host', b '127.0.0.1:8000'),
            (b 'accept-language', b 'en-US,en;q=0.8'),
            (b 'user-agent', b 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36 OPR/38.0.2220.41'),
            (b 'origin', b 'http://127.0.0.1:8000'),
            (b 'connection', b 'keep-alive, Upgrade'),
            (b 'dnt', b '1'), (b 'sec-websocket-version', b '13'),
            (b 'cache-control', b 'no-cache'),
        ],
        'client': ['127.0.0.1', 46522],
        'server': ['127.0.0.1', 8000],
        'reply_channel': 'websocket.send!otGzrJjU',
        'order': 0
    },
    'channel': < channels.channel.Channel object at 0x7f118630e748 > ,
    'channel_layer': < channels.asgi.ChannelLayerWrapper object at 0x7f1186b76ac8 >
}

Channel

Attributes:

  • name
  • channel_layer
  • send(content

Group

Attributes:

  • name -- unicode string
  • channel_layer -- ChannelLayer object
  • send(content) -- send the content dict to all members of the group
  • add(channel) -- add given channel (Channel object or unicode string name) to the group, else do nothing
  • discard(channel) -- remove the given Channel.

ChannelLayer

Get them by alias:

from channels import channel_layers
layer = channel_layers["default"]

Attributes:

  • alias
  • router
    • channels
    • match(message)

AsgiRequest

Attributes:

  • message
  • reply_channel
  • message

AsgiHangler

Attributes:

  • AsgiHandler(message)
  • encode_response(response)

Wiring

It looks something like this:

# consumers.py
from channels import Group
 
def ws_add(message):
    Group('chat').add(message.reply_channel)
 
def ws_message(message):
    Group('chat').send({
        'text': '[user] %s' % message.content['text'],
    })
 
def ws_disconnect(message):
    Group('chat').discard(message.reply_channel)
 
# routing.py
"""Routing for channels."""
from channels.routing import route
from chat.consumers import ws_add, ws_message, ws_disconnect
 
 
channel_routing = [
    route('websocket.connect', ws_add),
    route('websocket.receive', ws_message),
    route('websocket.disconnect', ws_disconnect),
]
 
# settings.py
CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "asgiref.inmemory.ChannelLayer",  # Use something like Redis for production
        "ROUTING": "chat.routing.channel_routing",
    },
}

Redis version, after doing pip install asgi_redis:

CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "asgi_redis.RedisChannelLayer",
        "CONFIG": {
            "hosts": [("localhost", 6379)],
        },
        "ROUTING": "chat.routing.channel_routing",
    },
}

Servers

Once channels is installed, manage.py will handle everything in development. You can also split things into separate processes in two terminal windows like this:

  • $ python manage.py runserver --noworker
  • $ python manage.py runworker -v 2

Enforcing Ordering

See enforcing ordering.

@enforce_ordering(slight=True)
@channel_session_user_from_http
def ws_add(message):
    # Add them to the right group
    Group("chat-%s" % message.user.username[0]).add(message.reply_channel)

Class-based Consumers

See generic consumers.

Subclassing

BaseConsumer

You can inherit from BaseConsumer:

from channels.generic import BaseConsumer
 
class MyConsumer(BaseConsumer):
 
    method_mapping = {
        "channel.name.here": "method_name",
    }
 
    def method_name(self, message, **kwargs):
        pass

WebsocketConsumer

Or from WebsocketConsumer:

from channels.generic.websockets import WebsocketConsumer
 
class MyConsumer(WebsocketConsumer):
 
    # Set to True to automatically port users from HTTP cookies
    # (you don't need channel_session_user, this implies it)
    http_user = True
 
    # Set to True if you want them, else leave out
    strict_ordering = False
    slight_ordering = False
 
    def connection_groups(self, **kwargs):
        """
        Called to return the list of groups to automatically add/remove
        this connection to/from.
        """
        return ['test']
 
    def connect(self, message, **kwargs):
        """
        Perform things on connection start
        """
        pass
 
    def receive(self, text=None, bytes=None, **kwargs):
        """
        Called when a message is received with either text or bytes
        filled out.
        """
        # Simple echo
        self.send(text=text, bytes=bytes)
 
    def disconnect(self, message, **kwargs):
        """
        Perform things on connection close
        """
        pass

WebsocketDemultiplexer

"It expects JSON-formatted WebSocket frames with two keys, stream and payload, and will match the stream against the mapping to find a channel name. It will then forward the message onto that channel while preserving reply_channel, so you can hook consumers up to them directly in the routing.py file, and use authentication decorators as you wish."

from channels.generic.websockets import WebsocketDemultiplexer
 
class Demultiplexer(WebsocketDemultiplexer):
 
    mapping = {
        "intval": "binding.intval",
        "stats": "internal.stats",
    }

Sessions and Users

This will wrap decorators around handler methods:

class MyConsumer(WebsocketConsumer):
 
    # Gives access to message.channel_session and message.user
    channel_session_user = True
 
    # Gives access to the user from the current Django session
    # message.user is like request.user
    http_user = True

To apply decorators directly, see the docs.

Routing

In routing, use route_class() instead of route():

from channels import route, route_class
 
channel_routing = [
    route_class(consumers.ChatServer, path=r"^/chat/"),
    route("websocket.connect", consumers.ws_connect, path=r"^/$"),
]

From the docs:

"Class-based consumers are instantiated once for each message they consume, so it’s safe to store things on self (in fact, self.message is the current message by default, and self.kwargs are the keyword arguments passed in from the routing)."

Data Binding

from django.db import models
from channels.binding.websockets import WebsocketBinding
 
 
class IntegerValue(models.Model):
 
    name = models.CharField(max_length=100, unique=True)
    value = models.IntegerField(default=0)
 
 
class IntegerValueBinding(WebsocketBinding):
 
    model = IntegerValue
    stream = 'intval'
    # Whitelisted fields. You can expose all of them with ['__all__']
    fields = ['name', 'value']
 
    # A list of groups to send to
    def group_names(self, instance, action):
        return ['intval-updates']
 
    # Check against permission system
    def has_permission(self, user, action, pk):
        return True
 
 
# "...you must use multiplexing if you use WebSocket data binding."
from channels.generic.websockets import WebsocketDemultiplexer
 
class Demultiplexer(WebsocketDemultiplexer):
 
    mapping = {
        'intval': 'binding.intval',
    }
 
    # A list of groups to put people in when they connect.
    def connection_groups(self):
        return ['intval-updates']
 
 
# Routing example
from channels import route_class, route
from .consumers import Demultiplexer
from .models import IntegerValueBinding
 
channel_routing = [
    route_class(Demultiplexer, path='^/binding/'),
    route('binding.intval', IntegerValueBinding.consumer),
]

Channels API

Notes from a video about channels-api.

# consumers.py
from channels.generic import websockets
 
# route multiple streams over a single websocket connection:
class ExampleDemultiplexer(websockets.WebsocketDemultiplexer):
    mapping = {
        'liveblogs': 'liveblog_channel'
    }
 
 
# routing.py
from channels.routing import route, route_class
 
routing = [
    route_class(ExampleDemultiplexer)
    route('liveblog_channel', LiveblogBinding.consumer)
]
 
 
# bindings.py
from channels import bindings
from channels_api bindings import ResourceBinding
from .serializer import LiveblogSerializer
from .models import Liveblog
 
class LiveblogBinding(bindings.WebsocketBinding):
 
    model = Liveblog
    stream = 'liveblogs'
 
class LiveBlogResourceBinding(ResourceBinding):
    model = Liveblog
    stream = 'liveblogs'
    serializer_class = LiveblogSerializer
    queryset = Liveblog.objects.all()
 
# serializer.py
from rest_framework import serializers
class LiveblogSerializer(serializers.ModelSerializer):
    class Meta:
        model = Liveblog
        fields = ('id', 'title')
        read_only_fields = ('slug', )

Frontend:

var ws = new WebSocket('ws://localhost:8000/');
 
var data = {
    stream: 'liveblogs',
    payload: {
        action: 'create',
        data: {
            title: 'the latest event'
        }
    }
};
 
var msg = JSON.stringify(data);
 
ws.send(msg);