No mundo do Django, o complemento Django Channels está ganhando popularidade. Esta biblioteca deve trazer para o Django a programação de rede assíncrona que estávamos esperando.
Artyom Malyshev , do Moscow Python Conf 2017, explicou como a primeira versão da biblioteca faz isso (agora o autor já compactou os canais2), por que faz e faz.
Antes de tudo, o Zen Zen diz que qualquer solução deve ser a única. Portanto,
em Python, existem pelo menos três cada . Já existem muitas estruturas assíncronas de rede:
- Torcido
- Eventlet
- Gevent
- Tornado;
- Asyncio
Parece, por que escrever outra biblioteca e se é necessário?
Sobre o palestrante: Artyom Malyshev é um desenvolvedor independente de Python. Ele está envolvido no desenvolvimento de sistemas distribuídos, fala em conferências em Python. O Artyom pode ser encontrado com o apelido
PROOFIT404 no Github e nas redes sociais.
O Django é síncrono por definição . Se estamos falando sobre ORM, o acesso síncrono ao banco de dados durante o acesso ao atributo, quando escrevemos, por exemplo, post.author.username, não custa nada.
Além disso, o Django é uma estrutura WSGI.
WSGI
O WSGI é uma interface síncrona para trabalhar com servidores da web.
def app (environ, callback) : status, headers = '200 OK', [] callback (status, headers) return ['Hello world!\n']
Sua principal característica é que temos uma função que recebe um argumento e retorna imediatamente um valor. Isso é tudo o que o servidor da web pode esperar de nós.
Não assíncrono e não tem cheiro .
Isso foi feito há muito tempo, em 2003, quando a web era simples, os usuários liam todos os tipos de notícias na Internet e acessavam os livros de visitas. Foi o suficiente apenas para aceitar a solicitação e processá-la. Dê uma resposta e esqueça que este usuário era mesmo.

Mas, por um segundo, agora não é o ano de 2003, então os usuários querem muito mais de nós.

Eles querem aplicativos da Web ricos, conteúdo ao vivo, querem que o aplicativo funcione muito bem na área de trabalho, no laptop, em outros tops, no relógio. Mais importante, os
usuários não querem pressionar F5 , porque, por exemplo, os tablets não possuem esse botão.

Navegadores da Web naturalmente vêm nos encontrar - eles adicionam novos protocolos e novos recursos. Se você e eu apenas desenvolvêssemos o frontend, usaríamos o navegador como uma plataforma e usaríamos seus principais recursos, pois ele está pronto para fornecê-los.
Mas, para programadores de back-end, tudo mudou muito . Soquetes da Web, HTTP2 e similares são uma enorme dor em termos de arquitetura, porque são conexões duradouras com seus estados que precisam ser manipuladas de alguma forma.

Este é o problema que os canais do Django para Django estão tentando resolver. Esta biblioteca foi projetada para permitir a você lidar com conexões, deixando o Django Core a que estamos acostumados.
Ele foi feito uma pessoa maravilhosa por
Andrew Godwin , o dono de um terrível sotaque inglês que fala muito rapidamente. Você deve conhecê-lo com coisas como as há muito esquecidas Django South e Django Migrations, que vieram até nós da versão 1.7. Desde que ele corrigiu as migrações para o Django, ele começou a reparar os soquetes da web e o HTTP2.
Como ele fez isso? Era uma vez uma imagem na Internet: quadrados vazios, setas, a inscrição “Boa arquitetura” - você insere suas tecnologias favoritas nesses pequenos quadrados, obtém um site que se adapta bem.

