In uno scenario moderno, in cui le applicazioni web stanno prendendo sempre più peso, è fondamentale per qualsiasi programmatore poter contare su un'interazione in tempo reale con i servizi esterni. Pensiamo ad esempio ad applicazioni di messaggistica istantanea, programmi che leggono dati da un sensore in tempo reale o sessioni di lavoro condiviso con altri utenti. In tutti questi casi ottenere le informazioni a tempo zero è un requisito essenziale.

HTTP e polling

Un'applicazione o sito web normalmente è composto da due componenti principali: client e server. Un client, tipicamente un browser, effettua diverse chiamate al server per ottenere i dati che servono a visualizzare la pagina, come ad esempio le risorse statiche, ovvero l'html, il css, il javascript o le immagini. Spesso capita che sia il sito stesso a rivestire i panni del client, effettuando chiamate asincrone al server tramite Javascript, nel caso ad esempio di AJAX. In entrambi i casi le chiamate vengono effettuate tramite il protocollo HTTP, il quale molto semplicemente riceve una richiesta e restituisce una risposta in modalità half-duplex, ovvero consentendo la comunicazione in una sola direzione per volta.

La natura di http, di per sè molto semplice, si porta dietro dei limiti nel momento in cui si vuole avere una trasmissione dati in tempo reale. Un vecchio metodo, ma ancora spesso utilizzato per ottenere dati in tempo reale, consiste nell'effettuare diverse chiamate http alla stessa risorsa su intervalli temporali predefiniti. Questa tecnica viene definita polling, e, sebbene molto semplice, risulta spesso molto inefficiente, soprattutto se non si ha la certezza che i dati possano mutare costantemente su un dato intervallo temporale. Nel caso ad esempio di un sensore, il quale per certo cambierà il suo valore su un arco temporale brevissimo, il polling si rivela una strategia efficace, ma se ad esempio dovessimo attendere un messaggio da parte di un utente, il polling rischierebbe di fare una grande quantità inutile di richieste, andano a mettettere sotto stress il server. Questo tipo di comunicazione può quindi non essere l'ideale in alcuni scenari, tuttavia esistono tecniche migliori.

I WebSocket

In moltissime applicazioni abbiamo la necessità che la nostra webapp non metta continuamente sotto stress il server, ma venga notificata solamente quando è necessario, ovvero nel momento in cui ci sono dei cambiamenti significativi. Per ottenere questo sono stati introdotti i WebSocket, un meccanismo di comunicazione bidirezionale client/server attraverso una singola e persistente connessione TCP, attraverso cui entrambe le parti possono rimanere connesse e scambiare dati (messaggi) tra loro.

La sua implementazione deve essere effettuata sia lato server che lato client. Ogni connessione WebSocket inizia con un handshake, effettuato tramite una chiamata http dal client al server. La differenza sostanziale sta in un parametro aggiuntivo nello header della richiesta, ovvero { Upgrade: websocket }, il quale dichiara la volontà di stabilire una connessione WebSocket.

Vediamo un semplice esempio di chiamata:

GET ws://www.mysite.example/my/websocket/endpoint/ HTTP/1.1
Origin: http://www.mysite.com
Connection: Upgrade
Host: www.mysite.com
Upgrade: websocket

A questo punto se il server supporta WebSocket avverrà l'handshake:

HTTP/1.1 101 WebSocket Protocol Handshake
Date: Wed, 03 Oct 2018 11:00:00 GMT
Connection: Upgrade
Upgrade: WebSocket

Una volta avvenuto l'handshake l'iniziale comunicazione HTTP viene sostituita dalla nuova connessione WebSocket, utilizzando la stessa connessione TCP/IP sottostante. Interessante è notare come gli url di WebSocket inizino con ws:// invece che con http://

I dati vengono quindi trasferiti tramite WebSocket sotto forma di messaggi, ciascuno dei quali contiene ciò che si vuole spedire.

Rappresentazione a diagramma dei WebSocket

Implementazioni

L'implementazione lato server varia a seconda del linguaggio e del framework utilizzato. In sostanza è necessario affiancare al server HTTP un server WebSocket ed entrambi devono essere strettamente connessi, in quanto, ricordiamo, la connessione a WebSocket ha inizio a partire da una richiesta HTTP. Solitamente è buona prassi provvedere un sistema di autenticazione, in modo da rendere sicure le connessioni ed accettare solamente richieste da fonti e utenti ben noti.

