チュートリアル: Azure Web PubSub サービスを使用してチャット アプリを作成する

メッセージの発行とサブスクライブのチュートリアルでは、Azure Web PubSub を使ったメッセージの発行とサブスクライブの基本について学習しました。 このチュートリアルでは、Azure Web PubSub のイベント システムについて学習し、それを使ってリアルタイム通信機能を備えた完全な Web アプリケーションを構築します。

このチュートリアルでは、次の作業を行う方法について説明します。

  • Web PubSub サービス インスタンスを作成する
  • Azure Web PubSub のイベント ハンドラー設定を構成する
  • アプリ サーバーでイベントを処理し、リアルタイム チャット アプリを構築する

Azure サブスクリプションをお持ちでない場合は、開始する前に Azure 無料アカウントを作成してください。

前提条件

  • Azure Cloud Shell で Bash 環境を使用します。 詳細については、「Azure Cloud Shell の Bash のクイックスタート」を参照してください。

  • CLI リファレンス コマンドをローカルで実行する場合、Azure CLI をインストールします。 Windows または macOS で実行している場合は、Docker コンテナーで Azure CLI を実行することを検討してください。 詳細については、「Docker コンテナーで Azure CLI を実行する方法」を参照してください。

    • ローカル インストールを使用する場合は、az login コマンドを使用して Azure CLI にサインインします。 認証プロセスを完了するには、ターミナルに表示される手順に従います。 その他のサインイン オプションについては、Azure CLI でのサインインに関するページを参照してください。

    • 初回使用時にインストールを求められたら、Azure CLI 拡張機能をインストールします。 拡張機能の詳細については、Azure CLI で拡張機能を使用する方法に関するページを参照してください。

    • az version を実行し、インストールされているバージョンおよび依存ライブラリを検索します。 最新バージョンにアップグレードするには、az upgrade を実行します。

  • このセットアップには、Azure CLI のバージョン 2.22.0 以降が必要です。 Azure Cloud Shell を使用している場合は、最新バージョンが既にインストールされています。

Azure Web PubSub インスタンスを作成する

リソース グループを作成する

リソース グループとは、Azure リソースのデプロイと管理に使用する論理コンテナーです。 az group create コマンドを使用して、myResourceGroup という名前のリソース グループを eastus の場所に作成します。

az group create --name myResourceGroup --location EastUS

Web PubSub インスタンスを作成する

az extension add を実行して、webpubsub 拡張機能をインストールするか、最新バージョンにアップグレードします。

az extension add --upgrade --name webpubsub

Azure CLI の az webpubsub create コマンドを使用して、作成したリソース グループに Web PubSub を作成します。 次のコマンドは、EastUS のリソース グループ myResourceGroup の下に "無料の" Web PubSub リソースを作成します。

重要

Web PubSub リソースには、それぞれ一意の名前を付ける必要があります。 次の例では、<your-unique-resource-name> をお使いの Web PubSub の名前に置き換えてください。

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

このコマンドの出力では、新しく作成したリソースのプロパティが表示されます。 次の 2 つのプロパティをメモしておきます。

  • Resource Name: 上記の --name パラメーターに指定した名前です。
  • hostName: この例では、ホスト名は <your-unique-resource-name>.webpubsub.azure.com/ です。

この時点で、お使いの Azure アカウントのみが、この新しいリソースで任意の操作を実行することを許可されています。

将来使用するために ConnectionString を取得する

重要

接続文字列には、アプリケーションが Azure Web PubSub サービスにアクセスするために必要な認可情報が含まれています。 接続文字列内のアクセス キーは、サービスのルート パスワードに似ています。 運用環境では、アクセス キーは常に慎重に保護してください。 キーを安全に管理およびローテーションするには、Azure Key Vault を使用します。 アクセス キーを他のユーザーに配布したり、ハードコーディングしたり、他のユーザーがアクセスできるプレーンテキストで保存したりしないでください。 キーが侵害された可能性があると思われる場合は、キーをローテーションしてください。

Azure CLI の az webpubsub key コマンドを使用して、サービスの ConnectionString を取得します。 プレースホルダー <your-unique-resource-name> を実際の Azure Web PubSub インスタンスの名前に置き換えます。

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

後で使うために接続文字列をコピーします。

フェッチした ConnectionString をコピーし、チュートリアルの後で読み取る環境変数 WebPubSubConnectionString に設定します。 次の <connection-string> を、フェッチした ConnectionString に置き換えます。

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

プロジェクトのセットアップ

前提条件

アプリケーションを作成する

Azure Web PubSub には、サーバーとクライアントの 2 つのロールがあります。 この概念は、Web アプリケーションにおけるサーバーとクライアントのロールに似ています。 サーバーはクライアントを管理し、リッスンしてクライアント メッセージに応答する役割を担います。 クライアントはサーバーとの間でユーザーのメッセージを送受信し、それらのエンド ユーザーへの視覚化を担います。