Andrew Godwin inseriu um servidor nessas caixas, que fica na frente e aceita todos os pedidos, sejam assíncronos, síncronos, e-mail, qualquer que seja. Entre eles está a chamada camada de canal, que armazena as mensagens recebidas em um formato acessível ao pool de trabalhadores síncronos. Assim que a conexão assíncrona nos enviou algo, nós a gravamos na camada de canal e, em seguida, o trabalhador síncrono pode buscá-la e processá-la da mesma maneira que qualquer Django View ou qualquer outra coisa, de forma síncrona. Assim que o código síncrono enviar uma resposta de volta à camada de canal, o servidor assíncrono fornecerá, transmitirá e fará o que for necessário. Assim, a abstração é realizada.
Isso implica várias implementações e, na produção, é proposto o uso do
Twisted como um servidor assíncrono que implementa o frontend do Django, e do
Redis , que será o mesmo canal de comunicação entre o Django síncrono e o Twisted assíncrono.
A boa notícia: para usar os canais do Django, você não precisa conhecer Twisted ou Redis - estes são todos os detalhes da implementação. Seus DevOps saberão disso ou você se encontrará quando reparar uma produção caída às três da manhã.
ASGI
Abstração é um protocolo chamado ASGI. Essa é a interface padrão que existe entre qualquer interface de rede, servidor, seja um protocolo síncrono ou assíncrono e seu aplicativo. Seu principal conceito é o canal.
Canal
Um canal é uma fila de mensagens primeiro a entrar, primeiro a sair. Essas mensagens podem ser entregues zero ou uma vez e só podem ser recebidas por um consumidor.
Consumidores
No Consumer, você está apenas escrevendo seu código.
def ws_message (message) : message.reply_channel.send ( { 'text': message.content ['text'], } )
Uma função que aceita uma mensagem pode enviar várias respostas ou pode não enviar nenhuma resposta. Muito semelhante à visualização, a única diferença é que não há função de retorno, para que possamos falar sobre quantas respostas retornamos da função.
Nós adicionamos essa função ao roteamento, por exemplo, desligue-a para receber uma mensagem em um soquete da web.
from channels.routing import route from myapp.consumers import ws_message channel_routing = [ route ('websocket.receive' ws_message), }
Nós escrevemos isso nas configurações do Django, assim como o banco de dados seria prescrito.
CHANNEL_LAYERS = { 'default': { 'BACKEND': 'asgiref.inmemory', 'ROUTING': 'myproject.routing', }, }
Um projeto pode ter várias camadas de canal, assim como vários bancos de dados. Essa coisa é muito semelhante ao roteador db, se alguém o usou.
Em seguida, definimos nosso aplicativo ASGI. Ele sincroniza como o Twisted é iniciado e como os trabalhadores sincronizados são iniciados - todos eles precisam desse aplicativo.
import os from channels.asgi import get_channel_layer os.environ.setdefault( 'DJANGO_SETTINGS_MODULE', 'myproject.settings', ) channel_layer = get_channel_layer()
Depois disso, implemente o código: execute gunicorn, envie uma solicitação HTTP de forma síncrona, com visualização, como você está acostumado. Iniciamos o servidor assíncrono, que será a frente do nosso Django síncrono, e os trabalhadores que processarão as mensagens.
$ gunicorn myproject.wsgi $ daphne myproject.asgi:channel_layer $ django-admin runworker
Canal de resposta
Como vimos, a mensagem tem um conceito como canal de resposta. Por que isso é necessário?
Canal unidirecional, respectivamente, recebimento de WebSocket, conexão de WebSocket, desconexão de WebSocket - este é um canal comum ao sistema para mensagens recebidas. Um canal de resposta é estritamente vinculado à conexão do usuário. Consequentemente, a mensagem possui um canal de entrada e saída. Este par permite identificar de quem veio esta mensagem.

