Cet article est l'article du 17ème jour de Django Advent Calendar 2016.
Django Channels — Channels 0.17.2 documentation
Channels is a project to make Django able to handle more than just plain HTTP requests, including WebSockets and HTTP2, as well as the ability to run code after a response has been sent for things like thumbnailing or background calculation.
Channels est un projet qui permet à Django d'exécuter du code après l'envoi d'une réponse, comme des vignettes et des calculs en arrière-plan, ainsi que des requêtes HTTP simples telles que WebSocket et HTTP2.
The core of the system is, unsurprisingly, a datastructure called a channel. What is a channel? It is an ordered, first-in first-out queue with message expiry and at-most-once delivery to only one listener at a time.
If you’ve used channels in Go: Go channels are reasonably similar to Django ones. The key difference is that Django channels are network-transparent; the implementations of channels we provide are all accessible across a network to consumers and producers running in different processes or on different machines.
ASGI (Asynchronous Server Gateway Interface) Draft Spec — Channels 0.17.2 documentation
This document proposes a standard interface between network protocol servers (particularly webservers) and Python applications, intended to allow handling of multiple common protocol styles (including HTTP, HTTP2, and WebSocket).
Interface standard entre les serveurs de protocole réseau (en particulier les serveurs Web) et les applications Python pour permettre la gestion de plusieurs styles de protocole courants (y compris HTTP, HTTP2, WebSocket)
Installez-le simplement avec pip et ajoutez-le à ʻINSTALLED_APPS`.
$ pip install -U channels
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
...
'channels',
)
Pour l'instant, jetons un œil à Getting Started with Channels dans la documentation.
$ pip install django channels
$ django-admin startproject myapp
$ tree
.
├── db.sqlite3
├── manage.py
└── myapp
├── __init__.py
├── settings.py
├── urls.py
└── wsgi.py
First Consumers
Tout d'abord, remplaçons la gestion des requêtes intégrée.
consumers.py
from django.http import HttpResponse
from channels.handler import AsgiHandler
def http_consumer(message):
# Make standard HTTP response - access ASGI path attribute directly
response = HttpResponse("Hello world! You asked for %s" % message.content['path'])
# Encode that response into message format (ASGI)
for chunk in AsgiHandler.encode_response(response):
message.reply_channel.send(chunk)
settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'myapp',
'channels',
]
...
CHANNEL_LAYERS = {
"default": {
"BACKEND": "asgiref.inmemory.ChannelLayer",
"ROUTING": "myproject.routing.channel_routing",
},
}
routing.py
from channels.routing import route
channel_routing = [
route("http.request", "myapp.consumers.http_consumer"),
]
Le réglage est maintenant terminé.
$ tree
.
├── db.sqlite3
├── manage.py
└── myapp
├── __init__.py
├── consumers.py
├── routing.py
├── settings.py
├── urls.py
└── wsgi.py
$ python manage.py runserver
Vérifiez http://127.0.0.1:8000/ et si le texte "Bonjour tout le monde! Vous avez demandé /" s'affiche, c'est OK. Cependant, c'est ennuyeux, alors créons un serveur de discussion de base en utilisant WebSockets.
Ce n'est pas du tout pratique, mais d'abord, créons un serveur qui renvoie le message au client qui a envoyé le message.
consumers.py
def ws_message(message):
# ASGI WebSocket packet-received and send-packet message types
# both have a "text" key for their textual data.
message.reply_channel.send({
"text": message.content['text'],
})
routing.py
from channels.routing import route
from myapp.consumers import ws_message
channel_routing = [
route("websocket.receive", ws_message),
]
$ python manage.py runserver
Accédez à http://127.0.0.1:8000/ et tapez dans la console js comme suit:
// Note that the path doesn't matter for routing; any WebSocket
// connection gets bumped over to WebSocket consumers
socket = new WebSocket("ws://" + window.location.host + "/chat/");
socket.onmessage = function(e) {
alert(e.data);
}
socket.onopen = function() {
socket.send("hello world");
}
// Call onopen directly if socket is already open
if (socket.readyState == WebSocket.OPEN) socket.onopen();
C'est OK si l'alerte "bonjour le monde" est affichée.
Groups
Ensuite, utilisez Groupes pour implémenter un vrai chat où vous pouvez vous parler.
cousumers.py
from channels import Group
# Connected to websocket.connect
def ws_add(message):
message.reply_channel.send({"accept": True})
Group("chat").add(message.reply_channel)
# Connected to websocket.receive
def ws_message(message):
Group("chat").send({
"text": "[user] %s" % message.content['text'],
})
# Connected to websocket.disconnect
def ws_disconnect(message):
Group("chat").discard(message.reply_channel)
routing.py
from channels.routing import route
from myapp.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),
]
$ python manage.py runserver
Ouvrez http://127.0.0.1:8000/ dans plusieurs onglets et tapez le même code js dans la console que précédemment. C'est OK si l'alerte de "[utilisateur] hello world" est affichée sur chaque onglet.
Running with Channels
Ensuite, commutons la couche Channel.
J'avais l'habitude d'utiliser ʻasgiref.inmemory.ChannelLayer, mais cela ne fonctionne que dans le même processus. Dans un environnement de production, utilisez un backend comme ʻasgi_redis
.
$ pip install asgi_redis
setting.py
CHANNEL_LAYERS = {
"default": {
"BACKEND": "asgi_redis.RedisChannelLayer",
"CONFIG": {
"hosts": [("localhost", 6379)],
},
"ROUTING": "myapp.routing.channel_routing",
},
}
$ python manage.py runserver --noworker
$ python manage.py runworker
Vous pouvez maintenant exécuter la commande runworker
.
Persisting Data
L'attribut replay_channel
que nous avons vu jusqu'à présent est un point unique pour les WebSockets connectés. Vous pouvez maintenant retrouver la provenance du message.
Dans un environnement de production, utilisez channel_session
pour rendre la session persistante comme un cookie dans une communication HTTP.
consumers.py
from channels import Group
from channels.sessions import channel_session
# Connected to websocket.connect
@channel_session
def ws_connect(message):
# Accept connection
message.reply_channel.send({"accept": True})
# Work out room name from path (ignore slashes)
room = message.content['path'].strip("/")
# Save room in session and add us to the group
message.channel_session['room'] = room
Group("chat-%s" % room).add(message.reply_channel)
# Connected to websocket.receive
@channel_session
def ws_message(message):
Group("chat-%s" % message.channel_session['room']).send({
"text": message['text'],
})
# Connected to websocket.disconnect
@channel_session
def ws_disconnect(message):
Group("chat-%s" % message.channel_session['room']).discard(message.reply_channel)
routing.py
from channels.routing import route
from myapp.consumers import ws_connect, ws_message, ws_disconnect
channel_routing = [
route("websocket.connect", ws_connect),
route("websocket.receive", ws_message),
route("websocket.disconnect", ws_disconnect),
]
Authentication
Les canaux peuvent obtenir la session Django requise pour l'authentification des utilisateurs des deux manières suivantes.
session_key
L'authentification à l'aide d'une session Django est effectuée en spécifiant un décorateur.
consumers.py
from channels import Channel, Group
from channels.sessions import channel_session
from channels.auth import http_session_user, channel_session_user, channel_session_user_from_http
# Connected to websocket.connect
@channel_session_user_from_http
def ws_add(message):
# Accept connection
message.reply_channel.send({"accept": True})
# Add them to the right group
Group("chat-%s" % message.user.username[0]).add(message.reply_channel)
# Connected to websocket.receive
@channel_session_user
def ws_message(message):
Group("chat-%s" % message.user.username[0]).send({
"text": message['text'],
})
# Connected to websocket.disconnect
@channel_session_user
def ws_disconnect(message):
Group("chat-%s" % message.user.username[0]).discard(message.reply_channel)
Routing
Vous pouvez définir de manière flexible routing.py
en utilisant des expressions régulières, telles que ʻurls.py` de Django.
routing.py
http_routing = [
route("http.request", poll_consumer, path=r"^/poll/$", method=r"^POST$"),
]
chat_routing = [
route("websocket.connect", chat_connect, path=r"^/(?P<room>[a-zA-Z0-9_]+)/$"),
route("websocket.disconnect", chat_disconnect),
]
routing = [
# You can use a string import path as the first argument as well.
include(chat_routing, path=r"^/chat"),
include(http_routing),
]
Models
L'ORM de Django facilite l'intégration de la persistance des messages.
consumers.py
from channels import Channel
from channels.sessions import channel_session
from .models import ChatMessage
# Connected to chat-messages
def msg_consumer(message):
# Save to model
room = message.content['room']
ChatMessage.objects.create(
room=room,
message=message.content['message'],
)
# Broadcast to listening sockets
Group("chat-%s" % room).send({
"text": message.content['message'],
})
# Connected to websocket.connect
@channel_session
def ws_connect(message):
# Work out room name from path (ignore slashes)
room = message.content['path'].strip("/")
# Save room in session and add us to the group
message.channel_session['room'] = room
Group("chat-%s" % room).add(message.reply_channel)
# Connected to websocket.receive
@channel_session
def ws_message(message):
# Stick the message onto the processing queue
Channel("chat-messages").send({
"room": message.channel_session['room'],
"message": message['text'],
})
# Connected to websocket.disconnect
@channel_session
def ws_disconnect(message):
Group("chat-%s" % message.channel_session['room']).discard(message.reply_channel)
Enforcing Ordering
En donnant un décorateur @enforce_ordering (light = True)
, vous pouvez changer l'ordre dans lequel le websocket.connect
a été créé en premier, plutôt que l'ordre dans lequel il a été mis en file d'attente.
consumers.py
from channels import Channel, Group
from channels.sessions import channel_session, enforce_ordering
from channels.auth import http_session_user, channel_session_user, channel_session_user_from_http
# Connected to websocket.connect
@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)
# Connected to websocket.receive
@enforce_ordering(slight=True)
@channel_session_user
def ws_message(message):
Group("chat-%s" % message.user.username[0]).send({
"text": message['text'],
})
# Connected to websocket.disconnect
@enforce_ordering(slight=True)
@channel_session_user
def ws_disconnect(message):
Group("chat-%s" % message.user.username[0]).discard(message.reply_channel
Un didacticiel d'application de chat comme celui que vous avez vu ici est disponible dans Heroku. Finally, Real-Time Django Is Here: Get Started with Django Channels
Si vous essayez jusqu'à ce que vous le déployez réellement sur Heroku, vous vous sentirez mieux.
Comme voluntas l'a écrit dans «Server Push with WebSocket in Django», si vous voulez des performances, vous devez le faire dans une autre langue.
Facile à utiliser pour les applications Django sans avoir besoin de lancer SwampDragon ou Tornado séparément. Le plus grand attrait est qu'il peut être incorporé.
Les canaux sont également envisagés pour inclusion dans Django 2.0, qui devrait sortir en décembre prochain. Utilisons-le et animons les chaînes!
Recommended Posts