Розбираємось з Django Channels на прикладі списку користувачів онлайн

17 хв. читання

В цьому туторіалі ми напишемо додаток з використанням Django channels, що буде оновлювати список користувачів онлайн в реальному часі.

Перш за все, ви повинні мати досвід роботи з Django і розуміти принцип роботи WebSockets.

Наш додаток буде використовувати:

  • Python (v3.6.0)
  • Django (v1.10.5)
  • Django Channels (v1.0.3)
  • Redis (v3.2.8 )

Після прочитання цього туторіалу ви зможете:

  1. Додати підтримку WebSockets до Django за допомогою Django Channels
  2. Налаштувати просте з'єднання між Django та сервером Redis
  3. Реалізувати базову аутентифікацію користувачів
  4. Реагувати на сигнали, коли користувач входить або виходить із системи.

Ну що ж...

Починаємо!

Перш за все потрібно створити віртуальне середовище, щоб ізолювати залежності.

$ 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-му порті.

Тепер потрібно оновити 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 для зв'язку клієнта та сервера.

  1. Клієнт відсилає HTTP-запит на сервер
  2. Django його отримує, парсить, отримує URL та знаходить потрібний View
  3. View обробляє запит і повертає HTTP-відповідь клієнту

Але на відміну від HTTP, протокол WebSockets дозволяє вільне спілкування в обох напрямках, тобто сервер може відправити клієнту дані без будь-якої ініціативи з його сторони. Також вони відрізняються тим, що WebSocket дозволяє відправляти повідомлення відразу багатьом отримувачам. Як ви побачите пізніше, ми будемо відправляти повідомлення, використовуючи протокол ws:// (а не http://).

Зауваження: я рекомендую вам ознайомитись з базовою концепцією каналів, якщо ви ще цього не зробили.

Отримувачі та групи

Створимо нашого першого отримувача (consumer), який буде обробляти з'єднання між клієнтом та сервером. Створіть файл 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)

Отримувачі схожі на звичайні представлення (view) в Django. Всі користувачі, що будуть приєднуватися до нашого додатку, будуть додані в групу users і будуть отримувати повідомлення від сервера. Коли користувач від'єднується від додатку, канал вилучається з групи і більше не отримує повідомлень.

Наступним кроком буде встановлення точок підключення. Це дуже схоже на налаштування звичайних URL. Додайте наступний код до нового файлу 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(). Зауважте, що ми привязали наших отримувачів до подій веб-сокетів.

Шаблони

Настав час написати трохи HTML (і JS), що буде спілкуватися з сервером через веб-сокети. Створіть в директорії "example" папку "templates", а в ній ще одну папку "example" — "example_channels/example/templates/example". Та створіть файл \_base.html:


  <meta charset="utf-8">
  <meta content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0" name="viewport">
  <meta content="ie=edge" http-equiv="X-UA-Compatible">
  <link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
  <title>Example Channels</title>

  <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 %}

Та 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 %}

Тепер, коли користувач успішно встановить веб-сокет підключення, ми побачимо повідомлення в консолі.

Представлення

Нам потрібно додати представлення, щоб користувач бачив наш шаблон. Додайте цей код в файл 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'),
]

І не забудьте оновити файл 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'))

В Джанго вже є базовий функціонал для форм аутентифікації. Ми можемо використовувати 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')
]

Також нам потрібна можливість створювати користувачів. Для цього створіть файл example_channels/example/templates/example/sign_up.html.

{% 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/, заповніть форму та створіть користувача.

Створіть ще декілька користувачів, вони знадобляться нам для тесту.

Сповіщення

В нас вже є базовий механізм аутентифікації користувачів, але все ще немає механізму, що буде відображати список користувачів онлайн. Тому нам потрібно відредагувати наші функції-отримувачі, щоб вони відправляли повідомлення після підключення та відключення користувачів. Повідомлення буде включати ім'я користувача та статус з'єднання.

Для цього оновіть файл 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)

Зауважте, що ми додали декоратори щоб отримувати користувача конкретної сесії. Також, всі повідомлення повинні бути серіалізовані.

Тепер оновіть файл 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 %}
      
      <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);
      // екануємо JavaScript щоб запобігти XSS-атакам
      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 %}

На нашій домашній сторінці ми відображаємо список користувачів. Ми зберігаємо нікнейм користувача як data-атрибут, щоб зручніше було до нього звертатися з JS. Також ми додали обробник події, що буде приймати повідомлення до веб-сокету. Коли ми приймаємо таке повідомлення, ми знаходимо відповідний <li> та оновлюємо статус користувача.

Джанго не відстежує логін користувачів, тому ми зробимо це самі. Створіть модель 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):
    """
    Цей код можна використовувати лише для тесту, але не на продакшені. Уявіть як просяде продуктивність при 100 000 користувачів.
    """
    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 до сторінки виходу та списку користувачів, щоб обмежити доступ тільки для користувачів, що залогінились.

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 Channels на прикладі списку користувачів онлайн

Якщо ви відкриєте консоль розробника на стороні клієнта та термінал, де запущений наш додаток, ви побачите, що наші веб-сокети працюють!

[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, WebSokets, аутентифікацію користувачів, сигнали та трохи фронтенд-розробки. Але головною метою було показати канали в дії і те, як вони роблять Джанго більш динамічним. Це дуже потужна штука. З ними ви можете створювати чати, мультиплеєрні ігри та додатки, де користувачі взаємодіють в режимі реального часу.

Цей туторіал — лише верхівка айсбергу. Почитайте документацію Django Channels щоб дізнатися більше. Готовий додаток з цього туторіалу ви можете знайти тут.

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 6.2K
Приєднався: 7 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація