[PYTHON] Web en temps réel avec les chaînes Django

introduction

Cet article est l'article du 17ème jour de Django Advent Calendar 2016.

Qu'est-ce que Django Channels?

Django Channels — Channels 0.17.2 documentation

Channels is a project to make Django able to handle more than just plain HTTP requests, including WebSockets and HTTP2, as well as the ability to run code after a response has been sent for things like thumbnailing or background calculation.

Channels est un projet qui permet à Django d'exécuter du code après l'envoi d'une réponse, comme des vignettes et des calculs en arrière-plan, ainsi que des requêtes HTTP simples telles que WebSocket et HTTP2.

The core of the system is, unsurprisingly, a datastructure called a channel. What is a channel? It is an ordered, first-in first-out queue with message expiry and at-most-once delivery to only one listener at a time.

If you’ve used channels in Go: Go channels are reasonably similar to Django ones. The key difference is that Django channels are network-transparent; the implementations of channels we provide are all accessible across a network to consumers and producers running in different processes or on different machines.

figure de modèle

Modèle traditionnel de demande / réponse

1473343845-django-asgi-websockets.png

Modèle de travail par canaux

1473343845-django-wsgi.png

Qu'est-ce que l'ASGI

ASGI (Asynchronous Server Gateway Interface) Draft Spec — Channels 0.17.2 documentation

This document proposes a standard interface between network protocol servers (particularly webservers) and Python applications, intended to allow handling of multiple common protocol styles (including HTTP, HTTP2, and WebSocket).

Interface standard entre les serveurs de protocole réseau (en particulier les serveurs Web) et les applications Python pour permettre la gestion de plusieurs styles de protocole courants (y compris HTTP, HTTP2, WebSocket)

Installation

