Tutorial: criar um aplicativo de chat com o serviço Azure Web PubSub

No Tutorial de mensagem de publicação e assinatura, você aprenderá os conceitos básicos de publicação e assinatura de mensagens com o Azure Web PubSub. Neste tutorial, você vai aprender sobre o sistema de eventos do Azure Web PubSub e usá-lo para criar um aplicativo web completo com funcionalidade de comunicação em tempo real.

Neste tutorial, você aprenderá a:

  • Criar uma instância do serviço Web PubSub
  • Configurar as definições do manipulador de eventos para o Azure Web PubSub
  • Lidar com eventos no servidor do aplicativo e criar um aplicativo de chat em tempo real

Caso você não tenha uma assinatura do Azure, crie uma conta gratuita do Azure antes de começar.

Pré-requisitos

  • Essa configuração requer a versão 2.22.0 ou superior da CLI do Azure. Se você está usando o Azure Cloud Shell, a versão mais recente já está instalada.

Criar uma instância do Azure Web PubSub

Criar um grupo de recursos

Um grupo de recursos é um contêiner lógico no qual os recursos do Azure são implantados e gerenciados. Use o comando az group create para criar um grupo de recursos chamado myResourceGroup no local eastus.

az group create --name myResourceGroup --location EastUS

Criar uma instância do Web PubSub

Execute az extension add para instalar ou atualizar a extensão webpubsub para a versão atual.

az extension add --upgrade --name webpubsub

Use o comando az webpubsub create da CLI do Azure para criar um Web PubSub no grupo de recursos criado. O seguinte comando cria um recurso gratuito do Web PubSub no grupo de recursos myResourceGroup no EastUS:

Importante

Cada recurso Web PubSub precisa ter um nome exclusivo. Substitua <nome-de recurso-exclusivo> pelo nome do Web PubSub nos exemplos a seguir.

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

A saída deste comando mostra as propriedades do recurso recém-criado. Anote as duas propriedades listadas abaixo:

  • Nome do Recurso: o nome que você forneceu ao parâmetro --name acima.
  • hostName: no exemplo, o nome do host é <your-unique-resource-name>.webpubsub.azure.com/.

Nesse ponto, sua conta do Azure é a única autorizada a executar qualquer operação nesse novo recurso.

Obter o ConnectionString para uso posterior

Importante

Uma cadeia de conexão inclui as informações de autorização necessárias para que o seu aplicativo acesse o serviço Azure Web PubSub. A chave de acesso dentro da cadeia de conexão é semelhante a uma senha raiz para o serviço. Em ambientes de produção, sempre tenha o cuidado de proteger as chaves de acesso. Utilize o Azure Key Vault para gerenciar e girar suas chaves com segurança. Evite distribuir chaves de acesso para outros usuários, fazer hard-coding com elas ou salvá-las em qualquer lugar em texto sem formatação que seja acessível a outras pessoas. Gire suas chaves se você acredita que elas podem ter sido comprometidas.

Use o comando az webpubsub key da CLI do Azure para obter a ConnectionString do serviço. Substitua o espaço reservado <your-unique-resource-name> pelo nome da instância do Azure Web PubSub.

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

Copie a cadeia de conexão para usar mais tarde.

Copie o ConnectionString buscado e defina-o na variável de ambiente WebPubSubConnectionString, que o tutorial lerá posteriormente. Substitua <connection-string> abaixo pelo ConnectionString buscado.

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

Configurar o projeto

Pré-requisitos

Criar o aplicativo

Há duas funções no Azure Web PubSub, servidor e cliente. Esse conceito é semelhante às funções de servidor e cliente em um aplicativo Web. O servidor é responsável por gerenciar os clientes, escutar e responder às mensagens do cliente. O cliente é responsável por enviar e receber mensagens do usuário do servidor e visualizá-las para o usuário final.

Neste tutorial, vamos criar um aplicativo web de chat em tempo real. Em um aplicativo Web real, a responsabilidade do servidor também inclui a autenticação de clientes e o fornecimento de páginas da Web estáticas para a interface do usuário do aplicativo.

Usaremos o ASP.NET Core 8 para hospedar as páginas da web e lidar com as solicitações de entrada.

Primeiro, vamos criar um aplicativo web do ASP.NET Core em uma pasta chatapp.

  1. Crie um novo aplicativo Web.

    mkdir chatapp
    cd chatapp
    dotnet new web
    
  2. Adicione app.UseStaticFiles() no Program.cs para dar suporte à hospedagem de páginas da web estáticas.

    var builder = WebApplication.CreateBuilder(args);
    var app = builder.Build();
    
    app.UseStaticFiles();
    
    app.Run();
    
  3. Crie um arquivo HTML e salve-o como wwwroot/index.html. Vamos usá-lo mais tarde para a interface do usuário do aplicativo de chat.

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

