Piperchat

Alessandro Mazzoli
alessandro.mazzoli9@studio.unibo.it

Luigi Borriello
luigi.borriello2@studio.unibo.it

Manuel Andruccioli
manuel.andruccioli@studio.unibo.it

Tommaso Patriti
tommaso.patriti@studio.unibo.it

Obiettivi e Requisiti

Il progetto si concentra sulla creazione di una piattaforma per la messaggistica e la comunicazione multimediale ispirata a Discord. Gli obiettivi del progetto includono:

Glossario dei termini
Termine Definizione
Server Raggruppamento di canali
Canale Luogo dove più utenti possono comunicare. Può essere testuale o multimediale
Messaggio Informazioni, in formato testuale, scambiate fra due utenti o all’interno di un canale testuale
Sessione Comunicazione audio/video fra due utenti o all’interno di un canale multimediale
Direct Interazione privata tra due utenti

Casi d’uso

Utente che accede al servizio

Un utente, che accede al servizio Piperchat, si trova davanti a due scelte:

  1. Accedere al servizio autenticandosi, che prevede la necessità di essersi registrati precedentemente.

  2. Visualizzare la Dashboard che permette il monitoring dello stato dei servizi.

Diagramma dei casi d’uso di un utente non autenticato

Utente autenticato

Dopo aver eseguito l’autenticazione, un utente acquisisce la possibilità di svolgere numerose azione, che riguardano diversi ambiti. Si possono identificare i seguenti scenari:

L’utente ha ora possibilità di gestire il proprio profilo ed è abilitato a ricevere le notifiche a lui indirizzate.

L’utente può utilizzare la gestione delle richieste di amicizia che prevede di poter

inviarne ad altri utenti,

accettare le richieste ricevute

rifiutare le richieste ricevute.

L’utente ha la possibilità di creare server oppure partecipare a server già creati.

Diagramma dei casi d’uso di un utente autenticato

Utente amministratore di server

Un utente, dopo aver creato un server, ne diventa amministratore. Questo permette di accedere alle funzionalità di gestione per esso, tra le quali si possono evidenziare le seguenti:

  1. L’amministratore può aggiornare le informazioni del server o eliminarlo.

  2. L’amministratore può rimuovere un utente dal server.

  3. L’amministratore può creare i canali (testuali o multimediali).

  4. L’amministratore può aggiornare o rimuovere i canali già creati.

Diagramma dei casi d’uso di un utente amministrazione di un server

Interazione tra utente e amici

Due utenti, dopo aver stretto amicizia, hanno la possibilità di interagire fra loro nei seguenti modi:

  1. Inviare messaggi all’interno della chat tra i due utenti.

  2. Partecipare alla sessione multimediale.

Diagramma dei casi d’uso di un utente che interagisce con un amico

Interazione di un utente partecipante ad un server

Un utente che partecipa ad un server ha la possibilità di interazione attraverso i canali che sono stati creati, nei quali può:

  1. Inviare messaggi all’interno dei canali testuali.

  2. Partecipare ad un sessione in un canale multimediale.

Diagramma dei casi d’uso di un utente che partecipa ad un server

Utente in sessione multimediale

Un utente in sessione multimediale, sia in una privata con un amico, che in un canale, ha la possibilità di gestire microfono e webcam e di uscire dalla sessione stessa.

Diagramma dei casi d’uso di un utente che partecipa ad una sessione

Politica di autovalutazione

Nella valutazione della qualità del software prodotto per il progetto PiperChat, verrà adottato un approccio che tiene in considerazione i seguenti fattori:

Per valutare l’efficacia degli esiti del progetto, verranno considerati i seguenti criteri:

Analisi dei Requirements

Di seguito vengono formalizzati i requisiti:

Funzionali

  1. Registrazione e Autenticazione:

    1. Possibilità di registrazione al sistema.

    2. Possibilità di login nel sistema.

  2. Sistema di amicizie:

    1. Possibilità di inviare richieste di amicizia ad altri utenti.

    2. Possibilità di accettare o rifiutare richieste di amicizia.

    3. Sistema di notifiche per la ricezione di nuovo richieste di amicizia.

    4. Sistema di gestione dello stato (online) e dell’ultimo accesso degli utenti.

  3. Interazioni con amici:

    1. Gestione messaggistica tra i propri amici.

    2. Sistema di notifiche per l’arrivo di nuovi messaggi.

    3. Possibilità di entrare in sessione con un amico.

  4. Gestione Server:

    1. Possibilità di creazione di un nuovo server.

    2. Possibilità di entrare un server esistente.

    3. Possibilità da parte del creatore del server di rimuovere i membri.

  5. Gestione canali:

    1. Possibilità di creazione di canali (testuali o multimediali) da parte del creatore del server.

    2. Possibilità di rimozione di canali da parte del creatore del server.

    3. Sistema di messaggistica per i canali testuali.

    4. Sistema di notifiche per i nuovi messaggi inviati all’interno di canali testuali.

    5. Possibilità di accedere alla sessione dei canali multimediali.

  6. Gestione sessione:

    1. Possibilità di comunicare attraverso gli altri partecipanti alla stessa sessione.

    2. Possibilità di accendere e spegnere il microfono e la fotocamera.

  7. Sistema di monitoring dei microservizi:

    1. Sistema di monitoraggio dello stato dei servizi.

Non funzionali

  1. Sicurezza:

    1. Autenticazione degli utenti per verificarne l’identità.

    2. Autorizzazione degli utenti per l’accesso alle risorse in base alle regole stabilite.

    3. Crittografia per garantire la confidenzialità delle password degli utenti.

  2. Scalabilità:

    1. Scalabilità orizzontale per gestire l’aumento del carico operativo.

  3. Manutenibilità:

    1. Modularità degli artefatti.

    2. Codice sorgente ben strutturato e comprensibile.

Design

Dominio

Il dominio di PiperChat ruota attorno ai seguenti concetti chiave: gli Utenti, le Amicizie, i Server e Canali.

