En el mundo de Django, el complemento Django Channels está ganando popularidad. Esta biblioteca debería traer a Django la programación de red asincrónica que hemos estado esperando.
Artyom Malyshev en Moscow Python Conf 2017 explicó cómo lo hace la primera versión de la biblioteca (ahora el autor ya ha comprimido los canales2), por qué lo hace y lo hace en absoluto.
En primer lugar, Zen Zen dice que cualquier solución debería ser la única. Por lo tanto,
en Python hay al menos tres cada uno . Ya hay muchos marcos asincrónicos de red:
- Torcido
- Eventlet
- Gevent
- Tornado;
- Asyncio
Parecería, por qué escribir otra biblioteca y si es necesario en absoluto.
Sobre el orador: Artyom Malyshev es un desarrollador independiente de Python. Se dedica al desarrollo de sistemas distribuidos, habla en conferencias sobre Python. Artyom se puede encontrar con el apodo
PROOFIT404 en Github y en las redes sociales.
Django es sincrónico por definición . Si estamos hablando de ORM, acceder sincrónicamente a la base de datos durante el acceso al atributo, cuando escribimos, por ejemplo, post.author.username, no cuesta nada.
Además, Django es un marco WSGI.
WSGI
WSGI es una interfaz síncrona para trabajar con servidores web.
def app (environ, callback) : status, headers = '200 OK', [] callback (status, headers) return ['Hello world!\n']
Su característica principal es que tenemos una función que toma un argumento e inmediatamente devuelve un valor. Eso es todo lo que el servidor web puede esperar de nosotros.
No asíncrono y no huele .
Esto se hizo hace mucho tiempo, en 2003, cuando la web era simple, los usuarios leían todo tipo de noticias en Internet y entraban en los libros de visitas. Bastaba con aceptar la solicitud y procesarla. Responde y olvida que este usuario era en absoluto.

Pero, por un segundo, ahora no es el año 2003, por lo que los usuarios quieren mucho más de nosotros.

Quieren una aplicación web enriquecida, contenido en vivo, quieren que la aplicación funcione bien en el escritorio, en la computadora portátil, en otras partes superiores, en el reloj. Lo más importante, los
usuarios no quieren presionar F5 , porque, por ejemplo, las tabletas no tienen dicho botón.

Los navegadores web, naturalmente, vienen a conocernos: agregan nuevos protocolos y nuevas funciones. Si usted y yo solo desarrollamos la interfaz, entonces simplemente tomaríamos el navegador como plataforma y usaríamos sus funciones principales, ya que está listo para proporcionarnos.
Pero, para los programadores de back-end, todo ha cambiado mucho . Los sockets web, HTTP2 y similares son un gran problema en términos de arquitectura, porque son conexiones duraderas con sus estados que deben manejarse de alguna manera.

Este es el problema que Django Channels for Django está tratando de resolver. Esta biblioteca está diseñada para darle la capacidad de manejar conexiones, dejando el Django Core al que estamos acostumbrados sin cambios.
Andrew Godwin lo convirtió en una persona maravillosa, el dueño de un terrible acento inglés que habla muy rápido. Debe saberlo por cosas como las olvidadas migraciones de Django South y Django, que nos llegaron de la versión 1.7. Desde que arregló las migraciones para Django, comenzó a reparar los sockets web y HTTP2.
¿Cómo lo hizo? Había una vez una imagen de ese tipo en Internet: cuadrados vacíos, flechas, la inscripción "Buena arquitectura": ingresas tus tecnologías favoritas en estos pequeños cuadrados, obtienes un sitio web que escala bien.