Grupos
Um grupo é uma coleção de canais. Se enviarmos uma mensagem para um grupo, ela será enviada automaticamente para todos os canais deste grupo. Isso é conveniente porque ninguém gosta de escrever para loops. Além disso, a implementação de grupos geralmente é feita usando as funções nativas da camada Channel, por isso é mais rápido do que apenas enviar mensagens uma por vez.
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'], })
Os grupos também são adicionados ao roteamento da mesma maneira.
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), ]
E assim que o canal for adicionado ao grupo, a resposta será enviada para todos os usuários conectados ao nosso site, e não apenas a resposta de eco para nós mesmos.
Consumidores genéricos
O que eu amo o Django é declarativo. Da mesma forma, existem consumidores declarativos.
O Consumidor base é básico, ele só pode mapear o canal que você definiu em algum método e chamá-lo.
from channels.generic import BaseConsumer class MyComsumer (BaseConsumer) : method_mapping = { 'channel.name.here': 'method_name', } def method_name (self, message, **kwargs) : pass
Há um grande número de consumidores predefinidos com comportamento deliberadamente aumentado, como o WebSocket Consumer, que pré-determina que ele manipulará a conexão com o WebSocket, o recebimento do WebSocket, a desconexão do WebSocket. Você pode indicar imediatamente em quais grupos adicionar um canal de resposta e, assim que usar self.send, ele entenderá se deve enviá-lo para um grupo ou para um usuário.
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)
Há também uma opção de consumidor WebSocket com JSON, ou seja, não texto, não bytes, mas o JSON já analisado chegará a receber, o que é conveniente.
No roteamento, é adicionado da mesma maneira via route_class. Myapp é obtido em route_class, que é determinado pelo consumidor, todos os canais são retirados de lá e todos os canais especificados em myapp são roteados. Escreva menos assim.
Encaminhamento
Vamos falar em detalhes sobre o roteamento e o que ele nos fornece.
Em primeiro lugar, estes são filtros.
// app.js S = new WebSocket ('ws://localhost:8000/chat/')
Esse pode ser o caminho que nos chegou a partir do URI de conexão do soquete da web ou do método de solicitação http. Pode ser qualquer campo de mensagem do canal, por exemplo, para e-mail: texto, corpo, cópia carbono, qualquer que seja. O número de argumentos de palavra-chave para a rota é arbitrário.
O roteamento permite fazer rotas aninhadas. Se vários consumidores forem determinados por algumas características comuns, é conveniente agrupá-los e adicionar todos à rota de uma só vez.
from channels import route, include blog_routes = [ route ( 'websocket.connect', blog, path = r'^/stream/') , ] routing = [ include (blog_routes, path= r'^/blog' ), ]
Multiplexação
Se abrirmos vários soquetes da Web, cada um terá um URI diferente e poderemos travar vários manipuladores. Mas, para ser sincero, abrir várias conexões apenas para fazer algo bonito no back-end não parece uma abordagem de engenharia.
Portanto, é possível chamar vários manipuladores em um soquete da web. Definimos esse WebsocketDemultiplexer que opera no conceito de fluxo em um único soquete da web. Por esse fluxo, ele redirecionará sua mensagem para outro canal.
from channels import WebsocketDemultiplexer class Demultiplexer (WebsocketDemultiplexer) : mapping = { 'intval': 'binding.intval', }
No roteamento, o multiplexador é adicionado da mesma maneira que em qualquer outro consumidor declarado 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 ) , ]
O argumento de fluxo é adicionado à mensagem para que o multiplexador possa descobrir onde colocar a mensagem fornecida. O argumento de carga útil contém tudo o que entra no canal depois que o multiplexador o processa.
É muito importante observar que, na camada de canal, a mensagem será exibida
duas vezes : antes do multiplexador e depois do multiplexador. Assim, assim que você começa a usar o multiplexador, você adiciona automaticamente latência às suas solicitações.
{ "stream" : "intval", "payload" : { … } }
Sessões
Cada canal tem suas próprias sessões. Isso é muito conveniente, por exemplo, para armazenar o estado entre chamadas para manipuladores. Você pode agrupá-los por canal de resposta, porque este é um identificador que pertence ao usuário. A sessão é armazenada no mesmo mecanismo que a sessão http normal. Por razões óbvias, os cookies assinados não são suportados, eles simplesmente não estão no soquete da 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 a conexão, você pode obter uma sessão http e usá-la em seu consumidor. Como parte do processo de negociação, configurando uma conexão de soquete da web, os cookies são enviados ao usuário. Portanto, você pode obter uma sessão de usuário, obter o objeto de usuário que você costumava usar no Django antes, como se estivesse trabalhando com o view.
from channels.sessions import http_session_user @http_session_user def ws_connect(message) : message.http_session ['room'] = room if message.user.username : …
Ordem das mensagens
Canais podem resolver um problema muito importante. Se estabelecermos uma conexão com um soquete da Web e enviarmos imediatamente, isso levará ao fato de que os dois eventos - o WebSocket connect e o WebSocket receive - estão muito próximos no tempo. É muito provável que o consumidor desses soquetes da Web seja executado em paralelo. Depurar isso será muito divertido.
Os canais do Django permitem inserir um bloqueio de dois tipos:
- Bloqueio fácil . Usando o mecanismo de sessão, garantimos que, até que o consumidor seja processado para receber uma mensagem, não processaremos nenhuma mensagem nos soquetes da web. Depois que a conexão é estabelecida, a ordem é arbitrária, a execução paralela é possível.
- Bloqueio rígido - apenas um consumidor de um usuário específico está executando por vez. Isso é uma sobrecarga para sincronização, porque usa um mecanismo de sessão lento. No entanto, existe essa oportunidade.
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 escrever isso, existem os mesmos decoradores que vimos anteriormente na sessão http, sessão de canal. No consumidor declarativo, você pode simplesmente escrever atributos, assim que os escrever, isso será aplicado automaticamente a todos os métodos desse consumidor.
Ligação de dados
Ao mesmo tempo, o Meteor ficou famoso pela ligação de dados.
Abrimos dois navegadores, vamos para a mesma página e em um deles clicamos na barra de rolagem. Ao mesmo tempo, no segundo navegador, nesta página, a barra de rolagem altera seu valor. Isso é legal.
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
O Django agora faz o mesmo.
Isso é implementado usando ganchos fornecidos pelo
Django Signals . Se a ligação for definida para o modelo, todas as conexões que estão no grupo para esta instância do modelo serão notificadas de cada evento. Criamos um modelo, alteramos o modelo, o excluímos - tudo isso será um aviso. A notificação ocorre nos campos indicados: o valor desse campo foi alterado - uma carga útil está sendo formada, enviada pelo soquete da web. Isso é conveniente.
É importante entender que, se no nosso exemplo clicarmos constantemente na barra de rolagem, as mensagens serão exibidas constantemente e o modelo será salvo. Isso funcionará até uma certa carga, e tudo ficará contra a base.
Camada Redis
Vamos falar um pouco mais sobre como é organizada a camada de canal mais popular para produção - Redis.
Está bem organizado:
- trabalha com conexões síncronas no nível dos trabalhadores;
- muito amigável ao Twisted, não diminui a velocidade, onde é especialmente necessário, ou seja, no seu servidor front-end;
- O MSGPACK é usado para serializar mensagens dentro do Redis, o que reduz o espaço ocupado em cada mensagem;
- você pode distribuir a carga para várias instâncias do Redis, ela será embaralhada automaticamente usando o algoritmo de hash consistente. Assim, um único ponto de falha desaparece.
Um canal é apenas uma lista de identificação da Redis. Por id é o valor de uma mensagem específica. Isso é feito para que você possa controlar a vida útil de cada mensagem e canal separadamente. Em princípio, isso é lógico.
>> SET "b6dc0dfce" " \x81\xa4text\xachello" >> RPUSH "websocket.send!sGOpfny" "b6dc0dfce" >> EXPIRE "b6dc0dfce" "60" >> EXPIRE "websocket.send!sGOpfny" "61"
Grupos são implementados por conjuntos classificados. A distribuição para grupos é realizada dentro do script Lua - isso é muito rápido.
>> type group:chat zset >> ZRANGE group:chat 0 1 WITHSCORES 1) "websocket.send!sGOpfny" 2) "1476199781.8159261"
Problemas
Vamos ver quais problemas essa abordagem tem.
Inferno de retorno de chamada
O primeiro problema é o inferno de retorno de chamada recém-inventado. É muito importante entender que a maioria dos problemas com os canais que você encontrará será em grande estilo: argumentos chegaram ao consumidor que ele não esperava. De onde eles vieram, quem os colocou no Redis - tudo isso é uma tarefa de investigação duvidosa. Depuração de sistemas distribuídos geralmente para os mais exigentes. O AsyncIO resolve esse problema.
Aipo
Na Internet, eles escrevem que o Django Channels substitui o aipo.