Un utente, dopo essersi registrato ed autenticato al sistema, si trova di fronte a due possibilità: poter interagire privatamente con un altri utenti, oppure partecipare a dei server.

Interazione tramite Direct

Al fine di permettere l’interazione mediante i Direct, due utenti devono, preventivamente, stringere un legame di amicizia.

Successivamente ogni utente ha la possibilità di inviare messaggi privati ai propri amici, oppure partecipare ad una sessione.

Interazione tramite server

Un utente può partecipare oppure creare dei Server.

Alla creazione di un server, l’utente ne diventa proprietario (o amministratore). Questo gli permette di accedere alle varie funzionalità di amministrazione come la modifica delle impostazioni, la rimozione degli altri partecipanti e la creazione di canali.

I Canali possono essere:

Struttura e relazioni

Di seguito è riportata la struttura del dominio e le interazioni tra le entità precedentemente descritti.

Diagramma delle classi del dominio e le loro relazioni

Microservizi

Per la realizzzione del sistama si è deciso di adottare un’architettura a microservizi, in modo da permette di suddividere la complessità in parti più piccole ad alta coesione, ma accoppiate in modo lasco tra loro. Inoltre, ogni microservizio, se necessario, dispone di un database, al quale può accedervi in modo esclusivo.

Un esempio di microservizio con il proprio database

A fronte di ciò sono stati identificati i seguenti microservizi:

Moduli

Per quanto riguarda la gestione dei moduli dell’applicazione si è scelto di optare per lo sviluppo di un modulo indipendente per ogni servizio.

Tuttavia sono presenti i seguenti moduli comuni:

Diagrammi dei package Piperchat

Comportamento

Di seguito, vengono descritti nel dettaglio i comportamenti dei vari microservizi presenti nel sistema.

Users Service

Microservizio responsabile della gestione degli utenti e tutte le operazioni relative a quest’ultimi, fra cui:

Piperchat Service

Microservizio relativo ai server e ai canali, gestisce tutte le operazioni quali:

Messages Service

Microservizio relativo alla messaggistica. Si occupa della gestione delle comunicazioni testuali tra gli utenti all’interno della piattaforma PiperChat, sia per chat dirette tra utenti che per i canali testuali.

Multimedia Service

Il microservizio si occupa della gestione delle chiamate multimediali tramite l’utilizzo del concetto di sessione multimediale, sia per le conversazioni dirette tra due utenti, che per i canali.

Notifications Service

Il microservizio si occupa di gestire la comunicazione real-time con i client. Esso si occupa sia di inviare al client tutte le notifiche su eventi che lo riguardano e tiene inoltre traccia degli utenti connessi, andando a rendere disponibili tali informazioni tramite API per ottenere lo stato di un determinato utente.

Monitoring Service

Il microservizio si occupa del monitoraggio dello stato operativo dei servizi, eseguendo regolarmente verifiche per stabilire se si trovino in uno stato online o offline.

Interazione

Al fine di garantire sia una comunicazione interna fra i microservizi, che una comunicazione verso l’esterno, sono stati identificati e realizzati i seguenti componenti:

Un microservizio collegato al Broker e all’API Gateway

Eventi

Di seguito sono riportate le interazioni che hanno i microservizi tra loro per quanto riguarda gli eventi che ricevono ed inviano:

Servizio Utenti

Per quanto riguarda il servizio degli utenti, esso non ascolta nessuna tipologia di evento ma produce tutti gli eventi relative alla creazione/modifica degli utenti e delle richieste di amicizia in modo che gli altri servizi possono ricostruire lo stato degli utenti e delle amicizie tra di loro.

image [fig:users-events]

Servizio Piperchat

Per quanto riguarda il servizio Piperchat anch’esso non ascolta nulla e produce tutti gli eventi relativi ai server/canali e alle partecipazioni/abbandoni dei server da parte degli utenti. In questo modo gli altri servizi possono rimanere sincronizzati su tali informazioni.

image [fig:piperchat-events]

Servizio Messaggi

Il servizio messaggi rimane in ascolto per tutti gli eventi che riguardano i canali e le amicizie in modo da sapere se l’invio di un messaggio è possibile. Inoltre si occupa della produzione degli eventi riguardanti l’invio di nuovi messaggi.

image [fig:messages-events]

Servizio Multimedia

Il servizio che si occupa della gestione delle sessioni multimediali rimane in ascolto degli eventi riguardanti i server e le amicizie anch’esso per capire quando una sessione multimediale tra due utenti o in un server possa essere effettuata. Si occupa anche della pubblicazione degli eventi riguardanti i join/left delle sessioni da parte degli utenti.

image [fig:multimedia-events]

Servizio Notifiche

Il servizio di notifiche ascolta la maggior parte degli eventi che propaga agli utenti interessati in modo da rendere l’interfaccia e l’interazione con il servizio reattivi. Tiene inoltre traccia degli utenti che sono a lui connessi per stabilire il loro status (online/offline/ultimo accesso).

image [fig:notfications-events]

Recap dell’architettura proposta

Un utente, per accedere al servizio sfrutta l’API Gateway. In questo modo è possibile nascondere l’implementazione retrostante.

Di seguito è riportato lo schema architetturale del sistema.

Architettura di Piperchat

Dettagli implementativi

Documentazione

Swagger

Swagger è un insieme di strumenti open source per progettare, creare, documentare e consumare servizi web RESTful, attraverso la specifica Open API. L’obiettivo principale di Swagger è semplificare e standardizzare il processo di sviluppo e integrazione di API, fornendo una documentazione interattiva e uno strumento per testare direttamente le API.

Le caratteristiche chiave di Swagger includono:

Utilizzo

Con Swagger, è stato possibile creare documentazione dettagliata delle API, specificando dettagli come gli endpoint, i parametri, i tipi di dati, le risposte possibili e persino esempi di richieste e risposte.

