Django Channels - la réponse au web moderne

Dans le monde de Django, le module complémentaire Django Channels gagne en popularité. Cette bibliothèque devrait apporter à Django la programmation réseau asynchrone que nous attendions. Artyom Malyshev à Moscou Python Conf 2017 a expliqué comment la première version de la bibliothèque le fait (maintenant l'auteur a déjà copié les canaux2), pourquoi le fait-il et le fait-il du tout.

Tout d'abord, Zen Zen dit que toute solution doit être la seule. Par conséquent, en Python, il y en a au moins trois chacun . Il existe déjà de nombreux frameworks asynchrones réseau:

  • Tordu
  • Eventlet
  • Gevent
  • Tornade;
  • Asyncio

Il semblerait, pourquoi écrire une autre bibliothèque et si c'est nécessaire du tout.


À propos du conférencier: Artyom Malyshev est un développeur Python indépendant. Il est engagé dans le développement de systèmes distribués, intervient lors de conférences sur Python. Artyom peut être trouvé par le surnom PROOFIT404 sur Github et sur les réseaux sociaux.

Django est synchrone par définition . Si nous parlons d'ORM, l'accès synchrone à la base de données pendant l'accès aux attributs, lorsque nous écrivons, par exemple, post.author.username, ne coûte rien.

De plus, Django est un framework WSGI.

WSGI


WSGI est une interface synchrone pour travailler avec des serveurs Web.

def app (environ, callback) : status, headers = '200 OK', [] callback (status, headers) return ['Hello world!\n'] 

Sa principale caractéristique est que nous avons une fonction qui prend un argument et renvoie immédiatement une valeur. C'est tout ce que le serveur Web peut attendre de nous. Pas asynchrone et ne sent pas .

Cela a été fait il y a longtemps, en 2003, lorsque le Web était simple, les utilisateurs lisaient toutes sortes de nouvelles sur Internet et se sont tournés vers les livres d'or. Il suffisait juste d'accepter la demande et de la traiter. Donnez une réponse et oubliez que cet utilisateur l'était.


Mais, pour une seconde, ce n'est pas l'année 2003, donc les utilisateurs veulent beaucoup plus de nous.

Ils veulent une application Web riche, du contenu en direct, ils veulent que l'application fonctionne très bien sur le bureau, sur l'ordinateur portable, sur d'autres sommets, sur l'horloge. Plus important encore, les utilisateurs ne veulent pas appuyer sur F5 , car, par exemple, les tablettes ne disposent pas d'un tel bouton.



Les navigateurs Web viennent naturellement à notre rencontre - ils ajoutent de nouveaux protocoles et de nouvelles fonctionnalités. Si vous et moi ne développions que l'interface, nous prendrions simplement le navigateur comme une plate-forme et utiliserions ses fonctionnalités principales, car il est prêt à nous les fournir.

Mais, pour les programmeurs backend, tout a beaucoup changé . Les sockets Web, HTTP2 et similaires sont une énorme douleur en termes d'architecture, car ce sont des connexions de longue durée avec leurs états qui doivent être gérées d'une manière ou d'une autre.


C'est le problème que Django Channels for Django essaie de résoudre. Cette bibliothèque est conçue pour vous donner la possibilité de gérer les connexions, laissant le Django Core que nous avons l'habitude de complètement inchangé.

Il est devenu une personne merveilleuse par Andrew Godwin , le propriétaire d'un terrible accent anglais qui parle très rapidement. Vous devriez le savoir par des choses comme les migrations Django Sud et Django longtemps oubliées, qui nous sont venues de la version 1.7. Depuis qu'il a corrigé les migrations pour Django, il a commencé à réparer les sockets web et HTTP2.

Comment l'a-t-il fait? Il était une fois l'image suivante sur Internet: des carrés vides, des flèches, l'inscription «Bonne architecture» - vous entrez vos technologies préférées dans ces petits carrés, vous obtenez un site Web qui évolue bien.



Andrew Godwin est entré dans un serveur dans ces boîtes, qui se tient devant et accepte toutes les demandes, qu'elles soient asynchrones, synchrones, e-mail, peu importe. Entre eux se trouve le soi-disant Channel Layer, qui stocke les messages reçus dans un format accessible au pool de travailleurs synchrones. Dès que la connexion asynchrone nous a envoyé quelque chose, nous l'enregistrons dans la couche de canal, puis le travailleur synchrone peut le récupérer à partir de là et le traiter de la même manière que n'importe quelle vue Django ou autre, de manière synchrone. Dès que le code synchrone a renvoyé une réponse à Channel Layer, le serveur asynchrone lui donnera, le diffusera, fera tout ce dont il a besoin. Ainsi, l'abstraction est effectuée.

Cela implique plusieurs implémentations, et en production, il est proposé d'utiliser Twisted comme serveur asynchrone qui implémente le frontend pour Django et Redis , qui sera le même canal de communication entre Django synchrone et Twisted asynchrone.

La bonne nouvelle: pour utiliser les canaux Django, vous n'avez pas besoin de connaître Twisted ou Redis - ce sont tous des détails d'implémentation. Votre DevOps le saura, ou vous vous rencontrerez lorsque vous réparerez une production tombée à trois heures du matin.

ASGI


L'abstraction est un protocole appelé ASGI. Il s'agit de l'interface standard qui se situe entre n'importe quelle interface réseau, serveur, qu'il s'agisse d'un protocole synchrone ou asynchrone et de votre application. Son concept principal est la chaîne.

Chaîne


Un canal est une file d'attente de messages premier entré, premier sorti qui a une durée de vie. Ces messages peuvent être remis zéro ou une fois, et ne peuvent être reçus que par un seul consommateur.

Les consommateurs


Chez Consumer, vous écrivez simplement votre code.

 def ws_message (message) : message.reply_channel.send ( { 'text': message.content ['text'], } ) 

Une fonction qui accepte un message peut envoyer plusieurs réponses ou ne pas envoyer de réponse du tout. Très similaire à la vue, la seule différence est qu'il n'y a pas de fonction de retour, nous pouvons donc parler du nombre de réponses que nous retournons de la fonction.

Nous ajoutons cette fonction au routage, par exemple, le suspendons pour recevoir un message sur une socket web.

 from channels.routing import route from myapp.consumers import ws_message channel_routing = [ route ('websocket.receive' ws_message), } 

Nous écrivons cela dans les paramètres de Django, tout comme la base de données serait prescrite.

 CHANNEL_LAYERS = { 'default': { 'BACKEND': 'asgiref.inmemory', 'ROUTING': 'myproject.routing', }, } 

Un projet peut avoir plusieurs couches de canaux, tout comme il peut y avoir plusieurs bases de données. Cette chose est très similaire au routeur db si quelqu'un l'a utilisé.

Ensuite, nous définissons notre application ASGI. Il synchronise le démarrage de Twisted et le démarrage des travailleurs synchronisés - ils ont tous besoin de cette application.

 import os from channels.asgi import get_channel_layer os.environ.setdefault( 'DJANGO_SETTINGS_MODULE', 'myproject.settings', ) channel_layer = get_channel_layer() 

Après cela, déployez le code: exécutez gunicorn, envoyez normalement une demande HTTP, de manière synchrone, avec vue, comme vous en avez l'habitude. Nous démarrons le serveur asynchrone, qui sera le devant devant notre Django synchrone, et les travailleurs qui traiteront les messages.

 $ gunicorn myproject.wsgi $ daphne myproject.asgi:channel_layer $ django-admin runworker 

Chaîne de réponse


Comme nous l'avons vu, le message a un concept comme le canal de réponse. Pourquoi est-ce nécessaire?

Canal unidirectionnel, respectivement réception WebSocket, connexion WebSocket, déconnexion WebSocket - il s'agit d'un canal commun au système pour les messages entrants. Un canal de réponse est un canal strictement lié à la connexion de l'utilisateur. Par conséquent, le message a un canal d'entrée et de sortie. Cette paire vous permet d'identifier de qui provient ce message.


Les groupes


Un groupe est une collection de canaux. Si nous envoyons un message à un groupe, il est automatiquement envoyé à tous les canaux de ce groupe. C'est pratique car personne n'aime écrire pour les boucles. De plus, l'implémentation des groupes se fait généralement à l'aide des fonctions natives de la couche Channel, c'est donc plus rapide que d'envoyer des messages un par un.

 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'], }) 

Les groupes sont également ajoutés au routage de la même manière.

 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), ] 

