你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn

教程:在无服务器模式下使用 Azure 函数构建聊天应用(预览版)

本教程介绍如何在无服务器模式下为 Socket.IO 服务创建 Web PubSub,并构建一个与 Azure 函数集成的聊天应用。

查找本教程中使用的完整代码示例:

重要说明

默认模式需要使用持久性服务器,因此无法在默认模式下将适用于 Socket.IO 的 Web PubSub 与 Azure 函数集成。

先决条件

在无服务器模式下为 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 函数项目。

  1. 按照步骤安装最新的 Azure Function Core 工具

  2. 在终端窗口中或通过命令提示符,运行以下命令在 SocketIOProject 文件夹中创建项目:

    func init SocketIOProject --worker-runtime javascript --model V4
    

    此命令创建 JavaScript 项目。 然后输入文件夹 SocketIOProject,运行以下命令。

  3. 目前,函数捆绑包不包括 Socket.IO 函数绑定,因此需要手动添加该包。

    1. 要消除函数捆绑包引用,请编辑 host.json 文件并删除以下几行。

      "extensionBundle": {
          "id": "Microsoft.Azure.Functions.ExtensionBundle",
          "version": "[4.*, 5.0.0)"
      }
      
    2. 运行以下命令:

      func extensions install -p Microsoft.Azure.WebJobs.Extensions.WebPubSubForSocketIO -v 1.0.0-beta.4
      
  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 绑定生成的协商结果。

  5. 创建用于处理消息的函数。

    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 绑定将消息广播到命名空间。

  6. 创建一个函数,以便返回供访问的索引 html。

    1. src/ 下创建文件夹 public

    2. 创建一个包含以下内容的 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>
      
    3. 若要返回 HTML 页面,请创建一个函数并复制代码:

      func new --template "Http Trigger" --name index
      
    4. 打开文件 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 存储服务。 如果继续使用它,可能会产生费用。
  1. 安装 Azurite
npm install -g azurite
  1. 启动 Azurite 存储模拟器:
azurite -l azurite -d azurite\debug.log
  1. 确保 local.settings.json 中的 AzureWebJobsStorage 设置为 UseDevelopmentStorage=true

为 Socket.IO 设置 Web PubSub 的配置

  1. 在函数 APP 中添加连接字符串:
func settings add WebPubSubForSocketIOConnectionString "<connection string>"
  1. 将中心设置添加到 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>

输出包含 primaryConnectionStringsecondaryConnectionString,任选其一。

设置隧道

在无服务器模式下,服务使用 Webhook 来触发函数。 尝试在本地运行应用时,一个关键问题是让服务能够访问你的本地函数终结点。

最简单的方法是使用隧道工具

  1. 安装隧道工具:

    npm install -g @azure/web-pubsub-tunnel-tool
    
  2. 运行隧道

    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 通过基于标识的身份验证在线部署应用: