sql >> Base de Datos >  >> RDS >> Database

Introducción a los canales de Django

En este tutorial, usaremos Django Channels para crear una aplicación en tiempo real que actualice una lista de usuarios a medida que inician y cierran sesión.

Con WebSockets (a través de Django Channels) administrando la comunicación entre el cliente y el servidor, siempre que se autentique un usuario, se transmitirá un evento a todos los demás usuarios conectados. La pantalla de cada usuario cambiará automáticamente, sin que tengan que volver a cargar sus navegadores.

NOTA: Le recomendamos que tenga algo de experiencia con Django antes de comenzar este tutorial. Además, debe estar familiarizado con el concepto de WebSockets.

Bono Gratis: Haga clic aquí para obtener acceso a una Guía de recursos de aprendizaje de Django (PDF) gratuita que le muestra consejos y trucos, así como las trampas comunes que debe evitar al crear aplicaciones web de Python + Django.

Nuestra aplicación utiliza:

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

Objetivos

Al final de este tutorial, podrá...

  1. Agregue soporte de sockets web a un proyecto de Django a través de los canales de Django
  2. Configure una conexión simple entre Django y un servidor Redis
  3. Implementar autenticación de usuario básica
  4. Aproveche Django Signals para tomar medidas cuando un usuario inicia o cierra sesión


Cómo empezar

Primero, crea un nuevo entorno virtual para aislar las dependencias de nuestro proyecto:

$ mkdir django-example-channels
$ cd django-example-channels
$ python3.6 -m venv env
$ source env/bin/activate
(env)$

Instale Django, Django Channels y ASGI Redis, y luego cree un nuevo proyecto y aplicación de 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

NOTA: Durante el curso de este tutorial, crearemos una variedad de diferentes archivos y carpetas. Consulte la estructura de carpetas del repositorio del proyecto si se atasca.

A continuación, descargue e instale Redis. Si tiene una Mac, le recomendamos que utilice Homebrew:

$ brew install redis

Inicie el servidor Redis en una nueva ventana de terminal y asegúrese de que se esté ejecutando en su puerto predeterminado, 6379. El número de puerto será importante cuando le digamos a Django cómo comunicarse con Redis.

Complete la configuración actualizando INSTALLED_APPS en el settings.py del proyecto archivo:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'channels',
    'example',
]

Luego configure las CHANNEL_LAYERS configurando un backend y enrutamiento predeterminados:

CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'asgi_redis.RedisChannelLayer',
        'CONFIG': {
            'hosts': [('localhost', 6379)],
        },
        'ROUTING': 'example_channels.routing.channel_routing',
    }
}

Esto utiliza un backend de Redis que también se necesita en producción.



WebSockets 101

Normalmente, Django usa HTTP para comunicarse entre el cliente y el servidor:

  1. El cliente envía una solicitud HTTP al servidor.
  2. Django analiza la solicitud, extrae una URL y luego la compara con una vista.
  3. La vista procesa la solicitud y devuelve una respuesta HTTP al cliente.

A diferencia de HTTP, el protocolo WebSockets permite la comunicación bidireccional, lo que significa que el servidor puede enviar datos al cliente sin que el usuario se lo solicite. Con HTTP, solo el cliente que realizó una solicitud recibe una respuesta. Con WebSockets, el servidor puede comunicarse con múltiples clientes simultáneamente. Como veremos más adelante en este tutorial, enviamos mensajes de WebSockets usando ws:// prefijo, a diferencia de http:// .

NOTA: Antes de sumergirse, revise rápidamente la documentación de Channels Concepts.



Consumidores y Grupos

Creemos nuestro primer consumidor, que maneja las conexiones básicas entre el cliente y el servidor. Cree un nuevo archivo llamado 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)   

Los consumidores son la contrapartida de las vistas de Django. Cualquier usuario que se conecte a nuestra aplicación se agregará al grupo de "usuarios" y recibirá mensajes enviados por el servidor. Cuando el cliente se desconecta de nuestra aplicación, el canal se elimina del grupo y el usuario dejará de recibir mensajes.

A continuación, configuremos rutas, que funcionan casi de la misma manera que la configuración de URL de Django, agregando el siguiente código a un nuevo archivo llamado 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),
]

Entonces, definimos channel_routing en lugar de urlpatterns y route() en lugar de url() . Tenga en cuenta que vinculamos nuestras funciones de consumidor a WebSockets.


Plantillas

Escribamos algo de HTML que pueda comunicarse con nuestro servidor a través de un WebSocket. Cree una carpeta de "plantillas" dentro de "ejemplo" y luego agregue una carpeta de "ejemplo" dentro de "plantillas" - "ejemplo_canales/ejemplo/plantillas/ejemplo".

Agregue un _base.html archivo:

<!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>

Y lista_de_usuarios.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 %}

