Neste tutorial, usaremos o Django Channels para criar um aplicativo em tempo real que atualiza uma lista de usuários conforme eles entram e saem.
Com WebSockets (via Django Channels) gerenciando a comunicação entre o cliente e o servidor, sempre que um usuário for autenticado, um evento será transmitido para todos os outros usuários conectados. A tela de cada usuário mudará automaticamente, sem que eles precisem recarregar seus navegadores.
OBSERVAÇÃO: Recomendamos que você tenha alguma experiência com Django antes de iniciar este tutorial. Além disso, você deve estar familiarizado com o conceito de WebSockets.
Bônus grátis: Clique aqui para obter acesso a um Guia de Recursos de Aprendizagem Django (PDF) gratuito que mostra dicas e truques, bem como armadilhas comuns a serem evitadas ao construir aplicativos web Python + Django.
Nosso aplicativo usa:
- Python (v3.6.0)
- Django (v1.10.5)
- Canais do Django (v1.0.3)
- Redis (v3.2.8)
Objetivos
Ao final deste tutorial, você será capaz de…
- Adicione suporte a soquetes da Web a um projeto Django por meio de Canais Django
- Configure uma conexão simples entre o Django e um servidor Redis
- Implementar autenticação básica de usuário
- Aproveite o Django Signals para agir quando um usuário fizer login ou logout
Primeiros passos
Primeiro, crie um novo ambiente virtual para isolar as dependências do nosso projeto:
$ mkdir django-example-channels
$ cd django-example-channels
$ python3.6 -m venv env
$ source env/bin/activate
(env)$
Instale Django, Django Channels e ASGI Redis e, em seguida, crie um novo projeto e aplicativo Django:
(env)$ pip install django==1.10.5 channels==1.0.2 asgi_redis==1.0.0
(env)$ django-admin.py startproject example_channels
(env)$ cd example_channels
(env)$ python manage.py startapp example
(env)$ python manage.py migrate
OBSERVAÇÃO: Durante o curso deste tutorial, criaremos uma variedade de arquivos e pastas diferentes. Por favor, consulte a estrutura de pastas do repositório do projeto se você ficar preso.
Em seguida, baixe e instale o Redis. Se você estiver em um Mac, recomendamos usar o Homebrew:
$ brew install redis
Inicie o servidor Redis em uma nova janela de terminal e verifique se ele está rodando em sua porta padrão, 6379. O número da porta será importante quando dissermos ao Django como se comunicar com o Redis.
Conclua a configuração atualizando
INSTALLED_APPS
no settings.py do projeto Arquivo:INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'channels',
'example',
]
Em seguida, configure os
CHANNEL_LAYERS
definindo um back-end e roteamento padrão:CHANNEL_LAYERS = {
'default': {
'BACKEND': 'asgi_redis.RedisChannelLayer',
'CONFIG': {
'hosts': [('localhost', 6379)],
},
'ROUTING': 'example_channels.routing.channel_routing',
}
}
Isso usa um back-end Redis que também é necessário na produção.
WebSockets 101
Normalmente, o Django usa HTTP para se comunicar entre o cliente e o servidor:
- O cliente envia uma solicitação HTTP ao servidor.
- O Django analisa a solicitação, extrai uma URL e a corresponde a uma visualização.
- A visualização processa a solicitação e retorna uma resposta HTTP ao cliente.
Ao contrário do HTTP, o protocolo WebSockets permite comunicação bidirecional, o que significa que o servidor pode enviar dados para o cliente sem ser solicitado pelo usuário. Com HTTP, apenas o cliente que fez uma solicitação recebe uma resposta. Com WebSockets, o servidor pode se comunicar com vários clientes simultaneamente. Como veremos mais adiante neste tutorial, enviamos mensagens WebSockets usando o
ws://
prefixo, em oposição a http://
.
OBSERVAÇÃO: Antes de mergulhar, revise rapidamente a documentação de Conceitos de Canais.
Consumidores e Grupos
Vamos criar nosso primeiro consumidor, que trata das conexões básicas entre o cliente e o servidor. Crie um novo arquivo chamado example_channels/example/consumers.py :
from channels import Group
def ws_connect(message):
Group('users').add(message.reply_channel)
def ws_disconnect(message):
Group('users').discard(message.reply_channel)
Os consumidores são a contrapartida das visualizações do Django. Qualquer usuário que se conectar ao nosso aplicativo será adicionado ao grupo “usuários” e receberá as mensagens enviadas pelo servidor. Quando o cliente se desconecta do nosso aplicativo, o canal é removido do grupo e o usuário deixa de receber mensagens.
Em seguida, vamos configurar as rotas, que funcionam quase da mesma maneira que a configuração de URL do Django, adicionando o seguinte código a um novo arquivo chamado example_channels/routing.py :
from channels.routing import route
from example.consumers import ws_connect, ws_disconnect
channel_routing = [
route('websocket.connect', ws_connect),
route('websocket.disconnect', ws_disconnect),
]
Então, definimos
channel_routing
em vez de urlpatterns
e route()
em vez de url()
. Observe que vinculamos nossas funções de consumidor a WebSockets. Modelos
Vamos escrever um HTML que possa se comunicar com nosso servidor por meio de um WebSocket. Crie uma pasta “templates” dentro de “example” e então adicione uma pasta “example” dentro de “templates” - “example_channels/example/templates/example”.
Adicione um _base.html Arquivo:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
<title>Example Channels</title>
</head>
<body>
<div class="container">
<br>
{% block content %}{% endblock content %}
</div>
<script src="//code.jquery.com/jquery-3.1.1.min.js"></script>
{% block script %}{% endblock script %}
</body>
</html>
E user_list.html :
{% extends 'example/_base.html' %}
{% block content %}{% endblock content %}
{% block script %}
<script>
var socket = new WebSocket('ws://' + window.location.host + '/users/');
socket.onopen = function open() {
console.log('WebSockets connection created.');
};
if (socket.readyState == WebSocket.OPEN) {
socket.onopen();
}
</script>
{% endblock script %}
Agora, quando o cliente abrir com sucesso uma conexão com o servidor usando um WebSocket, veremos uma mensagem de confirmação impressa no console.
Visualizações
Configure uma visualização de suporte do Django para renderizar nosso modelo dentro de example_channels/example/views.py :
from django.shortcuts import render
def user_list(request):
return render(request, 'example/user_list.html')
Adicione o URL a example_channels/example/urls.py :
from django.conf.urls import url
from example.views import user_list
urlpatterns = [
url(r'^$', user_list, name='user_list'),
]
Atualize também o URL do projeto em example_channels/example_channels/urls.py :
from django.conf.urls import include, url
from django.contrib import admin
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^', include('example.urls', namespace='example')),
]
Teste
Pronto para testar?
(env)$ python manage.py runserver
OBSERVAÇÃO: Você também pode executarpython manage.py runserver --noworker
epython manage.py runworker
em dois terminais diferentes para testar a interface e os servidores de trabalho como dois processos separados. Ambos os métodos funcionam!
Ao visitar http://localhost:8000/, você deverá ver a mensagem de conexão impressa no terminal:
[2017/02/19 23:24:57] HTTP GET / 200 [0.02, 127.0.0.1:52757]
[2017/02/19 23:24:58] WebSocket HANDSHAKING /users/ [127.0.0.1:52789]
[2017/02/19 23:25:03] WebSocket DISCONNECT /users/ [127.0.0.1:52789]
Autenticação do usuário
Agora que provamos que podemos abrir uma conexão, nosso próximo passo é lidar com a autenticação do usuário. Lembre-se:queremos que um usuário possa fazer login em nosso aplicativo e ver uma lista de todos os outros usuários inscritos no grupo desse usuário. Primeiro, precisamos de uma maneira para os usuários criarem contas e fazerem login. Comece criando uma página de login simples que permitirá que um usuário se autentique com um nome de usuário e senha.
Crie um novo arquivo chamado log_in.html dentro de “example_channels/example/templates/example”:
{% extends 'example/_base.html' %}
{% block content %}
<form action="{% url 'example:log_in' %}" method="post">
{% csrf_token %}
{% for field in form %}
<div>
{{ field.label_tag }}
{{ field }}
</div>
{% endfor %}
<button type="submit">Log in</button>
</form>
<p>Don't have an account? <a href="{% url 'example:sign_up' %}">Sign up!</a></p>
{% endblock content %}
Em seguida, atualize example_channels/example/views.py igual a:
from django.contrib.auth import login, logout
from django.contrib.auth.forms import AuthenticationForm
from django.core.urlresolvers import reverse
from django.shortcuts import render, redirect
def user_list(request):
return render(request, 'example/user_list.html')
def log_in(request):
form = AuthenticationForm()
if request.method == 'POST':
form = AuthenticationForm(data=request.POST)
if form.is_valid():
login(request, form.get_user())
return redirect(reverse('example:user_list'))
else:
print(form.errors)
return render(request, 'example/log_in.html', {'form': form})
def log_out(request):
logout(request)
return redirect(reverse('example:log_in'))
O Django vem com formulários que suportam a funcionalidade de autenticação comum. Podemos usar o
AuthenticationForm
para lidar com o login do usuário. Este formulário verifica o nome de usuário e a senha fornecidos e retorna um User
objeto se um usuário validado for encontrado. Efetuamos login no usuário validado e o redirecionamos para nossa página inicial. Um usuário também deve ter a capacidade de fazer logout do aplicativo, então criamos uma visualização de logout que fornece essa funcionalidade e, em seguida, leva o usuário de volta à tela de login. Em seguida, atualize example_channels/example/urls.py :
from django.conf.urls import url
from example.views import log_in, log_out, user_list
urlpatterns = [
url(r'^log_in/$', log_in, name='log_in'),
url(r'^log_out/$', log_out, name='log_out'),
url(r'^$', user_list, name='user_list')
]
Também precisamos de uma maneira de criar novos usuários. Crie uma página de inscrição da mesma maneira que o login, adicionando um novo arquivo chamado sign_up.html para “example_channels/example/templates/example”:
{% extends 'example/_base.html' %}
{% block content %}
<form action="{% url 'example:sign_up' %}" method="post">
{% csrf_token %}
{% for field in form %}
<div>
{{ field.label_tag }}
{{ field }}
</div>
{% endfor %}
<button type="submit">Sign up</button>
<p>Already have an account? <a href="{% url 'example:log_in' %}">Log in!</a></p>
</form>
{% endblock content %}
Observe que a página de login tem um link para a página de inscrição e a página de inscrição tem um link para o login.
Adicione a seguinte função às visualizações:
def sign_up(request):
form = UserCreationForm()
if request.method == 'POST':
form = UserCreationForm(data=request.POST)
if form.is_valid():
form.save()
return redirect(reverse('example:log_in'))
else:
print(form.errors)
return render(request, 'example/sign_up.html', {'form': form})
Usamos outro formulário interno para criação de usuários. Após a validação bem-sucedida do formulário, redirecionamos para a página de login.
Certifique-se de importar o formulário:
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
Atualizar example_channels/example/urls.py novamente:
from django.conf.urls import url
from example.views import log_in, log_out, sign_up, user_list
urlpatterns = [
url(r'^log_in/$', log_in, name='log_in'),
url(r'^log_out/$', log_out, name='log_out'),
url(r'^sign_up/$', sign_up, name='sign_up'),
url(r'^$', user_list, name='user_list')
]
Neste ponto, precisamos criar um usuário. Execute o servidor e visite
http://localhost:8000/sign_up/
no seu navegador. Preencha o formulário com um nome de usuário e senha válidos e envie-o para criar nosso primeiro usuário.
OBSERVAÇÃO: Tente usarmichael
como o nome de usuário ejohnson123
como a senha.
A
sign_up
view nos redireciona para o log_in
view, e a partir daí podemos autenticar nosso usuário recém-criado. Após efetuarmos login, podemos testar nossas novas visualizações de autenticação.
Use o formulário de inscrição para criar vários novos usuários em preparação para a próxima seção.
Alertas de login
Temos a autenticação básica do usuário funcionando, mas ainda precisamos exibir uma lista de usuários e precisamos que o servidor informe ao grupo quando um usuário faz login e logout. Precisamos editar nossas funções de consumidor para que enviem uma mensagem logo após um cliente se conecta e logo antes de um cliente se desconectar. Os dados da mensagem incluirão o nome de usuário do usuário e o status da conexão.
Atualizar example_channels/example/consumers.py igual a:
import json
from channels import Group
from channels.auth import channel_session_user, channel_session_user_from_http
@channel_session_user_from_http
def ws_connect(message):
Group('users').add(message.reply_channel)
Group('users').send({
'text': json.dumps({
'username': message.user.username,
'is_logged_in': True
})
})
@channel_session_user
def ws_disconnect(message):
Group('users').send({
'text': json.dumps({
'username': message.user.username,
'is_logged_in': False
})
})
Group('users').discard(message.reply_channel)
Observe que adicionamos decoradores às funções para obter o usuário da sessão do Django. Além disso, todas as mensagens devem ser serializáveis em JSON, portanto, despejamos nossos dados em uma string JSON.
Em seguida, atualize example_channels/example/templates/example/user_list.html :
{% extends 'example/_base.html' %}
{% block content %}
<a href="{% url 'example:log_out' %}">Log out</a>
<br>
<ul>
{% for user in users %}
<!-- NOTE: We escape HTML to prevent XSS attacks. -->
<li data-username="{{ user.username|escape }}">
{{ user.username|escape }}: {{ user.status|default:'Offline' }}
</li>
{% endfor %}
</ul>
{% endblock content %}
{% block script %}
<script>
var socket = new WebSocket('ws://' + window.location.host + '/users/');
socket.onopen = function open() {
console.log('WebSockets connection created.');
};
socket.onmessage = function message(event) {
var data = JSON.parse(event.data);
// NOTE: We escape JavaScript to prevent XSS attacks.
var username = encodeURI(data['username']);
var user = $('li').filter(function () {
return $(this).data('username') == username;
});
if (data['is_logged_in']) {
user.html(username + ': Online');
}
else {
user.html(username + ': Offline');
}
};
if (socket.readyState == WebSocket.OPEN) {
socket.onopen();
}
</script>
{% endblock script %}
Em nossa página inicial, expandimos nossa lista de usuários para exibir uma lista de usuários. Armazenamos o nome de usuário de cada usuário como um atributo de dados para facilitar a localização do item do usuário no DOM. Também adicionamos um ouvinte de eventos ao nosso WebSocket que pode manipular mensagens do servidor. Quando recebemos uma mensagem, analisamos os dados JSON, encontramos o
<li>
elemento para o determinado usuário e atualizar o status desse usuário. O Django não rastreia se um usuário está logado, então precisamos criar um modelo simples para fazer isso por nós. Crie um
LoggedInUser
modelo com uma conexão um-para-um para nosso User
modelo em example_channels/example/models.py :from django.conf import settings
from django.db import models
class LoggedInUser(models.Model):
user = models.OneToOneField(
settings.AUTH_USER_MODEL, related_name='logged_in_user')
Nosso aplicativo criará um
LoggedInUser
instância quando um usuário fizer login e o aplicativo excluirá a instância quando o usuário fizer logout. Faça a migração do esquema e depois migre nosso banco de dados para aplicar as alterações.
(env)$ python manage.py makemigrations
(env)$ python manage.py migrate
Em seguida, atualize nossa visualização de lista de usuários emexample_channels/example/views.py , para recuperar uma lista de usuários a serem renderizados:
from django.contrib.auth import get_user_model, login, logout
from django.contrib.auth.decorators import login_required
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
from django.core.urlresolvers import reverse
from django.shortcuts import render, redirect
User = get_user_model()
@login_required(login_url='/log_in/')
def user_list(request):
"""
NOTE: This is fine for demonstration purposes, but this should be
refactored before we deploy this app to production.
Imagine how 100,000 users logging in and out of our app would affect
the performance of this code!
"""
users = User.objects.select_related('logged_in_user')
for user in users:
user.status = 'Online' if hasattr(user, 'logged_in_user') else 'Offline'
return render(request, 'example/user_list.html', {'users': users})
def log_in(request):
form = AuthenticationForm()
if request.method == 'POST':
form = AuthenticationForm(data=request.POST)
if form.is_valid():
login(request, form.get_user())
return redirect(reverse('example:user_list'))
else:
print(form.errors)
return render(request, 'example/log_in.html', {'form': form})
@login_required(login_url='/log_in/')
def log_out(request):
logout(request)
return redirect(reverse('example:log_in'))
def sign_up(request):
form = UserCreationForm()
if request.method == 'POST':
form = UserCreationForm(data=request.POST)
if form.is_valid():
form.save()
return redirect(reverse('example:log_in'))
else:
print(form.errors)
return render(request, 'example/sign_up.html', {'form': form})
Se um usuário tiver um
LoggedInUser
associado , registramos o status do usuário como "Online" e, caso contrário, o usuário é "Offline". Também adicionamos um @login_required
decorador para nossas visualizações de lista de usuários e de logout para restringir o acesso apenas a usuários registrados. Adicione as importações também:
from django.contrib.auth import get_user_model, login, logout
from django.contrib.auth.decorators import login_required
Neste ponto, os usuários podem fazer login e logout, o que acionará o servidor para enviar mensagens para o cliente, mas não temos como saber quais usuários estão conectados quando o usuário faz o primeiro login. mudanças de estado. É aqui que o
LoggedInUser
entra em jogo, mas precisamos de uma maneira de criar um LoggedInUser
instância quando um usuário faz login e excluí-lo quando esse usuário faz logout. A biblioteca Django inclui um recurso conhecido como sinais que transmite notificações quando certas ações ocorrem. Os aplicativos podem ouvir essas notificações e, em seguida, agir sobre elas. Podemos explorar dois sinais internos úteis (
user_logged_in
e user_logged_out
) para lidar com nosso LoggedInUser
comportamento. Em “example_channels/example”, adicione um novo arquivo chamado signals.py :
from django.contrib.auth import user_logged_in, user_logged_out
from django.dispatch import receiver
from example.models import LoggedInUser
@receiver(user_logged_in)
def on_user_login(sender, **kwargs):
LoggedInUser.objects.get_or_create(user=kwargs.get('user'))
@receiver(user_logged_out)
def on_user_logout(sender, **kwargs):
LoggedInUser.objects.filter(user=kwargs.get('user')).delete()
Temos que disponibilizar os sinais na configuração do nosso aplicativo, example_channels/example/apps.py :
from django.apps import AppConfig
class ExampleConfig(AppConfig):
name = 'example'
def ready(self):
import example.signals
Atualizar example_channels/example/__init__.py também:
default_app_config = 'example.apps.ExampleConfig'
Verificação de integridade
Agora terminamos de codificar e estamos prontos para conectar ao nosso servidor com vários usuários para testar nosso aplicativo.
Execute o servidor Django, faça login como usuário e visite a página inicial. Devemos ver uma lista de todos os usuários em nosso aplicativo, cada um com o status "Offline". Em seguida, abra uma nova janela anônima e faça login como um usuário diferente e assista a ambas as telas. Logo quando entramos, o navegador normal atualiza o status do usuário para “Online”. Na nossa janela de navegação anônima, vemos que o usuário logado também tem o status “Online”. Podemos testar os WebSockets fazendo login e logout em nossos diferentes dispositivos com vários usuários.
Observando o console do desenvolvedor no cliente e a atividade do servidor em nosso terminal, podemos confirmar que as conexões WebSocket estão sendo formadas quando um usuário faz login e destruídas quando um usuário sai.
[2017/02/20 00:15:23] HTTP POST /log_in/ 302 [0.07, 127.0.0.1:55393]
[2017/02/20 00:15:23] HTTP GET / 200 [0.04, 127.0.0.1:55393]
[2017/02/20 00:15:23] WebSocket HANDSHAKING /users/ [127.0.0.1:55414]
[2017/02/20 00:15:23] WebSocket CONNECT /users/ [127.0.0.1:55414]
[2017/02/20 00:15:25] HTTP GET /log_out/ 302 [0.01, 127.0.0.1:55393]
[2017/02/20 00:15:26] HTTP GET /log_in/ 200 [0.02, 127.0.0.1:55393]
[2017/02/20 00:15:26] WebSocket DISCONNECT /users/ [127.0.0.1:55414]
OBSERVAÇÃO :Você também pode usar o ngrok para expor o servidor local à Internet com segurança. Fazer isso permitirá que você acesse o servidor local de vários dispositivos, como seu telefone ou tablet.
Pensamentos finais
Cobrimos muito neste tutorial - Django Channels, WebSockets, autenticação de usuário, sinais e algum desenvolvimento de front-end. A principal vantagem é esta:Canais estende a funcionalidade de um aplicativo Django tradicional, permitindo-nos enviar mensagens do servidor para grupos de usuários via WebSockets.
Isso é coisa poderosa!
Pense em algumas das aplicações. Podemos criar salas de bate-papo, jogos multiplayer e aplicativos colaborativos que permitem que os usuários se comuniquem em tempo real. Até mesmo tarefas mundanas são aprimoradas com WebSockets. Por exemplo, em vez de consultar periodicamente o servidor para ver se uma tarefa de longa duração foi concluída, o servidor pode enviar uma atualização de status para o cliente quando terminar.
Este tutorial apenas arranha a superfície do que podemos fazer com o Django Channels também. Explore a documentação do Django Channels e veja o que mais você pode criar.
Bônus grátis: Clique aqui para obter acesso a um Guia de Recursos de Aprendizagem Django (PDF) gratuito que mostra dicas e truques, bem como armadilhas comuns a serem evitadas ao construir aplicativos web Python + Django.
Pegue o código final do repositório django-example-channels. Saúde!