Esercitazione: Creare un'app di chat con il servizio PubSub Web di Azure

Nell'esercitazione Pubblica e sottoscrivi messaggio vengono fornite informazioni di base sulla pubblicazione e la sottoscrizione dei messaggi con PubSub Web di Azure. In questa esercitazione si apprenderà il sistema di eventi di Azure Web PubSub e lo si userà per creare un'applicazione Web completa con funzionalità di comunicazione in tempo reale.

In questa esercitazione apprenderai a:

  • Creare un'istanza del servizio Web PubSub
  • Configurare le impostazioni del gestore eventi per PubSub Web di Azure
  • Gestire gli eventi nel server app e creare un'app di chat in tempo reale

Se non si ha un account Azure, creare un account gratuito prima di iniziare.

Prerequisiti

  • Questa configurazione richiede la versione 2.22.0 o successiva dell'interfaccia della riga di comando di Azure. Se si usa Azure Cloud Shell, la versione più recente è già installata.

Crea un'istanza di Azure Web PubSub

Creare un gruppo di risorse

Un gruppo di risorse è un contenitore logico in cui vengono distribuite e gestite le risorse di Azure. Usare il comando az group create per creare un gruppo di risorse denominato myResourceGroup nella posizione eastus.

az group create --name myResourceGroup --location EastUS

Creare un'istanza di Web PubSub

Eseguire az extension add per installare o aggiornare l'estensione webpubsub alla versione corrente.

az extension add --upgrade --name webpubsub

Usa il comando dell'interfaccia della riga di comando di Azure az webpubsub create per creare un Web PubSub nel gruppo di risorse che hai creato. Il comando seguente crea una risorsa Web PubSub gratuita nel gruppo di risorse myResourceGroup in EastUS:

Importante

Ogni risorsa Web PubSub deve avere un nome univoco. Sostituire <your-unique-resource-name> con il nome di Web PubSub negli esempi seguenti.

az webpubsub create --name "<your-unique-resource-name>" --resource-group "myResourceGroup" --location "EastUS" --sku Free_F1

L'output di questo comando mostra le proprietà della risorsa appena creata. Prendere nota delle due proprietà elencate di seguito:

  • Nome risorsa: Il nome assegnato al parametro --name sopra.
  • hostName: nell'esempio il nome è <your-unique-resource-name>.webpubsub.azure.com/.

A questo punto, l'account di Azure è l'unico autorizzato a eseguire qualsiasi operazione su questo nuova risorsa.

Ottieni la stringa di connessione per utilizzi futuri

Importante

Le stringa di connessione non elaborate vengono visualizzate in questo articolo solo a scopo dimostrativo.

Una stringa di connessione include le informazioni di autorizzazione di cui la tua applicazione ha bisogno per accedere al servizio Azure Web PubSub. La chiave di accesso nella stringa di connessione agisce come una password root per il tuo servizio. Negli ambienti di produzione proteggere sempre le chiavi di accesso. Usare Azure Key Vault per gestire e ruotare le chiavi in modo sicuro e proteggere la connessione con WebPubSubServiceClient.

Evitare di distribuire le chiavi di accesso ad altri utenti, impostarle come hardcoded o salvarle in un file di testo normale accessibile ad altri. Ruotare le chiavi se si ritiene che siano state compromesse.

Usa il comando interfaccia della riga di comando di Azure az webpubsub key per ottenere la stringa di connessione del servizio. Sostituire il segnaposto <your-unique-resource-name> con il nome dell'istanza di Azure Web PubSub.

az webpubsub key show --resource-group myResourceGroup --name <your-unique-resource-name> --query primaryConnectionString --output tsv

Copiare la stringa di connessione da usare in un secondo momento.

Copiare la ConnectionString recuperata e impostarla nella variabile di ambiente WebPubSubConnectionString, che il tutorial leggerà in seguito. Sostituire <connection-string> di seguito con ConnectionStringrecuperato.

export WebPubSubConnectionString="<connection-string>"
SET WebPubSubConnectionString=<connection-string>

Configurare il progetto

Prerequisiti

Creare l'applicazione

