In der Welt von Django wird das Add-On Django Channels immer beliebter. Diese Bibliothek sollte Django die asynchrone Netzwerkprogrammierung bringen, auf die wir gewartet haben.
Artyom Malyshev auf der Moscow Python Conf 2017 erklärte, wie die erste Version der Bibliothek dies tut (jetzt hat der Autor bereits Kanäle gezippt2), warum sie es tut und überhaupt tut.
Zunächst sagt Zen Zen, dass jede Lösung die einzige sein sollte. Daher
gibt es in Python jeweils mindestens drei . Es gibt bereits viele asynchrone Netzwerk-Frameworks:
- Verdreht
- Eventlet
- Gevent
- Tornado;
- Asyncio
Es scheint, warum eine andere Bibliothek schreiben und ob es überhaupt notwendig ist.
Über den Sprecher: Artyom Malyshev ist ein unabhängiger Python-Entwickler. Er beschäftigt sich mit der Entwicklung verteilter Systeme und spricht auf Konferenzen über Python. Artyom kann unter dem Spitznamen
PROOFIT404 auf Github und in sozialen Netzwerken gefunden werden.
Django ist per Definition synchron . Wenn es sich um ORM handelt, kostet der synchrone Zugriff auf die Datenbank während des Attributzugriffs, wenn wir beispielsweise post.author.username schreiben, nichts.
Darüber hinaus ist Django ein WSGI-Framework.
WSGI
WSGI ist eine synchrone Schnittstelle für die Arbeit mit Webservern.
def app (environ, callback) : status, headers = '200 OK', [] callback (status, headers) return ['Hello world!\n']
Das Hauptmerkmal ist, dass wir eine Funktion haben, die ein Argument akzeptiert und sofort einen Wert zurückgibt. Das ist alles, was der Webserver von uns erwarten kann.
Nicht asynchron und riecht nicht .
Dies geschah vor langer Zeit, im Jahr 2003, als das Web einfach war, Benutzer alle Arten von Nachrichten im Internet lasen und in Gästebücher gingen. Es genügte, die Anfrage anzunehmen und zu bearbeiten. Geben Sie eine Antwort und vergessen Sie, dass dieser Benutzer überhaupt war.

Aber für eine Sekunde ist jetzt nicht das Jahr 2003, also wollen die Benutzer viel mehr von uns.

Sie möchten eine Rich-Webanwendung und Live-Inhalte. Sie möchten, dass die Anwendung auf dem Desktop, auf dem Laptop, auf anderen Seiten und auf der Uhr hervorragend funktioniert. Am wichtigsten ist, dass
Benutzer F5 nicht drücken möchten , da beispielsweise Tablets keine solche Taste haben.

Webbrowser kommen natürlich auf uns zu - sie fügen neue Protokolle und neue Funktionen hinzu. Wenn Sie und ich nur das Frontend entwickeln würden, würden wir den Browser einfach als Plattform nehmen und seine Kernfunktionen nutzen, da er bereit ist, sie uns zur Verfügung zu stellen.
Für Backend-Programmierer hat sich jedoch alles stark verändert . Web-Sockets, HTTP2 und dergleichen sind ein großer Schmerz in Bezug auf die Architektur, da es sich um langlebige Verbindungen zu ihren Zuständen handelt, die irgendwie behandelt werden müssen.

Dies ist das Problem, das Django Channels for Django zu lösen versucht. Diese Bibliothek wurde entwickelt, um Ihnen die Möglichkeit zu geben, Verbindungen zu handhaben, wobei der gewohnte Django Core völlig unverändert bleibt.
Er wurde von
Andrew Godwin , dem Besitzer eines schrecklichen englischen Akzents, der sehr schnell spricht, zu einer wundervollen Person gemacht. Sie sollten es an solchen Dingen wie den längst vergessenen Django South- und Django-Migrationen erkennen, die uns ab Version 1.7 zur Verfügung standen. Seit er die Migrationen für Django repariert hat, hat er begonnen, Web-Sockets und HTTP2 zu reparieren.
Wie hat er das gemacht? Es war einmal ein solches Bild im Internet: leere Quadrate, Pfeile, die Aufschrift „Gute Architektur“ - Sie geben Ihre Lieblingstechnologien in diese kleinen Quadrate ein und erhalten eine Website, die sich gut skalieren lässt.

