Una chat con ASP.NET Core e WebSockets

di Moreno Gentili, in ASP.NET Core,

In passato abbiamo parlato di ASP.NET SignalR come soluzione evoluta per aggiungere funzionalità realtime alle nostre applicazioni ASP.NET. Grazie a connessioni persistenti, client e server possono scambiarsi messaggi in maniera bidirezionale e senza che sia necessario alcun refresh di pagina. Al momento è in alpha per ASP.NET Core e torneremo di nuovo a parlare di questa tecnologia con il rilascio di una versione stabile.

Nel frattempo, possiamo comunque creare semplici applicazioni realtime dato che ASP.NET Core supporta già lo standard WebSocket. In questo script realizzeremo le parti server e client di una semplice chat.

La parte server


Per prima cosa usiamo il WebSocketMiddleware che possiede tutto il necessario a supportare la comunicazione via WebSocket. All'interno del metodo Configure della classe Startup invochiamo l'extension method UseWebSockets, fornendo eventuali opzioni.

var webSocketOptions = new WebSocketOptions()
{
  KeepAliveInterval = TimeSpan.FromSeconds(120),
  ReceiveBufferSize = 4 * 1024
};
app.UseWebSockets(webSocketOptions);
//Qui usiamo altri middleware come StaticFiles, MVC, ecc...

Per implementare la nostra logica applicativa, abbiamo bisogno di un ulteriore middleware che scriveremo noi stessi. Creiamo quindi un file ChatMiddleware.cs all'interno di una directory qualsiasi, come ad esempio in /Middlewares.

public class ChatMiddleware
{
  private readonly RequestDelegate next;
  private readonly ConcurrentDictionary<WebSocket, Guid> connectedClients;
  public ChatMiddleware(RequestDelegate next)
  {
  //Usiamo un ConcurrentDictionary per gestire l'elenco
  //dei client connessi in maniera thread-safe
    connectedClients = new ConcurrentDictionary<WebSocket, Guid>();
    this.next = next;
  }
  public async Task InvokeAsync(HttpContext context)
  {
    //Se non si tratta di una richiesta WebSocket, continuiamo come al solito,
  //lasciando che la richiesta venga gestita dal middleware successivo
    if (!context.WebSockets.IsWebSocketRequest)
    {
      await next.Invoke(context);
      return;
    }
  //Altrimenti, la gestiamo
    WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync();
    await HandleWebSocketCommunication(context, webSocket);
  }

  public async Task HandleWebSocketCommunication(HttpContext context, WebSocket webSocket)
  {
    //Aggiungiamo il webSocket di questo client alla lista dei client connessi
    //Forniamo anche un Guid che potrebbe essere usato come riferimento
    //in scenari avanzati, come ad esempio l'invio di messaggi privati tra client 
    connectedClients.TryAdd(webSocket, Guid.NewGuid());

    WebSocketReceiveResult result;
    do
    {
      //Iniziamo a ricevere messaggi dal client
      //Il buffer deve essere sufficientemente grande per accomodare l'intero messaggio
      var buffer = new byte[4 * 1024];
      result = await webSocket.ReceiveAsync(
        buffer: new ArraySegment<byte>(buffer),
        cancellationToken: CancellationToken.None);

      //Convertiamo il messaggio binario in stringa
      var message = Encoding.UTF8.GetString(buffer).Trim(' ', '\0');
      //E lo inoltriamo a tutti i client
      await SendMessageToClients(message);
      //TODO: Qui potremmo storicizzare il messaggio in un database
    
    //Continuiamo finché il client non si disconnette
    } while (!result.CloseStatus.HasValue);

  //Quando il client risulta disconnesso, lo rimuoviamo dall'elenco e chiudiamo il socket
    connectedClients.TryRemove(webSocket, out _);
    await webSocket.CloseAsync(
      closeStatus: result.CloseStatus.Value,
      statusDescription: result.CloseStatusDescription,
      cancellationToken: CancellationToken.None);
  }