In Azure Web PubSub sono disponibili due ruoli, server e client. Questo concetto è simile ai ruoli server e client in un'applicazione Web. Il server è responsabile della gestione dei client, dell'ascolto e della risposta ai messaggi client. Il client è responsabile dell'invio e della ricezione dei messaggi dell'utente dal server e della relativa visualizzazione per l'utente finale.

In questa esercitazione viene creata un'applicazione Web chat in tempo reale. In un'applicazione Web reale, la responsabilità del server include anche l'autenticazione dei client e la gestione di pagine Web statiche per l'interfaccia utente dell'applicazione.

Usiamo ASP.NET Core 8 per ospitare le pagine Web e gestire le richieste in ingresso.

Creare prima di tutto un'app Web ASP.NET Core in una chatapp cartella.

  1. Creare una nuova app Web.

    mkdir chatapp
    cd chatapp
    dotnet new web
    
  2. Aggiungere app.UseStaticFiles() Program.cs per supportare l'hosting di pagine Web statiche.

    var builder = WebApplication.CreateBuilder(args);
    var app = builder.Build();
    
    app.UseStaticFiles();
    
    app.Run();
    
  3. Creare un file HTML e salvarlo come wwwroot/index.html, lo usiamo per l'interfaccia utente dell'app di chat in un secondo momento.

    <html>
      <body>
        <h1>Azure Web PubSub Chat</h1>
      </body>
    </html>
    

È possibile testare il server eseguendo dotnet run --urls http://localhost:8080 e accedendo http://localhost:8080/index.html nel browser.

Aggiungere l'endpoint di negoziazione

Nel tutorial Pubblicazione e sottoscrizione di messaggi, il sottoscrittore usa direttamente la stringa di connessione. In un’applicazione reale, non è sicuro condividere la stringa di connessione con alcun client, perché la stringa di connessione dispone di privilegi elevati per eseguire qualsiasi operazione sul servizio. A questo punto, facciamo in modo che il server utilizzi la stringa di connessione ed esponga un endpoint negotiate affinché il client possa ottenere l'URL completo con il token di accesso. In questo modo, il server può aggiungere middleware di autenticazione prima dell'endpoint per impedire l'accesso negotiate non autorizzato.

Installare prima di tutto le dipendenze.

dotnet add package Microsoft.Azure.WebPubSub.AspNetCore

Aggiungere ora un /negotiate endpoint per il client per chiamare per generare il token.

using Azure.Core;
using Microsoft.Azure.WebPubSub.AspNetCore;
using Microsoft.Azure.WebPubSub.Common;
using Microsoft.Extensions.Primitives;

// Read connection string from environment
var connectionString = Environment.GetEnvironmentVariable("WebPubSubConnectionString");
if (connectionString == null)
{
    throw new ArgumentNullException(nameof(connectionString));
}

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddWebPubSub(o => o.ServiceEndpoint = new WebPubSubServiceEndpoint(connectionString))
    .AddWebPubSubServiceClient<Sample_ChatApp>();
var app = builder.Build();

app.UseStaticFiles();

// return the Client Access URL with negotiate endpoint
app.MapGet("/negotiate", (WebPubSubServiceClient<Sample_ChatApp> service, HttpContext context) =>
{
    var id = context.Request.Query["id"];
    if (StringValues.IsNullOrEmpty(id))
    {
        context.Response.StatusCode = 400;
        return null;
    }
    return new
    {
        url = service.GetClientAccessUri(userId: id).AbsoluteUri
    };
});
app.Run();

sealed class Sample_ChatApp : WebPubSubHub
{
}

AddWebPubSubServiceClient<THub>() viene usato per inserire il client WebPubSubServiceClient<THub>del servizio , con cui è possibile usare nel passaggio di negoziazione per generare il token di connessione client e nei metodi hub per richiamare le API REST del servizio quando vengono attivati gli eventi dell'hub. Questo codice per la generazione del token è simile a quello che abbiamo usato nel tutorial sulla pubblicazione e sottoscrizione di messaggi, con la differenza che passiamo un argomento in più (userId) durante la generazione del token. L'ID utente può essere usato per identificare il client, così quando ricevi un messaggio sai da dove proviene.

Il codice legge stringa di connessione dalla variabile di ambiente impostata WebPubSubConnectionString nel passaggio precedente.