Inizialmente, lo strumento è stato utilizzato per realizzare il design del sistema, permettendo di documentare ciò che si sarebbe dovuto implementare. I cicli iterativi di raffinamento hanno permesso di convergere all’attuale stato dell’API.

La documentazione delle API del progetto è consultabile sulle Github Pages: https://zucchero-sintattico.github.io/piperchat/api/rest/

AsyncApi

AsyncAPI è uno standard di specifica per la progettazione di API asincrone. Simile a come OpenAPI è utilizzato per definire e documentare API sincrone, AsyncAPI è progettato specificamente per gestire le comunicazioni asincrone, come quelle basate su messaggi o eventi.

Le principali caratteristiche di AsyncAPI includono:

Utilizzo

AsyncAPI è stato utilizzato come strumento di supporto e di documentazione di tutte le tipologie di messaggi rappresentanti gli eventi che vengono scambiati tramite il broker all’interno dell’architettura a microservizi.

La documentazione delle API per i messaggi infra-servizi è consultabile sulle Github Pages: https://zucchero-sintattico.github.io/piperchat/api/infra-service/

Inoltre, la tecnologia è stata sfruttata anche per la documentazione dei messaggi inviati ai client come notifiche di eventi. Tale documentazione è disponibile al seguente link: https://zucchero-sintattico.github.io/piperchat/api/notification/

View

Per la realizzazione del software in oggetto, abbiamo fatto ampio uso del framework Vue.js2, un potente e flessibile framework JavaScript per la costruzione di interfacce utente moderne e reattive. Vue.js si è rivelato una scelta eccellente per la nostra applicazione, offrendo una struttura chiara e modulare che ha semplificato lo sviluppo e la manutenzione del codice. La sua capacità di gestire in modo efficiente la visualizzazione dinamica dei dati e la reattività dell’interfaccia utente ha contribuito in modo significativo a garantire un’esperienza utente fluida e coinvolgente.

Pinia

Pinia JS3 è una libreria di gestione dello stato progettata per applicazioni Vue.js. Essa fornisce un’architettura di gestione dello stato centralizzata e reattiva, offrendo uno store centralizzato per memorizzare e gestire lo stato dell’applicazione. Pinia si basa sui concetti principali di Vue.js, come la reattività e la gestione delle modifiche dello stato in modo efficiente. Questo strumento consente agli sviluppatori di scrivere codice pulito e manutenibile, facilitando la gestione dello stato dell’applicazione Vue.js.

Quasar

Quasar4 è un framework open-source basato su Vue.js, progettato per semplificare lo sviluppo di applicazioni web e mobile con un unico codice sorgente. È noto per la sua flessibilità e la capacità di generare applicazioni per diverse piattaforme. Le caratteristiche principali di Quasar Framework includono:

  1. Componenti Vue.js predefiniti: Quasar offre una vasta libreria di componenti Vue.js personalizzati e ricchi di funzionalità, che semplificano la creazione di interfacce utente sofisticate.

  2. Responsive Design: Le applicazioni Quasar possono essere facilmente rese responsive per adattarsi a diversi dispositivi e dimensioni dello schermo.

  3. Material Design: Quasar aderisce a Material Design di Google, offrendo un aspetto moderno e uniforme per le applicazioni.

Api Gateway

Traefik

Traefik5 è un moderno reverse proxy e load balancer progettato per gestire il traffico web in ambienti complessi e distribuiti.

Utilizzo

Nel contesto del nostro progetto, l’utilizzo di Traefik è stato utilizzato per realizzare un API Gateway, instradando e distribuendo il traffico tra il frontend e i diversi microservizi del backend. All’interno di ciascun file Docker Compose relativo ai singoli servizi, sono state specificate le rotte accettate dal microservizio attraverso l’aggiunta di una label6. Questo approccio ci ha permesso di configurare facilmente e in modo dettagliato le direttive per il routing del traffico verso ciascun servizio, consentendo a Traefik di instradare le richieste in base alle specifiche esigenze di ciascun microservizio.

Di seguito è riportato un esempio della definizione delle rotte.

# Compose file

service:
  users-service:
    ...
    labels:
      - |
        traefik.http.routers.users-service.rule=
        (Method(`GET`) && Path(`/friends`)) ||
        (Method(`GET`) && Path(`/whoami`)) ||
        (Method(`GET`, `POST`) && Path(`/friends/requests`)) ||
        ...

Gestione comunicazione persistente

Per quanto riguarda la gestione delle comunicazioni persistenti tra backend e client è stata utilizzata la libreria Socket.io, impiegata sia nel servizio di notifiche che nel servizio multimediale per propagare i messaggi nel protocollo di join di una sessione multimediale.

Socket.io

Socket.IO è una libreria JavaScript che fornisce una comunicazione bidirezionale in tempo reale tra il server e il client in applicazioni web. È spesso utilizzata per implementare funzionalità di chat, giochi multiplayer, aggiornamenti in tempo reale e altre applicazioni che richiedono una comunicazione immediata tra il server e il browser. Le principali caratteristiche di Socket.IO per cui è stato scelto come framework sono:

Notifications Service: notifiche e gestione status

Un utente, quando si connette al sistema, deve essere considerato online e deve essere abilitato alla ricezione delle notifiche.

Date queste premesse, viene instaurata, tra il server e il client autenticato, una connessione tramite Socket.IO. Si è deciso di collassare, all’interno di questa socket, le due responsabilità:

  1. Permettere al server di inviare notifiche ai client.

  2. Finché la socket è aperta, il client viene considerato online.

Considerazioni su scalabilità e stato persistente

Dal momento che il microservizio mantiene uno stato, la scalabilità orizzontale potrebbe non essere ovvia. Di seguito vengono analizzate le considerazioni effettuate al fine di permettere la scalabilità orizzontale, senza incorrere in inconsistenze.

Utente stabilisce la connessione

Quando un utente fa richiesta verso il servizio di notifica, instaura una socket verso una specifica replica. Questa replica sarà colei che manterrà la connessione persistente verso l’utente. Inoltre, essa si occupa di aggiornare lo status (online/offline) nel database condiviso con le altre repliche.