Você pode testar o servidor executando dotnet run --urls http://localhost:8080 e acessando http://localhost:8080/index.html no navegador.

Adicionar o ponto de extremidade de negociação

No tutorial Publicar e assinar mensagem, o assinante consome a cadeia de conexão diretamente. Em um aplicativo real, não é seguro compartilhar a cadeia de conexão com nenhum cliente, pois a cadeia de conexão tem alto privilégio para realizar qualquer operação no serviço. Agora, vamos fazer com que o servidor consuma a cadeia de conexão e exponha um ponto de extremidade negotiate para o cliente obter a URL completa com o token de acesso. Dessa forma, o servidor pode adicionar o middleware de autenticação antes do ponto de extremidade negotiate para impedir o acesso não autorizado.

Primeiro, instale as dependências.

dotnet add package Microsoft.Azure.WebPubSub.AspNetCore

Agora, vamos adicionar um ponto de extremidade /negotiate para o cliente chamar para gerar o 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>() é usado para injetar o cliente de serviço WebPubSubServiceClient<THub>, com o qual podemos usar na etapa de negociação para gerar token de conexão de cliente e em métodos de Hub para invocar APIs REST de serviço quando eventos de Hub são disparados. Esse código de geração de token é semelhante ao que usamos no tutorial de mensagem de publicação e assinatura, com a diferença de que passamos um argumento a mais (userId) ao gerar o token. A ID de usuário pode ser usada para identificar a identidade do cliente, para que, ao receber uma mensagem, você saiba de onde ela vem.

O código lê a cadeia de conexão da variável de ambiente WebPubSubConnectionString que definimos na etapa anterior.

Execute novamente o servidor usando dotnet run --urls http://localhost:8080.

Você pode testar essa API acessando http://localhost:8080/negotiate?id=user1 e ele fornecerá a URL completa do Azure Web PubSub com um token de acesso.

Tratar eventos

No Azure Web PubSub, quando houver determinadas atividades acontecendo no lado do cliente (por exemplo, um cliente está se conectando, está conectado, desconectado ou um cliente está enviando mensagens), o serviço envia notificações ao servidor para que ele possa reagir a esses eventos.

Os eventos são entregues ao servidor na forma de webhook. O webhook é servido e exposto pelo servidor de aplicativos e registrado no lado do serviço do Azure Web PubSub. O serviço invoca os webhooks sempre que um evento acontece.

O Azure Web PubSub segue o CloudEvents para descrever os dados do evento.

Abaixo, lidamos com eventos do sistema connected quando um cliente está conectado e lida com eventos de usuário message quando um cliente está enviando mensagens para criar o aplicativo de chat.

O SDK do Web PubSub para AspNetCore Microsoft.Azure.WebPubSub.AspNetCore que instalamos na etapa anterior também pode ajudar a analisar e processar as solicitações do CloudEvents.

Primeiro, adicione manipuladores de eventos antes de app.Run(). Especifique o caminho do ponto de extremidade para os eventos, por exemplo /eventhandler.

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

Agora, dentro da classe Sample_ChatApp que criamos na etapa anterior, adicione um construtor para trabalhar com WebPubSubServiceClient<Sample_ChatApp> que usamos para invocar o serviço Web PubSub. E OnConnectedAsync() para responder quando o evento connected é disparado, OnMessageReceivedAsync() para lidar com mensagens do cliente.

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();
    }
}

No código acima, usamos o cliente de serviço para transmitir uma mensagem de notificação no formato JSON a todos os quais está ingressado com SendToAllAsync.

Atualizar a página da web

Agora, vamos atualizar index.html para adicionar a lógica para conectar, enviar mensagem e exibir mensagens recebidas na página.

<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>

Você pode ver no código acima que nos conectamos usamos a API WebSocket nativa no navegador e usamos WebSocket.send() para enviar mensagens e WebSocket.onmessage para ouvir as mensagens recebidas.

Você também pode usar SDKs do cliente para se conectar ao serviço, possibilitando que você use a reconexão automática, tratamento de erros e muito mais.

Falta apenas uma etapa para o chat funcionar. Vamos configurar quais eventos são relevantes para nós e para onde enviar os eventos no serviço do Web PubSub.

Configurar o manipulador de eventos

Definimos o manipulador de eventos no serviço do Web PubSub para informar ao serviço para onde enviar os eventos.