Ahora, cuando el cliente abra con éxito una conexión con el servidor mediante un WebSocket, veremos un mensaje de confirmación impreso en la consola.



Visualizaciones

Configure una vista de Django compatible para representar nuestra plantilla dentro de example_channels/example/views.py :

from django.shortcuts import render


def user_list(request):
    return render(request, 'example/user_list.html')

Agregue la 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'),
]

Actualice también la URL del proyecto en 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')),
]


Prueba

¿Listo para probar?

(env)$ python manage.py runserver

NOTA: Alternativamente, puede ejecutar python manage.py runserver --noworker y python manage.py runworker en dos terminales diferentes para probar la interfaz y los servidores de trabajo como dos procesos separados. ¡Ambos métodos funcionan!

Cuando visite http://localhost:8000/, debería ver el mensaje de conexión impreso en la 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]



Autenticación de usuario

Ahora que hemos probado que podemos abrir una conexión, nuestro próximo paso es manejar la autenticación del usuario. Recuerde:queremos que un usuario pueda iniciar sesión en nuestra aplicación y ver una lista de todos los demás usuarios que están suscritos al grupo de ese usuario. Primero, necesitamos una forma para que los usuarios creen cuentas e inicien sesión. Comience creando una página de inicio de sesión simple que permita a un usuario autenticarse con un nombre de usuario y contraseña.

Cree un nuevo archivo llamado log_in.html dentro de “ejemplo_canales/ejemplo/plantillas/ejemplo”:

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

A continuación, actualice example_channels/example/views.py así:

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 viene con formularios que admiten la funcionalidad de autenticación común. Podemos usar el AuthenticationForm para manejar el inicio de sesión del usuario. Este formulario verifica el nombre de usuario y la contraseña proporcionados, luego devuelve un User objeto si se encuentra un usuario validado. Iniciamos sesión con el usuario validado y lo redireccionamos a nuestra página de inicio. Un usuario también debe tener la capacidad de cerrar sesión en la aplicación, por lo que creamos una vista de cierre de sesión que proporciona esa funcionalidad y luego lleva al usuario de regreso a la pantalla de inicio de sesión.

Luego actualice 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')
]

También necesitamos una forma de crear nuevos usuarios. Cree una página de registro de la misma manera que el inicio de sesión agregando un nuevo archivo llamado sign_up.html a “ejemplo_canales/ejemplo/plantillas/ejemplo”:

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

Tenga en cuenta que la página de inicio de sesión tiene un enlace a la página de registro y la página de registro tiene un enlace de regreso al inicio de sesión.

Agregue la siguiente función a las vistas:

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 otro formulario integrado para la creación de usuarios. Después de la validación exitosa del formulario, redirigimos a la página de inicio de sesión.

Asegúrese de importar el formulario:

from django.contrib.auth.forms import AuthenticationForm, UserCreationForm

Actualice example_channels/example/urls.py de nuevo:

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')
]

En este punto, necesitamos crear un usuario. Ejecute el servidor y visite http://localhost:8000/sign_up/ en tu navegador. Complete el formulario con un nombre de usuario y contraseña válidos y envíelo para crear nuestro primer usuario.

NOTA: Intenta usar michael como nombre de usuario y johnson123 como contraseña.

El sign_up view nos redirige a log_in vista, y desde allí podemos autenticar nuestro usuario recién creado.

Después de iniciar sesión, podemos probar nuestras nuevas vistas de autenticación.

Utilice el formulario de registro para crear varios usuarios nuevos en preparación para la siguiente sección.



Alertas de inicio de sesión

Tenemos la autenticación de usuario básica funcionando, pero aún necesitamos mostrar una lista de usuarios y necesitamos que el servidor le diga al grupo cuándo un usuario inicia y cierra sesión. Necesitamos editar nuestras funciones de consumidor para que envíen un mensaje justo después de un el cliente se conecta y justo antes de que un cliente se desconecte. Los datos del mensaje incluirán el nombre de usuario y el estado de conexión del usuario.

Actualice example_channels/example/consumers.py así:

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)

Tenga en cuenta que hemos agregado decoradores a las funciones para obtener al usuario de la sesión de Django. Además, todos los mensajes deben ser serializables mediante JSON, por lo que volcamos nuestros datos en una cadena JSON.

A continuación, actualice 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 %}

En nuestra página de inicio, ampliamos nuestra lista de usuarios para mostrar una lista de usuarios. Almacenamos el nombre de usuario de cada usuario como un atributo de datos para facilitar la búsqueda del elemento de usuario en el DOM. También agregamos un detector de eventos a nuestro WebSocket que puede manejar mensajes del servidor. Cuando recibimos un mensaje, analizamos los datos JSON, buscamos el <li> para el usuario dado y actualizar el estado de ese usuario.