Tenho más notícias para você - não, não é isso.
Nos canais:
- sem nova tentativa, você não pode atrasar a execução de um manipulador;
- sem tela - apenas um retorno de chamada. O aipo também fornece grupos, cadeia, meu acorde favorito, que, após executar grupos em paralelo, causa outro retorno de chamada com sincronização. Tudo isso não está nos canais;
- não há configuração da hora de chegada das mensagens, alguns sistemas sem isso são simplesmente impossíveis de projetar.
Eu vejo o futuro como um suporte oficial para o uso de canais e aipo juntos, com custo mínimo e esforço mínimo. Mas os canais do Django não substituem o aipo.
Django para web moderna
Canais do Django é o Django para a web moderna. Este é o mesmo Django que estamos acostumados a usar: síncrono, declarativo, com muitas baterias. Canais Django é apenas mais uma bateria. Você sempre precisa entender onde usá-lo e se vale a pena fazê-lo. Se o Django não for necessário no projeto, os canais também não serão necessários. Eles são úteis apenas nos projetos nos quais o Django é justificado.
Moscow Python Conf ++
Uma conferência profissional para desenvolvedores de Python chega a um novo nível - nos dias 22 e 23 de outubro de 2018, reuniremos os 600 melhores programadores de Python da Rússia, apresentaremos os relatórios mais interessantes e, é claro, criaremos um ambiente de networking nas melhores tradições da comunidade Python de Moscou, com o apoio da equipe da Ontiko.
Convidamos especialistas para fazer um relatório. O comitê do programa já está trabalhando e aceita inscrições até 7 de setembro.
Para os participantes, um programa de brainstorming on-line está sendo realizado. Você pode adicionar tópicos ausentes neste documento ou palestrantes, cujos discursos são do seu interesse. O documento será atualizado, de fato, o tempo todo, você poderá acompanhar a formação do programa.