Nuovo evento per un utente

Quando un nuovo evento viene generato nel sistema ed un utente, deve essere notificato. Il broker invierà in broadcast l’evento a tutte le copie del servizio, ma sarà solo la replica che mantiene la connessione persistente ad inviare al client l’opportuna notifica.

Richiesta dello status di un utente

Quando un utente qualsiasi richieste lo stato di un altro utente, qualsiasi replica può assolvere a questa richiesta dal momento che lo status è salvato nel database condiviso.

Un utente crea una connessione persistente

Socket.IO vs Server Sent Event

Durante le fasi iniziali del progetto, al fine di inviare le notifiche dal microservizio verso il client, era stata adottato Server-Sent Events7, una tecnologia di comunicazione monodirezionale, che permette di ricevere dati da un server che li invia.

Questo approccio è stato successivamente sostituito da Socket.IO perché il server non può gestire in modo responsivo la chiusura di una connessione da parte del client.

WebRTC

WebRTC8, acronimo di “Web Real-Time Communication", è una tecnologia open-source che consente la comunicazione audio e video in tempo reale direttamente tra browser web senza richiedere plugin o software aggiuntivi. È utilizzato per creare applicazioni di videoconferenza, chat video, streaming multimediale e altro.

Le principali caratteristiche di WebRTC includono:

  1. Comunicazione peer-to-peer: WebRTC consente ai browser di comunicare direttamente tra loro, evitando la necessità di server intermediari per la trasmissione di dati in tempo reale.

  2. Supporto per audio e video: WebRTC supporta la comunicazione audio e video in tempo reale, consentendo agli utenti di interagire tramite chat video o conferenze online.

  3. Accesso ai dispositivi: WebRTC consente l’accesso ai dispositivi hardware come telecamere e microfoni per abilitare la cattura audio e video.

Funzionamento

Nel contesto di WebRTC, ci sono tre concetti chiave: offer, answer, e ICE candidates.

In sintesi, il flusso tipico di inizializzazione di una connessione WebRTC coinvolge la creazione di un’offerta da parte del peer iniziatore, la trasmissione di questa offerta al peer destinatario, la creazione di una risposta da parte del peer destinatario e, infine, lo scambio di candidati ICE per stabilire una connessione diretta tra i due peer. Questo processo è fondamentale per consentire la comunicazione bidirezionale in tempo reale tra browser senza passare attraverso un server intermedio.

Sessione multimediale