Riavviare il server utilizzando dotnet run --urls http://localhost:8080.

È possibile testare questa API accedendo http://localhost:8080/negotiate?id=user1 e fornisce l'URL completo di Azure Web PubSub con un token di accesso.

Gestire eventi

In Web PubSub di Azure, quando si verificano determinate attività sul lato client (ad esempio, un client si connette, è connesso, disconnesso o un client invia messaggi), il servizio invia notifiche al server in modo che possa reagire a questi eventi.

Gli eventi vengono recapitati al server sotto forma di webhook. Il webhook è ospitato ed esposto dal server applicativo e registrato nel servizio Azure Web PubSub. Il servizio richiama i webhook ogni volta che si verifica un evento.

Web PubSub di Azure segue CloudEvents per descrivere i dati dell'evento.

Di seguito vengono gestiti connected gli eventi di sistema quando un client è connesso e gestisce message gli eventi utente quando un client invia messaggi per compilare l'app di chat.

Web PubSub SDK per AspNetCore Microsoft.Azure.WebPubSub.AspNetCore installato nel passaggio precedente può anche aiutare a analizzare ed elaborare le richieste CloudEvents.

Per prima cosa, aggiungi i gestori di eventi prima di app.Run(). Specificare il percorso dell'endpoint per gli eventi, ad esempio /eventhandler.

app.MapWebPubSubHub<Sample_ChatApp>("/eventhandler/{*path}");
app.Run();

Ora, all'interno della classe Sample_ChatApp creata nel passaggio precedente, aggiungi un costruttore che funzioni con WebPubSubServiceClient<Sample_ChatApp>, che utilizziamo per invocare il servizio Web PubSub. E OnConnectedAsync() per rispondere quando connected viene attivato l'evento, OnMessageReceivedAsync() per gestire i messaggi dal client.

sealed class Sample_ChatApp : WebPubSubHub
{
    private readonly WebPubSubServiceClient<Sample_ChatApp> _serviceClient;

    public Sample_ChatApp(WebPubSubServiceClient<Sample_ChatApp> serviceClient)
    {
        _serviceClient = serviceClient;
    }

    public override async Task OnConnectedAsync(ConnectedEventRequest request)
    {
        Console.WriteLine($"[SYSTEM] {request.ConnectionContext.UserId} joined.");
    }

    public override async ValueTask<UserEventResponse> OnMessageReceivedAsync(UserEventRequest request, CancellationToken cancellationToken)
    {
        await _serviceClient.SendToAllAsync(RequestContent.Create(
        new
        {
            from = request.ConnectionContext.UserId,
            message = request.Data.ToString()
        }),
        ContentType.ApplicationJson);

        return new UserEventResponse();
    }
}

Nel codice sopra riportato, usiamo il client del servizio per inviare un messaggio di notifica in formato JSON a tutti coloro che sono collegati tramite SendToAllAsync.

Aggiornare la pagina Web

Ora aggiorniamo index.html per aggiungere la logica necessaria a connettersi, inviare messaggi e mostrare nella pagina i messaggi ricevuti.

<html>
  <body>
    <h1>Azure Web PubSub Chat</h1>
    <input id="message" placeholder="Type to chat...">
    <div id="messages"></div>
    <script>
      (async function () {
        let id = prompt('Please input your user name');
        let res = await fetch(`/negotiate?id=${id}`);
        let data = await res.json();
        let ws = new WebSocket(data.url);
        ws.onopen = () => console.log('connected');

        let messages = document.querySelector('#messages');
        
        ws.onmessage = event => {
          let m = document.createElement('p');
          let data = JSON.parse(event.data);
          m.innerText = `[${data.type || ''}${data.from || ''}] ${data.message}`;
          messages.appendChild(m);
        };

        let message = document.querySelector('#message');
        message.addEventListener('keypress', e => {
          if (e.charCode !== 13) return;
          ws.send(message.value);
          message.value = '';
        });
      })();
    </script>
  </body>

</html>

Nel codice precedente viene usata l'API WebSocket nativa nel browser e viene usata WebSocket.send() per inviare messaggi e WebSocket.onmessage per ascoltare i messaggi ricevuti.

