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
Gestione dell'annidamento delle regole dei layer in CSS
Evitare il flickering dei componenti nel prerender di Blazor 8
Effettuare il log delle chiamate a function di GPT in ASP.NET Web API
Change tracking e composition in Entity Framework
Escludere alcuni file da GitHub Secret Scanning
Gestire i dati con Azure Cosmos DB Data Explorer
Sviluppare un'interfaccia utente in React con Tailwind CSS e Preline UI
Migliorare l'organizzazione delle risorse con Azure Policy
Ordinare randomicamente una lista in C#
Sfruttare al massimo i topic space di Event Grid MQTT
Generare la software bill of material (SBOM) in GitHub
Utilizzare il nuovo modello GPT-4o con Azure OpenAI