Et dès que la chaîne est ajoutée au groupe, la réponse ira à tous les utilisateurs connectés à notre site, et pas seulement à la réponse d'écho à nous-mêmes.

Consommateurs génériques


Ce que j'aime Django est déclaratif. De même, il existe des consommateurs déclaratifs.

Le consommateur de base est un consommateur de base, il ne peut mapper que le canal que vous avez défini sur une méthode et l'appeler.

 from channels.generic import BaseConsumer class MyComsumer (BaseConsumer) : method_mapping = { 'channel.name.here': 'method_name', } def method_name (self, message, **kwargs) : pass 

Il existe un grand nombre de consommateurs prédéfinis avec un comportement délibérément augmenté, comme WebSocket Consumer, qui détermine à l'avance qu'il gérera la connexion WebSocket, la réception WebSocket, la déconnexion WebSocket. Vous pouvez immédiatement indiquer dans quels groupes ajouter un canal de réponse, et dès que vous utilisez self.send, il comprendra s'il faut l'envoyer à un groupe ou à un utilisateur.

 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) 

Il existe également une option client WebSocket avec JSON, c'est-à-dire pas de texte, pas d'octets, mais JSON déjà analysé viendra à recevoir, ce qui est pratique.

En routage, il est ajouté de la même manière via route_class. Myapp est pris dans route_class, qui est déterminé par le consommateur, tous les canaux sont pris à partir de là et tous les canaux spécifiés dans myapp sont routés. Écrivez moins de cette façon.

