教程:使用 Azure Functions 和 Azure Web PubSub 服务创建无服务器通知应用
本文内容
借助 Azure Web PubSub 服务,你可以使用 WebSocket 生成实时消息传递 Web 应用程序。 Azure Functions 是一个无服务器平台,可让你在不管理任何基础结构的情况下运行代码。 本教程介绍如何使用 Azure Web PubSub 服务和 Azure Functions,在通知方案下生成具有实时消息传递功能的无服务器应用程序。
在本教程中,你将了解如何:
生成无服务器通知应用
使用 Web PubSub 函数输入和输出绑定
在本地运行示例函数
将函数部署到 Azure 函数应用
先决条件
如果没有 Azure 试用版订阅 ,请在开始前创建 Azure 试用版订阅 。
登录 Azure
使用 Azure 帐户登录到 https://portal.azure.cn/ 的 Azure 门户。
创建 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 服务实例的新资源组的名称。
位置
中国北部 2
选择你附近的区域 。
定价层
免费
可以先免费试用 Azure Web PubSub 服务。 了解有关 Azure Web PubSub 服务定价层 的更多详细信息
单位计数
-
单位计数指定 Web PubSub 服务实例可接受的连接数。 每个单位最多支持 1000 个并发连接。 它只能在标准层中配置。
选择“创建”,开始部署 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
的 extensionBundle 到版本 4.* 或更高版本,以获取 Web PubSub 支持 。 若要更新 host.json
,请在编辑器中打开该文件,然后将现有的 extensionBundle 版本替换为 4.* 或更高版本。
{
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle",
"version": "[4.*, 5.0.0)"
}
}
确认或更新 host.json
的 extensionBundle 到版本 3.3.0 或更高版本,以获取 Web PubSub 支持 。 若要更新 host.json
,请在编辑器中打开该文件,然后将现有的 extensionBundle 替换为 3.3.0 或更高版本。
{
"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
更新 host.json
的 extensionBundle 到版本 3.3.0 或更高版本,以获取 Web PubSub 支持。 若要更新 host.json
,请在编辑器中打开该文件,然后将现有的 extensionBundle 替换为 3.3.0 或更高版本。
{
"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 代码。
{
"scriptFile": "__init__.py",
"bindings": [
{
"authLevel": "anonymous",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": [
"get",
"post"
]
},
{
"type": "http",
"direction": "out",
"name": "$return"
}
]
}
更新 index/__init__.py
并复制以下代码。
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
更新 src/functions/negotiate.js
并复制以下代码。
const { app, input } = require('@azure/functions');
const connection = input.generic({
type: 'webPubSubConnection',
name: 'connection',
hub: 'notification'
});
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": "notification",
"direction": "in"
}
]
}
创建文件夹 negotiate,然后更新 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 = "notification")] 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 = "notification")] WebPubSubConnection connectionInfo)
{
var response = req.CreateResponse(HttpStatusCode.OK);
response.WriteAsJsonAsync(connectionInfo);
return response;
}
创建文件夹 negotiate,然后更新 negotiate/function.json
并复制以下 json 代码。
{
"scriptFile": "__init__.py",
"bindings": [
{
"authLevel": "anonymous",
"type": "httpTrigger",
"direction": "in",
"name": "req"
},
{
"type": "http",
"direction": "out",
"name": "$return"
},
{
"type": "webPubSubConnection",
"name": "connection",
"hub": "notification",
"direction": "in"
}
]
}
更新 negotiate/__init__.py
并复制以下代码。
import logging
import azure.functions as func
def main(req: func.HttpRequest, connection) -> func.HttpResponse:
return func.HttpResponse(connection)
创建 notification
函数,以使用 TimerTrigger
生成通知。
func new -n notification -t TimerTrigger
更新 src/functions/notification.js
并复制以下代码。
const { app, output } = require('@azure/functions');
const wpsAction = output.generic({
type: 'webPubSub',
name: 'action',
hub: 'notification'
});
app.timer('notification', {
schedule: "*/10 * * * * *",
extraOutputs: [wpsAction],
handler: (myTimer, context) => {
context.extraOutputs.set(wpsAction, {
actionName: 'sendToAll',
data: `[DateTime: ${new Date()}] Temperature: ${getValue(22, 1)}\xB0C, Humidity: ${getValue(40, 2)}%`,
dataType: 'text',
});
},
});
function getValue(baseNum, floatNum) {
return (baseNum + 2 * floatNum * (Math.random() - 0.5)).toFixed(3);
}
更新 notification/function.json
并复制以下 json 代码。
{
"bindings": [
{
"name": "myTimer",
"type": "timerTrigger",
"direction": "in",
"schedule": "*/10 * * * * *"
},
{
"type": "webPubSub",
"name": "actions",
"hub": "notification",
"direction": "out"
}
]
}
更新 notification/index.js
并复制以下代码。
module.exports = function (context, myTimer) {
context.bindings.actions = {
"actionName": "sendToAll",
"data": `[DateTime: ${new Date()}] Temperature: ${getValue(22, 1)}\xB0C, Humidity: ${getValue(40, 2)}%`,
"dataType": "text"
}
context.done();
};
function getValue(baseNum, floatNum) {
return (baseNum + 2 * floatNum * (Math.random() - 0.5)).toFixed(3);
}
更新 notification.cs
并将 Run
函数替换为以下代码。
[FunctionName("notification")]
public static async Task Run([TimerTrigger("*/10 * * * * *")]TimerInfo myTimer, ILogger log,
[WebPubSub(Hub = "notification")] IAsyncCollector<WebPubSubAction> actions)
{
await actions.AddAsync(new SendToAllAction
{
Data = BinaryData.FromString($"[DateTime: {DateTime.Now}] Temperature: {GetValue(23, 1)}{'\xB0'}C, Humidity: {GetValue(40, 2)}%"),
DataType = WebPubSubDataType.Text
});
}
private static string GetValue(double baseNum, double floatNum)
{
var rng = new Random();
var value = baseNum + floatNum * 2 * (rng.NextDouble() - 0.5);
return value.ToString("0.000");
}
在标头中添加 using
语句来解析所需的依赖项。
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs.Extensions.WebPubSub;
using Microsoft.Azure.WebPubSub.Common;
更新 notification.cs
并将 Run
函数替换为以下代码。
[Function("notification")]
[WebPubSubOutput(Hub = "notification")]
public SendToAllAction Run([TimerTrigger("*/10 * * * * *")] MyInfo myTimer)
{
return new SendToAllAction
{
Data = BinaryData.FromString($"[DateTime: {DateTime.Now}] Temperature: {GetValue(23, 1)}{'\xB0'}C, Humidity: {GetValue(40, 2)}%"),
DataType = WebPubSubDataType.Text
};
}
private static string GetValue(double baseNum, double floatNum)
{
var rng = new Random();
var value = baseNum + floatNum * 2 * (rng.NextDouble() - 0.5);
return value.ToString("0.000");
}
创建文件夹 notification,然后更新 notification/function.json
并复制以下 json 代码。
{
"scriptFile": "__init__.py",
"bindings": [
{
"name": "myTimer",
"type": "timerTrigger",
"direction": "in",
"schedule": "*/10 * * * * *"
},
{
"type": "webPubSub",
"name": "actions",
"hub": "notification",
"direction": "out"
}
]
}
更新 notification/__init__.py
并复制以下代码。
import datetime
import random
import json
import azure.functions as func
def main(myTimer: func.TimerRequest, actions: func.Out[str]) -> None:
time = datetime.datetime.now().strftime("%A %d-%b-%Y %H:%M:%S")
actions.set(json.dumps({
'actionName': 'sendToAll',
'data': '\x5B DateTime: {0} \x5D Temperature: {1:.3f} \xB0C, Humidity: {2:.3f} \x25'.format(time, 22 + 2 * (random.random() - 0.5), 44 + 4 * (random.random() - 0.5)),
'dataType': 'text'
}))
在项目根文件夹中添加客户端单页 index.html
并复制内容。
<html>
<body>
<h1>Azure Web PubSub Notification</h1>
<div id="messages"></div>
<script>
(async function () {
let messages = document.querySelector('#messages');
let res = await fetch(`${window.location.origin}/api/negotiate`);
let url = await res.json();
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);
};
})();
</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 门户”,确认已成功创建前面部署的 Web PubSub 服务实例。 导航到该实例。
选择“密钥”并复制连接字符串。
在函数文件夹中运行命令设置来服务连接字符串。 根据需要将 <connection-string>
替换为你的值。
func settings add WebPubSubConnectionString "<connection-string>"
注意
示例中使用的 TimerTrigger
依赖于 Azure 存储,但是在本地运行 Azure 函数应用时,可以使用本地存储模拟器。 如果收到一些类似 There was an error performing a read operation on the Blob Storage Secret Repository. Please ensure the 'AzureWebJobsStorage' connection string is valid.
的错误,则需要下载并启用存储模拟器 。
现在,可通过命令运行本地函数。
func start --port 7071
若要检查正在运行的日志,可以通过访问 http://localhost:7071/api/index
来访问本地主机静态页面。
注意
某些浏览器会自动重定向到 https
,这会导致 URL 错误。 如果未能成功呈现内容,建议使用 Edge
并仔细检查 URL。
将函数应用部署到 Azure
在将函数代码部署到 Azure 之前,需要创建三个资源:
一个资源组:相关资源的逻辑容器。
一个存储帐户:用于维护有关函数的状态和其他信息。
一个函数应用:提供用于执行函数代码的环境。 函数应用映射到本地函数项目,并允许你将函数分组为一个逻辑单元,以便更轻松地管理、部署和共享资源。
使用以下命令创建这些项。
登录 Azure:
az cloud set -n AzureChinaCloud
az login
# az cloud set -n AzureCloud //means return to Public Azure.
创建资源组,或者可重用某个 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> --publish-local-settings
注意
在这里,我们将本地设置 local.settings.json
与命令参数 --publish-local-settings
一起部署。 如果你使用的是 Azure 存储仿真器,可以在出现以下提示消息后键入 no
,跳过在 Azure 上覆盖此值的步骤:App setting AzureWebJobsStorage is different between azure and local.settings.json, Would you like to overwrite value in azure? [yes/no/show]
。 除此之外,你还可以在“Azure 门户”->“设置”->“配置”中更新函数应用设置。
现在,你可以通过导航到 URL (https://<FUNCIONAPP_NAME>.chinacloudsites.cn/api/index
),从 Azure 函数应用检查你的站点。
清理资源
如果不打算继续使用此应用,请按照以下步骤删除本文档中创建的所有资源,以免产生任何费用:
在 Azure 门户的最左侧选择“资源组”,,然后选择创建的资源组。 改用搜索框按名称查找资源组。
在打开的窗口中选择资源组,然后选择“删除资源组”。
在新窗口中键入要删除的资源组的名称,然后选择“删除”。
后续步骤
本快速入门介绍了如何运行无服务器聊天应用程序。 现在,可以开始构建自己的应用程序。