È anche possibile usare gli SDK client per connettersi al servizio, che consente di riconnettersi automaticamente, gestire gli errori e altro ancora.

Ora resta solo un passaggio perché la chat funzioni. È possibile configurare gli eventi a cui si è interessati e dove inviare gli eventi nel servizio Web PubSub.

Configurare il gestore eventi

Il gestore eventi nel servizio Web PubSub viene impostato per indicare al servizio dove inviare gli eventi.

Quando il server Web viene eseguito localmente, in che modo il servizio Web PubSub richiama localhost se non dispone di un endpoint accessibile da Internet? Esistono in genere due modi. Uno consiste nell'esporre localhost al pubblico usando alcuni strumenti di tunnel generale e l'altro consiste nell'usare awps-tunnel per eseguire il tunneling del traffico dal servizio Web PubSub attraverso lo strumento al server locale.

In questa sezione si usa l'interfaccia della riga di comando di Azure per impostare i gestori eventi e usare awps-tunnel per instradare il traffico a localhost.

Configurare le impostazioni dell'hub

Impostiamo il modello di URL in modo che utilizzi lo schema tunnel affinché Web PubSub instradi i messaggi attraverso la connessione tunnel di awps-tunnel. I gestori di eventi possono essere impostati dal portale o tramite la CLI, come descritto in questo articolo; qui lo impostiamo tramite la CLI. Poiché rimaniamo in ascolto degli eventi nel percorso /eventhandler, come impostato nel passaggio precedente, impostiamo il modello dell'URL su tunnel:///eventhandler.

Usare il comando az webpubsub hub create dell'interfaccia della riga di comando di Azure per creare le impostazioni del gestore eventi per l'hubSample_ChatApp.

Importante

Sostituire <your-unique-resource-name> con il nome della risorsa Web PubSub creata nei passaggi precedenti.

az webpubsub hub create -n "<your-unique-resource-name>" -g "myResourceGroup" --hub-name "Sample_ChatApp" --event-handler url-template="tunnel:///eventhandler" user-event-pattern="*" system-event="connected"

Esegui awps-tunnel localmente

Scaricare e installare awps-tunnel

Lo strumento viene eseguito in Node.js versione 16 o successiva.

npm install -g @azure/web-pubsub-tunnel-tool

Usare la stringa di connessione del servizio ed eseguire

export WebPubSubConnectionString="<your connection string>"
awps-tunnel run --hub Sample_ChatApp --upstream http://localhost:8080

Eseguire il server Web

Ora tutto è impostato. Avviamo il server web e proviamo l'app di chat in azione.

Ora esegui il server con dotnet run --urls http://localhost:8080.

L'esempio di codice completo di questa esercitazione è disponibile qui.

Apri http://localhost:8080/index.html. È possibile immettere il nome utente e avviare la chat.

Autenticazione differita con gestore eventi connect

Nelle sezioni precedenti viene illustrato come usare l'endpoint negotiate per restituire l'URL del servizio Web PubSub e il token di accesso JWT per i client per connettersi al servizio Web PubSub. In alcuni casi, ad esempio per i dispositivi edge con risorse limitate, i client potrebbero preferire una connessione diretta alle risorse di Web PubSub. In questi casi, è possibile configurare il gestore eventi connect per autenticare i client in modo differito, assegnare ai client un ID utente, specificare i gruppi a cui i client si uniscono una volta connessi, configurare le autorizzazioni dei client e impostare il sottoprotocollo WebSocket come risposta WebSocket inviata al client e così via. Per i dettagli, fare riferimento alla specifica del gestore eventi di connessione.

A questo punto si userà connect il gestore eventi per ottenere un risultato simile a quello della sezione negotiate .

Aggiornare le impostazioni dell'hub

Prima di tutto, aggiornare le impostazioni dell'hub per includere connect anche il gestore eventi, è necessario consentire anche la connessione anonima in modo che i client senza token di accesso JWT possano connettersi al servizio.

Usare il comando az webpubsub hub update dell'interfaccia della riga di comando di Azure per creare le impostazioni del gestore eventi per l'hubSample_ChatApp.

Importante