Acheminement


Parlons en détail du routage et de ce qu'il nous offre.

Premièrement, ce sont des filtres.

 // app.js S = new WebSocket ('ws://localhost:8000/chat/') # routing.py route('websocket.connect', ws_connect, path=r'^/chat/$') 

Il peut s'agir du chemin qui nous est venu de l'URI de connexion de socket Web ou de la méthode de demande http. Il peut s'agir de n'importe quel champ de message du canal, par exemple pour le courrier électronique: texte, corps, copie conforme, etc. Le nombre d'arguments de mot-clé pour route est arbitraire.

Le routage vous permet de créer des itinéraires imbriqués. Si plusieurs consommateurs sont déterminés par certaines caractéristiques communes, il est pratique de les regrouper et d'ajouter tout le monde à l'itinéraire en même temps.

 from channels import route, include blog_routes = [ route ( 'websocket.connect', blog, path = r'^/stream/') , ] routing = [ include (blog_routes, path= r'^/blog' ), ] 

Multiplexage


Si nous ouvrons plusieurs sockets Web, chacun a un URI différent et nous pouvons y suspendre plusieurs gestionnaires. Mais pour être honnête, ouvrir plusieurs connexions juste pour faire quelque chose de beau sur le backend ne ressemble pas à une approche d'ingénierie.

Par conséquent, il est possible d'appeler plusieurs gestionnaires sur une même socket Web. Nous définissons un tel WebsocketDemultiplexer qui fonctionne sur le concept de flux au sein d'une seule socket Web. Grâce à ce flux, il redirigera votre message vers un autre canal.

 from channels import WebsocketDemultiplexer class Demultiplexer (WebsocketDemultiplexer) : mapping = { 'intval': 'binding.intval', } 

En routage, le multiplexeur est ajouté de la même manière que dans tout autre consommateur déclaratif route_class.

 from channels import route_class, route from .consumers import Demultiplexer, ws_message channel_routing = [ route_class (Demultiplexer, path='^/binding/') , route ('binding.intval', ws_message ) , ] 

L'argument stream est ajouté au message afin que le multiplexeur puisse déterminer où placer le message donné. L'argument de charge utile contient tout ce qui entre dans le canal une fois que le multiplexeur l'a traité.

