Dieser Artikel ist der 17. Tagesartikel von Django Adventskalender 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 ist ein Projekt, mit dem Django nach dem Senden einer Antwort Code ausführen kann, z. B. Miniaturansichten und Hintergrundberechnungen sowie einfache HTTP-Anforderungen wie WebSocket und 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).
Standardschnittstelle zwischen Netzwerkprotokollservern (insbesondere Webservern) und Python-Anwendungen, um die Verarbeitung mehrerer gängiger Protokollstile (einschließlich HTTP, HTTP2, WebSocket) zu ermöglichen.
Installieren Sie es einfach mit pip und fügen Sie es zu INSTALLED_APPS
hinzu.
$ pip install -U channels
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
...
'channels',
)
Sehen wir uns zunächst Erste Schritte mit Kanälen in der Dokumentation an.
$ pip install django channels
$ django-admin startproject myapp
$ tree
.
├── db.sqlite3
├── manage.py
└── myapp
├── __init__.py
├── settings.py
├── urls.py
└── wsgi.py
First Consumers
Lassen Sie uns zunächst die integrierte Anforderungsbehandlung überschreiben.
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"),
]
Die Einstellung ist jetzt abgeschlossen.
$ tree
.
├── db.sqlite3
├── manage.py
└── myapp
├── __init__.py
├── consumers.py
├── routing.py
├── settings.py
├── urls.py
└── wsgi.py
$ python manage.py runserver
Überprüfen Sie http://127.0.0.1:8000/ und wenn der Text "Hallo Welt! Sie haben nach / gefragt" angezeigt wird, ist dies in Ordnung. Dies ist jedoch langweilig. Erstellen wir also einen einfachen Chat-Server mit WebSockets.
Es ist überhaupt nicht praktisch, aber zuerst erstellen wir einen Server, der die Nachricht an den Client zurücksendet, der die Nachricht gesendet hat.
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
Gehen Sie zu http://127.0.0.1:8000/ und geben Sie die js-Konsole wie folgt ein:
// 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();
Es ist in Ordnung, wenn die Warnung "Hallo Welt" angezeigt wird.
Groups
Verwenden Sie dann Gruppen, um einen echten Chat zu implementieren, in dem Sie miteinander sprechen können.
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
Öffnen Sie http://127.0.0.1:8000/ auf mehreren Registerkarten und geben Sie denselben JS-Code in die Konsole ein. Es ist in Ordnung, wenn auf jeder Registerkarte die Warnung "[Benutzer] Hallo Welt" angezeigt wird.
Running with Channels
Als nächstes wechseln wir die Kanalebene. Früher habe ich "asgiref.inmemory.ChannelLayer" verwendet, aber dies funktioniert nur im selben Prozess. Verwenden Sie in einer Produktionsumgebung ein Backend wie "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
Sie können jetzt den Befehl runworker
ausführen.
Persisting Data
Das Attribut "replay_channel", das wir bisher gesehen haben, ist ein eindeutiger Punkt für verbundene WebSockets. Jetzt können Sie nachverfolgen, von wem die Nachricht stammt.
Verwenden Sie in einer Produktionsumgebung "channel_session", um die Sitzung wie ein Cookie in der HTTP-Kommunikation dauerhaft zu machen.
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
Kanäle können die für die Benutzerauthentifizierung erforderliche Django-Sitzung auf zwei Arten abrufen.
session_key
Die Authentifizierung mithilfe einer Django-Sitzung erfolgt durch Angabe eines Dekorateurs.
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
Wie bei Djangos urls.py
können Sierouting.py
mithilfe regulärer Ausdrücke flexibel einstellen.
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
Djangos ORM erleichtert die Integration der Nachrichtenpersistenz.
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
Indem Sie einen @ @ enforce_ordering (geringfügig = True) -Dekorator angeben, können Sie die Reihenfolge ändern, in der der websocket.connect
zuerst erstellt wurde, und nicht die Reihenfolge, in der sie in die Warteschlange gestellt wurden.
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
Ein Chat-App-Tutorial wie das hier gezeigte ist in Heroku verfügbar. Finally, Real-Time Django Is Here: Get Started with Django Channels
Wenn Sie es versuchen, bis Sie es tatsächlich auf Heroku bereitstellen, erhalten Sie ein besseres Gefühl.
Wie Volunteeras in "Server Push mit WebSocket in Django" geschrieben hat, sollten Sie dies in einer anderen Sprache tun, wenn Sie Leistung wünschen.
Einfache Verwendung für Django-Anwendungen, ohne dass SwampDragon oder Tornado separat gestartet werden müssen. Die größte Attraktion ist, dass es eingebaut werden kann.
Die Kanäle werden auch für die Aufnahme in Django 2.0 in Betracht gezogen, die im kommenden Dezember erscheinen soll. Lass es uns benutzen und die Kanäle beleben!
Recommended Posts