メッセージの発行とサブスクライブのチュートリアルでは、Azure Web PubSub を使ったメッセージの発行とサブスクライブの基本について学習しました。 このチュートリアルでは、Azure Web PubSub のイベント システムについて学習し、それを使ってリアルタイム通信機能を備えた完全な Web アプリケーションを構築します。
このチュートリアルでは、次の作業を行う方法について説明します。
- Web PubSub サービス インスタンスを作成する
- Azure Web PubSub のイベント ハンドラー設定を構成する
- アプリ サーバーでイベントを処理し、リアルタイム チャット アプリを構築する
Azure アカウントをお持ちでない場合は、開始する前に無料アカウントを作成してください。
前提条件
- このセットアップには、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 を使ってキーの管理とローテーションを安全に行い、WebPubSubServiceClient を使って接続をセキュリティ保護します。
アクセス キーを他のユーザーに配布したり、ハードコーディングしたり、他のユーザーがアクセスできるプレーンテキストで保存したりしないでください。 キーが侵害された可能性があると思われる場合は、キーをローテーションしてください。
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 フォルダーに作成します。
新しい Web アプリを作成します。
mkdir chatapp
cd chatapp
dotnet new web
静的 Web ページのホスティングをサポートするために、Program.cs で app.UseStaticFiles() を追加します。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseStaticFiles();
app.Run();
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 にアクセスすることで、サーバーをテストできます。
Node.js の一般的な Web フレームワークである express.js を使って、Web ページをホストし、受信要求を処理します。
まず、chatapp フォルダーに express Web アプリを作成します。
express.js をインストールします
mkdir chatapp
cd chatapp
npm init -y
npm install --save express
次に express サーバーを作成し、server.js として保存します
const express = require('express');
const app = express();
app.use(express.static('public'));
app.listen(8080, () => console.log('server started'));
また、HTML ファイルを作成し、public/index.html として保存します。これは、後でチャット アプリの UI に使います。
<html>
<body>
<h1>Azure Web PubSub Chat</h1>
</body>
</html>
node server を実行し、ブラウザーで http://localhost:8080 にアクセスすることで、サーバーをテストできます。
Javalin Web フレームワークを使って Web ページをホストし、受信要求を処理します。
Maven を使って新しいアプリ webpubsub-tutorial-chat を作成し、webpubsub-tutorial-chat フォルダーに切り替えます。
mvn archetype:generate --define interactiveMode=n --define groupId=com.webpubsub.tutorial --define artifactId=webpubsub-tutorial-chat --define archetypeArtifactId=maven-archetype-quickstart --define archetypeVersion=1.4
cd webpubsub-tutorial-chat
javalin Web フレームワークの依存関係を dependencies の pom.xml ノードに追加します。
-
javalin: Java 用の単純な Web フレームワーク
-
slf4j-simple: Java 用ロガー
<!-- https://mvnrepository.com/artifact/io.javalin/javalin -->
<dependency>
<groupId>io.javalin</groupId>
<artifactId>javalin</artifactId>
<version>6.1.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-simple -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.12</version>
</dependency>
/src/main/java/com/webpubsub/tutorial ディレクトリに移動します。 使用しているエディターで App.java ファイルを開きます 静的ファイルを提供するには、Javalin.create を使います。
package com.webpubsub.tutorial;
import io.javalin.Javalin;
public class App {
public static void main(String[] args) {
// start a server
Javalin app = Javalin.create(config -> {
config.staticFiles.add("public");
}).start(8080);
}
}
設定によっては、明示的に言語レベルを Java 8 に設定する必要がある場合があります。 この手順は pom.xml で行うことができます。 次のスニペットを追加します。
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
HTML ファイルを作成し、/src/main/resources/public/index.html に保存します。 これは、後でチャット アプリの UI に使います。
<html>
<body>
<h1>Azure Web PubSub Chat</h1>
</body>
</html>
サーバーをテストするには、pom.xml ファイルを含むディレクトリの下で次のコマンドを実行し、ブラウザーで http://localhost:8080 にアクセスします。
mvn compile & mvn package & mvn exec:java -Dexec.mainClass="com.webpubsub.tutorial.App" -Dexec.cleanupDaemonThreads=false
このジョブを実現するために、Python の人気のある Web フレームワークである Flask を使います。
まず、Flask が用意されたプロジェクト フォルダー chatapp を作成します。
環境を作成してアクティブにします
mkdir chatapp
cd chatapp
python3 -m venv .venv
. .venv/bin/activate
アクティブにした環境内で、Flask をインストールします
pip install Flask
次に、Flask サーバーを作成し、server.py として保存します
from flask import (
Flask,
send_from_directory,
)
app = Flask(__name__)
@app.route('/<path:filename>')
def index(filename):
return send_from_directory('public', filename)
if __name__ == '__main__':
app.run(port=8080)
また、HTML ファイルを作成し、public/index.html として保存します。これは、後でチャット アプリの UI に使います。
<html>
<body>
<h1>Azure Web PubSub Chat</h1>
</body>
</html>
python server.py を実行し、ブラウザーで http://127.0.0.1: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 を使ってサーバーを再実行します。
Azure Web PubSub SDK をインストールします。
npm install --save @azure/web-pubsub
次に、トークンを生成するための /negotiate API を追加します。
const express = require('express');
const { WebPubSubServiceClient } = require('@azure/web-pubsub');
const app = express();
const hubName = 'Sample_ChatApp';
let serviceClient = new WebPubSubServiceClient(process.env.WebPubSubConnectionString, hubName);
app.get('/negotiate', async (req, res) => {
let id = req.query.id;
if (!id) {
res.status(400).send('missing user id');
return;
}
let token = await serviceClient.getClientAccessToken({ userId: id });
res.json({
url: token.url
});
});
app.use(express.static('public'));
app.listen(8080, () => console.log('server started'));
このトークン生成コードは、メッセージの発行とサブスクライブに関するチュートリアルで使用したものと似ていますが、トークンを生成するときにもう 1 つの引数 (userId) を渡す点が異なります。 ユーザー ID を使用してクライアントの ID を識別すると、メッセージを受信したとき、メッセージがどこから来たかがわかります。
このコードは、WebPubSubConnectionStringで設定した環境変数 から接続文字列を読み取ります。
node server を実行してサーバーを再実行します。
まず、Azure Web PubSub SDK の依存関係と gson を dependencies の pom.xml ノードに追加します。
<!-- https://mvnrepository.com/artifact/com.azure/azure-messaging-webpubsub -->
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-messaging-webpubsub</artifactId>
<version>1.2.12</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
次に、/negotiate API を App.java ファイルに追加してトークンを生成します。
package com.webpubsub.tutorial;
import com.azure.messaging.webpubsub.WebPubSubServiceClient;
import com.azure.messaging.webpubsub.WebPubSubServiceClientBuilder;
import com.azure.messaging.webpubsub.models.GetClientAccessTokenOptions;
import com.azure.messaging.webpubsub.models.WebPubSubClientAccessToken;
import com.azure.messaging.webpubsub.models.WebPubSubContentType;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import io.javalin.Javalin;
public class App {
public static void main(String[] args) {
String connectionString = System.getenv("WebPubSubConnectionString");
if (connectionString == null) {
System.out.println("Please set the environment variable WebPubSubConnectionString");
return;
}
// create the service client
WebPubSubServiceClient service = new WebPubSubServiceClientBuilder()
.connectionString(connectionString)
.hub("Sample_ChatApp")
.buildClient();
// start a server
Javalin app = Javalin.create(config -> {
config.staticFiles.add("public");
}).start(8080);
// Handle the negotiate request and return the token to the client
app.get("/negotiate", ctx -> {
String id = ctx.queryParam("id");
if (id == null) {
ctx.status(400);
ctx.result("missing user id");
return;
}
GetClientAccessTokenOptions option = new GetClientAccessTokenOptions();
option.setUserId(id);
WebPubSubClientAccessToken token = service.getClientAccessToken(option);
ctx.contentType("application/json");
Gson gson = new Gson();
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("url", token.getUrl());
String response = gson.toJson(jsonObject);
ctx.result(response);
return;
});
}
}
このトークン生成コードは、メッセージの発行とサブスクライブに関するチュートリアルで使用したものと似ていますが、トークンを生成するときに setUserId メソッドを呼び出してユーザー ID を設定する点が異なります。 ユーザー ID を使用してクライアントの ID を識別すると、メッセージを受信したとき、メッセージがどこから来たかがわかります。
このコードは、WebPubSubConnectionStringで設定した環境変数 から接続文字列を読み取ります。
次のコマンドを使ってサーバーを再実行します。
mvn compile & mvn package & mvn exec:java -Dexec.mainClass="com.webpubsub.tutorial.App" -Dexec.cleanupDaemonThreads=false
まず、Azure Web PubSub SDK をインストールします。
pip install azure-messaging-webpubsubservice
次に、/negotiate API をサーバーに追加してトークンを生成します。
import os
from flask import (
Flask,
request,
send_from_directory,
)
from azure.messaging.webpubsubservice import (
WebPubSubServiceClient
)
hub_name = 'Sample_ChatApp'
connection_string = os.environ.get('WebPubSubConnectionString')
app = Flask(__name__)
service = WebPubSubServiceClient.from_connection_string(connection_string, hub=hub_name)
@app.route('/<path:filename>')
def index(filename):
return send_from_directory('public', filename)
@app.route('/negotiate')
def negotiate():
id = request.args.get('id')
if not id:
return 'missing user id', 400
token = service.get_client_access_token(user_id=id)
return {
'url': token['url']
}, 200
if __name__ == '__main__':
app.run(port=8080)
このトークン生成コードは、メッセージの発行とサブスクライブに関するチュートリアルで使用したものと似ていますが、トークンを生成するときにもう 1 つの引数 (user_id) を渡す点が異なります。 ユーザー ID を使用してクライアントの ID を識別すると、メッセージを受信したとき、メッセージがどこから来たかがわかります。
このコードは、WebPubSubConnectionStringで設定した環境変数 から接続文字列を読み取ります。
python server.py を使ってサーバーを再実行します。
この 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 形式で通知メッセージをブロードキャストします。
express 用の Web PubSub SDK @azure/web-pubsub-express は、CloudEvents 要求の解析とプロセスに役立ちます。
npm install --save @azure/web-pubsub-express
次のコードで server.js を更新して、/eventhandler で REST API を公開し (これは Web PubSub SDK によって提供される express ミドルウェアによって行われます)、クライアント接続イベントを処理します。
const express = require("express");
const { WebPubSubServiceClient } = require("@azure/web-pubsub");
const { WebPubSubEventHandler } = require("@azure/web-pubsub-express");
const app = express();
const hubName = "Sample_ChatApp";
let serviceClient = new WebPubSubServiceClient(process.env.WebPubSubConnectionString, hubName);
let handler = new WebPubSubEventHandler(hubName, {
path: "/eventhandler",
onConnected: async (req) => {
console.log(`${req.context.userId} connected`);
},
handleUserEvent: async (req, res) => {
if (req.context.eventName === "message")
await serviceClient.sendToAll({
from: req.context.userId,
message: req.data,
});
res.success();
},
});
app.get("/negotiate", async (req, res) => {
let id = req.query.id;
if (!id) {
res.status(400).send("missing user id");
return;
}
let token = await serviceClient.getClientAccessToken({ userId: id });
res.json({
url: token.url,
});
});
app.use(express.static("public"));
app.use(handler.getMiddleware());
app.listen(8080, () => console.log("server started"));
上記のコードでは、onConnected はクライアントの接続時にコンソールにメッセージを出力するだけです。 接続されているクライアントの ID を確認できるように、req.context.userId を使用していることを確認できます。 クライアントがメッセージを送信すると、handleUserEvent が呼び出されます。
WebPubSubServiceClient.sendToAll() を使って、JSON オブジェクトのメッセージをすべてのクライアントにブロードキャストします。
handleUserEvent には、イベントの送信者にメッセージを返信できる res オブジェクトもあることがわかります。 ここでは、単に res.success() を呼び出して、WebHook が 200 を返すようにします (この呼び出しは、クライアントに何も返したくない場合でも必要であることに注意してください。そうしないと、WebHook が戻らず、クライアント接続が閉じられます)。
現時点では、イベント ハンドラーを Java で独自に実装する必要があります。 手順はプロトコル仕様に従うと簡単で、次の一覧に示されています。
イベント ハンドラー パスの HTTP ハンドラーを追加します。たとえば /eventhandler です。
まず、不正使用防止の OPTIONS 要求を処理します。ヘッダーに WebHook-Request-Origin ヘッダーが含まれているかどうかを確認し、ヘッダー WebHook-Allowed-Origin を返します。 デモ用に簡単にするために、すべてのオリジンを許可するために * を返します。
// validation: https://learn.microsoft.com/azure/azure-web-pubsub/reference-cloud-events#protection
app.options("/eventhandler", ctx -> {
ctx.header("WebHook-Allowed-Origin", "*");
});
次に、受信要求が予想したとおりのイベントであるかどうかを調べます。 ここで、システム connected イベントについて考えてみましょう。これは、ヘッダー ce-type を azure.webpubsub.sys.connected として含むはずです。 不正使用防止の後にロジックを追加して、接続されたイベントをすべてのクライアントにブロードキャストし、誰がチャット ルームに参加したかを確認できるようにします。
// validation: https://learn.microsoft.com/azure/azure-web-pubsub/reference-cloud-events#protection
app.options("/eventhandler", ctx -> {
ctx.header("WebHook-Allowed-Origin", "*");
});
// handle events: https://learn.microsoft.com/azure/azure-web-pubsub/reference-cloud-events#events
app.post("/eventhandler", ctx -> {
String event = ctx.header("ce-type");
if ("azure.webpubsub.sys.connected".equals(event)) {
String id = ctx.header("ce-userId");
System.out.println(id + " connected.");
}
ctx.status(200);
});
上のコードでは、クライアントが接続されたときにコンソールにメッセージを出力するだけです。 接続されているクライアントの ID を確認できるように、ctx.header("ce-userId") を使用していることを確認できます。
ce-type イベントの message は常に azure.webpubsub.user.message です。 詳細については、イベント メッセージに関するページを参照してください。 メッセージが到着すると、接続されているすべてのクライアントにメッセージを JSON 形式でブロードキャストするように、メッセージを処理するロジックを更新します。
// handle events: https://learn.microsoft.com/azure/azure-web-pubsub/reference-cloud-events#events
app.post("/eventhandler", ctx -> {
String event = ctx.header("ce-type");
if ("azure.webpubsub.sys.connected".equals(event)) {
String id = ctx.header("ce-userId");
System.out.println(id + " connected.");
} else if ("azure.webpubsub.user.message".equals(event)) {
String id = ctx.header("ce-userId");
String message = ctx.body();
Gson gson = new Gson();
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("from", id);
jsonObject.addProperty("message", message);
String messageToSend = gson.toJson(jsonObject);
service.sendToAll(messageToSend, WebPubSubContentType.APPLICATION_JSON);
}
ctx.status(200);
});
現時点では、イベント ハンドラーを Python で独自に実装する必要があります。 手順はプロトコル仕様に従うと簡単で、次の一覧に示されています。
イベント ハンドラー パスの HTTP ハンドラーを追加します。たとえば /eventhandler です。
まず、不正使用防止の OPTIONS 要求を処理します。ヘッダーに WebHook-Request-Origin ヘッダーが含まれているかどうかを確認し、ヘッダー WebHook-Allowed-Origin を返します。 デモ用に簡単にするために、すべてのオリジンを許可するために * を返します。
# validation: https://learn.microsoft.com/azure/azure-web-pubsub/reference-cloud-events#protection
@app.route('/eventhandler', methods=['OPTIONS'])
def handle_event():
if request.method == 'OPTIONS':
if request.headers.get('WebHook-Request-Origin'):
res = Response()
res.headers['WebHook-Allowed-Origin'] = '*'
res.status_code = 200
return res
次に、受信要求が予想したとおりのイベントであるかどうかを調べます。 ここで、システム connected イベントについて考えてみましょう。これは、ヘッダー ce-type を azure.webpubsub.sys.connected として含むはずです。 不正使用防止の後にロジックを追加します。
# validation: https://learn.microsoft.com/azure/azure-web-pubsub/reference-cloud-events#protection
# handle events: https://learn.microsoft.com/azure/azure-web-pubsub/reference-cloud-events#events
@app.route('/eventhandler', methods=['POST', 'OPTIONS'])
def handle_event():
if request.method == 'OPTIONS':
if request.headers.get('WebHook-Request-Origin'):
res = Response()
res.headers['WebHook-Allowed-Origin'] = '*'
res.status_code = 200
return res
elif request.method == 'POST':
user_id = request.headers.get('ce-userid')
if request.headers.get('ce-type') == 'azure.webpubsub.sys.connected':
return user_id + ' connected', 200
else:
return 'Not found', 404
上のコードでは、クライアントが接続されたときにコンソールにメッセージを出力するだけです。 接続されているクライアントの ID を確認できるように、request.headers.get('ce-userid') を使用していることを確認できます。
ce-type イベントの message は常に azure.webpubsub.user.message です。 詳細については、イベント メッセージに関するページを参照してください。 メッセージが到着すると、接続されているすべてのクライアントにメッセージをブロードキャストするように、メッセージを処理するロジックを更新します。
@app.route('/eventhandler', methods=['POST', 'OPTIONS'])
def handle_event():
if request.method == 'OPTIONS':
if request.headers.get('WebHook-Request-Origin'):
res = Response()
res.headers['WebHook-Allowed-Origin'] = '*'
res.status_code = 200
return res
elif request.method == 'POST':
user_id = request.headers.get('ce-userid')
type = request.headers.get('ce-type')
if type == 'azure.webpubsub.sys.connected':
print(f"{user_id} connected")
return '', 204
elif type == 'azure.webpubsub.user.message':
# default uses JSON
service.send_to_all(message={
'from': user_id,
'message': request.data.decode('UTF-8')
})
# returned message is also received by the client
return {
'from': "system",
'message': "message handled by server"
}, 200
else:
return 'Bad Request', 400
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 を使ってサーバーを実行します。
このチュートリアルの完全なコード サンプルは、こちらにあります。
node server を使ってサーバーを実行します。
このチュートリアルの完全なコード サンプルは、こちらにあります。
次のコマンドを使ってサーバーを実行します。
mvn compile & mvn package & mvn exec:java -Dexec.mainClass="com.webpubsub.tutorial.App" -Dexec.cleanupDaemonThreads=false
このチュートリアルの完全なコード サンプルは、こちらにあります。
python server.py を使ってサーバーを実行します。
このチュートリアルの完全なコード サンプルは、こちらにあります。
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();
}
}
クライアント接続イベントを処理するように server.js を更新します。
const express = require("express");
const { WebPubSubServiceClient } = require("@azure/web-pubsub");
const { WebPubSubEventHandler } = require("@azure/web-pubsub-express");
const app = express();
const hubName = "Sample_ChatApp";
let serviceClient = new WebPubSubServiceClient(process.env.WebPubSubConnectionString, hubName);
let handler = new WebPubSubEventHandler(hubName, {
path: "/eventhandler",
handleConnect: async (req, res) => {
if (req.context.query.id){
res.success({ userId: req.context.query.id });
} else {
res.fail(401, "missing user id");
}
},
onConnected: async (req) => {
console.log(`${req.context.userId} connected`);
},
handleUserEvent: async (req, res) => {
if (req.context.eventName === "message")
await serviceClient.sendToAll({
from: req.context.userId,
message: req.data,
});
res.success();
},
});
app.use(express.static("public"));
app.use(handler.getMiddleware());
app.listen(8080, () => console.log("server started"));
次に、接続イベント azure.webpubsub.sys.connect を処理するロジックを追加します。
// validation: https://learn.microsoft.com/azure/azure-web-pubsub/reference-cloud-events#protection
app.options("/eventhandler", ctx -> {
ctx.header("WebHook-Allowed-Origin", "*");
});
// handle events: https://learn.microsoft.com/azure/azure-web-pubsub/reference-cloud-events#connect
app.post("/eventhandler", ctx -> {
String event = ctx.header("ce-type");
if ("azure.webpubsub.sys.connect".equals(event)) {
String body = ctx.body();
System.out.println("Reading from request body...");
Gson gson = new Gson();
JsonObject requestBody = gson.fromJson(body, JsonObject.class); // Parse JSON request body
JsonObject query = requestBody.getAsJsonObject("query");
if (query != null) {
System.out.println("Reading from request body query:" + query.toString());
JsonElement idElement = query.get("id");
if (idElement != null) {
JsonArray idInQuery = query.get("id").getAsJsonArray();
if (idInQuery != null && idInQuery.size() > 0) {
String id = idInQuery.get(0).getAsString();
ctx.contentType("application/json");
Gson response = new Gson();
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("userId", id);
ctx.result(response.toJson(jsonObject));
return;
}
}
} else {
System.out.println("No query found from request body.");
}
ctx.status(401).result("missing user id");
} else if ("azure.webpubsub.sys.connected".equals(event)) {
String id = ctx.header("ce-userId");
System.out.println(id + " connected.");
ctx.status(200);
} else if ("azure.webpubsub.user.message".equals(event)) {
String id = ctx.header("ce-userId");
String message = ctx.body();
service.sendToAll(String.format("{\"from\":\"%s\",\"message\":\"%s\"}", id, message), WebPubSubContentType.APPLICATION_JSON);
ctx.status(200);
}
});
次に、connect システム イベントを処理します。これには、ヘッダー ce-type が、azure.webpubsub.sys.connect として含まれる必要があります。 不正使用防止の後にロジックを追加します。
@app.route('/eventhandler', methods=['POST', 'OPTIONS'])
def handle_event():
if request.method == 'OPTIONS' or request.method == 'GET':
if request.headers.get('WebHook-Request-Origin'):
res = Response()
res.headers['WebHook-Allowed-Origin'] = '*'
res.status_code = 200
return res
elif request.method == 'POST':
user_id = request.headers.get('ce-userid')
type = request.headers.get('ce-type')
print("Received event of type:", type)
# Sample connect logic if connect event handler is configured
if type == 'azure.webpubsub.sys.connect':
body = request.data.decode('utf-8')
print("Reading from connect request body...")
query = json.loads(body)['query']
print("Reading from request body query:", query)
id_element = query.get('id')
user_id = id_element[0] if id_element else None
if user_id:
return {'userId': user_id}, 200
return 'missing user id', 401
elif type == 'azure.webpubsub.sys.connected':
return user_id + ' connected', 200
elif type == 'azure.webpubsub.user.message':
service.send_to_all(content_type="application/json", message={
'from': user_id,
'message': request.data.decode('UTF-8')
})
return Response(status=204, content_type='text/plain')
else:
return 'Bad Request', 400
直接接続するための 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 サービスでイベント システムがどのように機能するかについての基本的な考え方について説明しています。
サービスの使用方法の詳細については、他のチュートリアルを参照してください。