Lato client è possibile utilizzare le API WebSocket di W3C, che ovviamente vengono invocate tramite JavaScript. In questo articolo ne vedremo una semplice implementazione. Supponiamo di voler comunicare con il server echo.websocket.org, il quale ha una semplicissima implementazione di WebSocket che, dato un messaggio ricevuto risponde con gli stessi dati. In parole povere, se noi mandiamo un messaggio con scritto "Ciao", il server risponderà "Ciao" a sua volta. L'applicazione web di esempio deve quindi:

  • Connettersi a echo.websocket.org e ottenere una connessione WebSocket
  • Restare in ascolto di messaggi e stampare a video i nuovi messaggi in entrata dal server
  • Inviare un messaggio al server
  • Chiudere la connessione, se richiesto

L'esempio seguente dimostra come tramite l'utilizzo delle API native WebSocket sia possibile ottenere i requisiti espressi sopra:

<!DOCTYPE html>
<html>
  <head>
    <title>Echo Web Socket</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width">
  </head>
  <body> 
    <div>
      <label>Your message</label>
      <input type="text" id="message" />
    </div>
    <div>
      <button type="button" onclick="sendMessage();" >Send message</button>
      <button type="button" onclick="closeWsConnection();" >Close connection</button>
    </div>
    <div id="messages"></div>

    <script type="text/javascript">

      var webSocketUri = 'wss://echo.websocket.org/';
      var websocket = null;
      var messages = document.getElementById("messages");

      function init() {
        websocket = new WebSocket(webSocketUri);
        websocket.onopen = function(evt) { onOpen(evt) };
        websocket.onclose = function(evt) { onClose(evt) };
        websocket.onmessage = function(evt) { onMessage(evt) };
        websocket.onerror = function(evt) { onError(evt) };
      }

      function onOpen(evt) {
        console.log('Connection opened');
      }

      function onClose(evt) {
        console.log('Connection closed');
      }

      function onMessage(evt) {
        addMessage(evt.data);
      }

      function onError(evt) {
        console.log('An error occurred', evt.data);
      }

      document.onreadystatechange = function() {
        if (document.readyState === 'complete') {
          init();
        }
      };

      function sendMessage(){
        var text = document.getElementById("message").value;
        websocket.send(text);
      }

      function closeWsConnection(){
        if (websocket !== undefined && websocket.readyState !== WebSocket.CLOSED) {
          websocket.close();
        }
      }

      function addMessage(text){
        messages.innerHTML += "<br/>" + text;
      }

    </script>

  </body>
</html>

Andando nel dettaglio:

  • Una volta caricata la pagina viene invocata la funzione init, che avvia una connessione con il server tramite WebSocket. A questo punto si mette in ascolto di diversi eventi, quali:
    • onopen: è scatenato quando la connessione viene aperta
    • onclose: è scatenato quando la connessione viene chiusa o dal server o dal client (ricordiamo che la comunicazione è bidirezionale 
    • onmessage: è scatenato quando viene ricevuto un messaggio da parte del server
    • onerror: è scatenato quando si verifica un errore durante la comunicazione
  • Quando l'utente inserisce del testo nella casella e preme il pulsante di invio viene invocato il metodo sendMessage, che a sua volta invoca il metodo send di WebSocket. Quest'ultimo riceve come parametro un dato, che può essere di tipo:
    • una stringa in codifica utf-8
    • un ArrayBuffer, ovvero un array contenente dei byte. Questo viene usato nel caso in cui si voglia inviare del binario
    • un Blob, ovvero un oggetto rappresentante un file immutabile
    • un ArrayBufferView​​
  • Il dato della risposta del server (in questo caso la stessa stringa inviata) viene appeso all'html della pagina, sul contenitore con id messages
  • Se l'utente preme il pulsante di disconnessione, viene invocato il metodo closeWsConnection, che a sua volta invoca il metodo close di WebSocket, il quale si occupa di terminare la connessione corrente

Conclusioni

WebSocket è dunque una tecnologia molto utile nel caso in cui si sviluppino applicazioni che richiedono dati in tempo reale. In particolare presenta i seguenti vantaggi:

  • I dati vengono scambiati tramite messaggi, i quali vengono spediti solamente quando serve e non ad un intervallo prefefinito di tempo
  • I messaggi non richiedono intestazione, per cui la loro dimensione è contenuta, riducendo di conseguenza l'utilizzo di banda
  • La loro implementazione è piuttosto semplice grazie alle API native
  • Si tratta di una tecnologia ormai consolidata e supportata da moltissimi browser. Per una panoramica sul supporto di WebSocket rimandiamo al seguente link