教學課程:使用 Azure Functions 和 Azure Web PubSub 服務建立無伺服器即時聊天應用程式
Azure Web PubSub 服務可協助您輕鬆地使用 WebSocket 和發佈-訂閱模式來建置即時傳訊 Web 應用程式。 Azure Functions 是無伺服器平臺,可讓您執行程序代碼,而不需要管理任何基礎結構。 在本教學課程中,您將瞭解如何使用 Azure Web PubSub 服務和 Azure Functions 來建置具有即時傳訊和發佈訂閱模式的無伺服器應用程式。
在本教學課程中,您會了解如何:
- 建置無伺服器即時聊天應用程式
- 使用 Web PubSub 函式觸發程式系結和輸出系結
- 將函式部署至 Azure 函式應用程式
- 設定 Azure 驗證
- 設定 Web PubSub 事件處理程式,將事件和訊息路由傳送至應用程式
必要條件
如果您沒有 Azure 訂閱,請在開始之前,先建立 Azure 免費帳戶。
登入 Azure
請使用您的 Azure 帳戶登入 Azure 入口網站 (https://portal.azure.com/)。
建立 Azure Web PubSub 服務執行個體
您的應用程式將會連線至 Azure 中的 Web PubSub 服務執行個體。
選取 Azure 入口網站左上角的 [新增] 按鈕。 在 [新增] 畫面的搜尋方塊中輸入 Web PubSub 並按 Enter。 (您也可以從 Web
類別搜尋 Azure Web PubSub。)
從搜尋結果中選取 [Web PubSub],然後選取 [建立]。
輸入下列設定。
設定 |
建議的值 |
描述 |
資源名稱 |
全域唯一的名稱 |
識別新 Web PubSub 服務執行個體的全域唯一名稱。 有效字元:a-z 、A-Z 、0-9 和 - 。 |
訂用帳戶 |
您的訂用帳戶 |
將在其下建立這個新 Web PubSub 服務執行個體的 Azure 訂用帳戶。 |
資源群組 |
myResourceGroup |
要在其中建立 Web PubSub 服務執行個體之新資源群組的名稱。 |
地點 |
美國西部 |
選擇您附近的區域。 |
定價層 |
免費 |
您可以先免費試用 Azure Web PubSub 服務。 深入了解 Azure Web PubSub 服務定價層 |
單位計數 |
- |
單位計數會指出您的 Web PubSub 服務執行個體可接受的連線數目。 每個單位支援最多 1,000 個同時連線。 它只能在標準層中設定。 |
選取 [建立] 以開始部署 Web PubSub 服務執行個體。
建立函式
請確定您已安裝 Azure Functions Core Tools 。 然後為專案建立空的目錄。 在此工作目錄下執行命令。
func init --worker-runtime javascript --model V4
func init --worker-runtime javascript --model V3
func init --worker-runtime dotnet
func init --worker-runtime dotnet-isolated
func init --worker-runtime python --model V1
安裝 Microsoft.Azure.WebJobs.Extensions.WebPubSub
。
確認並更新 host.json
's extensionBundle 至 4.* 版或更新版本,以取得 Web PubSub 支援。
{
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle",
"version": "[4.*, 5.0.0)"
}
}
確認並更新 host.json
's extensionBundle 至 3.3.0 版或更新版本,以取得 Web PubSub 支援。
{
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle",
"version": "[3.3.*, 4.0.0)"
}
}
dotnet add package Microsoft.Azure.WebJobs.Extensions.WebPubSub
dotnet add package Microsoft.Azure.Functions.Worker.Extensions.WebPubSub --prerelease
{
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle",
"version": "[3.3.*, 4.0.0)"
}
建立函 index
式來讀取和裝載用戶端的靜態網頁。
func new -n index -t HttpTrigger
- 更新
src/functions/index.js
並複製下列程序代碼。
const { app } = require('@azure/functions');
const { readFile } = require('fs/promises');
app.http('index', {
methods: ['GET', 'POST'],
authLevel: 'anonymous',
handler: async (context) => {
const content = await readFile('index.html', 'utf8', (err, data) => {
if (err) {
context.err(err)
return
}
});
return {
status: 200,
headers: {
'Content-Type': 'text/html'
},
body: content,
};
}
});
更新 index/function.json
並複製下列 JSON 程式代碼。
{
"bindings": [
{
"authLevel": "anonymous",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": ["get", "post"]
},
{
"type": "http",
"direction": "out",
"name": "res"
}
]
}
更新 index/index.js
並複製下列程序代碼。
var fs = require("fs");
var path = require("path");
module.exports = function (context, req) {
var index =
context.executionContext.functionDirectory + "/../index.html";
context.log("index.html path: " + index);
fs.readFile(index, "utf8", function (err, data) {
if (err) {
console.log(err);
context.done(err);
}
context.res = {
status: 200,
headers: {
"Content-Type": "text/html",
},
body: data,
};
context.done();
});
};
- 以下列程式代碼更新
index.cs
和取代 Run
函式。
[FunctionName("index")]
public static IActionResult Run([HttpTrigger(AuthorizationLevel.Anonymous)] HttpRequest req, ExecutionContext context, ILogger log)
{
var indexFile = Path.Combine(context.FunctionAppDirectory, "index.html");
log.LogInformation($"index.html path: {indexFile}.");
return new ContentResult
{
Content = File.ReadAllText(indexFile),
ContentType = "text/html",
};
}
以下列程式代碼更新 index.cs
和取代 Run
函式。
[Function("index")]
public HttpResponseData Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req, FunctionContext context)
{
var path = Path.Combine(context.FunctionDefinition.PathToAssembly, "../index.html");
_logger.LogInformation($"index.html path: {path}.");
var response = req.CreateResponse();
response.WriteString(File.ReadAllText(path));
response.Headers.Add("Content-Type", "text/html");
return response;
}
更新 index/function.json
並複製下列 JSON 程式代碼。
{
"bindings": [
{
"authLevel": "anonymous",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": ["get", "post"]
},
{
"type": "http",
"direction": "out",
"name": "$return"
}
]
}
以下列程式代碼更新 __init__.py
和取代 main
函式。
import os
import azure.functions as func
def main(req: func.HttpRequest) -> func.HttpResponse:
f = open(os.path.dirname(os.path.realpath(__file__)) + "/../index.html")
return func.HttpResponse(f.read(), mimetype="text/html")
建立函 negotiate
式,以協助用戶端使用存取令牌取得服務連線 URL。
func new -n negotiate -t HttpTrigger
注意
在此範例中,我們使用 Microsoft Entra ID 使用者身分識別標頭 x-ms-client-principal-name
來擷取 userId
。 這不適用於本機函式。 您可以將它設為空白或變更為在本機播放時取得或產生 userId
的其他方式。 例如,讓用戶端輸入用戶名稱,並在查詢中傳遞它,例如 ?user={$username}
呼叫 negotiate
函式以取得服務連線 URL。 在函式中negotiate
,使用 值 {query.user}
進行設定userId
。
- 更新
src/functions/negotiate
並複製下列程序代碼。
const { app, input } = require('@azure/functions');
const connection = input.generic({
type: 'webPubSubConnection',
name: 'connection',
userId: '{headers.x-ms-client-principal-name}',
hub: 'simplechat'
});
app.http('negotiate', {
methods: ['GET', 'POST'],
authLevel: 'anonymous',
extraInputs: [connection],
handler: async (request, context) => {
return { body: JSON.stringify(context.extraInputs.get('connection')) };
},
});
- 更新
negotiate/function.json
並複製下列 JSON 程式代碼。
{
"bindings": [
{
"authLevel": "anonymous",
"type": "httpTrigger",
"direction": "in",
"name": "req"
},
{
"type": "http",
"direction": "out",
"name": "res"
},
{
"type": "webPubSubConnection",
"name": "connection",
"hub": "simplechat",
"userId": "{headers.x-ms-client-principal-name}",
"direction": "in"
}
]
}
- 更新
negotiate/index.js
並複製下列程序代碼。
module.exports = function (context, req, connection) {
context.res = { body: connection };
context.done();
};
- 以下列程式代碼更新
negotiate.cs
和取代 Run
函式。
[FunctionName("negotiate")]
public static WebPubSubConnection Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
[WebPubSubConnection(Hub = "simplechat", UserId = "{headers.x-ms-client-principal-name}")] WebPubSubConnection connection,
ILogger log)
{
log.LogInformation("Connecting...");
return connection;
}
- 在標頭中新增
using
語句,以解析必要的相依性。
using Microsoft.Azure.WebJobs.Extensions.WebPubSub;
- 以下列程式代碼更新
negotiate.cs
和取代 Run
函式。
[Function("negotiate")]
public HttpResponseData Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req,
[WebPubSubConnectionInput(Hub = "simplechat", UserId = "{headers.x-ms-client-principal-name}")] WebPubSubConnection connectionInfo)
{
var response = req.CreateResponse(HttpStatusCode.OK);
response.WriteAsJsonAsync(connectionInfo);
return response;
}
更新 negotiate/function.json
並複製下列 JSON 程式代碼。
{
"bindings": [
{
"authLevel": "anonymous",
"type": "httpTrigger",
"direction": "in",
"name": "req"
},
{
"type": "http",
"direction": "out",
"name": "$return"
},
{
"type": "webPubSubConnection",
"name": "connection",
"hub": "simplechat",
"userId": "{headers.x-ms-client-principal-name}",
"direction": "in"
}
]
}
更新 negotiate/__init__.py
並複製下列程序代碼。
import azure.functions as func
def main(req: func.HttpRequest, connection) -> func.HttpResponse:
return func.HttpResponse(connection)
建立函 message
式以透過服務廣播用戶端訊息。
func new -n message -t HttpTrigger
- 更新
src/functions/message.js
並複製下列程序代碼。
const { app, output, trigger } = require('@azure/functions');
const wpsMsg = output.generic({
type: 'webPubSub',
name: 'actions',
hub: 'simplechat',
});
const wpsTrigger = trigger.generic({
type: 'webPubSubTrigger',
name: 'request',
hub: 'simplechat',
eventName: 'message',
eventType: 'user'
});
app.generic('message', {
trigger: wpsTrigger,
extraOutputs: [wpsMsg],
handler: async (request, context) => {
context.extraOutputs.set(wpsMsg, [{
"actionName": "sendToAll",
"data": `[${context.triggerMetadata.connectionContext.userId}] ${request.data}`,
"dataType": request.dataType
}]);
return {
data: "[SYSTEM] ack.",
dataType: "text",
};
}
});
- 更新
message/function.json
並複製下列 JSON 程式代碼。
{
"bindings": [
{
"type": "webPubSubTrigger",
"direction": "in",
"name": "data",
"hub": "simplechat",
"eventName": "message",
"eventType": "user"
},
{
"type": "webPubSub",
"name": "actions",
"hub": "simplechat",
"direction": "out"
}
]
}
- 更新
message/index.js
並複製下列程序代碼。
module.exports = async function (context, data) {
context.bindings.actions = {
actionName: "sendToAll",
data: `[${context.bindingData.request.connectionContext.userId}] ${data}`,
dataType: context.bindingData.dataType,
};
// UserEventResponse directly return to caller
var response = {
data: "[SYSTEM] ack.",
dataType: "text",
};
return response;
};
- 以下列程式代碼更新
message.cs
和取代 Run
函式。
[FunctionName("message")]
public static async Task<UserEventResponse> Run(
[WebPubSubTrigger("simplechat", WebPubSubEventType.User, "message")] UserEventRequest request,
BinaryData data,
WebPubSubDataType dataType,
[WebPubSub(Hub = "simplechat")] IAsyncCollector<WebPubSubAction> actions)
{
await actions.AddAsync(WebPubSubAction.CreateSendToAllAction(
BinaryData.FromString($"[{request.ConnectionContext.UserId}] {data.ToString()}"),
dataType));
return new UserEventResponse
{
Data = BinaryData.FromString("[SYSTEM] ack"),
DataType = WebPubSubDataType.Text
};
}
- 在標頭中新增
using
語句,以解析必要的相依性。
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs.Extensions.WebPubSub;
using Microsoft.Azure.WebPubSub.Common;
- 以下列程式代碼更新
message.cs
和取代 Run
函式。
[Function("message")]
[WebPubSubOutput(Hub = "simplechat")]
public SendToAllAction Run(
[WebPubSubTrigger("simplechat", WebPubSubEventType.User, "message")] UserEventRequest request)
{
return new SendToAllAction
{
Data = BinaryData.FromString($"[{request.ConnectionContext.UserId}] {request.Data.ToString()}"),
DataType = request.DataType
};
}
- 更新
message/function.json
並複製下列 JSON 程式代碼。
{
"bindings": [
{
"type": "webPubSubTrigger",
"direction": "in",
"name": "request",
"hub": "simplechat",
"eventName": "message",
"eventType": "user"
},
{
"type": "webPubSub",
"name": "actions",
"hub": "simplechat",
"direction": "out"
}
]
}
- 更新
message/__init__.py
並複製下列程序代碼。
import json
import azure.functions as func
def main(request, actions: func.Out[str]) -> None:
req_json = json.loads(request)
actions.set(
json.dumps(
{
"actionName": "sendToAll",
"data": f'[{req_json["connectionContext"]["userId"]}] {req_json["data"]}',
"dataType": req_json["dataType"],
}
)
)
在專案根資料夾中新增用戶端單一頁面 index.html
,並複製內容。
<html>
<body>
<h1>Azure Web PubSub Serverless Chat App</h1>
<div id="login"></div>
<p></p>
<input id="message" placeholder="Type to chat..." />
<div id="messages"></div>
<script>
(async function () {
let authenticated = window.location.href.includes(
"?authenticated=true"
);
if (!authenticated) {
// auth
let login = document.querySelector("#login");
let link = document.createElement("a");
link.href = `${window.location.origin}/.auth/login/aad?post_login_redirect_url=/api/index?authenticated=true`;
link.text = "login";
login.appendChild(link);
} else {
// negotiate
let messages = document.querySelector("#messages");
let res = await fetch(`${window.location.origin}/api/negotiate`, {
credentials: "include",
});
let url = await res.json();
// connect
let ws = new WebSocket(url.url);
ws.onopen = () => console.log("connected");
ws.onmessage = (event) => {
let m = document.createElement("p");
m.innerText = event.data;
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>
因為 C# 專案會將檔案編譯至不同的輸出資料夾,所以您必須更新, *.csproj
讓內容頁面隨它一起執行。
<ItemGroup>
<None Update="index.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
因為 C# 專案會將檔案編譯至不同的輸出資料夾,所以您必須更新, *.csproj
讓內容頁面隨它一起執行。
<ItemGroup>
<None Update="index.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
建立及部署 Azure 函式應用程式
若要將函式程式碼部署至 Azure,您必須先建立三個資源:
- 資源群組,這是相關資源的邏輯容器。
- 記憶體帳戶,用來維護函式的狀態和其他資訊。
- 函數應用程式,其提供執行函式程式碼的環境。 函式應用程式會對應至本機函式專案,並可讓您將函式分組為邏輯單元,以便更輕鬆地管理、部署和共用資源。
請使用下列命令來建立這些項目。
如果您尚未登入 Azure,請於此時登入:
az login
建立資源群組,或者您可以重複使用其中一個 Azure Web PubSub 服務來略過:
az group create -n WebPubSubFunction -l <REGION>
在您的資源群組和區域中建立一般用途的儲存體帳戶:
az storage account create -n <STORAGE_NAME> -l <REGION> -g WebPubSubFunction
在 Azure 中建立函式應用程式:
az functionapp create --resource-group WebPubSubFunction --consumption-plan-location <REGION> --runtime node --runtime-version 18 --functions-version 4 --name <FUNCIONAPP_NAME> --storage-account <STORAGE_NAME>
az functionapp create --resource-group WebPubSubFunction --consumption-plan-location <REGION> --runtime node --runtime-version 18 --functions-version 4 --name <FUNCIONAPP_NAME> --storage-account <STORAGE_NAME>
az functionapp create --resource-group WebPubSubFunction --consumption-plan-location <REGION> --runtime dotnet --functions-version 4 --name <FUNCIONAPP_NAME> --storage-account <STORAGE_NAME>
az functionapp create --resource-group WebPubSubFunction --consumption-plan-location <REGION> --runtime dotnet-isolated --functions-version 4 --name <FUNCIONAPP_NAME> --storage-account <STORAGE_NAME>
az functionapp create --resource-group WebPubSubFunction --consumption-plan-location <REGION> --runtime python --runtime-version 3.9 --functions-version 4 --name <FUNCIONAPP_NAME> --os-type linux --storage-account <STORAGE_NAME>
將函式專案部署至 Azure:
在 Azure 中成功建立函式應用程式之後,您現在已準備好使用 func azure functionapp publish 命令來部署本機函式專案。
func azure functionapp publish <FUNCIONAPP_NAME>
WebPubSubConnectionString
設定函式應用程式的 :
首先,從 Azure 入口網站尋找您的 Web PubSub 資源,並在 [金鑰] 底下複製 連接字串。 然後,流覽至 Azure 入口網站 ->設定 ->Configuration 中的函式應用程式設定。 然後在 [應用程式設定] 底下新增新專案,名稱等於 WebPubSubConnectionString
且值為您的 Web PubSub 資源 連接字串。
在此範例中,我們使用 WebPubSubTrigger
來接聽服務上游要求。 因此,Web PubSub 必須知道函式的端點資訊,才能傳送目標用戶端要求。 Azure 函式應用程式需要系統密鑰,以取得擴充功能特定 Webhook 方法的安全性。 在上一個步驟中,我們在使用函式部署函式應用程式 message
之後,就能取得系統密鑰。
移至 Azure 入口網站 - 尋找您的函式應用程式資源 -應用程式金鑰 ->系統金鑰 -。webpubsub_extension
>>> 複製值複製為 <APP_KEY>
。
在 Azure Web PubSub 服務中設定 Event Handler
。 移至 Azure 入口網站 -> 尋找您的 Web PubSub 資源 ->設定。 將新的中樞設定對應新增至使用中的一個函式。 <FUNCTIONAPP_NAME>
將與 <APP_KEY>
取代您的 。
- 中樞名稱:
simplechat
- URL 範本: https://< FUNCTIONAPP_NAME.azurewebsites.net/runtime/webhooks/webpubsub?code>=<APP_KEY>
- 使用者事件模式: *
- 系統事件:-(不需要在此範例中設定)
移至 Azure 入口網站 -> 尋找您的函式應用程式資源 ->Authentication。 按一下 Add identity provider
。 將 App Service 驗證設定 設為 [允許未經驗證的存取],以便匿名使用者瀏覽您的用戶端索引頁面,再重新導向至驗證。 然後選取存檔。
在這裡,我們選擇Microsoft
做為識別提供者,其會在 x-ms-client-principal-name
函式中使用 negotiate
。userId
此外,您可以在鏈接之後設定其他識別提供者,並別忘了據此更新 userId
函式中的 negotiate
值。
試用應用程式
現在,您可以從函式應用程式測試頁面: https://<FUNCTIONAPP_NAME>.azurewebsites.net/api/index
。 請參閱快照集。
- 按兩下
login
即可自行驗證。
- 在要聊天的輸入方塊中輸入訊息。
在訊息函式中,我們會將呼叫端的訊息廣播給所有用戶端,並使用訊息 [SYSTEM] ack
傳回呼叫端。 因此,我們可以在範例聊天快照中知道,前四則訊息來自目前的客戶端,最後兩則訊息來自另一個用戶端。
清除資源
如果您不打算繼續使用此應用程式,請使用下列步驟刪除此檔所建立的所有資源,因此不會產生任何費用:
在 Azure 入口網站中選取最左側的 [資源群組],然後選取您所建立的資源群組。 您可以使用搜尋方塊,改為依其名稱尋找資源群組。
在開啟的視窗中,選取資源群組,然後選取 [ 刪除資源群組]。
在新視窗中,輸入要刪除的資源群組名稱,然後選取 [ 刪除]。
下一步
在本快速入門中,您已瞭解如何執行無伺服器聊天應用程式。 現在,您可以開始建置自己的應用程式。