Tutorial: Creación de una aplicación de chat con el servicio Azure Web PubSub

En el tutorial sobre publicación de mensajes y suscripción a ellos, aprenderá los conceptos básicos de la publicación de mensajes y la suscripción a ellos con Azure Web PubSub. En este tutorial, conocerá y aprenderá a usar el sistema de eventos de Azure Web PubSub para compilar una aplicación web completa con funcionalidad de comunicación en tiempo real.

En este tutorial, aprenderá a:

  • Crear una instancia del servicio Web PubSub
  • Configurar opciones del controlador de eventos para Azure Web PubSub
  • Control de eventos en el servidor de aplicaciones y compilación de una aplicación de chat en tiempo real

Si no tiene una suscripción a Azure, cree una cuenta gratuita de Azure antes de empezar.

Requisitos previos

  • Esta configuración requiere la versión 2.22.0, o cualquier versión superior, de la CLI de Azure. Si usa Azure Cloud Shell, ya está instalada la versión más reciente.

Creación de una instancia de Azure Web PubSub

Crear un grupo de recursos

Un grupo de recursos es un contenedor lógico en el que se implementan y se administran los recursos de Azure. Use el comando az group create para crear un grupo de recursos denominado myResourceGroup en la ubicación eastus.

az group create --name myResourceGroup --location EastUS

Creación de una instancia de Web PubSub

Ejecute az extension add para instalar o actualizar la extensión webpubsub en la versión actual.

az extension add --upgrade --name webpubsub

Use el comando az webpubsub create de la CLI de Azure para crear una instancia de Web PubSub en el grupo de recursos que ha creado. El comando siguiente crea un recurso Free de Web PubSub en el grupo de recursos myResourceGroup en EastUS:

Importante

Cada recurso de Web PubSub debe tener un nombre único. Reemplace <your-unique-resource-name> por el nombre de Web PubSub en los ejemplos siguientes.

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

La salida de este comando muestra las propiedades del recurso que acaba de crear. Tome nota de las dos propiedades siguientes:

  • Nombre del recurso: nombre que proporcionó al parámetro --name anterior.
  • hostName: en el ejemplo, el nombre de host es <your-unique-resource-name>.webpubsub.azure.com/.

En este momento, su cuenta de Azure es la única autorizada para realizar operaciones en este nuevo recurso.

Obtención de ConnectionString para usarlo en el futuro

Importante

Una cadena de conexión incluye la información de autorización necesaria para que la aplicación acceda al servicio Azure Web PubSub. La clave de acceso dentro de la cadena de conexión es similar a una contraseña raíz para el servicio. En los entornos de producción, siempre debe proteger las claves de acceso. Use Azure Key Vault para administrar y rotar las claves de forma segura. Evite distribuirlas a otros usuarios, codificarlas de forma rígida o guardarlas en un archivo de texto sin formato al que puedan acceder otros usuarios. Rote sus claves si cree que se han puesto en peligro.

Use el comando az webpubsub key de la CLI de Azure para obtener el valor de ConnectionString del servicio. Reemplace el marcador de posición <your-unique-resource-name> por el nombre de la instancia de Azure Web PubSub.

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

Copie la cadena de conexión para usarla más adelante.

Copie el ConnectionString capturado y establézcalo en la variable de entorno WebPubSubConnectionString, que el tutorial leerá más adelante. Reemplace <connection-string> por el ConnectionString que ha capturado.

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

Configuración del proyecto

Prerrequisitos

Creación de la aplicación

En Azure Web PubSub, hay dos roles: servidor y cliente. Este concepto es similar a los roles de servidor y cliente de una aplicación web. El servidor es responsable de administrar los clientes, escuchar y responder a los mensajes de los clientes. El cliente es responsable de enviar y recibir los mensajes del usuario desde el servidor y visualizarlos para el usuario final.

En este tutorial, compilaremos una aplicación web de chat en tiempo real. En una aplicación web real, la responsabilidad del servidor también incluye autenticar los clientes y proporcionar las páginas web estáticas para la interfaz de usuario de la aplicación.

Usaremos ASP.NET Core 8 para hospedar las páginas web y administrar las solicitudes entrantes.

En primer lugar, vamos a crear una aplicación web de ASP.NET Core en una carpeta chatapp.

  1. Cree una aplicación web nueva.

    mkdir chatapp
    cd chatapp
    dotnet new web
    
  2. Agregue Program.cs app.UseStaticFiles() para admitir el hospedaje de páginas web estáticas.

    var builder = WebApplication.CreateBuilder(args);
    var app = builder.Build();
    
    app.UseStaticFiles();
    
    app.Run();
    
  3. Cree un archivo HTML y guárdelo como wwwroot/index.html; lo usaremos para la interfaz de usuario de la aplicación de chat más adelante.

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