Il est très important de noter que dans Channel Layer, le message sera obtenu deux fois : avant le multiplexeur et après le multiplexeur. Ainsi, dès que vous commencez à utiliser le multiplexeur, vous ajoutez automatiquement une latence à vos demandes.

 { "stream" : "intval", "payload" : { … } } 

Séances


Chaque chaîne a ses propres sessions. C'est une chose très pratique, par exemple, pour stocker l'état entre les appels aux gestionnaires. Vous pouvez les regrouper par canal de réponse, car il s'agit d'un identifiant qui appartient à l'utilisateur. La session est stockée dans le même moteur que la session http standard. Pour des raisons évidentes, les cookies signés ne sont pas pris en charge, ils ne sont tout simplement pas dans la socket Web.

 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 ) 

Pendant la connexion, vous pouvez obtenir une session http et l'utiliser dans votre consommateur. Dans le cadre du processus de négociation, la mise en place d'une connexion socket Web, des cookies sont envoyés à l'utilisateur. Par conséquent, vous pouvez donc obtenir une session utilisateur, obtenir l'objet utilisateur que vous utilisiez auparavant dans Django, comme si vous travailliez avec view.

 from channels.sessions import http_session_user @http_session_user def ws_connect(message) : message.http_session ['room'] = room if message.user.username : … 

Ordre des messages


Les canaux peuvent résoudre un problème très important. Si nous établissons une connexion à un socket Web et envoyons immédiatement, cela conduit au fait que les deux événements - WebSocket Connect et WebSocket Receive - sont très proches dans le temps. Il est très probable que le consommateur de ces sockets Web s'exécute en parallèle. Le débogage sera très amusant.

Les canaux Django vous permettent d'entrer un verrou de deux types:

  1. Verrouillage facile . En utilisant le mécanisme de session, nous garantissons que tant que le consommateur ne sera pas traité pour recevoir un message, nous ne traiterons aucun message sur les sockets Web. Une fois la connexion établie, l'ordre est arbitraire, une exécution parallèle est possible.
  2. Verrouillage fixe - un seul consommateur d'un utilisateur particulier est en cours d'exécution à la fois. Il s'agit d'une surcharge pour la synchronisation, car elle utilise un moteur de session lente. Néanmoins, il existe une telle opportunité.

 from channels.generic import WebsocketConsumer class MyConsumer(WebsocketConsumer) : http_user = True slight_ordering = True strict_ordering = False def connection_groups (self, **kwargs) : return ['chat'] 

Pour écrire ceci, il y a les mêmes décorateurs que nous avons vu plus tôt dans la session http, la session channel. Dans le consommateur déclaratif, vous pouvez simplement écrire des attributs, dès que vous les écrivez, cela s'appliquera automatiquement à toutes les méthodes de ce consommateur.

Liaison de données


À une certaine époque, Meteor est devenu célèbre pour la liaison de données.

Nous ouvrons deux navigateurs, allons sur la même page et dans l'un d'eux, nous cliquons sur la barre de défilement. Dans le même temps, dans le deuxième navigateur, sur cette page, la barre de défilement change sa valeur. C'est 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 fait maintenant de même.

Ceci est implémenté à l'aide de crochets fournis par Django Signals . Si la liaison est définie pour le modèle, toutes les connexions qui sont dans le groupe pour cette instance du modèle seront notifiées de chaque événement. Nous avons créé un modèle, changé le modèle, supprimé - tout cela sera un avertissement. La notification se produit sur les champs indiqués: la valeur de ce champ a changé - une charge utile est en cours de formation, envoyée via le socket web. C'est pratique.

Il est important de comprendre que si dans notre exemple, nous cliquons constamment sur la barre de défilement, les messages iront constamment et le modèle sera enregistré. Cela fonctionnera jusqu'à une certaine charge, puis tout reposera contre la base.

Redis calque


Parlons un peu plus de la façon dont la couche de canaux de production la plus populaire - Redis.

