in ASPNET Core, Informatica, Programmazione

.Net Core e WebSocket

Uno dei problemi che maggiornamenti dobbiamo affrontare durante lo sviluppo di WebApi, è la necessità di dover mantenere aperta la connessione tra client e server: per esempio per gestire un flusso realtime. Inoltre si rende necessario mantenere un canale di comunicazione full-duplex tra client e server. Tutto questo è in contrasto con le caratteristiche del protocollo HTTP, che è un tipo protocollo domanda/risposta.

WebSocket è un protocollo che consente la comunicazione full-duplex tra client e server, mantentendola attiva con lifetime piuttosto alti. Ciò significa che sia il client che il server possono inviare e ricevere dati simultaneamente, consentendo aggiornamenti istantanei e interazione in tempo reale. Inoltre, consente di inviare messaggi dal server, sia verso un singolo client, ma anche in broadcast.

Nonostante i WebSocket offrano una quantità di vantaggi, richiedono la presenza di un server che implementi il protocollo WebSocket (porta 80 e porta 443), consentendo la gestione di connessioni in entrata, la gestione delle sessioni del client e l’instradamento dei messaggi tra i vari client.

E’ possibile ospitare WebSocket all’interno di Azure (come un’appservice ma bisogna abilitare il supporto all’utilizzo dei WebSocket, ed è ovviamente possibile scrivere del codice in .net core per poterlo utilizzare. In questo post vedremo come sia possibile realizzare un semplice websocket server e poterci interfacciare tramite un client.

Trattandosi di un WebSocket piuttosto semplice, verrà realizzato utilizzando le minimal api.

Iniziamo quindi con la creazione di un nuovo progetto di tipo AspNet Core Empty, in modo da poter implementare il tutto da zero.

Una volta creato il progetto, possiamo eliminare il contenuto del Program.cs e sostituirlo con:

using System.Net;
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseUrls("http://localhost:80");
var app = builder.Build();
app.UseWebSockets();
app.MapGet("/ws", async (HttpContext context) => 
{
   if (context.WebSockets.IsWebSocketRequest)
   {
     
   }
   else
   {
     context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
   }
}

Quello che differenzia il progettto da una webapi è la presenza di app.UseWebSockets() che di fatto abilita la possibilità di utilizzare i websocket all’interno dell’applicazione. Inoltre alla linea 3 vien anche indicata l’URL e la porta su cui verrà eseguito il WebSocket. Attenzione perchè all’interno di Azure è possibile esporre soltanto le porte 80 e 443.

All’interno del metodo che risponde (richiamando la funzione in maniera asincrona) all’url “/ws” viene verificato (alla riga 8) se siamo in presenza di una chiamata WebSocket oppure no. In caso negativo viene restituito al client un codice di errore BadRequest. Bene, a questo punto la struttura del server è stata definita. Non ci resta che procedere la lettura della richiesta, e della sua elaborazione.

Una volta verificata la presenza di una richiesta di tipo WebSocket è necessario ricevere l’oggetto associato alla connessione:

using var ws = await context.WebSockets.AcceptWebSocketAsync();

Questo oggetto è l’oggetto WebSocket associato alla connessione. Verificando lo stato dell’oggetto possiamo capire se la connessione è ancora aperta, chiusa, in chiusura ecc…

A questo punto, possiamo implementare il servizio di risposta del nostro WebSocket. Per prima cosa dobbiamo creare il messaggio che dovrà essere inviato al client:

var message = $"Current time: {Datetime.Now.ToString("HH:mm:ss")}";
var bytes = Encoding.UTF8.GetBytes(message);
var arraySegment = new ArraySegment<byte>(bytes, 0, bytes.Length);

In particolare dobbiamo porre attenzione al fatto che sul messaggio dovrà essere application un Encoding di tipo UTF8.

Siamo quindi pronti all’invio del messaggio al client:

await ws.SendAsync(arraySegment, WebSocketMessageType.Text, true, CancellationToken.None);

Supponendo di voler far in modo che il nostro client riceva una notifica con l’ora corrente del server, il codice finale dell’applicazione:

using System.Net;
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseUrls("http://localhost:80");
var app = builder.Build();
app.UseWebSockets();
app.MapGet("/ws", async (HttpContext context) => 
{
   if (context.WebSockets.IsWebSocketRequest)
   {
	 using var ws = await context.WebSockets.AcceptWebSocketAsync();     
     while(true)
     {
     	var message = $"Current time: {Datetime.Now.ToString("HH:mm:ss")}";
		var bytes = Encoding.UTF8.GetBytes(message);
		var arraySegment = new ArraySegment<byte>(bytes, 0, bytes.Length);

       if(ws.State==WebSocketState.Open)
        {
           await ws.SendAsync(arraySegment, WebSocketMessageType.Text, true, CancellationToken.None);
        }
       else if(ws.State == WebSocketState.Closed || ws.State == WebSocketState.Aborted)
             {
          	   break;
             }
       Thread.Sleep(1000)
     }
   }
   else
   {
     context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
   }
}

In pratica viene intercettata la presenza di una connessione websocket alla riga 8. Se si tratta di una connessione valida, viene recuperato l’oggetto ws alla riga 10. Si entra quindi in loop infinitato che consente di creare un messaggio contenente l’ora del server, e successivamente (se siamo in presenza di una connessione aperta) di inviarlo al server (linea 19).

Nel caso in cui la connessione sia stata chiusa oppure sia stata abortita, non viene inviato nessun messaggio al client.

In un prossimo post vedremo come sia piuttosto semplice scrivere un client che utilizza il protocollo WebSocket e che si interfacci con il server appena scritto.