Django no rastrea si un usuario ha iniciado sesión, por lo que necesitamos crear un modelo simple para que lo haga por nosotros. Crear un LoggedInUser modelo con una conexión uno a uno con nuestro User modelo en 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')

Nuestra aplicación creará un LoggedInUser instancia cuando un usuario inicia sesión y la aplicación eliminará la instancia cuando el usuario cierra sesión.

Realice la migración del esquema y luego migre nuestra base de datos para aplicar los cambios.

(env)$ python manage.py makemigrations
(env)$ python manage.py migrate

A continuación, actualice nuestra vista de lista de usuarios, en example_channels/example/views.py , para recuperar una lista de usuarios para renderizar:

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 usuario tiene un LoggedInUser asociado , luego registramos el estado del usuario como "En línea", y si no, el usuario está "Fuera de línea". También agregamos un @login_required decorador tanto a nuestra lista de usuarios como a las vistas de cierre de sesión para restringir el acceso solo a usuarios registrados.

Añade también las importaciones:

from django.contrib.auth import get_user_model, login, logout
from django.contrib.auth.decorators import login_required

En este punto, los usuarios pueden iniciar y cerrar sesión, lo que activará el servidor para enviar mensajes al cliente, pero no tenemos forma de saber qué usuarios están conectados cuando el usuario inicia sesión por primera vez. El usuario solo ve actualizaciones cuando otro usuario cambios de estado. Aquí es donde el LoggedInUser entra en juego, pero necesitamos una forma de crear un LoggedInUser instancia cuando un usuario inicia sesión y luego lo elimina cuando ese usuario cierra sesión.

La biblioteca de Django incluye una característica conocida como señales que transmite notificaciones cuando ocurren ciertas acciones. Las aplicaciones pueden escuchar esas notificaciones y luego actuar en consecuencia. Podemos explotar dos útiles señales integradas (user_logged_in y user_logged_out ) para manejar nuestro LoggedInUser comportamiento.

Dentro de "example_channels/example", agregue un nuevo archivo llamado 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()

Tenemos que hacer que las señales estén disponibles en la configuración de nuestra aplicación, example_channels/example/apps.py :

from django.apps import AppConfig


class ExampleConfig(AppConfig):
    name = 'example'

    def ready(self):
        import example.signals

Actualizar ejemplo_canales/ejemplo/__init__.py también:

default_app_config = 'example.apps.ExampleConfig'


Comprobación de estado

Ahora hemos terminado de codificar y estamos listos para conectarnos a nuestro servidor con múltiples usuarios para probar nuestra aplicación.

Ejecute el servidor Django, inicie sesión como usuario y visite la página de inicio. Deberíamos ver una lista de todos los usuarios en nuestra aplicación, cada uno con un estado de "Desconectado". A continuación, abra una nueva ventana de incógnito e inicie sesión como un usuario diferente y observe ambas pantallas. Justo cuando iniciamos sesión, el navegador normal actualiza el estado del usuario a "En línea". Desde nuestra ventana de Incógnito, vemos que el usuario que inició sesión también tiene un estado de "En línea". Podemos probar los WebSockets iniciando y cerrando sesión en nuestros diferentes dispositivos con varios usuarios.

Al observar la consola del desarrollador en el cliente y la actividad del servidor en nuestra terminal, podemos confirmar que las conexiones WebSocket se forman cuando un usuario inicia sesión y se destruyen cuando un usuario cierra sesión.

[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]

NOTA :También puede usar ngrok para exponer el servidor local a Internet de forma segura. Hacer esto le permitirá acceder al servidor local desde varios dispositivos, como su teléfono o tableta.



Pensamientos finales

Cubrimos mucho en este tutorial:Django Channels, WebSockets, autenticación de usuarios, señales y algo de desarrollo front-end. La conclusión principal es esta:los canales amplían la funcionalidad de una aplicación Django tradicional al permitirnos enviar mensajes desde el servidor a grupos de usuarios a través de WebSockets.

¡Esto es algo poderoso!

Piense en algunas de las aplicaciones. Podemos crear salas de chat, juegos multijugador y aplicaciones colaborativas que permitan a los usuarios comunicarse en tiempo real. Incluso las tareas mundanas se mejoran con WebSockets. Por ejemplo, en lugar de sondear periódicamente al servidor para ver si se completó una tarea de larga duración, el servidor puede enviar una actualización de estado al cliente cuando finaliza.

Este tutorial solo rasca la superficie de lo que podemos hacer con Django Channels también. Explore la documentación de Django Channels y vea qué más puede crear.

Bono Gratis: Haga clic aquí para obtener acceso a una Guía de recursos de aprendizaje de Django (PDF) gratuita que le muestra consejos y trucos, así como las trampas comunes que debe evitar al crear aplicaciones web de Python + Django.

Tome el código final del repositorio django-example-channels. ¡Salud!