你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn。
本教程介绍如何在无服务器模式下为 Socket.IO 服务创建 Web PubSub,并构建一个与 Azure 函数集成的聊天应用。
查找本教程中使用的完整代码示例:
重要说明
默认模式需要使用持久性服务器,因此无法在默认模式下将适用于 Socket.IO 的 Web PubSub 与 Azure 函数集成。
先决条件
- 具有活动订阅的 Azure 帐户。 如果没有帐户,可以创建一个免费帐户。
- Azure Function Core 工具
- 对 Socket.IO 库有一定的了解。
在无服务器模式下为 Socket.IO 资源创建 Web PubSub
若要为 Socket.IO 创建 Web PubSub,可以使用以下 Azure CLI 命令:
az webpubsub create -g <resource-group> -n <resource-name>--kind socketio --service-mode serverless --sku Premium_P1
在本地创建 Azure Function 项目
应按照以下步骤启动本地 Azure 函数项目。
按照步骤安装最新的 Azure Function Core 工具
在终端窗口中或通过命令提示符,运行以下命令在
SocketIOProject
文件夹中创建项目:func init SocketIOProject --worker-runtime javascript --model V4
此命令创建 JavaScript 项目。 然后输入文件夹
SocketIOProject
,运行以下命令。目前,函数捆绑包不包括 Socket.IO 函数绑定,因此需要手动添加该包。
要消除函数捆绑包引用,请编辑 host.json 文件并删除以下几行。
"extensionBundle": { "id": "Microsoft.Azure.Functions.ExtensionBundle", "version": "[4.*, 5.0.0)" }
运行以下命令:
func extensions install -p Microsoft.Azure.WebJobs.Extensions.WebPubSubForSocketIO -v 1.0.0-beta.4
创建用于协商的函数。 协商函数用于生成端点和令牌,以便客户端访问服务。
func new --template "Http Trigger" --name negotiate
打开
src/functions/negotiate.js
中的文件,并替换为以下代码:const { app, input } = require('@azure/functions'); const socketIONegotiate = input.generic({ type: 'socketionegotiation', direction: 'in', name: 'result', hub: 'hub' }); async function negotiate(request, context) { let result = context.extraInputs.get(socketIONegotiate); return { jsonBody: result }; }; // Negotiation app.http('negotiate', { methods: ['GET', 'POST'], authLevel: 'anonymous', extraInputs: [socketIONegotiate], handler: negotiate });
此步骤会创建一个带有 HTTP 触发器和
SocketIONegotiation
输出绑定的函数negotiate
,这意味着可以使用 HTTP 调用来触发函数,并返回由SocketIONegotiation
绑定生成的协商结果。创建用于处理消息的函数。
func new --template "Http Trigger" --name message
打开文件
src/functions/message.js
,替换为以下代码:const { app, output, trigger } = require('@azure/functions'); const socketio = output.generic({ type: 'socketio', hub: 'hub', }) async function chat(request, context) { context.extraOutputs.set(socketio, { actionName: 'sendToNamespace', namespace: '/', eventName: 'new message', parameters: [ context.triggerMetadata.socketId, context.triggerMetadata.message ], }); } // Trigger for new message app.generic('chat', { trigger: trigger.generic({ type: 'socketiotrigger', hub: 'hub', eventName: 'chat', parameterNames: ['message'], }), extraOutputs: [socketio], handler: chat });
这使用
SocketIOTrigger
由 Socket.IO 客户端消息触发,并使用SocketIO
绑定将消息广播到命名空间。创建一个函数,以便返回供访问的索引 html。
在
src/
下创建文件夹public
。创建一个包含以下内容的 HTML 文件
index.html
。<html> <body> <h1>Socket.IO Serverless Sample</h1> <div id="chatPage" class="chat-container"> <div class="chat-input"> <input type="text" id="chatInput" placeholder="Type your message here..."> <button onclick="sendMessage()">Send</button> </div> <div id="chatMessages" class="chat-messages"></div> </div> <script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script> <script> function appendMessage(message) { const chatMessages = document.getElementById('chatMessages'); const messageElement = document.createElement('div'); messageElement.innerText = message; chatMessages.appendChild(messageElement); hatMessages.scrollTop = chatMessages.scrollHeight; } function sendMessage() { const message = document.getElementById('chatInput').value; if (message) { document.getElementById('chatInput').value = ''; socket.emit('chat', message); } } async function initializeSocket() { const negotiateResponse = await fetch(`/api/negotiate`); if (!negotiateResponse.ok) { console.log("Failed to negotiate, status code =", negotiateResponse.status); return; } const negotiateJson = await negotiateResponse.json(); socket = io(negotiateJson.endpoint, { path: negotiateJson.path, query: { access_token: negotiateJson.token } }); socket.on('new message', (socketId, message) => { appendMessage(`${socketId.substring(0,5)}: ${message}`); }) } initializeSocket(); </script> </body> </html>
若要返回 HTML 页面,请创建一个函数并复制代码:
func new --template "Http Trigger" --name index
打开文件
src/functions/index.js
,替换为以下代码:const { app } = require('@azure/functions'); const fs = require('fs').promises; const path = require('path') async function index(request, context) { try { context.log(`HTTP function processed request for url "${request.url}"`); const filePath = path.join(__dirname,'../public/index.html'); const html = await fs.readFile(filePath); return { body: html, headers: { 'Content-Type': 'text/html' } }; } catch (error) { context.log(error); return { status: 500, jsonBody: error } } }; app.http('index', { methods: ['GET', 'POST'], authLevel: 'anonymous', handler: index });
如何在本地运行应用
准备好代码后,按照说明运行示例。
为 Azure Function 设置 Azure 存储
Azure Functions 需要一个存储帐户才能运行,即使是在本地运行。 选择以下两个选项之一:
- 运行免费的 Azurite 模拟器。
- 使用 Azure 存储服务。 如果继续使用它,可能会产生费用。
- 安装 Azurite
npm install -g azurite
- 启动 Azurite 存储模拟器:
azurite -l azurite -d azurite\debug.log
- 确保 local.settings.json 中的
AzureWebJobsStorage
设置为UseDevelopmentStorage=true
。
为 Socket.IO 设置 Web PubSub 的配置
- 在函数 APP 中添加连接字符串:
func settings add WebPubSubForSocketIOConnectionString "<connection string>"
- 将中心设置添加到 Web PubSub for Socket.IO
az webpubsub hub create -n <resource name> -g <resource group> --hub-name hub --event-handler url-template="tunnel:///runtime/webhooks/socketio" user-event-pattern="*"
连接字符串可通过 Azure CLI 命令来获取
az webpubsub key show -g <resource group> -n <resource name>
输出包含 primaryConnectionString
和 secondaryConnectionString
,任选其一。
设置隧道
在无服务器模式下,服务使用 Webhook 来触发函数。 尝试在本地运行应用时,一个关键问题是让服务能够访问你的本地函数终结点。
最简单的方法是使用隧道工具
安装隧道工具:
npm install -g @azure/web-pubsub-tunnel-tool
运行隧道
awps-tunnel run --hub hub --connection "<connection string>" --upstream http://127.0.0.1:7071
--upstream
是本地 Azure 函数公开的 URL。 端口可能有所不同,可以在下一步启动函数时检查输出。
运行示例应用
运行隧道工具后,可以在本地运行函数应用:
func start
并于 http://localhost:7071/api/index
访问网页。
后续步骤
接下来,可以尝试使用 Bicep 通过基于标识的身份验证在线部署应用: