Dans ce didacticiel, nous utiliserons Django Channels pour créer une application en temps réel qui met à jour une liste d'utilisateurs lorsqu'ils se connectent et se déconnectent.
Avec WebSockets (via Django Channels) gérant la communication entre le client et le serveur, chaque fois qu'un utilisateur est authentifié, un événement sera diffusé à tous les autres utilisateurs connectés. L'écran de chaque utilisateur changera automatiquement, sans qu'il ait à recharger son navigateur.
REMARQUE : Nous vous recommandons d'avoir une certaine expérience avec Django avant de commencer ce tutoriel. En outre, vous devez être familiarisé avec le concept de WebSockets.
Bonus gratuit : Cliquez ici pour accéder à un guide gratuit des ressources d'apprentissage Django (PDF) qui vous montre des trucs et astuces ainsi que les pièges courants à éviter lors de la création d'applications Web Python + Django.
Notre application utilise :
- Python (v3.6.0)
- Django (v1.10.5)
- Chaînes Django (v1.0.3)
- Redis (v3.2.8)
Objectifs
À la fin de ce didacticiel, vous serez en mesure de…
- Ajouter la prise en charge des sockets Web à un projet Django via Django Channels
- Configurer une connexion simple entre Django et un serveur Redis
- Mettre en œuvre l'authentification utilisateur de base
- Utilisez Django Signals pour agir lorsqu'un utilisateur se connecte ou se déconnecte
Mise en route
Tout d'abord, créez un nouvel environnement virtuel pour isoler les dépendances de notre projet :
$ mkdir django-example-channels
$ cd django-example-channels
$ python3.6 -m venv env
$ source env/bin/activate
(env)$
Installez Django, Django Channels et ASGI Redis, puis créez un nouveau projet et une nouvelle application 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
REMARQUE : Au cours de ce didacticiel, nous allons créer une variété de fichiers et de dossiers différents. Veuillez vous référer à la structure des dossiers du référentiel du projet si vous êtes bloqué.
Ensuite, téléchargez et installez Redis. Si vous êtes sur Mac, nous vous recommandons d'utiliser Homebrew :
$ brew install redis
Démarrez le serveur Redis dans une nouvelle fenêtre de terminal et assurez-vous qu'il s'exécute sur son port par défaut, 6379. Le numéro de port sera important lorsque nous indiquerons à Django comment communiquer avec Redis.
Terminez la configuration en mettant à jour INSTALLED_APPS
dans le fichier settings.py du projet fichier :
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'channels',
'example',
]
Configurez ensuite le CHANNEL_LAYERS
en définissant un backend et un routage par défaut :
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'asgi_redis.RedisChannelLayer',
'CONFIG': {
'hosts': [('localhost', 6379)],
},
'ROUTING': 'example_channels.routing.channel_routing',
}
}
Cela utilise un backend Redis qui est également nécessaire en production.
WebSocket 101
Normalement, Django utilise HTTP pour communiquer entre le client et le serveur :
- Le client envoie une requête HTTP au serveur.
- Django analyse la requête, extrait une URL, puis l'associe à une vue.
- La vue traite la requête et renvoie une réponse HTTP au client.
Contrairement à HTTP, le protocole WebSockets permet une communication bidirectionnelle, ce qui signifie que le serveur peut envoyer des données au client sans y être invité par l'utilisateur. Avec HTTP, seul le client qui a fait une requête reçoit une réponse. Avec WebSockets, le serveur peut communiquer simultanément avec plusieurs clients. Comme nous le verrons plus loin dans ce tutoriel, nous envoyons des messages WebSockets en utilisant le ws://
préfixe, par opposition à http://
.
REMARQUE : Avant de plonger, consultez rapidement la documentation Channels Concepts.
Consommateurs et groupes
Créons notre premier consommateur, qui gère les connexions de base entre le client et le serveur. Créez un nouveau fichier appelé 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)
Les consommateurs sont la contrepartie des vues Django. Tout utilisateur se connectant à notre application sera ajouté au groupe "utilisateurs" et recevra les messages envoyés par le serveur. Lorsque le client se déconnecte de notre application, la chaîne est supprimée du groupe et l'utilisateur ne reçoit plus de messages.
Ensuite, configurons les routes, qui fonctionnent presque de la même manière que la configuration d'URL Django, en ajoutant le code suivant à un nouveau fichier appelé 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),
]
Donc, nous avons défini channel_routing
au lieu de urlpatterns
et route()
au lieu de url()
. Notez que nous avons lié nos fonctions grand public à WebSockets.
Modèles
Écrivons du HTML qui peut communiquer avec notre serveur via un WebSocket. Créez un dossier "templates" dans "example" puis ajoutez un dossier "example" dans "templates" - "example_channels/example/templates/example".
Ajouter un _base.html fichier :
<!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>
Et 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 %}
Désormais, lorsque le client ouvre avec succès une connexion avec le serveur à l'aide d'un WebSocket, nous verrons un message de confirmation s'imprimer sur la console.
Vues
Configurez une vue Django prenant en charge le rendu de notre modèle dans example_channels/example/views.py :
from django.shortcuts import render
def user_list(request):
return render(request, 'example/user_list.html')
Ajoutez l'URL à 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'),
]
Mettez également à jour l'URL du projet dans 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')),
]
Tester
Prêt à tester ?
(env)$ python manage.py runserver
REMARQUE : Vous pouvez également exécuter
python manage.py runserver --noworker
etpython manage.py runworker
dans deux terminaux différents pour tester l'interface et les serveurs de travail comme deux processus distincts. Les deux méthodes fonctionnent !
Lorsque vous visitez http://localhost:8000/, vous devriez voir le message de connexion s'imprimer sur le 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]
Authentification de l'utilisateur
Maintenant que nous avons prouvé que nous pouvions ouvrir une connexion, notre prochaine étape consiste à gérer l'authentification des utilisateurs. N'oubliez pas :nous voulons qu'un utilisateur puisse se connecter à notre application et voir une liste de tous les autres utilisateurs qui sont abonnés au groupe de cet utilisateur. Tout d'abord, nous avons besoin d'un moyen pour les utilisateurs de créer des comptes et de se connecter. Commencez par créer une page de connexion simple qui permettra à un utilisateur de s'authentifier avec un nom d'utilisateur et un mot de passe.
Créez un nouveau fichier appelé log_in.html dans "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 %}
Ensuite, mettez à jour example_channels/example/views.py comme ç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'))
Django est fourni avec des formulaires prenant en charge les fonctionnalités d'authentification courantes. Nous pouvons utiliser le AuthenticationForm
pour gérer la connexion de l'utilisateur. Ce formulaire vérifie le nom d'utilisateur et le mot de passe fournis, puis renvoie un User
objet si un utilisateur validé est trouvé. Nous connectons l'utilisateur validé et le redirigeons vers notre page d'accueil. Un utilisateur doit également avoir la possibilité de se déconnecter de l'application. Nous créons donc une vue de déconnexion qui fournit cette fonctionnalité, puis ramène l'utilisateur à l'écran de connexion.
Ensuite, mettez à jour 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')
]
Nous avons également besoin d'un moyen de créer de nouveaux utilisateurs. Créez une page d'inscription de la même manière que la connexion en ajoutant un nouveau fichier appelé sign_up.html à "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 %}
Notez que la page de connexion contient un lien vers la page d'inscription et que la page d'inscription contient un lien vers la connexion.
Ajoutez la fonction suivante aux vues :
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})
Nous utilisons un autre formulaire intégré pour la création d'utilisateurs. Après validation réussie du formulaire, nous redirigeons vers la page de connexion.
Assurez-vous d'importer le formulaire :
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
Mettre à jour example_channels/example/urls.py encore :
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')
]
À ce stade, nous devons créer un utilisateur. Exécutez le serveur et visitez http://localhost:8000/sign_up/
dans votre navigateur. Remplissez le formulaire avec un nom d'utilisateur et un mot de passe valides et soumettez-le pour créer notre premier utilisateur.
REMARQUE : Essayez d'utiliser
michael
comme nom d'utilisateur etjohnson123
comme mot de passe.
Le sign_up
view nous redirige vers le log_in
vue, et à partir de là, nous pouvons authentifier notre utilisateur nouvellement créé.
Une fois connecté, nous pouvons tester nos nouvelles vues d'authentification.
Utilisez le formulaire d'inscription pour créer plusieurs nouveaux utilisateurs en préparation de la section suivante.
Alertes de connexion
L'authentification utilisateur de base fonctionne, mais nous devons toujours afficher une liste d'utilisateurs et nous avons besoin que le serveur indique au groupe quand un utilisateur se connecte et se déconnecte. Nous devons modifier nos fonctions grand public afin qu'elles envoient un message juste après un client se connecte et juste avant qu'un client ne se déconnecte. Les données du message incluront le nom d'utilisateur et l'état de connexion de l'utilisateur.
Mettre à jour example_channels/example/consumers.py comme ç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)
Notez que nous avons ajouté des décorateurs aux fonctions pour obtenir l'utilisateur de la session Django. De plus, tous les messages doivent être sérialisables en JSON, nous vidons donc nos données dans une chaîne JSON.
Ensuite, mettez à jour 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 %}
Sur notre page d'accueil, nous développons notre liste d'utilisateurs pour afficher une liste d'utilisateurs. Nous stockons le nom d'utilisateur de chaque utilisateur en tant qu'attribut de données pour faciliter la recherche de l'élément utilisateur dans le DOM. Nous ajoutons également un écouteur d'événement à notre WebSocket qui peut gérer les messages du serveur. Lorsque nous recevons un message, nous analysons les données JSON, trouvons le <li>
élément pour l'utilisateur donné et mettre à jour le statut de cet utilisateur.
Django ne sait pas si un utilisateur est connecté, nous devons donc créer un modèle simple pour le faire pour nous. Créer un LoggedInUser
modèle avec une connexion un à un à notre User
modèle dans 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')
Notre application créera un LoggedInUser
instance lorsqu'un utilisateur se connecte, et l'application supprimera l'instance lorsque l'utilisateur se déconnecte.
Effectuez la migration du schéma, puis migrez notre base de données pour appliquer les modifications.
(env)$ python manage.py makemigrations
(env)$ python manage.py migrate
Ensuite, mettez à jour notre vue de liste d'utilisateurs, dans example_channels/example/views.py , pour récupérer une liste d'utilisateurs à afficher :
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})
Si un utilisateur a un LoggedInUser
associé , nous enregistrons alors le statut de l'utilisateur comme "En ligne", et sinon, l'utilisateur est "Hors ligne". Nous ajoutons également un @login_required
décorateur à la fois à notre liste d'utilisateurs et à nos vues de déconnexion pour restreindre l'accès uniquement aux utilisateurs enregistrés.
Ajoutez également les importations :
from django.contrib.auth import get_user_model, login, logout
from django.contrib.auth.decorators import login_required
À ce stade, les utilisateurs peuvent se connecter et se déconnecter, ce qui déclenchera l'envoi de messages au client par le serveur, mais nous n'avons aucun moyen de savoir quels utilisateurs sont connectés lorsque l'utilisateur se connecte pour la première fois. L'utilisateur ne voit les mises à jour que lorsqu'un autre utilisateur changements de statut. C'est là que le LoggedInUser
entre en jeu, mais nous avons besoin d'un moyen de créer un LoggedInUser
instance lorsqu'un utilisateur se connecte, puis supprimez-la lorsque cet utilisateur se déconnecte.
La bibliothèque Django inclut une fonctionnalité connue sous le nom de signaux qui diffuse des notifications lorsque certaines actions se produisent. Les applications peuvent écouter ces notifications, puis agir en conséquence. Nous pouvons exploiter deux signaux intégrés utiles (user_logged_in
et user_logged_out
) pour gérer notre LoggedInUser
comportement.
Dans "example_channels/example", ajoutez un nouveau fichier appelé 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()
Nous devons rendre les signaux disponibles dans la configuration de notre application, example_channels/example/apps.py :
from django.apps import AppConfig
class ExampleConfig(AppConfig):
name = 'example'
def ready(self):
import example.signals
Mettre à jour example_channels/example/__init__.py aussi :
default_app_config = 'example.apps.ExampleConfig'
Contrôle d'intégrité
Nous avons maintenant terminé le codage et sommes prêts à nous connecter à notre serveur avec plusieurs utilisateurs pour tester notre application.
Exécutez le serveur Django, connectez-vous en tant qu'utilisateur et visitez la page d'accueil. Nous devrions voir une liste de tous les utilisateurs de notre application, chacun avec le statut "Hors ligne". Ensuite, ouvrez une nouvelle fenêtre Incognito et connectez-vous en tant qu'utilisateur différent et regardez les deux écrans. Dès que nous nous connectons, le navigateur habituel met à jour le statut de l'utilisateur sur "En ligne". De notre fenêtre de navigation privée, nous voyons que l'utilisateur connecté a également un statut de "En ligne". Nous pouvons tester les WebSockets en nous connectant et en nous déconnectant sur nos différents appareils avec différents utilisateurs.
En observant la console développeur sur le client et l'activité du serveur dans notre terminal, nous pouvons confirmer que les connexions WebSocket sont formées lorsqu'un utilisateur se connecte et détruites lorsqu'un utilisateur se déconnecte.
[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]
REMARQUE :Vous pouvez également utiliser ngrok pour exposer le serveur local à Internet en toute sécurité. Cela vous permettra d'accéder au serveur local à partir de divers appareils tels que votre téléphone ou votre tablette.
Réflexions finales
Nous avons couvert beaucoup de choses dans ce didacticiel - les canaux Django, les WebSockets, l'authentification des utilisateurs, les signaux et certains développements frontaux. Le principal avantage est le suivant :les canaux étendent les fonctionnalités d'une application Django traditionnelle en nous permettant de transmettre des messages du serveur à des groupes d'utilisateurs via WebSockets.
C'est quelque chose de puissant !
Pensez à certaines des applications. Nous pouvons créer des salons de discussion, des jeux multijoueurs et des applications collaboratives qui permettent aux utilisateurs de communiquer en temps réel. Même les tâches banales sont améliorées avec WebSockets. Par exemple, au lieu d'interroger périodiquement le serveur pour voir si une tâche de longue durée est terminée, le serveur peut transmettre une mise à jour de l'état au client lorsqu'elle se termine.
Ce didacticiel ne fait qu'effleurer la surface de ce que nous pouvons également faire avec les canaux Django. Explorez la documentation Django Channels et voyez ce que vous pouvez créer d'autre.
Bonus gratuit : Cliquez ici pour accéder à un guide gratuit des ressources d'apprentissage Django (PDF) qui vous montre des trucs et astuces ainsi que les pièges courants à éviter lors de la création d'applications Web Python + Django.
Récupérez le code final du dépôt django-example-channels. Santé !