  private async Task SendMessageToClients(string message)
  {
    //Inoltriamo messaggi a tutti i client
    foreach (var socket in connectedClients.Keys)
    {
      var responseBuffer = Encoding.UTF8.GetBytes(message);
      await socket.SendAsync(
        buffer: new ArraySegment<byte>(responseBuffer, 0, responseBuffer.Length),
        messageType: WebSocketMessageType.Text,
        endOfMessage: true,
        cancellationToken: CancellationToken.None);
    }
  }
}

È importante notare come il ChatMiddleware risulti "trasparente" nei confronti di normali richieste HTTP. Nel caso di richieste WebSocket, invece, il middleware si metterà attivamente in ascolto dei messaggi inviati dal client, fino alla sua disconnessione.


Non resta che tornare nella classe Startup per usare il ChatMiddleware. Mettiamo questa riga di codice subito dopo la registrazione del WebSocketMiddleware, già visto in precedenza.

app.UseMiddleware<ChatMiddleware>();

La parte client


Per realizzare la parte client, prepariamo una pagina con un'interfaccia HTML minimale che consentirà agli utenti di visualizzare i messaggi ricevuti e di inviarne di nuovi.

<ul id="messages"></ul>
<form onsubmit="sendMessage(this); return false;">
  <input type="text" name="text" />
  <button>Send</button>
</form>

All'invio del form, la funzione javascript sendMessage verrà invocata. Andiamo dunque a definire tale funzione, per poi stabilire la connessione persistente al server tramite WebSocket API.

<script>
var socket;
//A scopo di demo, lo username viene generato lato client (sconsigliato in produzione)
var username = "User" + Math.round(Math.random()*1000);

//Funzione per inviare i messaggi
function sendMessage(form) {
  var text = form.text.value;
  //Inviamo il messaggio solo se l'utente aveva digitato un testo
  if (!text) return;
  
  //Il messaggio è un oggetto javascript che serializziamo in JSON
  var message = JSON.stringify({sender: username, text: text});
  //Usiamo il websocket per inviare il messaggio  
  socket.send(message);
  form.text.value = "";
}

//Funzione per ricevere i messaggi
function receiveMessage(event) {
  //Ci aspettiamo che il contenuto del messaggio sia JSON
  var message = JSON.parse(event.data);

  //Otteniamo un riferimento alla lista dei messaggi (un elemento <ul>)
  var messages = $("#messages");
  
  //E creiamo un nuovo elemento per visualizzare il messaggio appena arrivato
  var messageElement = $("<li></li>");
  messageElement.html("<strong>" + message.sender + "</strong>: " + message.text);
  messages.append(messageElement);

  //Autoscroll in fondo alla lista
  messages.animate({scrollTop: messages[0].scrollHeight}, 500);
}

//Stabiliamo la connessione via WebSocket
var scheme = document.location.protocol == "https:" ? "wss" : "ws";
var port = document.location.port ? (":" + document.location.port) : "";
var connectionUrl = scheme + "://" + document.location.hostname + port + "/ws";
socket = new WebSocket(connectionUrl);
//Gestiamo l'evento onmessage
socket.onmessage = receiveMessage;
</script>

Aprendo la pagina in due tab o browser differenti, verifichiamo che gli utenti siano in grado di scambiarsi messaggi in tempo reale.


La disconnessione dalla chat può avvenire invocando esplicitamente il metodo close dell'oggetto WebSocket. In alternativa, il browser si occuperà esso stesso di terminare la connessione alla chiusura della pagina, come indicato nella specifica del W3C (http://www.w3.org/TR/websockets/#make-disappear).

La demo presentata in questo script è anche disponibile su GitHub al seguente indirizzo: https://github.com/BrightSoul/WebSocketsChatAspNetCore

Commenti

Visualizza/aggiungi commenti

| Condividi su: Twitter, Facebook, LinkedIn

Per inserire un commento, devi avere un account.

Fai il login e torna a questa pagina, oppure registrati alla nostra community.

Approfondimenti

I più letti di oggi