mirror of
https://github.com/django/daphne.git
synced 2025-07-11 00:12:18 +03:00
commit
32e047a320
|
@ -1,6 +1,8 @@
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
|
||||||
|
from .binding.base import BindingMetaclass
|
||||||
|
|
||||||
|
|
||||||
class ChannelsConfig(AppConfig):
|
class ChannelsConfig(AppConfig):
|
||||||
|
|
||||||
|
@ -18,3 +20,5 @@ class ChannelsConfig(AppConfig):
|
||||||
# Do django monkeypatches
|
# Do django monkeypatches
|
||||||
from .hacks import monkeypatch_django
|
from .hacks import monkeypatch_django
|
||||||
monkeypatch_django()
|
monkeypatch_django()
|
||||||
|
# Instantiate bindings
|
||||||
|
BindingMetaclass.register_all()
|
||||||
|
|
1
channels/binding/__init__.py
Normal file
1
channels/binding/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
from .base import Binding # NOQA isort:skip
|
187
channels/binding/base.py
Normal file
187
channels/binding/base.py
Normal file
|
@ -0,0 +1,187 @@
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
from django.apps import apps
|
||||||
|
from django.db.models.signals import post_save, post_delete
|
||||||
|
|
||||||
|
from ..channel import Group
|
||||||
|
|
||||||
|
|
||||||
|
class BindingMetaclass(type):
|
||||||
|
"""
|
||||||
|
Metaclass that tracks instantiations of its type.
|
||||||
|
"""
|
||||||
|
|
||||||
|
binding_classes = []
|
||||||
|
|
||||||
|
def __new__(cls, name, bases, body):
|
||||||
|
klass = type.__new__(cls, name, bases, body)
|
||||||
|
if bases != (object, ):
|
||||||
|
cls.binding_classes.append(klass)
|
||||||
|
return klass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def register_all(cls):
|
||||||
|
for binding_class in cls.binding_classes:
|
||||||
|
binding_class.register()
|
||||||
|
|
||||||
|
|
||||||
|
@six.add_metaclass(BindingMetaclass)
|
||||||
|
class Binding(object):
|
||||||
|
"""
|
||||||
|
Represents a two-way data binding from channels/groups to a Django model.
|
||||||
|
Outgoing binding sends model events to zero or more groups.
|
||||||
|
Incoming binding takes messages and maybe applies the action based on perms.
|
||||||
|
|
||||||
|
To implement outbound, implement:
|
||||||
|
- group_names, which returns a list of group names to send to
|
||||||
|
- serialize, which returns message contents from an instance + action
|
||||||
|
|
||||||
|
To implement inbound, implement:
|
||||||
|
- deserialize, which returns pk, data and action from message contents
|
||||||
|
- has_permission, which says if the user can do the action on an instance
|
||||||
|
- create, which takes the data and makes a model instance
|
||||||
|
- update, which takes data and a model instance and applies one to the other
|
||||||
|
|
||||||
|
Outbound will work once you implement the functions; inbound requires you
|
||||||
|
to place one or more bindings inside a protocol-specific Demultiplexer
|
||||||
|
and tie that in as a consumer.
|
||||||
|
"""
|
||||||
|
|
||||||
|
model = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def register(cls):
|
||||||
|
"""
|
||||||
|
Resolves models.
|
||||||
|
"""
|
||||||
|
# If model is None directly on the class, assume it's abstract.
|
||||||
|
if cls.model is None:
|
||||||
|
if "model" in cls.__dict__:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
raise ValueError("You must set the model attribute on Binding %r!" % cls)
|
||||||
|
# Optionally resolve model strings
|
||||||
|
if isinstance(cls.model, six.string_types):
|
||||||
|
cls.model = apps.get_model(cls.model)
|
||||||
|
cls.model_label = "%s.%s" % (
|
||||||
|
cls.model._meta.app_label.lower(),
|
||||||
|
cls.model._meta.object_name.lower(),
|
||||||
|
)
|
||||||
|
# Connect signals
|
||||||
|
post_save.connect(cls.save_receiver, sender=cls.model)
|
||||||
|
post_delete.connect(cls.delete_receiver, sender=cls.model)
|
||||||
|
|
||||||
|
# Outbound binding
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def save_receiver(cls, instance, created, **kwargs):
|
||||||
|
"""
|
||||||
|
Entry point for triggering the binding from save signals.
|
||||||
|
"""
|
||||||
|
cls.trigger_outbound(instance, "create" if created else "update")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def delete_receiver(cls, instance, **kwargs):
|
||||||
|
"""
|
||||||
|
Entry point for triggering the binding from save signals.
|
||||||
|
"""
|
||||||
|
cls.trigger_outbound(instance, "delete")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def trigger_outbound(cls, instance, action):
|
||||||
|
"""
|
||||||
|
Triggers the binding to possibly send to its group.
|
||||||
|
"""
|
||||||
|
self = cls()
|
||||||
|
self.instance = instance
|
||||||
|
# Check to see if we're covered
|
||||||
|
for group_name in self.group_names(instance, action):
|
||||||
|
group = Group(group_name)
|
||||||
|
group.send(self.serialize(instance, action))
|
||||||
|
|
||||||
|
def group_names(self, instance, action):
|
||||||
|
"""
|
||||||
|
Returns the iterable of group names to send the object to based on the
|
||||||
|
instance and action performed on it.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def serialize(self, instance, action):
|
||||||
|
"""
|
||||||
|
Should return a serialized version of the instance to send over the
|
||||||
|
wire (return value must be a dict suitable for sending over a channel -
|
||||||
|
e.g., to send JSON as a WebSocket text frame, you must return
|
||||||
|
{"text": json.dumps(instance_serialized_as_dict)}
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
# Inbound binding
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def trigger_inbound(cls, message, **kwargs):
|
||||||
|
"""
|
||||||
|
Triggers the binding to see if it will do something.
|
||||||
|
Also acts as a consumer.
|
||||||
|
"""
|
||||||
|
# Late import as it touches models
|
||||||
|
from django.contrib.auth.models import AnonymousUser
|
||||||
|
self = cls()
|
||||||
|
self.message = message
|
||||||
|
# Deserialize message
|
||||||
|
self.action, self.pk, self.data = self.deserialize(self.message)
|
||||||
|
self.user = getattr(self.message, "user", AnonymousUser())
|
||||||
|
# Run incoming action
|
||||||
|
self.run_action(self.action, self.pk, self.data)
|
||||||
|
|
||||||
|
consumer = trigger_inbound
|
||||||
|
|
||||||
|
def deserialize(self, message):
|
||||||
|
"""
|
||||||
|
Returns action, pk, data decoded from the message. pk should be None
|
||||||
|
if action is create; data should be None if action is delete.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def has_permission(self, user, action, pk):
|
||||||
|
"""
|
||||||
|
Return True if the user can do action to the pk, False if not.
|
||||||
|
User may be AnonymousUser if no auth hooked up/they're not logged in.
|
||||||
|
Action is one of "create", "delete", "update".
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def run_action(self, action, pk, data):
|
||||||
|
"""
|
||||||
|
Performs the requested action. This version dispatches to named
|
||||||
|
functions by default for update/create, and handles delete itself.
|
||||||
|
"""
|
||||||
|
# Check to see if we're allowed
|
||||||
|
if self.has_permission(self.user, action, pk):
|
||||||
|
if action == "create":
|
||||||
|
self.create(data)
|
||||||
|
elif action == "update":
|
||||||
|
self.update(pk, data)
|
||||||
|
elif action == "delete":
|
||||||
|
self.delete(pk)
|
||||||
|
else:
|
||||||
|
raise ValueError("Bad action %r" % action)
|
||||||
|
|
||||||
|
def create(self, data):
|
||||||
|
"""
|
||||||
|
Creates a new instance of the model with the data.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def update(self, pk, data):
|
||||||
|
"""
|
||||||
|
Updates the model with the data.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def delete(self, pk):
|
||||||
|
"""
|
||||||
|
Deletes the model instance.
|
||||||
|
"""
|
||||||
|
self.model.objects.filter(pk=pk).delete()
|
88
channels/binding/websockets.py
Normal file
88
channels/binding/websockets.py
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
import json
|
||||||
|
|
||||||
|
from django.core import serializers
|
||||||
|
|
||||||
|
from .base import Binding
|
||||||
|
from ..generic.websockets import WebsocketDemultiplexer
|
||||||
|
|
||||||
|
|
||||||
|
class WebsocketBinding(Binding):
|
||||||
|
"""
|
||||||
|
Websocket-specific outgoing binding subclass that uses JSON encoding
|
||||||
|
and the built-in JSON/WebSocket multiplexer.
|
||||||
|
|
||||||
|
To implement outbound, implement:
|
||||||
|
- group_names, which returns a list of group names to send to
|
||||||
|
|
||||||
|
To implement inbound, implement:
|
||||||
|
- has_permission, which says if the user can do the action on an instance
|
||||||
|
|
||||||
|
Optionally also implement:
|
||||||
|
- serialize_data, which returns JSON-safe data from a model instance
|
||||||
|
- create, which takes incoming data and makes a model instance
|
||||||
|
- update, which takes incoming data and a model instance and applies one to the other
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Mark as abstract
|
||||||
|
|
||||||
|
model = None
|
||||||
|
|
||||||
|
# Stream multiplexing name
|
||||||
|
|
||||||
|
stream = None
|
||||||
|
|
||||||
|
# Outbound
|
||||||
|
|
||||||
|
def serialize(self, instance, action):
|
||||||
|
payload = {
|
||||||
|
"action": action,
|
||||||
|
"pk": instance.pk,
|
||||||
|
"data": self.serialize_data(instance),
|
||||||
|
}
|
||||||
|
# Encode for the stream
|
||||||
|
assert self.stream is not None
|
||||||
|
return WebsocketDemultiplexer.encode(self.stream, payload)
|
||||||
|
|
||||||
|
def serialize_data(self, instance):
|
||||||
|
"""
|
||||||
|
Serializes model data into JSON-compatible types.
|
||||||
|
"""
|
||||||
|
data = serializers.serialize('json', [instance])
|
||||||
|
return json.loads(data)[0]['fields']
|
||||||
|
|
||||||
|
# Inbound
|
||||||
|
|
||||||
|
def deserialize(self, message):
|
||||||
|
"""
|
||||||
|
You must hook this up behind a Deserializer, so we expect the JSON
|
||||||
|
already dealt with.
|
||||||
|
"""
|
||||||
|
action = message['action']
|
||||||
|
pk = message.get('pk', None)
|
||||||
|
data = message.get('data', None)
|
||||||
|
return action, pk, data
|
||||||
|
|
||||||
|
def _hydrate(self, pk, data):
|
||||||
|
"""
|
||||||
|
Given a raw "data" section of an incoming message, returns a
|
||||||
|
DeserializedObject.
|
||||||
|
"""
|
||||||
|
s_data = [
|
||||||
|
{
|
||||||
|
"pk": pk,
|
||||||
|
"model": self.model_label,
|
||||||
|
"fields": data,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
# TODO: Avoid the JSON roundtrip by using encoder directly?
|
||||||
|
return list(serializers.deserialize("json", json.dumps(s_data)))[0]
|
||||||
|
|
||||||
|
def create(self, data):
|
||||||
|
self._hydrate(None, data).save()
|
||||||
|
|
||||||
|
def update(self, pk, data):
|
||||||
|
instance = self.model.objects.get(pk=pk)
|
||||||
|
hydrated = self._hydrate(pk, data)
|
||||||
|
for name in data.keys():
|
||||||
|
setattr(instance, name, getattr(hydrated.object, name))
|
||||||
|
instance.save()
|
|
@ -1,6 +1,6 @@
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from ..channel import Group
|
from ..channel import Group, Channel
|
||||||
from ..auth import channel_session_user_from_http
|
from ..auth import channel_session_user_from_http
|
||||||
from ..sessions import enforce_ordering
|
from ..sessions import enforce_ordering
|
||||||
from .base import BaseConsumer
|
from .base import BaseConsumer
|
||||||
|
@ -98,11 +98,12 @@ class WebsocketConsumer(BaseConsumer):
|
||||||
else:
|
else:
|
||||||
raise ValueError("You must pass text or bytes")
|
raise ValueError("You must pass text or bytes")
|
||||||
|
|
||||||
def group_send(self, name, text=None, bytes=None):
|
@classmethod
|
||||||
|
def group_send(cls, name, text=None, bytes=None):
|
||||||
if text is not None:
|
if text is not None:
|
||||||
Group(name, channel_layer=self.message.channel_layer).send({"text": text})
|
Group(name).send({"text": text})
|
||||||
elif bytes is not None:
|
elif bytes is not None:
|
||||||
Group(name, channel_layer=self.message.channel_layer).send({"bytes": bytes})
|
Group(name).send({"bytes": bytes})
|
||||||
else:
|
else:
|
||||||
raise ValueError("You must pass text or bytes")
|
raise ValueError("You must pass text or bytes")
|
||||||
|
|
||||||
|
@ -153,5 +154,61 @@ class JsonWebsocketConsumer(WebsocketConsumer):
|
||||||
"""
|
"""
|
||||||
super(JsonWebsocketConsumer, self).send(text=json.dumps(content))
|
super(JsonWebsocketConsumer, self).send(text=json.dumps(content))
|
||||||
|
|
||||||
def group_send(self, name, content):
|
@classmethod
|
||||||
super(JsonWebsocketConsumer, self).group_send(name, json.dumps(content))
|
def group_send(cls, name, content):
|
||||||
|
WebsocketConsumer.group_send(name, json.dumps(content))
|
||||||
|
|
||||||
|
|
||||||
|
class WebsocketDemultiplexer(JsonWebsocketConsumer):
|
||||||
|
"""
|
||||||
|
JSON-understanding WebSocket consumer subclass that handles demultiplexing
|
||||||
|
streams using a "stream" key in a top-level dict and the actual payload
|
||||||
|
in a sub-dict called "payload". This lets you run multiple streams over
|
||||||
|
a single WebSocket connection in a standardised way.
|
||||||
|
|
||||||
|
Incoming messages on streams are mapped into a custom channel so you can
|
||||||
|
just tie in consumers the normal way. The reply_channels are kept so
|
||||||
|
sessions/auth continue to work. Payloads must be a dict at the top level,
|
||||||
|
so they fulfill the Channels message spec.
|
||||||
|
|
||||||
|
Set a mapping from streams to channels in the "mapping" key. We make you
|
||||||
|
whitelist channels like this to allow different namespaces and for security
|
||||||
|
reasons (imagine if someone could inject straight into websocket.receive).
|
||||||
|
"""
|
||||||
|
|
||||||
|
mapping = {}
|
||||||
|
|
||||||
|
def receive(self, content, **kwargs):
|
||||||
|
# Check the frame looks good
|
||||||
|
if isinstance(content, dict) and "stream" in content and "payload" in content:
|
||||||
|
# Match it to a channel
|
||||||
|
stream = content['stream']
|
||||||
|
if stream in self.mapping:
|
||||||
|
# Extract payload and add in reply_channel
|
||||||
|
payload = content['payload']
|
||||||
|
if not isinstance(payload, dict):
|
||||||
|
raise ValueError("Multiplexed frame payload is not a dict")
|
||||||
|
payload['reply_channel'] = self.message['reply_channel']
|
||||||
|
# Send it onto the new channel
|
||||||
|
Channel(self.mapping[stream]).send(payload)
|
||||||
|
else:
|
||||||
|
raise ValueError("Invalid multiplexed frame received (stream not mapped)")
|
||||||
|
else:
|
||||||
|
raise ValueError("Invalid multiplexed frame received (no channel/payload key)")
|
||||||
|
|
||||||
|
def send(self, stream, payload):
|
||||||
|
self.message.reply_channel.send(self.encode(stream, payload))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def group_send(cls, name, stream, payload):
|
||||||
|
Group(name).send(cls.encode(stream, payload))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def encode(cls, stream, payload):
|
||||||
|
"""
|
||||||
|
Encodes stream + payload for outbound sending.
|
||||||
|
"""
|
||||||
|
return {"text": json.dumps({
|
||||||
|
"stream": stream,
|
||||||
|
"payload": payload,
|
||||||
|
})}
|
||||||
|
|
|
@ -348,6 +348,11 @@ A channel layer implementing the ``groups`` extension must also provide:
|
||||||
* ``group_discard(group, channel)``, a callable that removes the ``channel``
|
* ``group_discard(group, channel)``, a callable that removes the ``channel``
|
||||||
from the ``group`` if it is in it, and does nothing otherwise.
|
from the ``group`` if it is in it, and does nothing otherwise.
|
||||||
|
|
||||||
|
* ``group_channels(group)``, a callable that returns an iterable which yields
|
||||||
|
all of the group's member channel names. The return value should be serializable
|
||||||
|
with regards to local adds and discards, but best-effort with regards to
|
||||||
|
adds and discards on other nodes.
|
||||||
|
|
||||||
* ``send_group(group, message)``, a callable that takes two positional
|
* ``send_group(group, message)``, a callable that takes two positional
|
||||||
arguments; the group to send to, as a unicode string, and the message
|
arguments; the group to send to, as a unicode string, and the message
|
||||||
to send, as a serializable ``dict``. It may raise MessageTooLarge but cannot
|
to send, as a serializable ``dict``. It may raise MessageTooLarge but cannot
|
||||||
|
|
149
docs/binding.rst
Normal file
149
docs/binding.rst
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
Data Binding
|
||||||
|
============
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
Data Binding is new and might change slightly in the
|
||||||
|
upcoming weeks, and so don't consider this API totally stable yet.
|
||||||
|
|
||||||
|
The Channels data binding framework automates the process of tying Django
|
||||||
|
models into frontend views, such as javascript-powered website UIs. It provides
|
||||||
|
a quick and flexible way to generate messages on Groups for model changes
|
||||||
|
and to accept messages that chanes models themselves.
|
||||||
|
|
||||||
|
The main target for the moment is WebSockets, but the framework is flexible
|
||||||
|
enough to be used over any protocol.
|
||||||
|
|
||||||
|
What does data binding allow?
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
Data binding in Channels works two ways:
|
||||||
|
|
||||||
|
* Outbound, where model changes made through Django are sent out to listening
|
||||||
|
clients. This includes creation, update and deletion of instances.
|
||||||
|
|
||||||
|
* Inbound, where a standardised message format allow creation, update and
|
||||||
|
deletion of instances to be made by clients sending messages.
|
||||||
|
|
||||||
|
Combined, these allow a UI to be designed that automatically updates to
|
||||||
|
reflect new values and reflects across clients. A live blog is easily done
|
||||||
|
using data binding against the post object, for example, or an edit interface
|
||||||
|
can show data live as it's edited by other users.
|
||||||
|
|
||||||
|
It has some limitations:
|
||||||
|
|
||||||
|
* Signals are used to power outbound binding, so if you change the values of
|
||||||
|
a model outside of Django (or use the ``.update()`` method on a QuerySet),
|
||||||
|
the signals are not triggered and the change will not be sent out. You
|
||||||
|
can trigger changes yourself, but you'll need to source the events from the
|
||||||
|
right place for your system.
|
||||||
|
|
||||||
|
* The built-in serializers are based on the built-in Django ones and can only
|
||||||
|
handle certain field types; for more flexibility, you can plug in something
|
||||||
|
like the Django REST Framework serializers.
|
||||||
|
|
||||||
|
Getting Started
|
||||||
|
---------------
|
||||||
|
|
||||||
|
A single Binding subclass will handle outbound and inbound binding for a model,
|
||||||
|
and you can have multiple bindings per model (if you want different formats
|
||||||
|
or permission checks, for example).
|
||||||
|
|
||||||
|
You can inherit from the base Binding and provide all the methods needed, but
|
||||||
|
we'll focus on the WebSocket JSON variant here, as it's the easiest thing to
|
||||||
|
get started and likely close to what you want. Start off like this::
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
def group_names(self, instance, action):
|
||||||
|
return ["intval-updates"]
|
||||||
|
|
||||||
|
def has_permission(self, user, action, pk):
|
||||||
|
return True
|
||||||
|
|
||||||
|
This defines a WebSocket binding - so it knows to send outgoing messages
|
||||||
|
formatted as JSON WebSocket frames - and provides the two methods you must
|
||||||
|
always provide:
|
||||||
|
|
||||||
|
* ``group_names`` returns a list of groups to send outbound updates to based
|
||||||
|
on the model and action. For example, you could dispatch posts on different
|
||||||
|
liveblogs to groups that included the parent blog ID in the name; here, we
|
||||||
|
just use a fixed group name.
|
||||||
|
|
||||||
|
* ``has_permission`` returns if an inbound binding update is allowed to actually
|
||||||
|
be carried out on the model. We've been very unsafe and made it always return
|
||||||
|
``True``, but here is where you would check against either Django's or your
|
||||||
|
own permission system to see if the user is allowed that action.
|
||||||
|
|
||||||
|
For reference, ``action`` is always one of the unicode strings ``"create"``,
|
||||||
|
``"update"`` or ``"delete"``. You also supply the :ref:`multiplexing`
|
||||||
|
stream name to provide to the client - you must use multiplexing if you
|
||||||
|
use WebSocket data binding.
|
||||||
|
|
||||||
|
Just adding the binding like this in a place where it will be imported will
|
||||||
|
get outbound messages sending, but you still need a Consumer that will both
|
||||||
|
accept incoming binding updates and add people to the right Groups when they
|
||||||
|
connect. The WebSocket binding classes use the standard :ref:`multiplexing`,
|
||||||
|
so you just need to use that::
|
||||||
|
|
||||||
|
from channels.generic.websockets import WebsocketDemultiplexer
|
||||||
|
|
||||||
|
class Demultiplexer(WebsocketDemultiplexer):
|
||||||
|
|
||||||
|
mapping = {
|
||||||
|
"intval": "binding.intval",
|
||||||
|
}
|
||||||
|
|
||||||
|
def connection_groups(self):
|
||||||
|
return ["intval-updates"]
|
||||||
|
|
||||||
|
As well as the standard stream-to-channel mapping, you also need to set
|
||||||
|
``connection_groups``, a list of groups to put people in when they connect.
|
||||||
|
This should match the logic of ``group_names`` on your binding - we've used
|
||||||
|
our fixed group name again.
|
||||||
|
|
||||||
|
Tie that into your routing, and tie each demultiplexed channel into the
|
||||||
|
``.consumer`` attribute of the Binding, and you're ready to go::
|
||||||
|
|
||||||
|
from channels import route_class
|
||||||
|
from .consumers import BindingConsumer
|
||||||
|
from .models import IntegerValueBinding
|
||||||
|
|
||||||
|
channel_routing = [
|
||||||
|
route_class(BindingConsumer, path="^binding/"),
|
||||||
|
route("binding.intval", IntegerValueBinding.consumer),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
Frontend Considerations
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
You can use the standard Channels WebSocket wrapper **(not yet available)**
|
||||||
|
to automatically run demultiplexing, and then tie the events you receive into
|
||||||
|
your frontend framework of choice based on ``action``, ``pk`` and ``data``.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Common plugins for data binding against popular JavaScript frameworks are
|
||||||
|
wanted; if you're interested, please get in touch.
|
||||||
|
|
||||||
|
|
||||||
|
Custom Serialization/Protocols
|
||||||
|
------------------------------
|
||||||
|
|
||||||
|
Rather than inheriting from the ``WebsocketBinding``, you can inherit directly
|
||||||
|
from the base ``Binding`` class and implement serialization and deserialization
|
||||||
|
yourself. Until proper reference documentation for this is written, we
|
||||||
|
recommend looking at the source code in ``channels/bindings/base.py``; it's
|
||||||
|
reasonably well-commented.
|
|
@ -175,6 +175,40 @@ that will do this for you if you call it explicitly.
|
||||||
``self.close()`` is also provided to easily close the WebSocket from the server
|
``self.close()`` is also provided to easily close the WebSocket from the server
|
||||||
end once you are done with it.
|
end once you are done with it.
|
||||||
|
|
||||||
|
.. _multiplexing:
|
||||||
|
|
||||||
|
WebSocket Multiplexing
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
Channels provides a standard way to multiplex different data streams over
|
||||||
|
a single WebSocket, called a ``Demultiplexer``. You use it like this::
|
||||||
|
|
||||||
|
from channels.generic.websockets import WebsocketDemultiplexer
|
||||||
|
|
||||||
|
class Demultiplexer(WebsocketDemultiplexer):
|
||||||
|
|
||||||
|
mapping = {
|
||||||
|
"intval": "binding.intval",
|
||||||
|
"stats": "internal.stats",
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
You cannot use class-based consumers this way as the messages are no
|
||||||
|
longer in WebSocket format, though. If you need to do operations on
|
||||||
|
``connect`` or ``disconnect``, override those methods on the ``Demultiplexer``
|
||||||
|
itself (you can also provide a ``connection_groups`` method, as it's just
|
||||||
|
based on the JSON WebSocket generic consumer).
|
||||||
|
|
||||||
|
The :doc:`data binding <binding>` code will also send out messages to clients
|
||||||
|
in the same format, and you can encode things in this format yourself by
|
||||||
|
using the ``WebsocketDemultiplexer.encode`` class method.
|
||||||
|
|
||||||
|
|
||||||
Sessions and Users
|
Sessions and Users
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
|
|
|
@ -60,7 +60,7 @@ Here's what that looks like::
|
||||||
"ROUTING": "myproject.routing.channel_routing",
|
"ROUTING": "myproject.routing.channel_routing",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
..
|
|
||||||
::
|
::
|
||||||
|
|
||||||
# In routing.py
|
# In routing.py
|
||||||
|
|
|
@ -31,6 +31,7 @@ Contents:
|
||||||
deploying
|
deploying
|
||||||
generics
|
generics
|
||||||
routing
|
routing
|
||||||
|
binding
|
||||||
backends
|
backends
|
||||||
testing
|
testing
|
||||||
cross-compat
|
cross-compat
|
||||||
|
|
Loading…
Reference in New Issue
Block a user