Ejecute dotnet run --urls http://localhost:8080 para probar el servidor y acceda a http://localhost:8080/index.html en el explorador.

Adición de un punto de conexión negotiate

En el tutorial Publicar y suscribir un mensaje, el suscriptor consume directamente la cadena de conexión. En una aplicación real, no es seguro compartir la cadena de conexión con ningún cliente, ya que la cadena de conexión tiene privilegios elevados para realizar cualquier operación en el servicio. Ahora, vamos a hacer que el servidor consuma la cadena de conexión y exponga un punto de conexión negotiate para que el cliente obtenga la dirección URL completa con el token de acceso. De este modo, el servidor puede agregar un middleware de autenticación antes del punto de conexión negotiate para evitar el acceso no autorizado.

En primer lugar, instale las dependencias.

dotnet add package Microsoft.Azure.WebPubSub.AspNetCore

Ahora vamos a agregar un punto de conexión /negotiate para que el cliente llame a para generar el 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>() se usa para insertar el cliente de servicio WebPubSubServiceClient<THub>, que se puede usar en el paso de negociación para generar el token de conexión de cliente y en los métodos del centro para invocar las API REST del servicio cuando se desencadenan eventos del centro. Este código de generación de tokens es similar al que se usó en el tutorial de publicación de mensajes y suscripción a ellos, salvo que se pasa un argumento más (userId) al generar el token. El identificador de usuario se puede usar para obtener la identidad del cliente, de modo que, cuando reciba un mensaje, sabrá de dónde viene.

El código lee la cadena de conexión de la variable de entorno WebPubSubConnectionString que establecimos en el paso anterior.

Vuelva a ejecutar el servidor mediante dotnet run --urls http://localhost:8080.

Puede probar esta API accediendo a http://localhost:8080/negotiate?id=user1 y le proporcionará la dirección URL completa de Azure Web PubSub con un token de acceso.

Control de eventos

En Azure Web PubSub, cuando se producen determinadas actividades en el lado cliente (por ejemplo, un cliente está conectando, conectado, desconectado o enviando mensajes), el servicio envía notificaciones al servidor para que pueda reaccionar a estos eventos.

Los eventos se entregan al servidor en forma de webhook. El servidor de aplicaciones sirve y expone el webhook, que se registra en el lado del servicio Azure Web PubSub. El servicio invoca los webhooks cada vez que se produce un evento.

Azure Web PubSub sigue CloudEvents para describir los datos del evento.

A continuación se controlan los eventos del sistema connected cuando un cliente está conectado y se controlan los eventos de usuario message cuando un cliente envía mensajes para compilar la aplicación de chat.

El SDK de Web PubSub para AspNetCore Microsoft.Azure.WebPubSub.AspNetCore que instalamos en el paso anterior también podría ayudar a analizar y procesar las solicitudes de CloudEvents.

En primer lugar, agregue controladores de eventos antes de app.Run(). Especifique la ruta de acceso del punto de conexión para los eventos; por ejemplo, /eventhandler.

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

Ahora, dentro de la clase Sample_ChatApp que creamos en el paso anterior, agregue un constructor para trabajar con el WebPubSubServiceClient<Sample_ChatApp> que usamos para invocar el servicio Web PubSub. Y OnConnectedAsync() para responder cuando se desencadena el evento connected, OnMessageReceivedAsync() para controlar los mensajes del 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();
    }
}

En el código anterior, usamos el cliente de servicio para difundir un mensaje de notificación en formato JSON a todos los que se unen con SendToAllAsync.

Actualización de la página web

Ahora vamos a actualizar index.html para agregar la lógica para conectarse, enviar mensajes y mostrar mensajes recibidos en la 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>

Puede ver en el código anterior que conectamos mediante la API nativa de WebSocket en el explorador y usar WebSocket.send() para enviar mensajes y WebSocket.onmessage para escuchar los mensajes recibidos.

También puede usar el SDK de cliente para conectarse al servicio, lo que le permite volver a conectarse automáticamente, controlar errores y mucho más.

Ahora queda un paso para que el chat funcione. Vamos a configurar los eventos que nos importan y dónde enviar los eventos al servicio Web PubSub.

Configuración del controlador de eventos

Establecemos el controlador de eventos en el servicio Web PubSub para indicar al servicio dónde enviar los eventos.

Cuando el servidor web se ejecuta localmente, ¿cómo invoca el servicio Web PubSub el localhost si no tiene ningún punto de conexión accesible a Internet? Normalmente hay dos maneras. Una consiste en exponer localhost a público mediante alguna herramienta de túnel general y la otra es usar awps-tunnel para tunelizar el tráfico desde el servicio Web PubSub a través de la herramienta al servidor local.

