В този урок ще използваме Django Channels, за да създадем приложение в реално време, което актуализира списък с потребители, когато влизат и излизат.
С WebSockets (чрез Django Channels), управляващи комуникацията между клиента и сървъра, всеки път, когато потребител е удостоверен, събитие ще бъде излъчвано на всеки друг свързан потребител. Екранът на всеки потребител ще се промени автоматично, без да се налага да презарежда браузъра си.
ЗАБЕЛЕЖКА: Препоръчваме ви да имате известен опит с Django, преди да започнете този урок. Освен това трябва да сте запознати с концепцията за WebSockets.
Безплатен бонус: Щракнете тук, за да получите достъп до безплатно ръководство за учебни ресурси на Django (PDF), което ви показва съвети и трикове, както и често срещани клопки, които трябва да избягвате, когато създавате уеб приложения на Python + Django.
Нашето приложение използва:
- Python (v3.6.0)
- Django (v1.10.5)
- Django Channels (v1.0.3)
- Redis (v3.2.8)
Цели
До края на този урок ще можете да...
- Добавете поддръжка на уеб сокети към Django проект чрез Django Channels
- Настройте проста връзка между Django и Redis сървър
- Внедряване на основно удостоверяване на потребителя
- Използвайте сигналите на Django, за да предприемете действия, когато потребител влезе или излезе
Първи стъпки
Първо, създайте нова виртуална среда, за да изолирате зависимостите на нашия проект:
$ mkdir django-example-channels
$ cd django-example-channels
$ python3.6 -m venv env
$ source env/bin/activate
(env)$
Инсталирайте Django, Django Channels и ASGI Redis и след това създайте нов проект и приложение на 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
ЗАБЕЛЕЖКА: По време на този урок ще създадем различни файлове и папки. Моля, вижте структурата на папките от хранилището на проекта, ако се затрудните.
След това изтеглете и инсталирайте Redis. Ако използвате Mac, препоръчваме да използвате Homebrew:
$ brew install redis
Стартирайте Redis сървъра в нов терминален прозорец и се уверете, че той работи на неговия порт по подразбиране, 6379. Номерът на порта ще бъде важен, когато кажем на Django как да комуникира с Redis.
Завършете настройката, като актуализирате INSTALLED_APPS
в settings.py на проекта файл:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'channels',
'example',
]
След това конфигурирайте CHANNEL_LAYERS
като зададете бекенд по подразбиране и маршрутизиране:
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'asgi_redis.RedisChannelLayer',
'CONFIG': {
'hosts': [('localhost', 6379)],
},
'ROUTING': 'example_channels.routing.channel_routing',
}
}
Това използва бекенд на Redis, който също е необходим в производството.
WebSockets 101
Обикновено Django използва HTTP за комуникация между клиента и сървъра:
- Клиентът изпраща HTTP заявка до сървъра.
- Django анализира заявката, извлича URL и след това го съпоставя с изглед.
- Изгледът обработва заявката и връща HTTP отговор на клиента.
За разлика от HTTP, протоколът WebSockets позволява двупосочна комуникация, което означава, че сървърът може да изпраща данни към клиента, без да бъде подканван от потребителя. При HTTP само клиентът, който е направил заявка, получава отговор. С WebSockets сървърът може да комуникира с множество клиенти едновременно. Както ще видим по-нататък в този урок, ние изпращаме съобщения на WebSockets с помощта на ws://
префикс, за разлика от http://
.
ЗАБЕЛЕЖКА: Преди да се потопите, прегледайте бързо документацията за концепциите на каналите.
Потребители и групи
Нека създадем нашия първи потребител, който обработва основните връзки между клиента и сървъра. Създайте нов файл, наречен 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)
Потребителите са двойник на възгледите на Django. Всеки потребител, който се свързва с нашето приложение, ще бъде добавен към групата „потребители“ и ще получава съобщения, изпратени от сървъра. Когато клиентът прекъсне връзката с нашето приложение, каналът се премахва от групата и потребителят ще спре да получава съобщения.
След това нека настроим маршрути, които работят по почти същия начин като конфигурацията на URL адреса на Django, като добавим следния код към нов файл, наречен 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),
]
И така, дефинирахме channel_routing
вместо urlpatterns
и route()
вместо url()
. Забележете, че свързахме нашите потребителски функции с WebSockets.
Шаблони
Нека напишем малко HTML, който може да комуникира с нашия сървър чрез WebSocket. Създайте папка „templates“ в рамките на „example“ и след това добавете папка „example“ в рамките на „templates“ - „example_channels/example/templates/example“.
Добавете _base.html файл:
<!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>
И 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 %}
Сега, когато клиентът успешно отвори връзка със сървъра с помощта на WebSocket, ще видим съобщение за потвърждение, отпечатано на конзолата.
Прегледи
Настройте поддържащ изглед на Django, за да изобразите нашия шаблон в example_channels/example/views.py :
from django.shortcuts import render
def user_list(request):
return render(request, 'example/user_list.html')
Добавете 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'),
]
Актуализирайте и URL адреса на проекта в 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')),
]
Тест
Готови ли сте за тест?
(env)$ python manage.py runserver
ЗАБЕЛЕЖКА: Като алтернатива можете да стартирате
python manage.py runserver --noworker
иpython manage.py runworker
в два различни терминала, за да тествате интерфейса и работните сървъри като два отделни процеса. И двата метода работят!
Когато посетите http://localhost:8000/, трябва да видите съобщението за връзка, отпечатано на терминала:
[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]
Удостоверяване на потребителя
След като доказахме, че можем да отворим връзка, следващата ни стъпка е да се справим с удостоверяването на потребителя. Запомнете:Искаме потребителят да може да влезе в нашето приложение и да види списък с всички други потребители, които са абонирани за групата на този потребител. Първо, имаме нужда от начин потребителите да създават акаунти и да влизат. Започнете със създаване на проста страница за вход, която ще позволи на потребителя да се удостовери с потребителско име и парола.
Създайте нов файл, наречен log_in.html в рамките на „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 %}
След това актуализирайте example_channels/example/views.py така:
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 идва с формуляри, които поддържат обща функционалност за удостоверяване. Можем да използваме AuthenticationForm
за обработка на потребителско влизане. Този формуляр проверява предоставените потребителско име и парола, след което връща User
обект, ако бъде намерен валидиран потребител. Влизаме в валидирания потребител и го пренасочваме към нашата начална страница. Потребителят също трябва да има възможност да излезе от приложението, така че ние създаваме изглед за излизане, който предоставя тази функционалност и след това връща потребителя обратно към екрана за влизане.
След това актуализирайте 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')
]
Нуждаем се и от начин за създаване на нови потребители. Създайте страница за регистрация по същия начин като входа, като добавите нов файл, наречен 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 %}
Забележете, че страницата за вход има връзка към страницата за регистрация, а страницата за регистрация има връзка към входа.
Добавете следната функция към изгледите:
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})
Използваме друга вградена форма за създаване на потребители. След успешно валидиране на формуляра, ние пренасочваме към страницата за вход.
Не забравяйте да импортирате формуляра:
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
Актуализирайте example_channels/example/urls.py отново:
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')
]
В този момент трябва да създадем потребител. Стартирайте сървъра и посетете http://localhost:8000/sign_up/
във вашия браузър. Попълнете формуляра с валидно потребителско име и парола и го изпратете, за да създадем първия ни потребител.
ЗАБЕЛЕЖКА: Опитайте да използвате
michael
като потребителско име иjohnson123
като парола.
sign_up
view ни пренасочва към log_in
преглед и от там можем да удостоверим нашия новосъздадения потребител.
След като влезем, можем да тестваме новите си изгледи за удостоверяване.
Използвайте формуляра за регистрация, за да създадете няколко нови потребители в подготовка за следващия раздел.
Сигнали за влизане
Работим основно удостоверяване на потребителя, но все още трябва да показваме списък с потребители и имаме нужда от сървъра да каже на групата кога потребител влиза и излиза. Трябва да редактираме нашите потребителски функции, така че да изпращат съобщение веднага след клиент се свързва и точно преди клиентът да се прекъсне. Данните за съобщението ще включват потребителското име на потребителя и състоянието на връзката.
Актуализирайте example_channels/example/consumers.py така:
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)
Забележете, че сме добавили декоратори към функциите, за да изведем потребителя от сесията на Django. Освен това всички съобщения трябва да бъдат JSON-сериализиращи, така че изхвърляме данните си в JSON низ.
След това актуализирайте 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 %}
На нашата начална страница разширяваме нашия списък с потребители, за да покажем списък с потребители. Ние съхраняваме потребителското име на всеки потребител като атрибут на данни, за да улесним намирането на потребителския елемент в DOM. Ние също така добавяме слушател на събития към нашия WebSocket, който може да обработва съобщения от сървъра. Когато получим съобщение, анализираме JSON данните, намираме <li>
елемент за дадения потребител и актуализирайте състоянието на този потребител.
Django не проследява дали даден потребител е влязъл, така че трябва да създадем прост модел, който да направи това вместо нас. Създайте LoggedInUser
модел с връзка едно към едно с нашия User
модел в 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')
Нашето приложение ще създаде LoggedInUser
екземпляр, когато потребител влезе, и приложението ще изтрие екземпляра, когато потребителят излезе.
Направете миграцията на схемата и след това мигрирайте нашата база данни, за да приложите промените.
(env)$ python manage.py makemigrations
(env)$ python manage.py migrate
След това актуализирайте нашия изглед на списък с потребители вexample_channels/example/views.py , за да извлечете списък с потребители за изобразяване:
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})
Ако потребител има свързан LoggedInUser
, тогава записваме състоянието на потребителя като „Онлайн“, а ако не, потребителят е „Офлайн“. Добавяме и @login_required
декоратор както на нашия списък с потребители, така и на изгледи за излизане, за да ограничите достъпа само до регистрирани потребители.
Добавете и импортираните:
from django.contrib.auth import get_user_model, login, logout
from django.contrib.auth.decorators import login_required
В този момент потребителите могат да влизат и излизат, което ще задейства сървъра да изпраща съобщения до клиента, но ние нямаме начин да знаем кои потребители са влезли, когато потребителят влезе за първи път. Потребителят вижда актуализации само когато друг потребител промени в състоянието. Това е мястото, където LoggedInUser
влиза в игра, но имаме нужда от начин да създадем LoggedInUser
екземпляр, когато потребител влезе, и след това го изтрийте, когато този потребител излезе.
Библиотеката на Django включва функция, известна като сигнали, която излъчва известия, когато възникнат определени действия. Приложенията могат да слушат тези известия и след това да действат по тях. Можем да използваме два полезни, вградени сигнала (user_logged_in
и user_logged_out
), за да обработва нашия LoggedInUser
поведение.
В рамките на „example_channels/example“ добавете нов файл, наречен 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()
Трябва да направим сигналите налични в конфигурацията на нашето приложение, example_channels/example/apps.py :
from django.apps import AppConfig
class ExampleConfig(AppConfig):
name = 'example'
def ready(self):
import example.signals
Актуализирайте example_channels/example/__init__.py също така:
default_app_config = 'example.apps.ExampleConfig'
Проверка за здравина
Сега приключихме с кодирането и сме готови да се свържем с нашия сървър с множество потребители, за да тестваме нашето приложение.
Стартирайте Django сървъра, влезте като потребител и посетете началната страница. Трябва да видим списък с всички потребители в нашето приложение, всеки със състояние „Офлайн“. След това отворете нов прозорец в режим „инкогнито“ и влезте като различен потребител и гледайте и двата екрана. Веднага когато влезем, обикновеният браузър актуализира потребителското състояние на „Онлайн“. От нашия прозорец „инкогнито“ виждаме, че потребителят, който е влязъл, също има състояние „Онлайн“. Можем да тестваме WebSockets, като влизаме и излизаме на нашите различни устройства с различни потребители.
Наблюдавайки конзолата за разработчици на клиента и активността на сървъра в нашия терминал, можем да потвърдим, че WebSocket връзките се формират, когато потребител влезе, и се унищожават, когато потребителят излезе.
[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]
ЗАБЕЛЕЖКА :Можете също да използвате ngrok за безопасно излагане на локалния сървър в интернет. Това ще ви позволи да удряте локалния сървър от различни устройства като вашия телефон или таблет.
Заключителни мисли
Разгледахме много в този урок - Django Channels, WebSockets, удостоверяване на потребителя, сигнали и някои front-end разработки. Основният извод е следният:Channels разширява функционалността на традиционно Django приложение, като ни позволява да изпращаме съобщения от сървъра към групи потребители чрез WebSockets.
Това е мощно нещо!
Помислете за някои от приложенията. Можем да създаваме чат стаи, мултиплейър игри и съвместни приложения, които позволяват на потребителите да общуват в реално време. Дори обикновените задачи се подобряват с WebSockets. Например, вместо периодично да допитва до сървъра, за да види дали е изпълнена продължителна задача, сървърът може да изпрати актуализация на състоянието на клиента, когато приключи.
Този урок просто надрасква повърхността на това, което можем да правим и с Django Channels. Разгледайте документацията на Django Channels и вижте какво още можете да създадете.
Безплатен бонус: Щракнете тук, за да получите достъп до безплатно ръководство за учебни ресурси на Django (PDF), което ви показва съвети и трикове, както и често срещани клопки, които трябва да избягвате, когато създавате уеб приложения на Python + Django.
Вземете окончателния код от репозито на django-example-channels. Наздраве!