Archivio

Posts Tagged ‘channels’

Django channels: chat

15 Gennaio 2019 Commenti chiusi

Django-channels è stato creato per gestire, con django, i protocolli di comunicazione asincrona come i websockets.
Oltre alla gestione dei websockets, channels permette di eseguire processi asincroni in background.

In ogni tipica applicazione Django, il traffico HTTP è gestito in maniera Sincrona, ovvero, quando un browser manda una request,
aspetta che questa venga instradata a Django, il quale dopo le opportune elaborazioni rispedirà al mittente una response.

Invece la cosa è molto più versatile con i websockets, perchè la comunicazione è di tipo bidirezionale.
Quando una connessione ad un websocket viene stabilita, il browser può spedire e ricevere messaggi e la visualizzazione avviene automaticamente senza bisogno di “ricaricare” ogni volta il browser.

Vediamo un esempio chiarificatore.
Creiamo una chat (è il solito esempio che troverete sul web in migliaia di forme simili) con django e django-channels.

Per prima cosa creiamo un virtual-environment:

C:\tmp\django-dev>python -m venv venv

lo attiviamo:

C:\tmp\django-dev>venv\Scripts\activate

ed installiamo le librerie necessarie:

(venv) C:\tmp\django-dev>pip install django

Prima di installare channels, installiamo Twisted da wheel per non incorrere in un fastidioso errore.
Da questo sito scaricare la versione corretta, nel mio caso quella per python 3.7 a 32 bit e posizionare il file scaricato, all’interno del venv creato in precedenza. Installare il file:

(venv) C:\tmp\django-dev>pip install venv\Twisted-18.9.0-cp37-cp37m-win32.whl
Processing c:\tmp\django-dev\venv\twisted-18.9.0-cp37-cp37m-win32.whl
...
Installing collected packages: Twisted
Successfully installed Twisted-18.9.0

Installato twisted procedere con i pacchetti channels ed asgi_redis:

(venv) C:\tmp\django-dev>pip install channels channels_redis pywin32

Pywin32 è necessario per poter utilizzare Redis-server su windows.

La fase successiva è quella di creare il progetto django e l’applicazione chat:

(venv) C:\tmp\django-dev>django-admin startproject djangosite
(venv) C:\tmp\django-dev>cd djangosite
(venv) C:\tmp\django-dev\djangosite>python manage.py startapp chat

Ora andremo a modificare il codice per creare la nostra chat.
Nel file djangosite\setiings.py aggiungere channels e chat:

...

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

...

Ora puntiamo al nostro server REDIS, sempre nel file djangosite\setiings.py:

...

ASGI_APPLICATION = "djangosite.routing.application"

CHANNEL_LAYERS = {
    'default': {'BACKEND': 'channels_redis.core.RedisChannelLayer',
                'CONFIG': {"hosts": [('127.0.0.1', 6379)], }, },
                 }

Come si nota nella prima riga di codice inserita, puntiamo ad un file routing, che ora andiamo a creare:

# djangosite/routing.py

from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
import chat.routing


application = ProtocolTypeRouter({
    # (http->django views is added by default)
    'websocket': AuthMiddlewareStack(
        URLRouter(chat.routing.websocket_urlpatterns)
    ),
}
)

Qui viene definito il ProtocolTypeRouter e si richiamano le “url” riferite ai websockets dell’applicazione chat.
Dobbiamo quindi creare anche il file routing.py dell’applicazione chat:

# chat/routing.py

from django.conf.urls import url

from . import consumers

websocket_urlpatterns = [
    url(r'^ws/chat/$', consumers.ChatConsumer),
]

Come si nota, qui importiamo e facciamo riferimento ad un Consumer (ChatConsumer).
I consumers sono l’equivalente delle views in una app tradizionale di Django.
Per approfondimenti sui Consumers fare riferimento alla doc ufficiale.

Creiamo il file consumers quindi:

# chat/consumers.py

from asgiref.sync import AsyncToSync
from channels.generic.websocket import WebsocketConsumer
import json


class ChatConsumer(WebsocketConsumer):
    @property
    def _topic_name(self):
        return "topic_DjangoWebsocketTest"

    @property
    def _chat_channel_name(self):
        return "DjangoWebsocketTest"

    def connect(self):
        AsyncToSync(self.channel_layer.group_add)(
            self._chat_channel_name,
            self.channel_name)
        self.accept()

    def disconnect(self, close_code):
        AsyncToSync(self.channel_layer.group_discard)(
            self._chat_channel_name,
            self.channel_name)

    def receive(self, text_data):

        json_text_data = '{"message": "%s"}' % text_data
        text_data_json = json.loads(json_text_data)
        message = text_data_json['message']

        AsyncToSync(self.channel_layer.group_send)(
            self._chat_channel_name,
            {
                'type': 'chat_message',
                'message': message
            })

    # Handles the "chat_message" event when it's sent to our group
    def chat_message(self, event):
        message = event['message']
        self.send(text_data=message)

Ora dobbiamo modificare i vari file urls, cominciando con quello principale:

# djangosite\urls.py

from django.contrib import admin
from django.conf.urls import include, url

urlpatterns = [
    url(r'^', include('chat.urls')),
    url(r'^admin/', admin.site.urls),
]

Poi, richiamandolo tramite include, creiamo quello specifico della app chat:

