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!
Django Channels PyStPete
By Dairon Medina Caro
Django Channels PyStPete
- 1,430