Sostituire <your-unique-resource-name> con il nome della risorsa Web PubSub creata nei passaggi precedenti.

az webpubsub hub update -n "<your-unique-resource-name>" -g "myResourceGroup" --hub-name "Sample_ChatApp" --allow-anonymous true --event-handler url-template="tunnel:///eventhandler" user-event-pattern="*" system-event="connected" system-event="connect"

Aggiornare la logica upstream per gestire l'evento di connessione

È ora possibile aggiornare la logica upstream per gestire l'evento di connessione. Potremmo anche rimuovere ora l'endpoint di negoziazione.

Come facciamo nell'endpoint di negoziazione a scopo dimostrativo, leggiamo anche l'ID dai parametri della query. In caso di connessione, la query client originale viene mantenuta nel corpo della richiesta di evento connect.

All'interno della classe Sample_ChatApp, eseguire l'override OnConnectAsync() per gestire connect l'evento:

sealed class Sample_ChatApp : WebPubSubHub
{
    private readonly WebPubSubServiceClient<Sample_ChatApp> _serviceClient;

    public Sample_ChatApp(WebPubSubServiceClient<Sample_ChatApp> serviceClient)
    {
        _serviceClient = serviceClient;
    }

    public override ValueTask<ConnectEventResponse> OnConnectAsync(ConnectEventRequest request, CancellationToken cancellationToken)
    {
        if (request.Query.TryGetValue("id", out var id))
        {
            return new ValueTask<ConnectEventResponse>(request.CreateResponse(userId: id.FirstOrDefault(), null, null, null));
        }

        // The SDK catches this exception and returns 401 to the caller
        throw new UnauthorizedAccessException("Request missing id");
    }

    public override async Task OnConnectedAsync(ConnectedEventRequest request)
    {
        Console.WriteLine($"[SYSTEM] {request.ConnectionContext.UserId} joined.");
    }

    public override async ValueTask<UserEventResponse> OnMessageReceivedAsync(UserEventRequest request, CancellationToken cancellationToken)
    {
        await _serviceClient.SendToAllAsync(RequestContent.Create(
        new
        {
            from = request.ConnectionContext.UserId,
            message = request.Data.ToString()
        }),
        ContentType.ApplicationJson);

        return new UserEventResponse();
    }
}

Aggiornare index.html per la connessione diretta

Ora aggiorniamo la pagina web in modo che si connetta direttamente al servizio Web PubSub. Una cosa da notare è che, al momento, a scopo dimostrativo, l'endpoint del servizio Web PubSub è codificato in modo statico nel codice del client; aggiorna il nome host del servizio <the host name of your service> nell'HTML seguente con il valore del tuo servizio. Potrebbe essere comunque utile recuperare il valore dell'endpoint del servizio Web PubSub dal server, offre maggiore flessibilità e controllabilità a dove si connette il client.

<html>
  <body>
    <h1>Azure Web PubSub Chat</h1>
    <input id="message" placeholder="Type to chat...">
    <div id="messages"></div>
    <script>
      (async function () {
        // sample host: mock.webpubsub.azure.com
        let hostname = "<the host name of your service>";
        let id = prompt('Please input your user name');
        let ws = new WebSocket(`wss://${hostname}/client/hubs/Sample_ChatApp?id=${id}`);
        ws.onopen = () => console.log('connected');

        let messages = document.querySelector('#messages');
        
        ws.onmessage = event => {
          let m = document.createElement('p');
          let data = JSON.parse(event.data);
          m.innerText = `[${data.type || ''}${data.from || ''}] ${data.message}`;
          messages.appendChild(m);
        };

        let message = document.querySelector('#message');
        message.addEventListener('keypress', e => {
          if (e.charCode !== 13) return;
          ws.send(message.value);
          message.value = '';
        });
      })();
    </script>
  </body>

</html>

Riavviare il server

Ora riavvia il server e apri la pagina web seguendo le istruzioni precedenti. Se hai interrotto awps-tunnel, riesegui anche lo strumento tunnel.

Passaggi successivi

Questa esercitazione offre un'idea di base del funzionamento del sistema eventi nel servizio Web PubSub di Azure.

Vedere altre esercitazioni per approfondire l'uso del servizio.