Andrew Godwin ingresó a un servidor en estos cuadros, que se encuentra al frente y acepta cualquier solicitud, ya sea asíncrona, sincrónica, de correo electrónico, lo que sea. Entre ellos se encuentra la llamada capa de canal, que almacena los mensajes recibidos en un formato accesible para el grupo de trabajadores sincrónicos. Tan pronto como la conexión asíncrona nos envió algo, lo registramos en la capa de canal, y luego el trabajador sincrónico puede recogerlo desde allí y procesarlo de la misma manera que cualquier vista de Django o cualquier otra cosa, sincrónicamente. Tan pronto como el código sincrónico envíe una respuesta a Channel Layer, el servidor asincrónico lo dará, transmitirá, hará lo que sea necesario. Por lo tanto, se realiza la abstracción.
Esto implica varias implementaciones, y en producción se propone utilizar
Twisted como un servidor asíncrono que implementa la interfaz para Django y
Redis , que será el mismo canal de comunicación entre Django síncrono y Twisted asíncrono.
La buena noticia: para utilizar los canales de Django, no es necesario que conozca Twisted o Redis, estos son todos detalles de implementación. Sus DevOps lo sabrán o se encontrarán cuando reparen una producción caída a las tres de la mañana.
ASGI
La abstracción es un protocolo llamado ASGI. Esta es la interfaz estándar que se encuentra entre cualquier interfaz de red, servidor, ya sea un protocolo síncrono o asíncrono y su aplicación. Su concepto principal es el canal.
Canal
Un canal es una cola de mensajes primero en entrar, primero en salir que tienen una vida útil. Estos mensajes se pueden entregar cero o una vez, y solo pueden ser recibidos por un Consumidor.
Los consumidores
En Consumer, solo está escribiendo su código.
def ws_message (message) : message.reply_channel.send ( { 'text': message.content ['text'], } )
Una función que acepta un mensaje puede enviar varias respuestas, o puede no enviar una respuesta en absoluto. Muy similar a la vista, la única diferencia es que no hay una función de retorno, por lo que podemos hablar sobre cuántas respuestas devolvemos de la función.
Agregamos esta función al enrutamiento, por ejemplo, colgarlo para recibir un mensaje en un socket web.
from channels.routing import route from myapp.consumers import ws_message channel_routing = [ route ('websocket.receive' ws_message), }
Escribimos esto en la configuración de Django, tal como se prescribirá la base de datos.
CHANNEL_LAYERS = { 'default': { 'BACKEND': 'asgiref.inmemory', 'ROUTING': 'myproject.routing', }, }
Un proyecto puede tener múltiples capas de canal, al igual que puede haber múltiples bases de datos. Esto es muy similar al enrutador db si alguien lo usó.
A continuación, definimos nuestra aplicación ASGI. Sincroniza cómo comienza Twisted y cómo comienzan los trabajadores sincronizados: todos necesitan esta aplicación.
import os from channels.asgi import get_channel_layer os.environ.setdefault( 'DJANGO_SETTINGS_MODULE', 'myproject.settings', ) channel_layer = get_channel_layer()
Después de eso, implemente el código: ejecute gunicorn, envíe de manera estándar una solicitud HTTP, sincrónicamente, con la vista, como está acostumbrado. Iniciamos el servidor asincrónico, que estará al frente de nuestro Django sincrónico, y los trabajadores que procesarán los mensajes.
$ gunicorn myproject.wsgi $ daphne myproject.asgi:channel_layer $ django-admin runworker
Canal de respuesta
Como hemos visto, el mensaje tiene un concepto como Canal de respuesta. ¿Por qué se necesita esto?
Canal unidireccional, respectivamente recepción de WebSocket, conexión de WebSocket, desconexión de WebSocket: este es un canal común al sistema para los mensajes entrantes. Un canal de respuesta es un canal que está estrictamente vinculado a la conexión del usuario. En consecuencia, el mensaje tiene un canal de entrada y salida. Este par le permite identificar de quién proviene este mensaje.