Installez-le simplement avec pip et ajoutez-le à ʻINSTALLED_APPS`.

$ pip install -U channels
INSTALLED_APPS = (
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.sites',
    ...
    'channels',
)

Essayez pour la première fois

Pour l'instant, jetons un œil à Getting Started with Channels dans la documentation.

$ pip install django channels
$ django-admin startproject myapp
$ tree
.
├── db.sqlite3
├── manage.py
└── myapp
    ├── __init__.py
    ├── settings.py
    ├── urls.py
    └── wsgi.py

First Consumers

Tout d'abord, remplaçons la gestion des requêtes intégrée.

consumers.py


from django.http import HttpResponse
from channels.handler import AsgiHandler


def http_consumer(message):
    # Make standard HTTP response - access ASGI path attribute directly
    response = HttpResponse("Hello world! You asked for %s" % message.content['path'])
    # Encode that response into message format (ASGI)
    for chunk in AsgiHandler.encode_response(response):
        message.reply_channel.send(chunk)

settings.py


INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'myapp',
    'channels',
]
...
CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "asgiref.inmemory.ChannelLayer",
        "ROUTING": "myproject.routing.channel_routing",
    },
}

routing.py


from channels.routing import route

channel_routing = [
    route("http.request", "myapp.consumers.http_consumer"),
]

Le réglage est maintenant terminé.

$ tree
.
├── db.sqlite3
├── manage.py
└── myapp
    ├── __init__.py
    ├── consumers.py
    ├── routing.py
    ├── settings.py
    ├── urls.py
    └── wsgi.py
$ python manage.py runserver

Vérifiez http://127.0.0.1:8000/ et si le texte "Bonjour tout le monde! Vous avez demandé /" s'affiche, c'est OK. Cependant, c'est ennuyeux, alors créons un serveur de discussion de base en utilisant WebSockets.

Ce n'est pas du tout pratique, mais d'abord, créons un serveur qui renvoie le message au client qui a envoyé le message.

consumers.py


def ws_message(message):
    # ASGI WebSocket packet-received and send-packet message types
    # both have a "text" key for their textual data.
    message.reply_channel.send({
        "text": message.content['text'],
    })

routing.py


from channels.routing import route
from myapp.consumers import ws_message

channel_routing = [
    route("websocket.receive", ws_message),
]
$ python manage.py runserver

Accédez à http://127.0.0.1:8000/ et tapez dans la console js comme suit:

// Note that the path doesn't matter for routing; any WebSocket
// connection gets bumped over to WebSocket consumers
socket = new WebSocket("ws://" + window.location.host + "/chat/");
socket.onmessage = function(e) {
    alert(e.data);
}
socket.onopen = function() {
    socket.send("hello world");
}
// Call onopen directly if socket is already open
if (socket.readyState == WebSocket.OPEN) socket.onopen();

C'est OK si l'alerte "bonjour le monde" est affichée.

Groups

Ensuite, utilisez Groupes pour implémenter un vrai chat où vous pouvez vous parler.

cousumers.py


from channels import Group


# Connected to websocket.connect
def ws_add(message):
    message.reply_channel.send({"accept": True})
    Group("chat").add(message.reply_channel)


# Connected to websocket.receive
def ws_message(message):
    Group("chat").send({
        "text": "[user] %s" % message.content['text'],
    })


# Connected to websocket.disconnect
def ws_disconnect(message):
    Group("chat").discard(message.reply_channel)

routing.py


from channels.routing import route
from myapp.consumers import ws_add, ws_message, ws_disconnect

channel_routing = [
    route("websocket.connect", ws_add),
    route("websocket.receive", ws_message),
    route("websocket.disconnect", ws_disconnect),
]
$ python manage.py runserver

Ouvrez http://127.0.0.1:8000/ dans plusieurs onglets et tapez le même code js dans la console que précédemment. C'est OK si l'alerte de "[utilisateur] hello world" est affichée sur chaque onglet.

Running with Channels

Ensuite, commutons la couche Channel. J'avais l'habitude d'utiliser ʻasgiref.inmemory.ChannelLayer, mais cela ne fonctionne que dans le même processus. Dans un environnement de production, utilisez un backend comme ʻasgi_redis.

$ pip install asgi_redis

setting.py


CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "asgi_redis.RedisChannelLayer",
        "CONFIG": {
            "hosts": [("localhost", 6379)],
        },
        "ROUTING": "myapp.routing.channel_routing",
    },
}
$ python manage.py runserver --noworker
$ python manage.py runworker

Vous pouvez maintenant exécuter la commande runworker.

Persisting Data

L'attribut replay_channel que nous avons vu jusqu'à présent est un point unique pour les WebSockets connectés. Vous pouvez maintenant retrouver la provenance du message.

Dans un environnement de production, utilisez channel_session pour rendre la session persistante comme un cookie dans une communication HTTP.

consumers.py


from channels import Group
from channels.sessions import channel_session


# Connected to websocket.connect
@channel_session
def ws_connect(message):
    # Accept connection
    message.reply_channel.send({"accept": True})
    # Work out room name from path (ignore slashes)
    room = message.content['path'].strip("/")
    # Save room in session and add us to the group
    message.channel_session['room'] = room
    Group("chat-%s" % room).add(message.reply_channel)


# Connected to websocket.receive
@channel_session
def ws_message(message):
    Group("chat-%s" % message.channel_session['room']).send({
        "text": message['text'],
    })


# Connected to websocket.disconnect
@channel_session
def ws_disconnect(message):
    Group("chat-%s" % message.channel_session['room']).discard(message.reply_channel)

routing.py


from channels.routing import route
from myapp.consumers import ws_connect, ws_message, ws_disconnect

channel_routing = [
    route("websocket.connect", ws_connect),
    route("websocket.receive", ws_message),
    route("websocket.disconnect", ws_disconnect),
]

Authentication

Les canaux peuvent obtenir la session Django requise pour l'authentification des utilisateurs des deux manières suivantes.

L'authentification à l'aide d'une session Django est effectuée en spécifiant un décorateur.

consumers.py


from channels import Channel, Group
from channels.sessions import channel_session
from channels.auth import http_session_user, channel_session_user, channel_session_user_from_http


# Connected to websocket.connect
@channel_session_user_from_http
def ws_add(message):
    # Accept connection
    message.reply_channel.send({"accept": True})
    # Add them to the right group
    Group("chat-%s" % message.user.username[0]).add(message.reply_channel)


# Connected to websocket.receive
@channel_session_user
def ws_message(message):
    Group("chat-%s" % message.user.username[0]).send({
        "text": message['text'],
    })


# Connected to websocket.disconnect
@channel_session_user
def ws_disconnect(message):
    Group("chat-%s" % message.user.username[0]).discard(message.reply_channel)

Routing

Vous pouvez définir de manière flexible routing.py en utilisant des expressions régulières, telles que ʻurls.py` de Django.

routing.py


http_routing = [
    route("http.request", poll_consumer, path=r"^/poll/$", method=r"^POST$"),
]

chat_routing = [
    route("websocket.connect", chat_connect, path=r"^/(?P<room>[a-zA-Z0-9_]+)/$"),
    route("websocket.disconnect", chat_disconnect),
]

routing = [
    # You can use a string import path as the first argument as well.
    include(chat_routing, path=r"^/chat"),
    include(http_routing),
]

Models

L'ORM de Django facilite l'intégration de la persistance des messages.

consumers.py


from channels import Channel
from channels.sessions import channel_session
from .models import ChatMessage


# Connected to chat-messages
def msg_consumer(message):
    # Save to model
    room = message.content['room']
    ChatMessage.objects.create(
        room=room,
        message=message.content['message'],
    )
    # Broadcast to listening sockets
    Group("chat-%s" % room).send({
        "text": message.content['message'],
    })


# Connected to websocket.connect
@channel_session
def ws_connect(message):
    # Work out room name from path (ignore slashes)
    room = message.content['path'].strip("/")
    # Save room in session and add us to the group
    message.channel_session['room'] = room
    Group("chat-%s" % room).add(message.reply_channel)