Andrew Godwin hat in diesen Feldern einen Server eingegeben, der vorne steht und alle Anfragen akzeptiert, sei es asynchron, synchron, E-Mail, was auch immer. Dazwischen befindet sich die sogenannte Kanalschicht, in der empfangene Nachrichten in einem Format gespeichert werden, auf das der Pool synchroner Mitarbeiter zugreifen kann. Sobald die asynchrone Verbindung etwas an uns gesendet hat, zeichnen wir es in der Kanalebene auf, und der Synchronarbeiter kann es von dort abholen und auf dieselbe Weise wie jede Django-Ansicht oder irgendetwas anderes synchron verarbeiten. Sobald der synchrone Code eine Antwort an Channel Layer zurückgesendet hat, gibt der asynchrone Server sie, überträgt sie und tut, was immer er benötigt. Somit wird eine Abstraktion durchgeführt.
Dies impliziert mehrere Implementierungen, und in der Produktion wird vorgeschlagen,
Twisted als asynchronen Server zu verwenden , der das Frontend für Django implementiert, und
Redis , der der gleiche Kommunikationskanal zwischen synchronem Django und asynchronem Twisted sein wird.
Die gute Nachricht: Um Django-Kanäle nutzen zu können, müssen Sie weder Twisted noch Redis kennen - dies sind alles Implementierungsdetails. Ihre DevOps werden dies wissen, oder Sie werden sich treffen, wenn Sie um drei Uhr morgens eine heruntergefallene Produktion reparieren.
ASGI
Abstraktion ist ein Protokoll namens ASGI. Dies ist die Standardschnittstelle zwischen jeder Netzwerkschnittstelle und jedem Server, unabhängig davon, ob es sich um ein synchrones oder asynchrones Protokoll handelt, und Ihrer Anwendung. Sein Hauptkonzept ist der Kanal.
Kanal
Ein Kanal ist eine First-In-First-Out-Warteschlange von Nachrichten mit einer Lebensdauer. Diese Nachrichten können null oder einmal zugestellt werden und können nur von einem Verbraucher empfangen werden.
Verbraucher
Bei Consumer schreiben Sie nur Ihren Code.
def ws_message (message) : message.reply_channel.send ( { 'text': message.content ['text'], } )
Eine Funktion, die eine Nachricht akzeptiert, sendet möglicherweise mehrere Antworten oder überhaupt keine Antwort. Der einzige Unterschied besteht darin, dass es keine Rückgabefunktion gibt, sodass wir darüber sprechen können, wie viele Antworten wir von der Funktion zurückgeben.
Wir fügen diese Funktion dem Routing hinzu, hängen sie beispielsweise auf, um eine Nachricht an einem Web-Socket zu empfangen.
from channels.routing import route from myapp.consumers import ws_message channel_routing = [ route ('websocket.receive' ws_message), }
Wir schreiben dies in die Django-Einstellungen, so wie die Datenbank vorgeschrieben wäre.
CHANNEL_LAYERS = { 'default': { 'BACKEND': 'asgiref.inmemory', 'ROUTING': 'myproject.routing', }, }
Ein Projekt kann mehrere Kanalebenen haben, genauso wie es mehrere Datenbanken geben kann. Dieses Ding ist dem DB-Router sehr ähnlich, wenn jemand es benutzt hat.
Als nächstes definieren wir unsere ASGI-Anwendung. Es synchronisiert, wie Twisted startet und wie synchronisierte Worker starten - alle benötigen diese Anwendung.
import os from channels.asgi import get_channel_layer os.environ.setdefault( 'DJANGO_SETTINGS_MODULE', 'myproject.settings', ) channel_layer = get_channel_layer()
Stellen Sie danach den Code bereit: Führen Sie gunicorn aus und senden Sie standardmäßig eine HTTP-Anfrage synchron mit der Ansicht, wie Sie es gewohnt sind. Wir starten den asynchronen Server, der die Vorderseite vor unserem synchronen Django sein wird, und die Mitarbeiter, die die Nachrichten verarbeiten.
$ gunicorn myproject.wsgi $ daphne myproject.asgi:channel_layer $ django-admin runworker
Antwortkanal
Wie wir gesehen haben, hat Nachricht ein Konzept wie Antwortkanal. Warum wird das benötigt?
Kanal unidirektional bzw. WebSocket empfangen, WebSocket verbinden, WebSocket trennen - Dies ist ein allgemeiner Kanal für eingehende Nachrichten zum System. Ein Antwortkanal ist ein Kanal, der streng an die Verbindung des Benutzers gebunden ist. Dementsprechend hat die Nachricht einen Eingangs- und Ausgangskanal. Mit diesem Paar können Sie identifizieren, von wem diese Nachricht stammt.

