Iacopo a DjangoCon Europe con il talk "Real time applications with Django"

Applicazioni real time con Django

Channels ti permette di farlo riducendo la complessità, scopri come

Il web è andato arricchendosi ed oggi non c’è più il semplice meccanismo request e response perché via via le app sono diventate più articolate. Questo significa che servono molti strumenti per svilupparle, fattore che per i developers può essere interessante per imparare a utilizzare i tool ma dall’altro lato rende il tutto più macchinoso.

Channels - Concetti base

Esistono diversi framework per poter creare applicazioni in real time. Durante il mio talk “Building real time applications” a DjangoCon Europe 2018 ho parlato di come farlo con Django e in particolare con Channels, un framework per la realizzazione di applicazioni asincrone che permette di andare oltre al meccanismo request-response tipico dell’HTTP. 

Con Channels è possibile utilizzare vari protocolli asincroni. In questo post faremo riferimento ai websocket, una tecnologia web che fornisce canali di comunicazione full duplex, che trasmettono e ricevono messaggi bidirezionalmente in maniera simultanea.  Si tratta di una delle tecnologie maggiormente utilizzate in quanto il server non deve essere sollecitato continuamente affinchè fornisca contenuti al browser.

Rispetto alla precedente versione, la release 2.0 di Channels porta un grande cambiamento. Channels 1.0 permetteva solamente di scrivere codice sincrono, nascondendo quello asincrono, mentre la nuova release espone anche un'interfaccia asincrona, lasciando così lo sviluppatore libero di decidere quale approccio utilizzare.   

Channels utilizza il protocollo Asynchronous Server Gateway Interface (ASGI) e lo implementa ad ogni livello: ogni parte di Channels è un’applicazione ASGI in grado di funzionare autonomamente. Questa struttura fa sì che il software sia modulare, permettendo allo sviluppatore di realizzare una propria pipeline.

Il primo elemento che deve essere impostato è il protocol server, una parte di software che interagisce con la rete, ovvero intercetta le chiamate e traduce tutto in ASGI all’applicazione. Nel caso di Websockets e di HTTP possiamo usare Daphne. Una volta stabilita la connessione, verrà creato uno scope, che raccoglie tutte le informazioni sulla connessione stessa e il pacchetto connessione dovrà essere “instradato” attraverso il routing. Lo scope fa sì che la connessione e l’istanza dell’applicazione dialoghino, mentre il routing mappa i messaggi al consumer. 

In Channels i consumer sono delle astrazioni di alto livello, delle classi che gestiscono gli eventi. In generale i consumer sono indipendenti dal protocollo, mentre fanno eccezione i consumer websocket, che sono una specializzazione legata ai websocket. Nello specifico, in questo post approfondiremo cosa è possibile fare con i consumer websocket.

Da dove prende nome l’app? Channels è, ironicamente, un elemento “minore” in quanto si tratta di un meccanismo che ha la funzione di far passare i messaggi attraverso le istanze dei vari consumer. 

LA DEMO CHE HO REALIZZATO

Per dimostrare le potenzialità di Channels ho realizzato un’applicazione web che permette di eseguire le seguenti azioni:

  • Contare gli utenti attivi
    gli utenti hanno la possibilità di sapere se vi sono altri utenti collegati alla dashboard  
  • Concurrency checking
    gli utenti possono verificare se vi sono altri users che effettuato l’accesso o stanno modificando una data risorsa
  • Avere tutte le notifiche utili sul browser
    tutte le azioni svolte dagli utenti sono visibili dalla dashboard  

QUALI SONO LE APP ASGI CHE SERVONO PER REALIZZARLA

Per realizzare questa applicazione ho operato su 3 livelli: 

  • configurazione di channels
  • routing
  • consumers

CONFIGURAZIONE DI CHANNELS

ASGI_APPLICATION = 'dashboard.routing.application'
CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'channels_redis.core.RedisChannelLayer',
        'CONFIG': {
            'hosts': [('localhost', 6379)],
        },
    },
}


La configurazione di channels permette la comunicazione tra le varie istanze dell’applicazione e la sua funzione è quella di comunicare al serverdove deve mandare i messaggi. Channels fornisce uno storage di default che dovrà essere configurato. L’unico requisito obbligatorio da definire all’interno della configurazione di channels è quale applicazione ASGI deve utilizzare che, nell’esempio preso in esame, è il routing.  