このチュートリアルでは、リアルタイム チャット Web アプリケーションを構築します。 実際の Web アプリケーションでは、サーバーの役割には、クライアントの認証や、アプリケーション UI のための静的 Web ページの提供も含まれます。

ASP.NET Core 8 を使って Web ページをホストし、受信要求を処理します。

まず、ASP.NET Core Web アプリを chatapp フォルダーに作成します。

  1. 新しい Web アプリを作成します。

    mkdir chatapp
    cd chatapp
    dotnet new web
    
  2. 静的 Web ページのホスティングをサポートするために、Program.cs で app.UseStaticFiles() を追加します。

    var builder = WebApplication.CreateBuilder(args);
    var app = builder.Build();
    
    app.UseStaticFiles();
    
    app.Run();
    
  3. HTML ファイルを作成し、wwwroot/index.html として保存します。これは、後でチャット アプリの UI に使います。

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

dotnet run --urls http://localhost:8080 を実行し、ブラウザーで http://localhost:8080/index.html にアクセスすることで、サーバーをテストできます。

ネゴシエート エンドポイントを追加

メッセージの発行とサブスクライブのチュートリアルでは、サブスクライバーは接続文字列を直接使います。 実際のアプリケーションでは、接続文字列にはサービスに対するあらゆる操作を実行するための高い特権があるため、接続文字列をクライアントと共有することは安全ではありません。 次に、サーバーで接続文字列を使い、クライアントがアクセス トークンを持つ完全な URL を取得するための negotiate エンドポイントを公開します。 このようにして、サーバーは未承認のアクセスを防ぐために、negotiate エンドポイントの前に認証ミドルウェアを追加できます。

まず依存関係をインストールします。

dotnet add package Microsoft.Azure.WebPubSub.AspNetCore

次に、クライアントがトークンを生成するために呼び出す /negotiate エンドポイントを追加します。

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>() は、サービス クライアント WebPubSubServiceClient<THub> を挿入するために使用されます。これを使用すると、ネゴシエーション ステップでクライアント接続トークンを生成し、ハブのイベントがトリガーされたときにハブ メソッドでサービス REST API を呼び出すことができます。 このトークン生成コードは、メッセージの発行とサブスクライブに関するチュートリアルで使用したものと似ていますが、トークンを生成するときにもう 1 つの引数 (userId) を渡す点が異なります。 ユーザー ID を使用してクライアントの ID を識別すると、メッセージを受信したとき、メッセージがどこから来たかがわかります。

このコードは、前の手順で設定した環境変数 WebPubSubConnectionString から接続文字列を読み取ります。

dotnet run --urls http://localhost:8080 を使ってサーバーを再実行します。

この API は、http://localhost:8080/negotiate?id=user1 にアクセスすることでテストでき、アクセス トークンを持つ Azure Web PubSub の完全な URL が得られます。

イベントを処理する

Azure Web PubSub では、クライアント側で特定のアクティビティが発生すると (たとえば、クライアントが接続中、接続済、切断済、またはクライアントがメッセージを送信中)、サービスはこれらのイベントに反応できるようにサーバーに通知を送信します。

イベントは、Webhook の形式でサーバーに配信されます。 Webhook はアプリケーション サーバーによって提供および公開され、Azure Web PubSub サービス側で登録されます。 サービスは、イベントが発生するたびに Webhook を呼び出します。

Azure Web PubSub は CloudEvents に従ってイベント データを記述します。

以下では、クライアントが接続されているときに connected システム イベントを処理し、クライアントがチャット アプリを構築するためにメッセージを送信しているときに message ユーザー イベントを処理します。

前の手順でインストールした AspNetCore 用 Web PubSub SDK Microsoft.Azure.WebPubSub.AspNetCore も、CloudEvents 要求の解析と処理に役立ちます。

まず、app.Run() の前にイベント ハンドラーを追加します。 イベントのエンドポイント パスを指定します。たとえば、/eventhandler です。

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

ここで、前の手順で作成したクラス Sample_ChatApp 内に、Web PubSub サービスの呼び出しに使う WebPubSubServiceClient<Sample_ChatApp> と連携するコンストラクターを追加します。 OnConnectedAsync()connected イベントがトリガーされたときに応答し、OnMessageReceivedAsync() はクライアントからのメッセージを処理します。

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

上記のコードでは、サービス クライアントを使って、SendToAllAsync に参加しているすべてのユーザーに JSON 形式で通知メッセージをブロードキャストします。

Web ページを更新する

ここで、index.html を更新して、接続、メッセージの送信、受信したメッセージをページに表示するロジックを追加します。

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

上記のコードでは、ブラウザーのネイティブ WebSocket API を使って接続し、WebSocket.send() を使ってメッセージを送信し、WebSocket.onmessage を使って受信メッセージをリッスンしています。

クライアント SDK を使ってサービスに接続することもできます。これにより、自動再接続やエラー処理などが可能になります。