Quando o servidor Web é executado localmente, como o serviço do Web PubSub invoca o localhost se ele não tem nenhum ponto de extremidade acessível pela Internet? Geralmente há duas maneiras. Uma é expor o localhost ao público usando alguma ferramenta de túnel geral. A outra forma é usar awps-tunnel para enviar por túnel o tráfego do serviço do Web PubSub por meio da ferramenta para o servidor local.

Nesta seção, usamos a CLI do Azure para definir os manipuladores de eventos e usar awps-tunnel para rotear o tráfego para localhost.

Definir as configurações do hub

Definimos o modelo de URL para usar o esquema tunnel para que o Web PubSub encaminhe mensagens por meio da conexão de túnel do awps-tunnel. Os manipuladores de eventos podem ser definidos no portal ou na CLI, conforme descrito neste artigo; definimos aqui por meio da CLI. Como ouvimos eventos no caminho /eventhandler como conjuntos de etapas anteriores, definimos o modelo de URL como tunnel:///eventhandler.

Use o comando az webpubsub hub create da CLI do Azure para criar as configurações do manipulador de eventos para o hub Sample_ChatApp.

Importante

Substitua <your-unique-resource-name> pelo nome do recurso Web PubSub criado nas etapas anteriores.

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"

Executar o awps-tunnel localmente

Baixar e instalar awps-tunnel

A ferramenta é executada no Node.js, versão 16 ou superior.

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

Usar a cadeia de conexão de serviço executar

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

Executar o servidor Web

Tudo está configurado. Vamos executar o servidor Web e testar o aplicativo de chat na prática.

Agora, execute o servidor usando dotnet run --urls http://localhost:8080.

O exemplo de código completo deste tutorial pode ser encontrado aqui.

Abra o http://localhost:8080/index.html. Você pode inserir seu nome de usuário e começar a conversar.

Autenticação lenta com o manipulador de eventos connect

Nas seções anteriores, demonstramos como usar o ponto de extremidade negotiate para retornar a URL de serviço do Web PubSub e o token de acesso JWT para que os clientes se conectem ao serviço Web PubSub. Em alguns casos, por exemplo, dispositivos de borda com recursos limitados, os clientes podem preferir se conectar diretamente aos recursos do Web PubSub. Nesses casos, você pode configurar o manipulador de eventos connect para autenticar lentamente os clientes, atribuir ID de usuário aos clientes, especificar os grupos que os clientes ingressarem depois de se conectarem, configurar as permissões que os clientes têm e o subprotocolo WebSocket como a resposta WebSocket ao cliente, etc. Para detalhes, confira especificação do manipulador de eventos de conexão.

Agora, vamos usar o manipulador de eventos connect para obter o semelhante ao que a seção negotiate faz.

Atualizar configurações do hub

Primeiro, vamos atualizar as configurações do hub para incluir também o manipulador de eventos connect, precisamos também permitir a conexão anônima para que os clientes sem o token de acesso JWT possam se conectar ao serviço.

Use o comando az webpubsub hub update da CLI do Azure para criar as configurações do manipulador de eventos para o hub Sample_ChatApp.

Importante

Substitua <your-unique-resource-name> pelo nome do recurso Web PubSub criado nas etapas anteriores.

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"

Atualizar a lógica upstream para lidar com o evento de conexão

Agora, vamos atualizar a lógica upstream para lidar com o evento de conexão. Podemos também remover o ponto de extremidade negotiate agora.

Como semelhante ao que fazemos no ponto de extremidade negotiate como finalidade de demonstração, também lemos a ID dos parâmetros de consulta. No evento de conexão, a consulta de cliente original é preservada no corpo da solicitação de evento de conexão.

Dentro da classe Sample_ChatApp, substitua OnConnectAsync() para manipular o evento connect:

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();
    }
}

Atualizar index.html para conectar-se diretamente

Agora, vamos atualizar a página da Web para conectar-se diretamente ao serviço Web PubSub. Uma coisa a mencionar é que, agora, para fins de demonstração, o ponto de extremidade de serviço do Web PubSub é codificado no código do cliente, atualize o nome do host <the host name of your service> do serviço no html abaixo com o valor de seu próprio serviço. Pode ainda ser útil buscar o valor do ponto de extremidade de serviço do Web PubSub do servidor, oferecendo mais flexibilidade e controlabilidade para onde o cliente se conecta.

<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>

Executar novamente o servidor

Agora execute novamente o servidor e visite a página da Web seguindo as instruções anteriores. Se você tiver parado awps-tunnel, execute novamente a ferramenta de túnel.

Próximas etapas

Este tutorial oferece uma ideia básica de como o sistema de eventos funciona no serviço do Azure Web PubSub.

Confira outros tutoriais para saber mais sobre como usar o serviço.