La gestione delle sessioni per le videochiamate WebRTC si basa su un modello che prevede l’utilizzo di sessioni individuali, ciascuna identificata da un unico ID e caratterizzata da un insieme di utenti consentiti (“allowed users") e dagli attuali partecipanti durante la chiamata in corso. Ogni amicizia tra utenti è associata a una propria sessione, e lo stesso vale per i canali multimediali, i quali fanno riferimento a una sessione per agevolare le chiamate multimediali.

Le sessioni vengono distintamente gestite in base al contesto. Nel caso delle amicizie, l’insieme di utenti consentiti è limitato esclusivamente ai due partecipanti dell’amicizia, garantendo una comunicazione privata tra i diretti interessati. Invece, nei canali multimediali, l’insieme di utenti consentiti comprende tutti i partecipanti del server, consentendo così chiamate multimediali che coinvolgono più membri contemporaneamente.

Questo approccio alla gestione delle sessioni offre un controllo granulare sull’accesso alle videochiamate, garantendo che la comunicazione sia personalizzata in base al contesto delle relazioni tra gli utenti. Inoltre, consente una scalabilità efficiente per le chiamate multimediali su larga scala, consentendo a tutti i partecipanti del server di partecipare alle conversazioni senza compromettere la sicurezza o la privacy delle comunicazioni one-to-one.

Protocollo Signaling

Di seguito il protocollo utilizzato per la procedura di signaling webrtc che permette lo scambio delle informazioni necessarie all instauramento delle connessioni p2p.

image [fig:piperchat-signaling]

Problema P2P e TURN

Uno dei problemi principali che WebRTC deve affrontare è la presenza di dispositivi di rete con NAT (Network Address Translation) non compatibili, che possono complicare la creazione di connessioni dirette tra i partecipanti.

Il NAT consente a più dispositivi di condividere un singolo indirizzo IP pubblico, fornendo una forma di sicurezza e limitando la quantità di traffico Internet diretto verso dispositivi locali. Tuttavia, questo può creare problemi quando si tenta di stabilire connessioni dirette tramite WebRTC, in quanto alcuni NAT possono impedire il passaggio dei pacchetti dati necessari.

Per superare questo problema, WebRTC utilizza un concetto chiamato Traversal Using Relays around NAT (TURN). Un server TURN agisce come intermediario tra i partecipanti, aiutando a instradare i dati quando le connessioni dirette non sono possibili. Quando due peer non possono stabilire una connessione diretta a causa di NAT non compatibili, i dati vengono instradati attraverso il server TURN, consentendo comunque la comunicazione in tempo reale.

Per ovviare a questa problematica è stato quindi aggiunto al deploy anche un servizio adibito a funzionare da TURN in caso non sia possibile la connessione p2p.

Autenticazione degli utenti

Per gestire l’autenticazione degli utenti all’interno del nostro sistema, è stata adottata la tecnologia JSON Web Token (JWT).

JWT

JSON Web Token (JWT) è uno standard aperto (RFC 7519) che definisce un modo compatto per rappresentare informazioni tra due parti. Queste informazioni possono essere verificate e fidate, poiché sono firmate digitalmente.

Struttura dei JWT

Un JWT è costituito da tre parti separate da punti (.), che sono:

Funzionamento

Quando un utente si autentica, riceve un JWT che può includere informazioni come l’identità e i diritti di accesso. Da quel momento in poi, il client invia il JWT con ogni richiesta successiva al server, che può verificare la firma del token per assicurarsi che sia valido. In questo modo, il server può fidarsi delle informazioni contenute nel JWT senza doverle verificare ad ogni richiesta.

Utilizzo

Nel contesto di Piperchat, quando un utente si autentica con successo, il microservizio Users genera un JWT contenente nel payload le informazioni relative all’identità dell’utente. Questo JWT viene restituito al client, che lo utilizzerà nelle successive richieste per dimostrare la propria autenticità.

Inoltre, viene memorizzato nel database un token di refresh, che consente di ottenere nuovi token di accesso senza dover effettuare nuovamente l’accesso.

Quando il token di accesso scade, il client può richiedere un nuovo token di accesso al server utilizzando il token di refresh. Questo processo aiuta a mantenere un’esperienza utente fluida, riducendo al contempo il rischio di accessi non autorizzati.

Definizione delle API

Per quanto riguarda la gestione delle api dei vari microservizi si è optato per avere una struttura che rappresentasse ogni endpoint, incapsulando sia i dati richiesti sia le possibili risposte.

A supporto di ciò è stato creato un modulo api che incapsula tutti gli endpoint del backend con le relativi informazioni.

Tale modulo viene sfruttato sia dal backend, per avere un typing migliore e un controllo di validazione dei parametri richiesti, che dal frontend per sapere già quali sono codice di risposta e tipologie di risposta per un determinato endpoint.

export module LoginApi {

  export module Request {
    export type Body = {
      username: string
      password: string
    }
    export const Schema: RequestSchema = {
      Body: {
        username: 'string',
        password: 'string',
      },
    }
  }

  export module Responses {
    export class Success extends Response {
      statusCode = 200
      message = 'Logged in' as const
      jwt: string
    }
  }

  export module Errors {
    export class UsernameOrPasswordIncorrect extends ErrorResponse {
      statusCode = 401
      error = 'Username or password incorrect' as const
    }
  }
}

Definizione degli Endpoint

Una volta costruita la struttura rappresentante le singole API è stata creata l’utility Route, che a partire da un api permette di implementare l’endpoint aggiungendo il supporto automatico alla validazione dei dati in modo che se i dati in ingresso non dovessero essere corretti, l’handler non venga notificato e venga restituito un messaggio di errore Bad Request. Inoltre permette e di dichiarare come reagire per ogni tipo di errore senza doverli controllare all’interno dell’handler.

Altra utilità offerta dalla classe è il typing dei parametri della richiesta basati sulle api specificate.

export const LoginApiRoute = new Route<
  ...
>({
  method: 'post',
  path: '/login',
  schema: LoginApi.Request.Schema,
  handler: async (req, res) => {
    const token = await authController.login(req.body.username, req.body.password)
    res.sendResponse(new LoginApi.Responses.Success(token))
  },
  exceptions: [
    {
      exception: InvalidUsernameOrPassword,
      onException: (e, req, res) => {
        res.sendResponse(new UsernameOrPasswordIncorrect())
      },
    },
  ],
})

Definizione dei Controller

Per interfacciarsi con gli endpoint del backend sono quindi stati realizzati i Controller lato frontend, che incapsulano la gestione delle API fruttando il modulo opportuno per ottenere un typing delle richieste e delle risposte.

export class AuthControllerImpl extends AxiosController implements AuthController {
  async register(request: RegisterApi.Request.Type): Promise<RegisterApi.Response> {
    const body = request as RegisterApi.Request.Body
    return await this.post<RegisterApi.Response>('/auth/register', body)
  }

  async login(request: LoginApi.Request.Type): Promise<LoginApi.Response> {
    const body = request as LoginApi.Request.Body
    return await this.post<LoginApi.Response>('/auth/login', body)
  }

  async logout(): Promise<LogoutApi.Response> {
    return await this.post<LogoutApi.Response>('/auth/logout', {})
  }

  async refreshToken(): Promise<RefreshTokenApi.Response> {
    return await this.post<RefreshTokenApi.Response>(
        '/auth/refresh-token', {})
  }
}

Monitoring

Per monitorare lo stato dei servizi, è stato esposto un endpoint che restituisce un oggetto JSON contenente lo stato corrente e l’orario dell’ultimo aggiornamento. Per garantire l’aggiornamento continuo del client rispetto a queste informazioni, si è optato per l’implementazione di un meccanismo di polling dal client al server. Questa scelta consente al client di mantenere costantemente aggiornato lo stato dei servizi, anche nel caso in cui il server o i singoli microservizi diventino temporaneamente indisponibili. In questo modo, il comportamento del client rimarrà invariato, assicurando una sincronizzazione continua con lo stato più recente dei servizi monitorati. Inoltre, il microservizio di monitoraggio effettua anch’esso polling nei confronti degli altri microservizi per verificare se riceve risposta o meno.

onMounted(async () => {
  await monitoringStore.refreshServicesStatus()
  setInterval(monitoringStore.refreshServicesStatus, 2000)
})

Recap tecnologie utilizzate

Di seguito un elenco di tutte le tecnologie utilizzate all’interno del progetto:

Infrastruttura

Microservizio

Frontend

Comunicazione

Documentazione

Autovalutazione / Validazione

Struttura del repository e analisi statica

Il progetto è stato sviluppato adottando una struttura mono repository - multi project. Per garantire la coerenza, la qualità e la manutenibilità del codice sorgente, durante lo sviluppo abbiamo seguito GitFlow e adottato fin da subito diversi strumenti per l’analisi statica. Inoltre, al fine rendere estendibile e meno prolissa la manutenibilità di ogni progetto, sono stati adottati meccanismi di estensione nei file di configurazione.

Lint

Come strumento di Linting abbiamo adottato ESLint9, grazie al quale, il nostro team ha potuto identificare e correggere errori, migliorare la coerenza del codice e rispettare le best practices di programmazione durante il processo di sviluppo.

Prettier

Prettier10 è uno strumento per la formattazione del codice, integrato nel nostro flusso di lavoro per garantire uno stile uniforme in tutto il progetto. Le principali considerazioni includono:

L’utilizzo di Prettier ha migliorato la leggibilità del codice e semplificato notevolmente la gestione dello stile del codice degli artefatti.

Pre-commit Hook (Git)

Gli hooks di Git11 sono strumenti che permettono di eseguire automaticamente azioni specifiche al verificarsi di eventi.

È stato utilizzato l’hook di pre-commit, al fine di eseguire controlli di formattazione prima di eseguire un commit. Questo ha permesso di migliorare la qualità del codice, evitando commit con formattazione non conforme.

Testing

La fase di testing è strutturata per verificare l’integrazione tra l’intero microservizio e le sue dipendenze, tra cui il Database e il Broker.

Jest

Ogni microservizio definisce degli Unit Testing mediante Jest in modo da verificare la correttezza delle richieste e delle relative risposte, dei singoli microservizi.

In questo modo, siamo stati in grado di concentrarci sui seguenti aspetti durante i test:

Questo approccio ci ha permesso di sviluppare test solidi e garantire che i microservizi rispettassero gli standard richiesti.

Test sui Microservizi

Tutti i microservizi posseggono una suite di test. Di seguito un esempio con il core del testing del servizio degli utenti:

const userMicroservice: Microservice = new Microservice(UserServiceConfiguration)
let request: supertest.SuperTest<supertest.Test>

beforeAll(async () => {
  await userMicroservice.start()
  request = supertest(userMicroservice.getServer())
})

afterAll(async () => {
  await userMicroservice.stop()
})

afterEach(async () => {
  await userMicroservice.clearDatabase()
})

describe('Register', () => {
  it('A user must provide username, password and email', async () => {
    let response = await request
      .post('/auth/register')
      .send({ username: 'test', password: 'test' })
    expect(response.status).toBe(400)
    // other test stuff
    response = await request
      .post('/auth/register')
      .send({ username: 'test', password: 'test', email: 'test' })
    expect(response.status).toBe(200)
  })
    // other test stuff
})

Esecuzione dei test

Al fine di eseguire le suite di test è necessario rendere disponibili le dipendenze dei microservizi. Procedere come segue:

# Un terminale (directory = projectRoot)
npm i
cd dev
./runDev.sh

# Altro terminale (e.g. test su microservizio users)
npm run --workspace services/users test
# oppure (per eseguire tutti i test)
npm run test

Continuous Integration

Il progetto è ospitato in due servizi di hosting: GitHub e GitLab. Il primo servizio è stato utilizzato per l’intera fase di sviluppo, mentre il secondo meramente per eseguire una copia all’interno del repository fornito per il progetto.

Github Action

Le GitHub Actions sono un sistema di automazione integrato direttamente nella piattaforma GitHub, il servizio di hosting scelto per ospitare la fase di sviluppo del progetto.

Sono stati realizzati i seguenti workflow:

I test automatici eseguiti all’interno dei workflows necessitano delle dipendenze sopra citate, tra cui Database e Broker. Al fine di risolverle, vengono utilizzati i services delle GitHub Action, che permettono di istanziare gli opportuni servizi.

Dispatch tests e Testing services

Nell’ottica di uno sviluppo coadiuvato da un elevato numero di commit, l’idea iniziale per cui è stato realizzato il workflow Testing services era per evitare di eseguire l’intera suite di test ad ogni push. Infatti, il workflow esegue i test del microservizio modificato. Il supporto fornito dalle API delle GitHub Action è limitato, infatti si è dovuto scrivere un complesso script bash.

A seguito dello sviluppo è stato constatato che ciò non era necessario questo workflow aggiuntivo, infatti, l’intera suite di test non richiede eccessivo tempo per essere eseguita.

Deployment

Per eseguire il deploy dell’applicazione abbiamo scelto di utilizzare Docker, nello specifico, l’architettura viene servita attraverso i seguenti container:

Per ogni microservizio quindi, viene effettuato il deploy di due diversi container, uno per il Webserver, mentre l’altro per il relativo database non relazionale.

Ogni microservizio possiede le seguenti reti docker:

Build dell’immagine Docker di PiperChat

Si è deciso di realizzare un’immagine Docker unica per l’intero progetto. Per eseguire successivamente i vari servizi è possibile utilizzare tale immagine e modificare opportunamente il comando iniziale.

Di seguito è riportato un esempio per utilizzare l’immagine di piperchat:

docker build . --tag piperchat
docker run piperchat npm run --workspace ./services/users start

Deploy singolo Microservizio

Per il deploy di ogni singolo microservizio è stato scritto un file di Docker Compose. Esso predispone l’ambiente necessario al container (reti, labels, volumi, etc.) per essere eseguito correttamente. Successivamente viene utilizzata l’immagine precedentemente creata, con il comando sovrascritto opportunamente per lo specifico microservizio.

Esempio di Docker compose relativo al singolo microservizio:

services:
  piperchat-service:
    image: piperchat
    command: [
        'npm', 
        'run', 
        '--workspace', 
        './services/piperchat', 
        'start'
    ]
    expose:
      - '${PIPERCHAT_SERVICE_PORT}'
    depends_on:
      db-piperchat-service:
        condition: service_healthy
      broker:
        condition: service_healthy
    networks:
      piperchat-network:
      backend:
        aliases:
          - ${PIPERCHAT_SERVICE_NAME}
      frontend:
    environment:
      - PORT=${PIPERCHAT_SERVICE_PORT}
      - AMQP_URI=${BROKER_URI}
      - MONGO_URI=
        mongodb://db-piperchat-service:27017/piperchat
    labels:
      - |
        traefik.http.routers.piperchat-service.rule=
        (Method(`GET`, `POST`) && Path(`/servers`)) ||
            ... other paths

  db-piperchat-service:
    image: mongo
    expose:
      - '27017'
    volumes:
      - './.docker/db-piperchat:/data/db'
    healthcheck:
      test: |
        host=`hostname --ip-address || echo '127.0.0.1'`;
        mongo --quiet $${host}/test --eval 
        'quit(db.runCommand({ ping: 1 }).ok ? 0 : 2)' 
        && echo 0 || echo 1
    networks:
      - piperchat-network

networks:
  piperchat-network:

Deploy dell’architettura

Come accennato in precedenza, per ogni microservizio è stato scritto un apposito file di Docker Compose. L’unione di tutti i docker-compose.yaml è stata automatizzata tramite uno script bash, che permette di operare sull’intera architettura, aggregando precedentemente i file di tutti i microservizi. Lo script é ./composeAll.sh.

Di seguito è riportata l’intera architettura di cui viene eseguito il deploy, con tutti i microservizi, i relativi database, le reti, il broker ed il gateway.

image [fig:architecture-deployment]

Testing deploy

Per effettuare il testing lato Backend, è stato realizzato un ulteriore Docker Compose il quale ci ha permesso deployare un infrastruttura “alleggerita", dotata unicamente dei container necessari a testare i singoli microservizi, nonché:

Questo Docker Compose è situato nella cartella /dev, insieme allo script che ne automatizza l’esecuzione, chiamato runDev.sh.

Di seguito riportato il Docker Compose dell’infrastruttura usata per il testing:

services:
  db-service:
    image: mongo
    ports:
      - '27017:27017'
    healthcheck:
      test: |
        host=`hostname --ip-address || echo '127.0.0.1'`;
        mongo --quiet $${host}/test --eval
            'quit(db.runCommand({ ping: 1 }).ok ? 0 : 2)' 
            && echo 0 || echo 1

  broker:
    image: rabbitmq:3-management-alpine
    ports:
      - '5672:5672'
      - '15672:15672'
    healthcheck:
      test: ['CMD', 'rabbitmq-diagnostics', '-q', 'ping']

  mongo-express:
    image: mongo-express:latest
    restart: always
    ports:
      - '8081:8081'
    environment:
      ME_CONFIG_MONGODB_SERVER: 'db-service'
    depends_on:
      db-service:
        condition: service_healthy

Istruzioni per il Deployment

Di seguito, elencate le istruzioni per eseguire il deploy dell’applicazione:

  1. Clone del Repository:

    $ git clone https://github.com/zucchero-sintattico/piperchat.git
  2. Installazione delle dipendenze:

    $ npm i
  3. Copia del file contenente le variabili di environment dal template:

    $ cp .env.template .env
  4. Eseguire il deploy dell’architettura tramite lo script che esegue anche il build dell’immagine:

    $ ./cleanDeploy.sh
  5. Dalla seconda volta in poi, per rieseguire il deploy dell’architettura mantenendo i volumi e le immagini precedentemente create, basterà lanciare lo script:

    $ ./deploy.sh

Esempi d’utilizzo

Di seguito vengono riportati alcuni esempi di utilizzo della nostra applicazione:

Login/Registrazione

L’accesso a questa sezione non richiede alcuna autenticazione preliminare. Gli utenti hanno la possibilità di effettuare la registrazione o il login per accedere alle funzionalità offerte.

image [fig:login_img]

image [fig:register_demo]

Monitoring/Healthcheck

L’accesso a questa sezione è libero e non richiede credenziali di accesso. Qui gli utenti possono monitorare lo stato dei microservizi in modo rapido e semplice.

image [fig:moitoring_demo]

HomePage

Per accedere a questa sezione è necessario eseguire l’accesso. In caso di assenza di un access token valido, verrà automaticamente reindirizzati alla pagina di login. La struttura della pagina è articolata in due sezioni principali:

image [fig:webrtc_demo]

Conclusioni

Il progetto Piperchat è stato sviluppato con l’obiettivo di creare una piattaforma di comunicazione ispirata a Discord, offrendo agli utenti la possibilità di interagire in varie forme, creare connessioni sociali e gestire server personalizzati. L’architettura a microservizi è stata scelta per gestire la complessità del sistema, consentendo una maggiore coesione e scalabilità.

Durante lo sviluppo, sono stati affrontati diversi aspetti, tra cui la gestione delle amicizie, la messaggistica, la creazione e gestione di server e canali multimediali. L’implementazione di funzionalità come notifiche, chiamate vocali e video ha arricchito l’esperienza dell’utente.

La valutazione della qualità del software prodotto ha considerato aspetti cruciali come funzionalità, affidabilità, scalabilità, sicurezza, usabilità e prestazioni. L’approccio a microservizi ha dimostrato la sua efficacia nel garantire una struttura modulare e gestibile.

Sviluppi futuri

A causa delle scarse tempistiche, l’implementazione della dashboard relativa ai log dell’intera applicazione, è stata messa in secondo piano. Detto ciò dunque, la prima feature da realizzare in termini di sviluppi futuri sarebbe proprio quest’ultima.

Altro miglioramento che potremmo apportare alla nostra applicazione, riguarderebbe il deploy dell’architettura, difatti attualmente, l’applicazione di videochat viene distribuita utilizzando Docker Compose con un approccio basato su una singola macchina. Tuttavia, per migliorare la scalabilità, la resilienza e la gestione delle risorse, è previsto un futuro passaggio a un’architettura basata su Docker Swarm.

Cosa abbiamo imparato

Durante lo sviluppo di Piperchat, abbiamo acquisito una comprensione approfondita dell’architettura a microservizi e delle sfide associate. Abbiamo imparato a bilanciare aspetti cruciali come la sicurezza, la scalabilità e l’usabilità in un progetto di comunicazione in tempo reale.

La collaborazione tra diversi team per lo sviluppo dei singoli microservizi e la gestione di comunicazione tra di essi attraverso un message broker hanno ampliato la nostra conoscenza delle best practice nello sviluppo di sistemi distribuiti.

Inoltre, l’importanza di valutare la qualità del software in termini di funzionalità, affidabilità e prestazioni ci ha fornito un quadro completo delle sfide e delle opportunità nell’implementazione di una piattaforma di comunicazione complessa come Piperchat.

Benchmarking

Di seguito sono riportate le prove di benchmarking effettuate, al fine di mettere sotto sforzo il sistema per verificare che la scalabilità orizzontale porti parte dei risultati attesi: aumentare il numero di richieste gestite nell’arco di un determinato periodo.

Obiettivo: il test consiste nell’eseguire lo stesso carico di lavoro lato client variando il numero di repliche del servizio che le deve gestire.

Lo scenario prevede un client che effettua richieste HTTP verso il server, dove è in esecuzione il software di Piperchat, all’interno degli appositi container docker.

Configurazione

Sono state utilizzate le seguenti macchine:

Componente Server
S.O. Ubuntu 23.10
CPU Intel Core i7-8700 6 core
RAM 16 GB
Connessione

Lo strumento utilizzato per effettuare le richieste HTTP lato client è wrk, un software di benchmarking che permette di generare carichi di lavoro significativi, sfruttando una singola CPU multi-core.

Il microservizio sotto osservazione è users-service, del quale è stata testata la rotta /auth/login con le credenziali di un utente già registrato, attraverso la seguente configurazione:

$ wrk -t10 -c150 -d30s http://<server-ip>/auth/login -s ./post.lua

// Configurazione script del software (post.lua)
wrk.method = "POST"
wrk.body = '{"username": "user", "password": "12341234"}'
wrk.headers["Content-Type"] = "application/json"

Il test

Il test è stato eseguito più volte, mediante la configurazione esposta in precedenza, variando il numero di repliche del servizio.

Di seguito vengono riportati i risultati, ognuno dei quali è la media di 4 esecuzioni.

1 Replica
Thread Stats Avg
Latency 122.43ms
Req/Sec 125.04
Requests 37424
Data Read 9.17MB
Socket Timeout 49
2 Repliche
Thread Stats Avg
Latency 88.17ms
Req/Sec 202.99
Requests 60708
Data Read 14.88MB
Socket Timeout 20
3 Repliche
Thread Stats Avg
Latency 71.24ms
Req/Sec 255.34
Requests 76351
Data Read 18.71MB
Socket Timeout 10
5 Repliche
Thread Stats Avg
Latency 61.14ms
Req/Sec 332.08
Requests 100839
Data Read 24.28MB
Socket Timeout 6
10 Repliche
Thread Stats Avg
Latency 43.87ms
Req/Sec 370.84
Requests 110072
Data Read 26.98MB
Socket Timeout 0

[tab:bench-replica-20]

20 Repliche
Thread Stats Avg
Latency 137.94ms
Req/Sec 162.18
Requests 15308
Data Read 3.52MB
Socket Timeout 550

Risultati

Il test effettuato ha portato ai risultati attesi. I dati di maggior rilievo da prendere che possono essere osservati sono la latenza ed il numero di richieste.

Come si può notare, il numero di richieste tendere ad aumentare finché non si arriva al numero di repliche uguale al numero di core effettivi della macchina che deve prendere in carico la richiesta, per poi stabilizzarsi.

Invece, la latenza di risposta alle richieste tende a diminuire costantemente finché non si raggiunge il numero di threads12 di cui dispone la macchina.

Superati i numeri evidenziati in precedenza, le prestazioni iniziano a degradare. Come si può notare in Tabella [tab:bench-replica-20], il numero di socket che non riesce ad effettuare una connessione tende ad aumentare notevolmente.

Tentativo di deployment reale

Abbiamo proceduto con il deploy dell’applicazione online con l’obiettivo di testare e utilizzare concretamente l’applicativo. Durante questo processo, sono emersi alcuni problemi rilevanti relativi all’implementazione delle WebSocket.

Problema con WebSocket non Sicure

Durante il deploy, ci siamo accorti che l’applicazione utilizzava WebSocket non sicure (ws) invece di WebSocket sicure (wss). Questo ha causato il blocco delle connessioni da parte dei browser, poiché molti moderni browser richiedono connessioni sicure per proteggere la privacy degli utenti.

Per affrontare questo problema, abbiamo implementato una soluzione intermedia utilizzando Nginx come proxy SSL. Nginx è stato configurato per gestire la crittografia SSL/TLS delle connessioni WebSocket, consentendo l’utilizzo di connessioni sicure (wss). Questo approccio ci ha permesso di ottenere connessioni sicure senza dover apportare modifiche dirette al codice sorgente dell’applicazione.

location / {
    # Configurazione del proxy pass verso il server sulla porta 80
    proxy_pass http://localhost:80;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}

location /notification {
    proxy_pass http://localhost:80;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
}

location /webrtc {
    proxy_pass http://localhost:80;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
}

Sperimentale: deploy con Dev Containers

A fini sperimentali, è stato eseguito il deploy dell’infrastruttura attraverso la tecnologia Dev Containers di GitHub.

Di seguito sono riportate le istruzioni per eseguire il deploy:

  1. Fork del repository

  2. Dalla pagina di GitHub

    Code > Codespaces > Create codespace on main
  3. Attendere la creazione dell’ambiente.

  4. Utilizzare il servizio tramite il link fornito dall’ambiente di codespace.

  5. (Opzionale) Modificare la visibilità delle porte tramite l’interfaccia grafica nell’apposita sezione, in modo da permettere di utilizzare il sistema anche a terzi, tramite il link fornito.


  1. https://discord.com↩︎

  2. https://vuejs.org↩︎

  3. https://pinia.vuejs.org↩︎

  4. https://quasar.dev↩︎

  5. https://traefik.io/traefik/↩︎

  6. https://doc.traefik.io/traefik/providers/docker/#routing-configuration-with-labels↩︎

  7. https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events↩︎

  8. https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API↩︎

  9. https://eslint.org↩︎

  10. https://prettier.io↩︎

  11. https://git-scm.com/book/it/v2/Customizing-Git-Git-Hooks↩︎

  12. Intel® Hyper-Threading Technology↩︎