Gruppen
Eine Gruppe ist eine Sammlung von Kanälen. Wenn wir eine Nachricht an eine Gruppe senden, wird diese automatisch an alle Kanäle dieser Gruppe gesendet. Dies ist praktisch, da niemand gerne für Schleifen schreibt. Außerdem erfolgt die Implementierung von Gruppen normalerweise mithilfe der nativen Funktionen der Kanalebene. Dies ist also schneller als nur das Senden von Nachrichten nacheinander.
from channels import Group def ws_connect (message): Group ('chat').add (message.reply_channel) def ws_disconnect (message): Group ('chat').discard(message.reply_channel) def ws_message (message): Group ('chat'). Send ({ 'text': message.content ['text'], })
Auf die gleiche Weise werden auch Gruppen zum Routing hinzugefügt.
from channels.routing import route from myapp.consumers import * channel_routing = [ route ('websocket.connect' , ws_connect), route ('websocket.disconnect' , ws_disconnect), route ('websocket.receive' , ws_message), ]
Sobald der Kanal zur Gruppe hinzugefügt wurde, geht die Antwort an alle Benutzer, die mit unserer Website verbunden sind, und nicht nur an die Echoantwort an uns.
Generische Verbraucher
Wofür ich Django liebe, ist deklarativ. Ebenso gibt es deklarative Verbraucher.
Base Consumer ist ein Basis-Consumer. Er kann nur den Kanal zuordnen, den Sie für eine Methode definiert haben, und ihn aufrufen.
from channels.generic import BaseConsumer class MyComsumer (BaseConsumer) : method_mapping = { 'channel.name.here': 'method_name', } def method_name (self, message, **kwargs) : pass
Es gibt eine große Anzahl vordefinierter Konsumenten mit absichtlich erweitertem Verhalten, z. B. WebSocket Consumer, das vorab festlegt, ob WebSocket Connect, WebSocket Receive und WebSocket Disconnect verarbeitet werden. Sie können sofort angeben, in welchen Gruppen ein Antwortkanal hinzugefügt werden soll. Sobald Sie self.send verwenden, wird er verstehen, ob dies an eine Gruppe oder an einen Benutzer gesendet werden soll.
from channels.generic import WebsocketConsumer class MyConsumer (WebsocketConsumer) : def connection_groups (self) : return ['chat'] def connect (self, message) : pass def receive (self, text=None, bytes=None) : self.send (text=text, bytes=bytes)
Es gibt auch eine WebSocket-Consumer-Option mit JSON, dh kein Text, keine Bytes, aber bereits analysiertes JSON wird empfangen, was praktisch ist.
Beim Routing wird es auf die gleiche Weise über route_class hinzugefügt. Myapp wird in route_class übernommen, das vom Verbraucher bestimmt wird, alle Kanäle werden von dort übernommen und alle in myapp angegebenen Kanäle werden weitergeleitet. Schreiben Sie weniger auf diese Weise.
Routing
Lassen Sie uns im Detail über das Routing und dessen Möglichkeiten sprechen.
Erstens sind dies Filter.
// app.js S = new WebSocket ('ws://localhost:8000/chat/')
Dies kann der Pfad sein, der vom URI der Web-Socket-Verbindung oder der http-Anforderungsmethode zu uns gekommen ist. Dies kann ein beliebiges Nachrichtenfeld aus dem Kanal sein, z. B. für E-Mails: Text, Text, Durchschlag, was auch immer. Die Anzahl der Schlüsselwortargumente für die Route ist beliebig.
Mit Routing können Sie verschachtelte Routen erstellen. Wenn mehrere Verbraucher durch einige gemeinsame Merkmale bestimmt werden, ist es zweckmäßig, sie zu gruppieren und alle gleichzeitig zur Route hinzuzufügen.
from channels import route, include blog_routes = [ route ( 'websocket.connect', blog, path = r'^/stream/') , ] routing = [ include (blog_routes, path= r'^/blog' ), ]
Multiplexing
Wenn wir mehrere Web-Sockets öffnen, hat jeder einen anderen URI und wir können mehrere Handler daran hängen. Aber um ehrlich zu sein, sieht es nicht nach einem technischen Ansatz aus, mehrere Verbindungen zu öffnen, um im Backend etwas Schönes zu tun.
Daher ist es möglich, mehrere Handler an einem Web-Socket aufzurufen. Wir definieren einen solchen WebsocketDemultiplexer, der nach dem Konzept des Streams innerhalb eines einzelnen Web-Sockets arbeitet. Über diesen Stream wird Ihre Nachricht auf einen anderen Kanal umgeleitet.
from channels import WebsocketDemultiplexer class Demultiplexer (WebsocketDemultiplexer) : mapping = { 'intval': 'binding.intval', }
Beim Routing wird der Multiplexer auf dieselbe Weise hinzugefügt wie bei jeder anderen deklarativen Consumer-Routenklasse.
from channels import route_class, route from .consumers import Demultiplexer, ws_message channel_routing = [ route_class (Demultiplexer, path='^/binding/') , route ('binding.intval', ws_message ) , ]
Das Stream-Argument wird der Nachricht hinzugefügt, damit der Multiplexer herausfinden kann, wo die angegebene Nachricht abgelegt werden soll. Das Payload-Argument enthält alles, was in den Kanal gelangt, nachdem der Multiplexer ihn verarbeitet hat.
Es ist sehr wichtig zu beachten, dass in der Kanalebene die Nachricht
zweimal angezeigt wird : vor dem Multiplexer und nach dem Multiplexer. Sobald Sie den Multiplexer verwenden, fügen Sie Ihren Anforderungen automatisch eine Latenz hinzu.
{ "stream" : "intval", "payload" : { … } }
Sitzungen
Jeder Kanal hat seine eigenen Sitzungen. Dies ist beispielsweise sehr praktisch, um den Status zwischen Aufrufen von Handlern zu speichern. Sie können sie nach Antwortkanal gruppieren, da dies eine Kennung ist, die dem Benutzer gehört. Die Sitzung wird in derselben Engine wie die reguläre http-Sitzung gespeichert. Signierte Cookies werden aus offensichtlichen Gründen nicht unterstützt, sie befinden sich einfach nicht im Web-Socket.
from channels.sessions import channel_session @channel_session def ws_connect(message) : room=message.content ['path'] message.channel_session ['room'] = room Croup ('chat-%s' % room).add ( message.reply_channel )
Während der Verbindung können Sie eine http-Sitzung abrufen und in Ihrem Consumer verwenden. Im Rahmen des Verhandlungsprozesses beim Einrichten einer Web-Socket-Verbindung werden Cookies an den Benutzer gesendet. Dementsprechend können Sie eine Benutzersitzung abrufen und das Benutzerobjekt abrufen, das Sie zuvor in Django verwendet haben, als würden Sie mit der Ansicht arbeiten.
from channels.sessions import http_session_user @http_session_user def ws_connect(message) : message.http_session ['room'] = room if message.user.username : …
Nachrichtenreihenfolge
Kanäle können ein sehr wichtiges Problem lösen. Wenn wir eine Verbindung zu einem Web-Socket herstellen und sofort senden, führt dies dazu, dass die beiden Ereignisse - WebSocket Connect und WebSocket Receive - zeitlich sehr nahe beieinander liegen. Es ist sehr wahrscheinlich, dass Verbraucher für diese Web-Sockets parallel ausgeführt werden. Das Debuggen wird viel Spaß machen.
Mit Django-Kanälen können Sie zwei Arten von Sperren eingeben:
- Einfache Verriegelung . Mithilfe des Sitzungsmechanismus garantieren wir, dass wir keine Nachricht auf Web-Sockets verarbeiten, bis der Verbraucher zum Empfang einer Nachricht verarbeitet wird. Nachdem die Verbindung hergestellt wurde, ist die Reihenfolge beliebig, eine parallele Ausführung ist möglich.
- Hard Lock - Es wird jeweils nur ein Verbraucher eines bestimmten Benutzers ausgeführt. Dies ist ein Overhead für die Synchronisation, da eine langsame Sitzungs-Engine verwendet wird. Trotzdem gibt es eine solche Möglichkeit.
from channels.generic import WebsocketConsumer class MyConsumer(WebsocketConsumer) : http_user = True slight_ordering = True strict_ordering = False def connection_groups (self, **kwargs) : return ['chat']
Um dies zu schreiben, gibt es dieselben Dekorateure, die wir zuvor in http session, channel session gesehen haben. Im deklarativen Consumer können Sie einfach Attribute schreiben. Sobald Sie diese schreiben, gilt dies automatisch für alle Methoden dieses Consumer.
Datenbindung
Zu einer Zeit wurde Meteor berühmt für Datenbindung.
Wir öffnen zwei Browser, gehen zur gleichen Seite und klicken in einem von ihnen auf die Bildlaufleiste. Gleichzeitig ändert die Bildlaufleiste im zweiten Browser auf dieser Seite ihren Wert. Das ist cool.
class IntegerValueBinding (WebsocketBinding) : model = IntegerValue stream = intval' fields= ['name', 'value'] def group_names (self, instance, action ) : return ['intval-updates'] def has_permission (self, user, action, pk) : return True
Django macht jetzt genau das Gleiche.
Dies wird mithilfe von Hooks implementiert, die von
Django Signals bereitgestellt werden. Wenn für das Modell eine Bindung definiert ist, werden alle Verbindungen, die sich in der Gruppe für diese Instanz des Modells befinden, über jedes Ereignis benachrichtigt. Wir haben ein Modell erstellt, das Modell geändert, es gelöscht - all dies ist eine Warnung. Die Benachrichtigung erfolgt in den angegebenen Feldern: Der Wert dieses Feldes hat sich geändert - es wird eine Nutzlast gebildet, die über den Web-Socket gesendet wird. Das ist bequem.
Es ist wichtig zu verstehen, dass, wenn wir in unserem Beispiel ständig auf die Bildlaufleiste klicken, ständig Nachrichten gesendet werden und das Modell gespeichert wird. Dies funktioniert bis zu einer bestimmten Last, dann liegt alles an der Basis an.
Redis Schicht
Lassen Sie uns etwas mehr darüber sprechen, wie die beliebteste Channel-Ebene für die Produktion angeordnet ist - Redis.
Es ist gut arrangiert:
- arbeitet mit synchronen Verbindungen auf der Ebene der Arbeiter;
- sehr freundlich zu Twisted, verlangsamt sich nicht, wo es besonders notwendig ist, dh auf Ihrem Front-End-Server;
- MSGPACK wird zum Serialisieren von Nachrichten in Redis verwendet, wodurch der Platzbedarf für jede Nachricht verringert wird.
- Sie können die Last auf mehrere Redis-Instanzen verteilen. Sie wird automatisch mithilfe des konsistenten Hash-Algorithmus gemischt. Somit verschwindet ein einzelner Fehlerpunkt.
Ein Kanal ist nur eine ID-Liste von Redis. By id ist der Wert einer bestimmten Nachricht. Dies geschieht, damit Sie die Lebensdauer jeder Nachricht und jedes Kanals separat steuern können. Dies ist im Prinzip logisch.
>> SET "b6dc0dfce" " \x81\xa4text\xachello" >> RPUSH "websocket.send!sGOpfny" "b6dc0dfce" >> EXPIRE "b6dc0dfce" "60" >> EXPIRE "websocket.send!sGOpfny" "61"
Gruppen werden durch sortierte Mengen implementiert. Die Verteilung an Gruppen erfolgt innerhalb des Lua-Skripts - dies ist sehr schnell.
>> type group:chat zset >> ZRANGE group:chat 0 1 WITHSCORES 1) "websocket.send!sGOpfny" 2) "1476199781.8159261"
Probleme
Mal sehen, welche Probleme dieser Ansatz hat.
Rückruf Hölle
Das erste Problem ist die neu erfundene Rückrufhölle. Es ist sehr wichtig zu verstehen, dass die meisten Probleme mit den Kanälen, auf die Sie stoßen werden, stilvoll sind: Dem Verbraucher kamen Argumente, die er nicht erwartet hatte. Woher sie kamen, wer sie auf Redis setzte - all dies ist eine zweifelhafte Ermittlungsaufgabe. Debuggen verteilter Systeme im Allgemeinen für Willensstarke. AsyncIO löst dieses Problem.
Sellerie
Im Internet schreiben sie, dass Django Channels ein Ersatz für Sellerie ist.

