メッセージの発行とサブスクライブのチュートリアルでは、Azure Web PubSub を使ったメッセージの発行とサブスクライブの基本について学習しました。 このチュートリアルでは、Azure Web PubSub のイベント システムについて学習し、それを使ってリアルタイム通信機能を備えた完全な Web アプリケーションを構築します。
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 を識別すると、メッセージを受信したとき、メッセージがどこから来たかがわかります。
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 を識別すると、メッセージを受信したとき、メッセージがどこから来たかがわかります。
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 を識別すると、メッセージを受信したとき、メッセージがどこから来たかがわかります。
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 を識別すると、メッセージを受信したとき、メッセージがどこから来たかがわかります。
<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 を使って受信メッセージをリッスンしています。
@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>