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
Per inserire un commento, devi avere un account.
Fai il login e torna a questa pagina, oppure registrati alla nostra community.
Approfondimenti
Implementare il throttling in ASP.NET Core
Eseguire query manipolando le liste contenute in un oggetto mappato verso una colonna JSON
Gestire undefined e partial nelle reactive forms di Angular
Catturare la telemetria degli eventi di output cache in ASP.NET Core
Usare Refit e Polly in Blazor per creare client affidabili e fortemente tipizzati
Eseguire attività con Azure Container Jobs
Definire lo stile CSS in base alle dimensioni del container
Usare un KeyedService di default in ASP.NET Core 8
Le novità di Angular: i miglioramenti alla CLI
Installare le Web App site extension tramite una pipeline di Azure DevOps
Cache policy su route groups di Minimal API in ASP.NET Core 7
Effettuare delete massive con Entity Framework Core 7