Ich habe schlechte Nachrichten für dich - nein, das ist es nicht.
In Kanälen:
- Kein erneuter Versuch, Sie können die Ausführung eines Handlers nicht verzögern.
- Keine Leinwand - nur ein Rückruf. Sellerie bietet auch Gruppen, Ketten, meinen Lieblingsakkord, der nach paralleler Ausführung von Gruppen einen weiteren Rückruf mit Synchronisation verursacht. All dies ist nicht in Kanälen;
- Es gibt keine Einstellung der Ankunftszeit von Nachrichten, einige Systeme ohne diese sind einfach nicht zu entwerfen.
Ich sehe die Zukunft als offizielle Unterstützung für die gemeinsame Nutzung von Kanälen und Sellerie mit minimalen Kosten und minimalem Aufwand. Aber Django Channels ist kein Ersatz für Sellerie.
Django für modernes Web
Django Channels ist Django für das moderne Web. Dies ist derselbe Django, den wir alle gewohnt sind: synchron, deklarativ, mit vielen Batterien. Django Channels ist nur plus eine Batterie. Sie müssen immer verstehen, wo Sie es verwenden und ob es sich lohnt, es zu tun. Wenn Django im Projekt nicht benötigt wird, werden dort auch keine Kanäle benötigt. Sie sind nur in solchen Projekten nützlich, in denen Django gerechtfertigt ist.
Moskau Python Conf ++
Eine professionelle Konferenz für Python-Entwickler erreicht ein neues Niveau - am 22. und 23. Oktober 2018 werden wir die 600 besten Python-Programmierer in Russland zusammenbringen, die interessantesten Berichte präsentieren und natürlich mit Unterstützung des Ontiko-Teams eine Umgebung für die Vernetzung in den besten Traditionen der Moskauer Python-Community schaffen.
Wir laden Experten ein, einen Bericht zu erstellen. Das Programmkomitee arbeitet bereits und nimmt Bewerbungen bis zum 7. September entgegen.
Für die Teilnehmer wird ein Online-Brainstorming-Programm durchgeführt. Sie können diesem Dokument oder den Rednern, deren Reden für Sie von Interesse sind, sofort fehlende Themen hinzufügen. Das Dokument wird in der Tat aktualisiert, sobald Sie die Programmbildung verfolgen können.