# chat\urls.py

from django.contrib import admin
from django.conf.urls import include, url
from chat import views

urlpatterns = [
    url(r'^chat$', views.chat, name='chat'),
]
Creiamo la views per effettuare la chat:

# chat/views.py

from django.shortcuts import render

# Create your views here.


def chat(request):
    return render(request, 'chat/chat.html', {})

Ed infine creiamo le templates, partendo da quella base. Creare quindi la solita directory sotto chat e
dentro quest ultima, creaiamo un’altra sottodirectory chat. Creeremo quindi due file:

chat\templates\chat\base.html
chat\templates\chat\chat.html

Il file base dal quale erediteremo sarà:

{% load staticfiles %}
{% load static %}
{# Load the tag library #}
{# Load CSS and JavaScript #}

{% block head %}
{% endblock %}

<title>{% block title %}Channels Chat{% endblock %}</title>

{% block navbar %}
{% endblock %}

<html>

  <head>
      {% block script %}{% endblock script %}
  </head>

  <body>
      {% block content %}{% endblock %}
  </body>

</html>

il file specifico invece che erediterà, sarà:

{% extends 'chat/base.html' %}

{% block title %}<title>Channels Chat</title>{% endblock %}

{% block content %}
  <html>
   <script>
    window.onload = function() {

      // Get references to elements on the page.
      var form = document.getElementById('message-form');
      var messageField = document.getElementById('message');
      var messagesList = document.getElementById('messages');
      var socketStatus = document.getElementById('status');
      var closeBtn = document.getElementById('close');
      var wsurl = "ws://127.0.0.1:8000/ws/chat/";

      // create a new socket
      var socket = new WebSocket(wsurl);

      // event listener all'apertura del socket
      socket.onopen = function open(event) {
            socketStatus.innerHTML = 'Connected to: ' + event.currentTarget.url;
            socketStatus.className = 'open';
       };

    // Send a message when the form is submitted.
    form.onsubmit = function(e) {
      e.preventDefault();

      // Retrieve the message from the textarea.
      var message = messageField.value;

      // Send the message through the WebSocket.
      socket.send(message);

      // Add the message to the messages list.
      messagesList.innerHTML += '<li class="sent"><span>Sent:</span>' + message +
                                '</li>';

      // Clear out the message field.
      messageField.value = '';

      return false;
    };

    // Handle messages sent by the server.
    socket.onmessage = function(event) {
      var message = event.data;
      messagesList.innerHTML += '<li class="received"><span>Received:</span>' +
                                 message + '</li>';
    };

    };
    </script>

   <body>
   <b><font color="green">Django Chat</font></b>
    <div id="page-wrapper">
        <h1>WebSockets with Django and Channels</h1>

        <div id="status">Connecting...</div>

        <ul id="messages"></ul>

        <form id="message-form" action="#" method="post">
          <textarea id="message" placeholder="Write your message here..." required></textarea>
          <button type="submit">Send Message</button>
          <button type="button" id="close">Close Connection</button>
        </form>
      </div>
    </body>
  </html>
{% endblock %}

Ora vediamo che tutto funzioni a dovere.
Da un altro prompt dei comandi, lanciamo il server redis con l’eseguibile redis-server:

(venv) C:\tmp\redis>redis-server.exe
...
[1780] 15 Jan 12:11:24.518 # Server started, Redis version 3.2.100
[1780] 15 Jan 12:11:24.518 * DB loaded from disk: 0.000 seconds
[1780] 15 Jan 12:11:24.518 * The server is now ready to accept connections on port 6379

e lanciamo il nostro server Django:

(venv) C:\tmp\django-dev\djangosite>python manage.py runserver
Django version 2.1.5, using settings 'djangosite.settings'
Starting ASGI/Channels version 2.1.6 development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.

Channels è attivo come si può notare:

Starting ASGI/Channels version 2.1.6 development server at http://127.0.0.1:8000/

Ora proviamo a raggiungere la pagina chat all’indirizzo http://127.0.0.1:8000/chat:


La console di django con debug attivo, mi direbbe che tutto funziona a dovere:

HTTP GET /chat 200 [0.05, 127.0.0.1:52135]
WebSocket HANDSHAKING /ws/chat/ [127.0.0.1:52139]
WebSocket CONNECT /ws/chat/ [127.0.0.1:52139]

Proviamo a scrivere un messaggio, otterremmo ciò che segue.

Il messaggio che spediamo, viene anche ricevuto inquanto per come abbiamo strutturato il consumer, facendo parte del gruppo su cui scriviamo, riceviamo lo stesso messaggio spedito.


Se ora aprissimo un secondo browser e ci collegassimo allo stesso indirizzo, la chat funzionerebbe.
Spediamo pertanto un paio di messaggi da entrambi i browser e verifichiamo che tutto funzioni.
Scrivo dal Secondo client:


Ricevo dal Primo client:


Scrivo dal Primo Client:


Ricevo dal Secondo Client:


Non resta che chiudere la connessione con l’apposito pulsante, disconnessione che verrà mostrata correttamente anche nella console di django:

WebSocket DISCONNECT /ws/chat/ [127.0.0.1:52200]
WebSocket DISCONNECT /ws/chat/ [127.0.0.1:52139]

Link utili:
django
django-channels
redis
example code on github