これで、チャットが機能するまで残りの手順はあと 1 つです。 関心のあるイベントと、Web PubSub サービスのどこにイベントを送信するかを構成します。

イベント ハンドラーを設定する

Web PubSub サービスにイベント ハンドラーを設定して、イベントの送信先をサービスに指示します。

Web サーバーがローカルで実行されている場合、インターネットにアクセスできるエンドポイントがない場合、Web PubSub サービスはどのように localhost を呼び出すのでしょうか? 通常、2 つの方法があります。 1 つは、一般的なトンネル ツールを使って localhost をパブリックに公開する方法であり、もう 1 つは、awps-tunnel を使って、ツールを介して Web PubSub サービスからローカル サーバーにトラフィックをトンネリングする方法です。

このセクションでは、Azure CLI を使ってイベント ハンドラーを設定し、awps-tunnel を使ってトラフィックを localhost にルーティングします。

ハブ設定を構成する

tunnel スキームを使うように URL テンプレートを設定して、Web PubSub が awps-tunnel のトンネル接続を介してメッセージをルーティングするようにします。 イベント ハンドラーは、この記事で説明されているように、ポータルまたは CLI のいずれかから設定できます。ここでは CLI を使用して設定します。 前の手順で設定したようにパス /eventhandler でイベントをリッスンするため、URL テンプレートを tunnel:///eventhandler に設定します。

Azure CLI の az webpubsub hub create コマンドを使って、Sample_ChatApp ハブのイベント ハンドラー設定を作成します。

重要

<your-unique-resource-name> を、前の手順で作成した Web PubSub リソースの名前に置き換えます。

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"

awps-tunnel をローカルで実行する

awps-tunnel をダウンロードしてインストールする

このツールは、Node.js バージョン 16 以降で実行されます。

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

サービス接続文字列を使って実行する

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

Web サーバーを実行する

これですべてが設定されました。 Web サーバーを実行し、チャット アプリを動作させてみましょう。

dotnet run --urls http://localhost:8080 を使ってサーバーを実行します。

このチュートリアルの完全なコード サンプルは、こちらにあります。

http://localhost:8080/index.htmlを開きます。 ユーザー名を入力してチャットを開始できます。

connect イベント ハンドラーを使用した Lazy Auth

前のセクションでは、negotiate エンドポイントを使用して、Web PubSub サービス URL とクライアントが Web PubSub サービスに接続するための JWT アクセス トークンを返す方法を示します。 リソースが限られているエッジ デバイスなど、クライアントが Web PubSub リソースへの直接接続を優先する場合があります。 このような場合は、クライアントに対して Lazy Auth を行う、クライアントにユーザー ID を割り当てる、接続後にクライアントが参加するグループを指定する、クライアントが持つアクセス許可を構成する、クライアントへの WebSocket 応答として WebSocket サブプロトコルを構成するなどを実行するように、connect イベント ハンドラーを構成できます。詳細については、接続イベント ハンドラーの仕様を参照してください。

次に、connect イベント ハンドラーを使用して、negotiate セクションが実行するのと同様の方法を実現します。

ハブ設定の更新

まず、ハブ設定を更新して、connect イベント ハンドラーも含めます。JWT アクセス トークンのないクライアントがサービスに接続できるように、匿名接続も許可する必要があります。

Azure CLI の az webpubsub hub create コマンドを使って、Sample_ChatApp ハブのイベント ハンドラー設定を更新します。

重要

<your-unique-resource-name> を、前の手順で作成した Web PubSub リソースの名前に置き換えます。

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"

接続イベントを処理するようにアップストリーム ロジックを更新

次に、アップストリーム ロジックを更新して、接続イベントを処理します。 negotiate エンドポイントを削除することもできます。

デモの目的として negotiate エンドポイントで行うのと同様に、クエリ パラメーターから ID も読み取ります。 接続イベントでは、元のクライアント クエリは接続イベントのリケット本文に保持されます。

クラス Sample_ChatApp 内で、OnConnectAsync() をオーバーライドし、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();
    }
}

直接接続するための index.html の更新

次に、Web PubSub サービスに直接接続するように Web ページを更新します。 ここで言及すべきことの 1 つは、デモ目的で Web PubSub サービス エンドポイントがクライアント コードにハードコーディングされていることです。次の html のサービス ホスト名 <the host name of your service> を、独自のサービスの値で更新します。 サーバーから Web PubSub サービス エンドポイントの値をフェッチすると、クライアントの接続先に対し、柔軟性と制御性を向上するうえで有益な可能性があります。

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

サーバーの再実行

次に、サーバーを再実行し、前の手順に従って Web ページにアクセスします。 awps-tunnel を停止した場合は、トンネル ツールも再実行してください。

次のステップ

このチュートリアルでは、Azure Web PubSub サービスでイベント システムがどのように機能するかについての基本的な考え方について説明しています。

サービスの使用方法の詳細については、他のチュートリアルを参照してください。