教學課程:使用 Azure Web PubSub 服務建立聊天應用程式
本文內容
在發佈和訂閱訊息教學課程 中,您會了解使用 Azure Web PubSub 發佈和訂閱訊息的基本概念。 在本教學課程中,您將瞭解 Azure Web PubSub 的事件系統,並用它來建置具有即時通訊功能的完整 Web 應用程式。
在本教學課程中,您會了解如何:
建立 Web PubSub 服務執行個體
設定 Azure Web PubSub 的事件處理常式設定
處理應用程式伺服器中的事件,並建置即時聊天應用程式
如果您沒有 Azure 訂閱 ,請在開始之前,先建立 Azure 免費帳戶 。
必要條件
此設定需要 2.22.0 版或更新版本的 Azure CLI。 如果您是使用 Azure Cloud Shell,就已安裝最新版本。
建立 Azure Web PubSub 執行個體
建立資源群組
資源群組是在其中部署與管理 Azure 資源的邏輯容器。 使用 az group create 命令,在 eastus
位置中建立名為 myResourceGroup
的資源群組。
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 資源都必須有唯一的名稱。 使用下列範例中的 Web PubSub 名稱取代 <your-unique-resource-name>。
az webpubsub create --name "<your-unique-resource-name>" --resource-group "myResourceGroup" --location "EastUS" --sku Free_F1
此命令的輸出顯示新建資源的屬性。 請記下下列兩個屬性:
資源名稱 :您提供給上述 --name
參數的名稱。
hostName :在此範例中,主機名稱為 <your-unique-resource-name>.webpubsub.azure.com/
。
此時,您的 Azure 帳戶是唯一獲得授權在此新資源上執行任何作業的帳戶。
取得 ConnectionString 以供後續使用
重要
原始 連接字串 只會針對示範目的出現在本文中。
連接字串包含應用程式存取 Azure Web PubSub 服務所需的授權資訊。 連接字串內的存取金鑰類似於服務的根密碼。 在生產環境中,請一律保護您的存取金鑰。 使用 Azure 金鑰保存庫,安全地管理和輪替密鑰,並使用保護連線WebPubSubServiceClient
。
避免將存取金鑰散發給其他使用者、寫入程式碼,或將其以純文字儲存在他人可以存取的位置。 如果您認為金鑰可能已遭盜用,請輪替金鑰。
使用 Azure CLI az webpubsub key 命令取得服務的 ConnectionString 。 使用您的 Azure Web PubSub 執行個體名稱取代 <your-unique-resource-name>
預留位置。
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 中有兩個角色:伺服器和用戶端。 此概念類似於 Web 應用程式中的伺服器和用戶端角色。 伺服器負責管理用戶端、接聽及回應用戶端訊息。 用戶端負責傳送和接收來自伺服器的使用者訊息,並將其呈現給終端使用者。
在此教學課程中,我們會建置即時聊天 Web 應用程式。 在實際的 Web 應用程式中,伺服器的責任也包括驗證用戶端,以及為應用程式 UI 提供靜態網頁。
我們使用 ASP.NET Core 8 來裝載網頁並處理傳入要求。
首先,讓我們在資料夾中建立 ASP.NET Core Web 應用程式 chatapp
。
建立新的 Web 應用程式。
mkdir chatapp
cd chatapp
dotnet new web
新增 app.UseStaticFiles()
Program.cs以支援裝載靜態網頁。
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
來測試伺服器。
我們使用 express.js ,這是一種熱門的Web架構,可Node.js裝載網頁並處理傳入要求。
首先,讓我們在 chatapp
資料夾中建立快速 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 架構來裝載網頁以及處理傳入要求。
使用 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
我們使用 Flask ,這是適用於 Python 的熱門 Web 架構,可達成這項工作。
請先建立已備妥 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 。
新增交涉端點
在發佈和訂閱訊息教學課程 中,訂閱者會直接取用 連接字串。 在真實世界應用程式中,與任何用戶端共用 連接字串 並不安全,因為 連接字串 具有對服務執行任何作業的高許可權。 現在,讓我們讓伺服器取用 連接字串,並公開negotiate
用戶端的端點,以取得具有存取令牌的完整 URL。 如此一來,伺服器就可以在端點之前 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。 此權杖產生程式碼類似於我們在發佈和訂閱訊息教學課程 中使用的程式碼,差別在於,產生權杖時會多傳遞一個引數 (userId
)。 使用者識別碼可用來識別用戶端的身分識別,讓您在收到訊息時能知道訊息來自何處。
程序代碼會從我們在上一個步驟中 設定的環境變數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'));
此權杖產生程式碼類似於我們在發佈和訂閱訊息教學課程 中使用的程式碼,差別在於,產生權杖時會多傳遞一個引數 (userId
)。 使用者識別碼可用來識別用戶端的身分識別,讓您在收到訊息時能知道訊息來自何處。
此程式代碼會從我們在上一個步驟中 設定的環境變數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
方法來設定使用者識別碼。 使用者識別碼可用來識別用戶端的身分識別,讓您在收到訊息時能知道訊息來自何處。
程式代碼會從我們在上一個步驟中設定的 環境變數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)
此權杖產生程式碼類似於我們在發佈和訂閱訊息教學課程 中使用的程式碼,差別在於,產生權杖時會多傳遞一個引數 (user_id
)。 使用者識別碼可用來識別用戶端的身分識別,讓您在收到訊息時能知道訊息來自何處。
程序代碼會從我們在上一個步驟中 設定的環境變數WebPubSubConnectionString
讀取 連接字串。
使用 python server.py
重新執行伺服器。
您可以藉由存取 http://localhost:8080/negotiate?id=user1
來測試此 API,並提供 Azure Web PubSub 的完整 URL 與存取令牌。
處理事件
在 Azure Web PubSub 中,當用戶端發生某些活動時(例如用戶端正在連線、連線、中斷連線或用戶端正在傳送訊息),服務會將通知傳送至伺服器,以便回應這些事件。
事件會以 Webhook 的形式傳遞至伺服器。 Webhook 由應用程式伺服器提供並公開,並註冊於 Azure Web PubSub 服務端。 每當有事件發生時,服務就會叫用 Webhook。
Azure Web PubSub 會依照 CloudEvents 來描述事件資料。
以下我們會在用戶端連線時處理 connected
系統事件,並在用戶端傳送訊息以建置聊天應用程式時處理 message
使用者事件。
我們在上一個步驟中安裝的適用於 AspNetCore Microsoft.Azure.WebPubSub.AspNetCore
的 Web PubSub SDK 也可以協助剖析及處理 CloudEvents 要求。
首先,在 之前 app.Run()
新增事件處理程式。 指定事件的端點路徑,假設是 /eventhandler
。
app.MapWebPubSubHub<Sample_ChatApp>("/eventhandler/{*path}");
app.Run();
現在,在我們在上一個步驟中建立的類別 Sample_ChatApp
內,新增建構函式來使用 WebPubSubServiceClient<Sample_ChatApp>
,以便用來叫用Web PubSub服務。 以及在 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();
}
}
在上述程式代碼中,我們會使用服務用戶端,以 JSON 格式廣播通知訊息給所有已加入 SendToAllAsync
的通知訊息。
適用於 Express @azure/web-pubsub-express
的 Web PubSub SDK 可協助剖析及處理 CloudEvents 要求。
npm install --save @azure/web-pubsub-express
使用下列程式代碼更新server.js,以公開 REST API, /eventhandler
此 API 是由 Web PubSub SDK 提供的快速中間件所完成,以處理用戶端連線的事件:
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
只要在用戶端連線時,將訊息列印至主控台即可。 如您所見,我們使用了 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
事件,其中應包含 azure.webpubsub.sys.connected
這樣的標頭 ce-type
。 我們會在濫用保護之後新增邏輯,將所有連線事件廣播給所有用戶端,讓他們可以看到誰加入聊天室。
// 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);
});
在上述程式碼中,我們會在用戶端連線時將訊息列印至主控台。 如您所見,我們使用了 ctx.header("ce-userId")
,因此可以看到連線用戶端的身分識別。
message
事件的 ce-type
一律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
事件,其中應包含 azure.webpubsub.sys.connected
這樣的標頭 ce-type
。 我們會在濫用保護後面新增邏輯:
# 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
在上述程式碼中,我們會在用戶端連線時將訊息列印至主控台。 如您所見,我們使用了 request.headers.get('ce-userid')
,因此可以看到連線用戶端的身分識別。
message
事件的 ce-type
一律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
更新網頁
現在讓我們更新 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 來連線到服務,讓您能夠自動重新連線、錯誤處理等等。
現在還有一個步驟可供聊天運作。 讓我們設定我們關心的事件,以及在 Web PubSub 服務中將事件傳送至何處。
設定事件處理常式
我們會在 Web PubSub 服務中設定事件處理程式,以告知服務將事件傳送至何處。
當 Web 伺服器在本機執行時,如果 Web PubSub 服務沒有可存取因特網的端點,如何叫用 localhost? 通常有兩種方式。 其中一個是使用一些一般通道工具向公用公開 localhost,另一個是使用 wps 通道 ,透過工具將來自 Web PubSub 服務的流量通道傳送至您的本地伺服器。
在本節中,我們會使用 Azure CLI 來設定事件處理程式,並使用 wps 通道 將流量路由傳送至 localhost。
我們將URL範本設定為使用 tunnel
配置,讓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
執行網頁伺服器
現在一切都已設定。 讓我們執行網頁伺服器,並搭配聊天應用程式運作。
現在使用 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
延遲驗證
在上一節中,我們將示範如何使用 交涉 端點來傳回 Web PubSub 服務 URL,以及用戶端連線至 Web PubSub 服務的 JWT 存取令牌。 例如,在某些情況下,具有有限資源的邊緣裝置,用戶端可能偏好直接連線到 Web PubSub 資源。 在這種情況下,您可以設定 connect
事件處理程式來延遲驗證用戶端、將使用者標識碼指派給用戶端、指定客戶端在連線後加入的群組、設定用戶端擁有的許可權,以及將 WebSocket 子程式設定為用戶端的 WebSocket 回應等等。詳細數據請參閱 連線事件處理程式規格 。
現在,讓我們使用connect
事件處理程式來達成與交涉區段所做的類似 。
更新中樞設定
首先,讓我們更新中樞設定以同時包含 connect
事件處理程式,我們也必須允許匿名連線,讓沒有 JWT 存取令牌的用戶端可以連線到服務。
使用 Azure CLI az webpubsub hub update 命令來建立中樞的 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"
更新上游邏輯以處理連線事件
現在讓我們更新上游邏輯來處理連線事件。 我們現在可以移除交涉端點。
如同我們在交涉端點中做為示範用途,我們也從查詢參數讀取標識符。 在 connect 事件中,原始客戶端查詢會保留在 connect 事件要求本文中。
在類別 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 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>
重新執行伺服器
現在 ,請重新執行伺服器 ,並依照先前的指示瀏覽網頁。 如果您已 awps-tunnel
停止 ,請同時 重新執行通道工具 。
下一步
本教學課程提供關於事件系統在 Azure Web PubSub 服務中如何運作的基本概念。
請參考其他教學課程,進一步探討如何使用此服務。