W tym samouczku użyjemy Django Channels do stworzenia aplikacji w czasie rzeczywistym, która aktualizuje listę użytkowników podczas logowania i wylogowania.
Dzięki WebSockets (poprzez kanały Django) zarządzającymi komunikacją między klientem a serwerem, za każdym razem, gdy użytkownik zostanie uwierzytelniony, zdarzenie zostanie wyemitowane do każdego innego podłączonego użytkownika. Ekran każdego użytkownika zmieni się automatycznie, bez konieczności ponownego ładowania przeglądarki.
UWAGA: Zalecamy, abyś miał trochę doświadczenia z Django przed rozpoczęciem tego samouczka. Powinieneś także znać koncepcję WebSockets.
Bezpłatny bonus: Kliknij tutaj, aby uzyskać dostęp do bezpłatnego przewodnika po zasobach edukacyjnych Django (PDF), który zawiera wskazówki i triki, a także typowe pułapki, których należy unikać podczas tworzenia aplikacji internetowych Python + Django.
Nasza aplikacja wykorzystuje:
- Python (wersja 3.6.0)
- Django (v1.10.5)
- Kanały Django (v1.0.3)
- Redis (v3.2.8)
Cele
Pod koniec tego samouczka będziesz mógł…
- Dodaj obsługę gniazd sieciowych do projektu Django za pośrednictwem kanałów Django
- Skonfiguruj proste połączenie między Django a serwerem Redis
- Zaimplementuj podstawowe uwierzytelnianie użytkownika
- Wykorzystaj sygnały Django do podjęcia działań, gdy użytkownik się loguje lub wylogowuje
Pierwsze kroki
Najpierw utwórz nowe środowisko wirtualne, aby wyizolować zależności naszego projektu:
$ mkdir django-example-channels
$ cd django-example-channels
$ python3.6 -m venv env
$ source env/bin/activate
(env)$
Zainstaluj Django, Django Channels i ASGI Redis, a następnie utwórz nowy projekt i aplikację 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
UWAGA: W trakcie tego samouczka utworzymy wiele różnych plików i folderów. Jeśli utkniesz, zapoznaj się ze strukturą folderów z repozytorium projektu.
Następnie pobierz i zainstaluj Redis. Jeśli korzystasz z komputera Mac, zalecamy użycie Homebrew:
$ brew install redis
Uruchom serwer Redis w nowym oknie terminala i upewnij się, że działa na domyślnym porcie 6379. Numer portu będzie ważny, gdy powiemy Django, jak komunikować się z Redis.
Zakończ konfigurację, aktualizując INSTALLED_APPS
w pliku settings.py projektu plik:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'channels',
'example',
]
Następnie skonfiguruj CHANNEL_LAYERS
ustawiając domyślny backend i routing:
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'asgi_redis.RedisChannelLayer',
'CONFIG': {
'hosts': [('localhost', 6379)],
},
'ROUTING': 'example_channels.routing.channel_routing',
}
}
Używa backendu Redis, który jest również potrzebny w produkcji.
WebSockets 101
Zwykle Django używa protokołu HTTP do komunikacji między klientem a serwerem:
- Klient wysyła żądanie HTTP do serwera.
- Django analizuje żądanie, wyodrębnia adres URL, a następnie dopasowuje go do widoku.
- Widok przetwarza żądanie i zwraca odpowiedź HTTP do klienta.
W przeciwieństwie do HTTP, protokół WebSockets umożliwia komunikację dwukierunkową, co oznacza, że serwer może przesyłać dane do klienta bez pytania użytkownika. W przypadku protokołu HTTP odpowiedź otrzymuje tylko klient, który wysłał żądanie. Dzięki WebSockets serwer może komunikować się z wieloma klientami jednocześnie. Jak zobaczymy w dalszej części tego samouczka, wysyłamy wiadomości WebSockets przy użyciu ws://
prefiks, w przeciwieństwie do http://
.
UWAGA: Przed zanurzeniem się szybko przejrzyj dokumentację Koncepcji kanałów.
Konsumenci i grupy
Stwórzmy naszego pierwszego konsumenta, który obsługuje podstawowe połączenia między klientem a serwerem. Utwórz nowy plik o nazwie 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)
Konsumenci są odpowiednikiem poglądów Django. Każdy użytkownik łączący się z naszą aplikacją zostanie dodany do grupy „users” i będzie otrzymywał wiadomości wysyłane przez serwer. Gdy klient odłączy się od naszej aplikacji, kanał zostanie usunięty z grupy, a użytkownik przestanie otrzymywać wiadomości.
Następnie skonfigurujmy trasy, które działają prawie tak samo jak konfiguracja adresu URL w Django, dodając następujący kod do nowego pliku o nazwie 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),
]
Tak więc zdefiniowaliśmy channel_routing
zamiast urlpatterns
i route()
zamiast url()
. Zauważ, że połączyliśmy nasze funkcje konsumenckie z WebSockets.
Szablony
Napiszmy trochę HTML, który może komunikować się z naszym serwerem przez WebSocket. Utwórz folder „templates” w ramach „example”, a następnie dodaj folder „example” w ramach „templates” - „example_channels/example/templates/example”.
Dodaj _base.html plik:
<!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>
I 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 %}
Teraz, gdy klient pomyślnie otworzy połączenie z serwerem za pomocą WebSocket, zobaczymy komunikat potwierdzający wydrukowany na konsoli.
Wyświetlenia
Skonfiguruj pomocniczy widok Django, aby renderować nasz szablon w example_channels/example/views.py :
from django.shortcuts import render
def user_list(request):
return render(request, 'example/user_list.html')
Dodaj adres URL do 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'),
]
Zaktualizuj również adres URL projektu w 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')),
]
Test
Gotowy do testowania?
(env)$ python manage.py runserver
UWAGA: Alternatywnie możesz uruchomić python manage.py runserver --noworker
i python manage.py runworker
w dwóch różnych terminalach, aby przetestować interfejs i serwery robocze jako dwa oddzielne procesy. Obie metody działają!
Kiedy odwiedzasz http://localhost:8000/, powinieneś zobaczyć komunikat o połączeniu drukowany na terminalu:
[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]
Uwierzytelnianie użytkownika
Teraz, gdy udowodniliśmy, że możemy otworzyć połączenie, następnym krokiem jest obsługa uwierzytelniania użytkowników. Pamiętaj:Chcemy, aby użytkownik mógł zalogować się do naszej aplikacji i zobaczyć listę wszystkich innych użytkowników, którzy są zasubskrybowani do grupy tego użytkownika. Po pierwsze, potrzebujemy sposobu, w jaki użytkownicy mogą tworzyć konta i logować się. Zacznij od stworzenia prostej strony logowania, która pozwoli użytkownikowi uwierzytelnić się za pomocą nazwy użytkownika i hasła.
Utwórz nowy plik o nazwie log_in.html w ramach „przykładowe_kanały/przykład/szablony/przykład”:
{% 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 %}
Następnie zaktualizuj example_channels/example/views.py tak:
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 jest dostarczany z formularzami, które obsługują wspólną funkcjonalność uwierzytelniania. Możemy użyć AuthenticationForm
do obsługi logowania użytkownika. Ten formularz sprawdza podaną nazwę użytkownika i hasło, a następnie zwraca User
obiekt, jeśli zostanie znaleziony zweryfikowany użytkownik. Logujemy zweryfikowanego użytkownika i przekierowujemy go na naszą stronę główną. Użytkownik powinien również mieć możliwość wylogowania się z aplikacji, dlatego tworzymy widok wylogowania, który zapewnia tę funkcjonalność, a następnie przenosi użytkownika z powrotem do ekranu logowania.
Następnie zaktualizuj 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')
]
Potrzebujemy również sposobu na tworzenie nowych użytkowników. Utwórz stronę rejestracji w taki sam sposób jak login, dodając nowy plik o nazwie sign_up.html do „przykładowe_kanały/przykład/szablony/przykład”:
{% 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 %}
Zauważ, że strona logowania zawiera link do strony rejestracji, a strona rejestracji zawiera link do strony logowania.
Dodaj następującą funkcję do widoków:
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})
Używamy innego wbudowanego formularza do tworzenia użytkownika. Po pomyślnej weryfikacji formularza przekierowujemy do strony logowania.
Pamiętaj, aby zaimportować formularz:
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
Zaktualizuj example_channels/example/urls.py ponownie:
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')
]
W tym momencie musimy stworzyć użytkownika. Uruchom serwer i odwiedź http://localhost:8000/sign_up/
w Twojej przeglądarce. Wypełnij formularz poprawną nazwą użytkownika i hasłem i prześlij go, aby utworzyć naszego pierwszego użytkownika.
UWAGA: Spróbuj użyć michael
jako nazwę użytkownika i johnson123
jako hasło.
sign_up
widok przekierowuje nas do log_in
widoku i stamtąd możemy uwierzytelnić naszego nowo utworzonego użytkownika.
Po zalogowaniu możemy przetestować nasze nowe widoki uwierzytelniania.
Skorzystaj z formularza rejestracji, aby utworzyć kilku nowych użytkowników w ramach przygotowań do następnej sekcji.
Alerty logowania
Mamy działające podstawowe uwierzytelnianie użytkowników, ale nadal musimy wyświetlać listę użytkowników i potrzebujemy, aby serwer informował grupę, kiedy użytkownik się loguje i wylogowuje. Musimy edytować nasze funkcje konsumenckie, aby wysyłali wiadomość zaraz po klient łączy się i tuż przed rozłączeniem klienta. Dane wiadomości będą zawierać nazwę użytkownika i stan połączenia.
Zaktualizuj example_channels/example/consumers.py tak:
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)
Zauważ, że dodaliśmy dekoratory do funkcji, aby pobrać użytkownika z sesji Django. Ponadto wszystkie wiadomości muszą być możliwe do serializacji w formacie JSON, więc zrzucamy nasze dane do ciągu JSON.
Następnie zaktualizuj 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 %}
Na naszej stronie głównej rozszerzamy naszą listę użytkowników, aby wyświetlić listę użytkowników. Przechowujemy nazwę użytkownika każdego użytkownika jako atrybut danych, aby ułatwić znalezienie elementu użytkownika w DOM. Do naszego WebSocket dodajemy również detektor zdarzeń, który może obsługiwać komunikaty z serwera. Po otrzymaniu wiadomości analizujemy dane JSON, znajdujemy <li>
elementu dla danego użytkownika i zaktualizuj status tego użytkownika.
Django nie śledzi, czy użytkownik jest zalogowany, więc musimy stworzyć prosty model, który zrobi to za nas. Utwórz LoggedInUser
model z połączeniem jeden do jednego z naszym User
model w 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')
Nasza aplikacja utworzy LoggedInUser
wystąpienie, gdy użytkownik się zaloguje, a aplikacja usunie to wystąpienie, gdy użytkownik się wyloguje.
Dokonaj migracji schematu, a następnie zmigruj naszą bazę danych, aby zastosować zmiany.
(env)$ python manage.py makemigrations
(env)$ python manage.py migrate
Następnie zaktualizuj nasz widok listy użytkowników w example_channels/example/views.py , aby pobrać listę użytkowników do renderowania:
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})
Jeśli użytkownik ma powiązanego LoggedInUser
, wtedy rejestrujemy status użytkownika jako „Online”, a jeśli nie, użytkownik jest „Offline”. Dodajemy również @login_required
dekorator zarówno do naszej listy użytkowników, jak i do widoku wylogowania, aby ograniczyć dostęp tylko do zarejestrowanych użytkowników.
Dodaj również importy:
from django.contrib.auth import get_user_model, login, logout
from django.contrib.auth.decorators import login_required
W tym momencie użytkownicy mogą się logować i wylogowywać, co spowoduje, że serwer będzie wysyłać wiadomości do klienta, ale nie mamy możliwości sprawdzenia, którzy użytkownicy są zalogowani, gdy użytkownik loguje się po raz pierwszy. Użytkownik widzi aktualizacje tylko wtedy, gdy inny użytkownik zmiany statusu. To tutaj LoggedInUser
wchodzi w grę, ale potrzebujemy sposobu na utworzenie LoggedInUser
na przykład, gdy użytkownik się zaloguje, a następnie usuń go, gdy ten się wyloguje.
Biblioteka Django zawiera funkcję znaną jako sygnały, która emituje powiadomienia, gdy wystąpią określone działania. Aplikacje mogą nasłuchiwać tych powiadomień, a następnie na nich działać. Możemy wykorzystać dwa pomocne, wbudowane sygnały (user_logged_in
i user_logged_out
) do obsługi naszego LoggedInUser
zachowanie.
W obrębie „example_channels/example” dodaj nowy plik o nazwie 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()
Musimy udostępnić sygnały w konfiguracji naszej aplikacji, example_channels/example/apps.py :
from django.apps import AppConfig
class ExampleConfig(AppConfig):
name = 'example'
def ready(self):
import example.signals
Zaktualizuj example_channels/example/__init__.py a także:
default_app_config = 'example.apps.ExampleConfig'
Sprawdzenie stanu zdrowia
Teraz zakończyliśmy kodowanie i jesteśmy gotowi do połączenia się z naszym serwerem z wieloma użytkownikami w celu przetestowania naszej aplikacji.
Uruchom serwer Django, zaloguj się jako użytkownik i odwiedź stronę główną. Powinniśmy zobaczyć listę wszystkich użytkowników naszej aplikacji, każdy ze statusem „Offline”. Następnie otwórz nowe okno incognito, zaloguj się jako inny użytkownik i obejrzyj oba ekrany. Gdy się logujemy, zwykła przeglądarka aktualizuje status użytkownika na „Online”. Z naszego okna Incognito widzimy, że zalogowany użytkownik również ma status „Online”. Możemy przetestować WebSockets, logując się i wylogowując na naszych różnych urządzeniach z różnymi użytkownikami.
Obserwując konsolę programisty na kliencie i aktywność serwera w naszym terminalu, możemy potwierdzić, że połączenia WebSocket są tworzone podczas logowania użytkownika i niszczone, gdy użytkownik się wylogowuje.
[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]
UWAGA :Możesz również użyć ngrok do bezpiecznego udostępnienia lokalnego serwera w Internecie. W ten sposób możesz trafić na lokalny serwer z różnych urządzeń, takich jak telefon lub tablet.
Myśli zamykające
W tym samouczku omówiliśmy wiele rzeczy - kanały Django, WebSockets, uwierzytelnianie użytkowników, sygnały i trochę programowania front-end. Główny wniosek jest taki:Kanały rozszerzają funkcjonalność tradycyjnej aplikacji Django, pozwalając nam przesyłać wiadomości z serwera do grup użytkowników za pośrednictwem WebSockets.
To potężna rzecz!
Pomyśl o niektórych aplikacjach. Możemy tworzyć czaty, gry wieloosobowe i aplikacje do współpracy, które pozwalają użytkownikom komunikować się w czasie rzeczywistym. Dzięki WebSockets można usprawnić nawet przyziemne zadania. Na przykład, zamiast okresowego odpytywania serwera w celu sprawdzenia, czy długotrwałe zadanie zostało ukończone, serwer może przesłać aktualizację statusu do klienta po jego zakończeniu.
Ten samouczek tylko zarysowuje powierzchnię tego, co możemy zrobić z kanałami Django. Zapoznaj się z dokumentacją kanałów Django i zobacz, co jeszcze możesz stworzyć.
Bezpłatny bonus: Kliknij tutaj, aby uzyskać dostęp do bezpłatnego przewodnika po zasobach edukacyjnych Django (PDF), który zawiera wskazówki i triki, a także typowe pułapki, których należy unikać podczas tworzenia aplikacji internetowych Python + Django.
Pobierz ostateczny kod z repozytorium django-example-channels. Pozdrawiam!