En esta sección, usamos la CLI de Azure para establecer los controladores de eventos y usar awps-tunnel para enrutar el tráfico a localhost.

Configuración de las opciones del centro de conectividad

Establecemos la plantilla de dirección URL para usar el esquema tunnel para que Web PubSub enrute los mensajes a través de la conexión de túnel de awps-tunnel. Los controladores de eventos se pueden establecer desde el portal o la CLI, tal y como se describe en este artículo; aquí lo establecemos a través de la CLI. Dado que escuchamos eventos en la ruta de acceso /eventhandler como los conjuntos de pasos anteriores, establecemos la plantilla de dirección URL en tunnel:///eventhandler.

Use la CLI de Azure comando az webpubsub hub create para crear la configuración del controlador de eventos para el centro de Sample_ChatApp.

Importante

Reemplace <your-unique-resource-name> por el nombre del recurso de Web PubSub que creó en los pasos 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"

Ejecución local de awps-tunnel

Descarga e instalación de awps-tunnel

La herramienta se ejecuta en Node.js versión 16 o posterior.

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

Uso de la cadena de conexión de servicio y ejecución

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

Ejecución del servidor web

Ya está todo listo. Vamos a ejecutar el servidor web y jugar con la aplicación de chat en acción.

Ejecute el servidor mediante dotnet run --urls http://localhost:8080.

El código de ejemplo completo de este tutorial se puede encontrar aquí.

Abra http://localhost:8080/index.html. Puede escribir el nombre de usuario y empezar a chatear.

Autenticación diferida con controlador de eventos connect

En las secciones anteriores, se muestra cómo usar negociar punto de conexión para devolver la dirección URL del servicio Web PubSub y el token de acceso JWT para que los clientes se conecten al servicio Web PubSub. En algunos casos, por ejemplo, los dispositivos perimetrales que tienen recursos limitados, los clientes pueden preferir la conexión directa a los recursos de Web PubSub. En tales casos, puede configurar el controlador de eventos connect para realizar autenticación diferida de los clientes, asignar ID de usuario a los clientes, especificar los grupos a los que se unen los clientes una vez que se conectan, configurar los permisos que tienen los clientes y el subprotocolo WebSocket como respuesta de WebSocket al cliente y mucho más. Para obtener más información, consulte las especificaciones del controlador de eventos de conexión.

Ahora usemos el controlador de eventos connect para lograr algo similar a lo que hace la sección de negociación.

Actualizar la configuración del centro de conectividad

En primer lugar, vamos a actualizar la configuración del centro para incluir también connect controlador de eventos, es necesario permitir también la conexión anónima para que los clientes sin token de acceso JWT puedan conectarse al servicio.

Use el comando az webpubsub hub update de la CLI de Azure para crear la configuración del controlador de eventos del centro de Sample_ChatApp.

Importante

Reemplace <your-unique-resource-name> por el nombre del recurso de Web PubSub que creó en los pasos 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"

Actualice la lógica ascendente para manejar el evento de conexión

Ahora vamos a actualizar la lógica ascendente para controlar el evento de conexión. También podríamos quitar el punto de conexión de negociación ahora.

De forma similar a lo que hacemos en el punto de conexión de negociación como propósito de demostración, también se lee el identificador de los parámetros de consulta. En el evento de conexión, la consulta del cliente original se conserva en el cuerpo de la solicitud del evento de conexión.

Dentro de la clase Sample_ChatApp, invalide OnConnectAsync() para controlar el 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();
    }
}

Actualizar index.html para conectar directamente

Ahora vamos a actualizar la página web para conectarse directamente al servicio Web PubSub. Lo que hay que mencionar es que ahora, para fines de demostración, el punto de conexión de servicio de Web PubSub está codificado de forma rígida en el código de cliente, actualice el nombre de host del servicio <the host name of your service> en el código html siguiente con el valor de su propio servicio. Es posible que siga siendo útil capturar el valor del punto de conexión de servicio de Web PubSub del servidor, lo que proporciona más flexibilidad y capacidad de control a la ubicación a la que se conecta el cliente.

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

Volver a ejecutar el servidor

Ahora vuelva a ejecutar el servidor y viste la página web siguiendo las instrucciones anteriores. Si ha detenido awps-tunnel, también vuelva a ejecutar la herramienta de túnel.

Pasos siguientes

En este tutorial se proporciona una idea básica de cómo funciona el sistema de eventos en el servicio Azure Web PubSub.

En otros tutoriales podrá profundizar más en cómo usar el servicio.