Grupos
Un grupo es una colección de canales. Si enviamos un mensaje a un grupo, se envía automáticamente a todos los canales de este grupo. Esto es conveniente porque a nadie le gusta escribir para bucles. Además, la implementación de grupos generalmente se realiza utilizando las funciones nativas de la capa del canal, por lo que es más rápido que enviar mensajes uno por uno.
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'], })
Los grupos también se agregan al enrutamiento de la misma manera.
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), ]
Y tan pronto como se agregue el canal al grupo, la respuesta se enviará a todos los usuarios que estén conectados a nuestro sitio, y no solo a la respuesta de eco para nosotros.
Consumidores genéricos
Lo que amo a Django es declarativo. Del mismo modo, hay consumidores declarativos.
Base Consumer es básico, solo puede asignar el canal que definió en algún método y llamarlo.
from channels.generic import BaseConsumer class MyComsumer (BaseConsumer) : method_mapping = { 'channel.name.here': 'method_name', } def method_name (self, message, **kwargs) : pass
Hay una gran cantidad de consumidores predefinidos con un comportamiento deliberadamente aumentado, como WebSocket Consumer, que determina de antemano que manejará la conexión WebSocket, la recepción WebSocket y la desconexión WebSocket. Puede indicar inmediatamente en qué grupos agregar un canal de respuesta, y tan pronto como use self.send, él comprenderá si debe enviar esto a un grupo oa un usuario.
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)
También hay una opción de consumidor de WebSocket con JSON, es decir, no texto, no bytes, pero JSON ya se analizará, lo cual es conveniente.
En el enrutamiento, se agrega de la misma manera a través de route_class. Myapp se toma en route_class, que se determina desde el consumidor, todos los canales se toman desde allí y todos los canales especificados en myapp se enrutan. Escribe menos de esta manera.
Enrutamiento
Hablemos en detalle sobre el enrutamiento y lo que nos proporciona.
En primer lugar, estos son filtros.
// app.js S = new WebSocket ('ws://localhost:8000/chat/')
Esta puede ser la ruta que nos llegó desde el URI de conexión de socket web, o el método de solicitud http. Puede ser cualquier campo de mensaje del canal, por ejemplo, para correo electrónico: texto, cuerpo, copia al carbón, lo que sea. El número de argumentos de palabras clave para la ruta es arbitrario.
El enrutamiento le permite hacer rutas anidadas. Si varios consumidores están determinados por algunas características comunes, es conveniente agruparlos y agregar a todos a la ruta a la vez.
from channels import route, include blog_routes = [ route ( 'websocket.connect', blog, path = r'^/stream/') , ] routing = [ include (blog_routes, path= r'^/blog' ), ]
Multiplexación
Si abrimos varios sockets web, cada uno tiene un URI diferente y podemos colgar varios manejadores en ellos. Pero para ser honesto, abrir varias conexiones solo para hacer algo hermoso en el backend no parece un enfoque de ingeniería.
Por lo tanto, es posible llamar a varios controladores en un socket web. Definimos un WebsocketDemultiplexer que funciona con el concepto de transmisión dentro de un único socket web. A través de esta transmisión, redirigirá su mensaje a otro canal.
from channels import WebsocketDemultiplexer class Demultiplexer (WebsocketDemultiplexer) : mapping = { 'intval': 'binding.intval', }
En el enrutamiento, el multiplexor se agrega de la misma manera que en cualquier otro consumidor declarativo 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 ) , ]
El argumento de flujo se agrega al mensaje para que el multiplexor pueda determinar dónde colocar el mensaje dado. El argumento de la carga útil contiene todo lo que entra en el canal después de que el multiplexor lo procesa.
Es muy importante tener en cuenta que en Channel Layer, el mensaje se obtendrá
dos veces : antes del multiplexor y después del multiplexor. Por lo tanto, tan pronto como comience a usar el multiplexor, automáticamente agregará latencia a sus solicitudes.
{ "stream" : "intval", "payload" : { … } }
Sesiones
Cada canal tiene sus propias sesiones. Esto es algo muy conveniente, por ejemplo, para almacenar el estado entre llamadas a manejadores. Puede agruparlos por canal de respuesta, ya que este es un identificador que pertenece al usuario. La sesión se almacena en el mismo motor que la sesión http normal. Por razones obvias, las cookies firmadas no son compatibles, simplemente no están en el 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 )
Durante la conexión, puede obtener una sesión http y usarla en su consumidor. Como parte del proceso de negociación, al configurar una conexión de socket web, se envían cookies al usuario. En consecuencia, por lo tanto, puede obtener una sesión de usuario, obtener el objeto de usuario que solía usar en Django antes, como si estuviera trabajando con view.
from channels.sessions import http_session_user @http_session_user def ws_connect(message) : message.http_session ['room'] = room if message.user.username : …
Orden del mensaje
Los canales pueden resolver un problema muy importante. Si establecemos una conexión a un socket web y enviamos de inmediato, esto lleva al hecho de que los dos eventos, WebSocket connect y WebSocket reciben, están muy cerca en el tiempo. Es muy probable que el consumidor de estos sockets web se ejecute en paralelo. Depurar esto será muy divertido.
Los canales de Django le permiten ingresar el bloqueo de dos tipos:
- Cerradura fácil . Usando el mecanismo de sesión, garantizamos que hasta que el consumidor sea procesado para recibir un mensaje, no procesaremos ningún mensaje en los sockets web. Una vez establecida la conexión, el orden es arbitrario, es posible la ejecución paralela.
- Bloqueo duro : solo se ejecuta un consumidor de un usuario en particular a la vez. Esto es una sobrecarga para la sincronización, ya que utiliza un motor de sesión lenta. Sin embargo, existe tal oportunidad.
from channels.generic import WebsocketConsumer class MyConsumer(WebsocketConsumer) : http_user = True slight_ordering = True strict_ordering = False def connection_groups (self, **kwargs) : return ['chat']
Para escribir esto, hay los mismos decoradores que vimos anteriormente en la sesión http, sesión de canal. En el consumidor declarativo, simplemente puede escribir atributos, tan pronto como los escriba, esto se aplicará automáticamente a todos los métodos de este consumidor.
Enlace de datos
En un momento, Meteor se hizo famoso por el enlace de datos.
Abrimos dos navegadores, vamos a la misma página y en uno de ellos hacemos clic en la barra de desplazamiento. Al mismo tiempo, en el segundo navegador, en esta página, la barra de desplazamiento cambia su valor. Esto es genial
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 ahora hace lo mismo.
Esto se implementa utilizando los ganchos proporcionados por
Django Signals . Si se define el enlace para el modelo, todas las conexiones que están en el grupo para esta instancia del modelo serán notificadas de cada evento. Creamos un modelo, cambiamos el modelo, lo eliminamos; todo esto será una advertencia. La notificación se produce en los campos indicados: el valor de este campo ha cambiado; se está formando una carga útil que se envía a través del socket web. Esto es conveniente
Es importante comprender que si en nuestro ejemplo hacemos clic constantemente en la barra de desplazamiento, los mensajes irán constantemente y el modelo se guardará. Esto funcionará hasta una cierta carga, entonces todo descansa contra la base.
Capa Redis
Hablemos un poco más sobre cómo se organiza el Channel Layer más popular para la producción: Redis.
Está bien organizado:
- trabaja con conexiones sincrónicas a nivel de trabajadores;
- muy amigable con Twisted, no se ralentiza, donde es especialmente necesario, es decir, en su servidor front-end;
- MSGPACK se usa para serializar mensajes dentro de Redis, lo que reduce la huella en cada mensaje;
- puede distribuir la carga a varias instancias de Redis, se barajará automáticamente usando el algoritmo hash consistente. Por lo tanto, un solo punto de falla desaparece.
Un canal es solo una lista de identificación de Redis. Por id es el valor de un mensaje en particular. Esto se hace para que pueda controlar la vida de cada mensaje y canal por separado. En principio, esto es lógico.
>> SET "b6dc0dfce" " \x81\xa4text\xachello" >> RPUSH "websocket.send!sGOpfny" "b6dc0dfce" >> EXPIRE "b6dc0dfce" "60" >> EXPIRE "websocket.send!sGOpfny" "61"
Los grupos se implementan por conjuntos ordenados. La distribución a grupos se realiza dentro del script Lua, esto es muy rápido.
>> type group:chat zset >> ZRANGE group:chat 0 1 WITHSCORES 1) "websocket.send!sGOpfny" 2) "1476199781.8159261"
Problemas
Veamos qué problemas tiene este enfoque.
Callback hell
El primer problema es el infierno de devolución de llamadas recién inventado. Es muy importante comprender que la mayoría de los problemas con los canales con los que se encontrará serán de estilo: llegaron al consumidor argumentos que no esperaba. De dónde vinieron, quién los puso en Redis: todo esto es una dudosa tarea de investigación. Depuración de sistemas distribuidos en general para los de carácter fuerte. AsyncIO resuelve este problema.
Apio
En Internet, escriben que Django Channels es un reemplazo para Celery.