Il est bien agencé:

  • fonctionne avec des connexions synchrones au niveau des travailleurs;
  • très convivial pour Twisted, ne ralentit pas, là où c'est particulièrement nécessaire, c'est-à-dire sur votre serveur frontal;
  • MSGPACK est utilisé pour sérialiser les messages à l'intérieur de Redis, ce qui réduit l'empreinte sur chaque message;
  • vous pouvez répartir la charge sur plusieurs instances Redis, elle sera automatiquement mélangée à l'aide de l'algorithme de hachage cohérent. Ainsi, un seul point de défaillance disparaît.

Un canal est juste une liste d'id de Redis. Par id est la valeur d'un message particulier. Cette opération permet de contrôler séparément la durée de vie de chaque message et canal. En principe, c'est logique.

 >> SET "b6dc0dfce" " \x81\xa4text\xachello" >> RPUSH "websocket.send!sGOpfny" "b6dc0dfce" >> EXPIRE "b6dc0dfce" "60" >> EXPIRE "websocket.send!sGOpfny" "61" 

Les groupes sont implémentés par des ensembles triés. La distribution aux groupes est effectuée à l'intérieur du script Lua - c'est très rapide.

 >> type group:chat zset >> ZRANGE group:chat 0 1 WITHSCORES 1) "websocket.send!sGOpfny" 2) "1476199781.8159261" 

Problèmes


Voyons quels sont les problèmes de cette approche.

Enfer de rappel


Le premier problème est l'enfer de rappel nouvellement inventé. Il est très important de comprendre que la plupart des problèmes avec les chaînes que vous rencontrerez seront de style: des arguments sont venus au consommateur auxquels il ne s'attendait pas. D'où ils viennent, qui les a mis sur Redis - tout cela est une tâche d'enquête douteuse. Débogage des systèmes distribués en général pour les volontaires. AsyncIO résout ce problème.

Céleri


Sur Internet, ils écrivent que Django Channels est un remplacement de Celery.

J'ai de mauvaises nouvelles pour vous - non, ce n'est pas ça.

Dans les canaux:

  • pas de nouvelle tentative, vous ne pouvez pas retarder l'exécution d'un gestionnaire;
  • pas de toile - juste un rappel. Celery fournit également des groupes, une chaîne, mon accord préféré, qui, après avoir exécuté des groupes en parallèle, provoque un autre rappel avec synchronisation. Tout cela n'est pas dans les canaux;
  • il n'y a pas de réglage de l'heure d'arrivée des messages, certains systèmes sans cela sont tout simplement impossibles à concevoir.

Je vois l'avenir comme un support officiel pour l'utilisation conjointe des chaînes et du céleri, à un coût minimal, avec un effort minimal. Mais Django Channels ne remplace pas le céleri.

Django pour le web moderne


Django Channels est Django pour le web moderne. C'est le même Django que nous sommes tous habitués à utiliser: synchrone, déclaratif, avec beaucoup de batteries. Django Channels est juste plus une batterie. Vous devez toujours savoir où l'utiliser et si cela vaut la peine. Si Django n'est pas nécessaire dans le projet, les canaux n'y sont pas non plus nécessaires. Ils ne sont utiles que dans les projets pour lesquels Django est justifié.

Moscou Python Conf ++

Une conférence professionnelle pour les développeurs Python atteint un nouveau niveau - les 22 et 23 octobre 2018, nous réunirons les 600 meilleurs programmeurs Python en Russie, présenterons les rapports les plus intéressants et, bien sûr, créerons un environnement de réseautage dans les meilleures traditions de la communauté Python de Moscou avec le soutien de l'équipe Ontiko.

Nous invitons des experts à faire un rapport. Le comité de programme travaille déjà et accepte les candidatures jusqu'au 7 septembre.

Pour les participants, un programme de brainstorming en ligne est en cours. Vous pouvez ajouter des sujets manquants à ce document ou des conférenciers dont les discours vous intéressent. Le document sera mis à jour, en fait, tout le temps que vous pourrez suivre la formation du programme.

Source: https://habr.com/ru/post/fr418445/


All Articles