# Connected to websocket.receive
@channel_session
def ws_message(message):
    # Stick the message onto the processing queue
    Channel("chat-messages").send({
        "room": message.channel_session['room'],
        "message": message['text'],
    })


# Connected to websocket.disconnect
@channel_session
def ws_disconnect(message):
    Group("chat-%s" % message.channel_session['room']).discard(message.reply_channel)

Enforcing Ordering

En donnant un décorateur @enforce_ordering (light = True), vous pouvez changer l'ordre dans lequel le websocket.connect a été créé en premier, plutôt que l'ordre dans lequel il a été mis en file d'attente.

consumers.py


from channels import Channel, Group
from channels.sessions import channel_session, enforce_ordering
from channels.auth import http_session_user, channel_session_user, channel_session_user_from_http


# Connected to websocket.connect
@enforce_ordering(slight=True)
@channel_session_user_from_http
def ws_add(message):
    # Add them to the right group
    Group("chat-%s" % message.user.username[0]).add(message.reply_channel)


# Connected to websocket.receive
@enforce_ordering(slight=True)
@channel_session_user
def ws_message(message):
    Group("chat-%s" % message.user.username[0]).send({
        "text": message['text'],
    })


# Connected to websocket.disconnect
@enforce_ordering(slight=True)
@channel_session_user
def ws_disconnect(message):
    Group("chat-%s" % message.user.username[0]).discard(message.reply_channel

Didacticiel

Un didacticiel d'application de chat comme celui que vous avez vu ici est disponible dans Heroku. Finally, Real-Time Django Is Here: Get Started with Django Channels

Si vous essayez jusqu'à ce que vous le déployez réellement sur Heroku, vous vous sentirez mieux.

Django Channels Example

Référence

en conclusion

Comme voluntas l'a écrit dans «Server Push with WebSocket in Django», si vous voulez des performances, vous devez le faire dans une autre langue.

Facile à utiliser pour les applications Django sans avoir besoin de lancer SwampDragon ou Tornado séparément. Le plus grand attrait est qu'il peut être incorporé.

Les canaux sont également envisagés pour inclusion dans Django 2.0, qui devrait sortir en décembre prochain. Utilisons-le et animons les chaînes!

Recommended Posts

Web en temps réel avec les chaînes Django
Créer une application Web avec Django
Créer une application Web avec Django
Internationalisation avec Django
CRUD avec Django
J'ai fait une application WEB avec Django
Django 1.11 a démarré avec Python3.6
Résumé du développement avec Django
Framework Web Django Python
Sortie PDF avec Django
Sortie Markdown avec Django
Utiliser Gentelella avec Django
Twitter OAuth avec Django
Premiers pas avec Django 1
Envoyer des e-mails avec Django
Dessin en temps réel avec matplotlib
La mutualisation mécanise avec Django
Utiliser MySQL avec Django
Django à partir d'aujourd'hui
Premiers pas avec Django 2
Retour sur la création d'un service Web avec Django 1
Retour sur la création d'un service Web avec Django 2
Application Web réalisée avec Python3.4 + Django (Construction de l'environnement Part.1)
Déployez des applications Web en temps réel avec Swampdragon x Apache
Faites Django avec CodeStar (Python3.6.8, Django2.2.9)
Web scraping avec python + JupyterLab
Lancez-vous avec Django! ~ Tutoriel ⑤ ~
Environnement de site Web de configuration minimale avec django
Créer une API avec Django
Construction et déploiement faciles du serveur Web avec EB CLI + git + Django
Faites Django avec CodeStar (Python3.8, Django2.1.15)
Déployer Django sans serveur avec Lambda
Python3 + Django ~ Mac ~ avec Apache
Enregistrez des images avec le web scraping
Développement d'applications Web avec Flask
Créer une page d'accueil avec django
Configurer un serveur Web avec CentOS7 + Anaconda + Django + Apache
Lancez-vous avec Django! ~ Tutoriel ④ ~
Premiers pas avec Python Django (4)
Premiers pas avec Python Django (3)
Grattage Web facile avec Scrapy
Combinez Fast API avec Django ORM
Lancez-vous avec Django! ~ Tutoriel ⑥ ~
API Web avec Python + Falcon
Utilisez Django pour enregistrer les données de tweet
Créez une API Web capable de fournir des images avec Django
Combinez deux images avec Django
Premiers pas avec Django avec PyCharm
(Pour les débutants) Essayez de créer une API Web simple avec Django
Web scraping débutant avec python
Suppression de double envoi avec Django
Framework Django REST avec Vue.js
Utilisez prefetch_related commodément avec Django
Premiers pas avec Python Django (5)
Rationalisez la recherche Web avec Python
Connectez-vous avec Django Rest Framework
Application Web avec Python + Flask ④
Qiita API Oauth avec Django
Pratique de développement d'applications Web: Créez une page de création d'équipe avec Django! (Page de création de décalage)
Lancement d'une application Web sur AWS avec django et modification des tâches