Tengo malas noticias para ti, no, no es eso.
En canales:
- no vuelva a intentarlo, no puede retrasar la ejecución de un controlador;
- sin lienzo, solo una devolución de llamada. El apio también proporciona grupos, cadenas, mi acorde favorito, que, después de ejecutar grupos en paralelo, provoca otra devolución de llamada con sincronización. Todo esto no está en canales;
- no hay una configuración de la hora de llegada de los mensajes, algunos sistemas sin esto son simplemente imposibles de diseñar.
Veo el futuro como un soporte oficial para usar canales y apio juntos, con un costo mínimo y un esfuerzo mínimo. Pero Django Channels no es un reemplazo para el apio.
Django para web moderna
Django Channels es Django para la web moderna. Este es el mismo Django que todos estamos acostumbrados a usar: sincrónico, declarativo, con muchas baterías. Django Channels es solo más una batería. Siempre debe comprender dónde usarlo y si vale la pena hacerlo. Si no se necesita Django en el proyecto, tampoco se necesitan canales allí. Son útiles solo en aquellos proyectos en los que Django está justificado.
Moscow Python Conf ++
Una conferencia profesional para desarrolladores de Python llega a un nuevo nivel: los días 22 y 23 de octubre de 2018 reuniremos a los 600 mejores programadores de Python en Rusia, presentaremos los informes más interesantes y, por supuesto, crearemos un entorno para establecer contactos en las mejores tradiciones de la comunidad de Python de Moscú con el apoyo del equipo de Ontiko.
Invitamos a expertos a hacer un informe. El comité del programa ya está trabajando y acepta solicitudes hasta el 7 de septiembre.
Para los participantes, se está llevando a cabo un programa de lluvia de ideas en línea. Puede agregar temas faltantes a este documento u oradores de inmediato cuyos discursos le interesen. El documento se actualizará, de hecho, todo el tiempo que pueda seguir la formación del programa.