ROUTING

Per l’applicazione presentata durante il talk ho impostato tre routing che vengono eseguiti in serie. 

# equivalent to my_project.urls
# tipically used to include application routing
application = ProtocolTypeRouter({
    'websocket': AuthMiddlewareStack(
        URLRouter([
            path('status/', documents_routing),
        ])
    ),
})


Come primo elemento si inserisce il routing del protocollo, nel nostro caso useremo solo websocket, subito dopo usiamo il middleware, che "migra" i dati di autenticazione di Django sullo scope.
Dentro il middleware si inseriscono i router, che mapperanno i path delle chiamate websocket verso i consumer. I router sono specifici per il protocollo utilizzato e tra questi vi è anche l’URL router, specifico per Websocket che instrada i messaggi in accordo con il percorso fornito. Infine, è stato impostato l’application routing, anche questo specifico per websocket, che ha la funzione di collegare il percorso stabilito ad ogni singolo consumer.

channel_routing = URLRouter([
    path('users/', UserCounterConsumer),
    path('documents/', DocumentListConsumer),
    path('document/<str:slug>/<str:phase>/', DocumentDetailConsum
])

CONSUMER

I consumer sono organizzati in una gerarchia di classi che gestisce il funzionamento dell’applicazione su più livelli.    

Prendendo in esame un consumer websocket vi sono tre eventi principali che devono essere impostati e gestiti:
• connessione
• disconnessione
• ricezione (di un messaggio)

Nell’esempio che stiamo analizzando è stato creato un consumer che conta il numero di utenti che si collegano all’applicazione. Come funziona?

class UserCounterConsumer(JsonWebsocketConsumer):
    groups = 'users',

    def connect(self):
    """ Increment users on connect and notify other consumers"""
    super().connect()
    if self.scope['user'].is_authenticated:
        increment_users(message.user)
    msg = {'users': count_users(),
    'type': 'users.count'}
    async_to_sync(self.channel_layer.group_send)('users', msg


Affinchè gli altri users, collegati sullo stesso gruppo, possano ricevere il messaggio di un nuovo collegamento è necessario che il routing instradi la connessione al metodo connect, che gestisce l’evento connect. 

def users_count(self, event):
    """ Notify connected user """
    self.send_json(content=event['message'])


Successivamente all’invio del messaggio, l’applicazione richiama il metodo users.count, che viene gestito da users_count. In sintesi si genera un evento di tipo custom gestito da una funzione con un nome specifico. 

Nel caso del concurrency monitoring, i gruppi vengono utilizzati in maniera più marcata

class DocumentListConsumer(JsonWebsocketConsumer):
        @property
        def groups(self):
            return Document.Status.list,

        def connect(self):
            super(DocumentListConsumer, self).connect()
            async_to_sync(self.channel_layer.group_send)(
                 self.slug, {
                     'type': 'document.status',
                     'message': self.get_status_packet()
        })

        def document_status(self, event):
            self.send_json(content=event['message'])


Ad ogni documento, rappresentato da slug, corrisponde un gruppo. Quando un utente si collega ad un path la sua istanza è registrata sul gruppo definito con lo slug specifico e alla connessione il contatore degli utenti attivi viene aggiornato.

In sintesi il cuore del funzionamento dell’app è 

def connect():
    ...
    async_to_sync(self.channel_layer.group_send)(
         self.slug, {
             ...
    })


Questo metodo permette di inviare messaggi e generare eventi che vengono gestiti dal consumer e permette a una funzione sincrona di interagire con una funzione asincrona. Connect è l’elemento che permette di mandare un messaggio sul gruppo. Nel caso dell’applicazione realizzata viene inviato agli utenti presenti nel gruppo un dizionario ma si possono inviare diversi messaggi, l’unica cosa obbligatoria da definire è il tipo di messaggio perché è ciò che definisce quale evento verrà generato. Per il funzionamento dell’applicazione presentata è stato definito un evento di tipo document.status
Tutti i consumer in ascolto sul gruppo slug dovranno implementare il metodo di cui abbiamo appena parlato.

Speriamo di aver suscitato la vostra curiosità e di avervi invogliato a sperimentare con Channels, continuate a seguire il nostro blog per altre notizie su applicativi Django e tips di sviluppo!