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

使用 Azure SignalR 服务进行 Azure Functions 开发和配置

Azure Functions 应用程序可以利用 Azure SignalR 服务绑定来添加实时功能。 客户端应用程序使用以多种语言编写的客户端 SDK 连接到 Azure SignalR 服务和接收实时消息。

本文介绍有关开发和配置与 SignalR 服务集成的 Azure 函数应用的概念。

重要

本文中出现的原始连接字符串仅用于演示目的。

连接字符串包括应用程序访问 Azure Web PubSub 服务所需的授权信息。 连接字符串中的访问密钥类似于服务的根密码。 在生产环境中,请始终保护访问密钥。 使用 Azure Key Vault 安全地管理和轮换密钥,使用 Microsoft Entra ID 保护连接字符串,并使用 Microsoft Entra ID 授权访问

避免将访问密钥分发给其他用户、对其进行硬编码或将其以纯文本形式保存在其他人可以访问的任何位置。 如果你认为访问密钥可能已泄露,请轮换密钥。

SignalR 服务配置

可以用不同的模式配置 Azure SignalR 服务。 与 Azure Functions 一起使用时,必须以“无服务器”模式配置服务。

在 Azure 门户中,找到 SignalR 服务资源的“设置”页。 将“服务模式”设置为“无服务器”。

SignalR 服务模式

Azure Functions 开发

使用 Azure Functions 和 Azure SignalR 服务构建的无服务器实时应用程序通常需要两个 Azure 函数:

  • 客户端调用negotiate 函数来获取有效的 SignalR 服务访问令牌和服务终结点 URL。
  • 处理从 SignalR 服务发送到客户端的消息的一个或多个函数。

协商函数

客户端应用程序需要获取有效的访问令牌才能连接到 Azure SignalR 服务。 访问令牌可以是匿名的,也可以是经过身份验证的用户 ID。 无服务器 SignalR 服务应用程序需要使用名为 negotiate 的 HTTP 终结点来获取令牌和其他连接信息,例如 SignalR 服务终结点 URL。

使用 HTTP 触发的 Azure 函数和 SignalRConnectionInfo 输入绑定生成连接信息对象。 该函数必须包含以 /negotiate 结尾的 HTTP 路由。

使用 C# 中基于类的模型,无需执行 SignalRConnectionInfo 输入绑定,可以更轻松地添加自定义声明。 有关详细信息,请参阅基于类的模型中的协商体验

有关 negotiate 函数的详细信息,请参阅 Azure Functions 开发

若要了解如何创建经过身份验证的令牌,请参阅使用 Azure 应用服务身份验证

处理从 SignalR 服务发送的消息

使用 SignalRTrigger 绑定来处理从 SignalR 服务发送的消息。 可在客户端发送消息或客户端连接或断开连接时收到通知。

有关详细信息,请参阅 SignalR 服务触发器绑定参考

此外,需要将函数终结点配置为上游终结点,让服务在收到来自客户端的消息时触发函数。 有关如何配置上游的详细信息,请参阅上游终结点

注意

SignalR 服务不支持无服务器模式下客户端发出的 StreamInvocation 消息。

发送消息和管理组成员身份

使用 SignalR 输出绑定将消息发送到与 Azure SignalR 服务连接的客户端。 你可以将消息广播到所有客户端,也可以将消息发送到客户端子集。 例如,仅向使用特定用户 ID 进行身份验证的客户端或仅向特定组发送消息。

可将用户添加到一个或多个组。 还可以使用 SignalR 输出绑定在组中添加或删除用户。

有关详细信息,请参阅 SignalR 输出绑定参考

SignalR 中心

SignalR 具有“中心”的概念。 每个客户端连接以及从 Azure Functions 发送的每个消息的范围限定为特定的中心。 可以使用中心将连接和消息划分到逻辑命名空间。

基于类的模型

基于类的模型专用于 C#。

基于类的模型提供了更好的编程体验,可以替代 SignalR 输入和输出绑定,具有以下特点:

  • 更灵活的协商、发送消息和管理组体验。
  • 支持更多管理功能,包括关闭连接、检查连接、用户或组是否存在。
  • 强类型中心
  • 支持在同一处设置中心名称和连接字符串。

以下代码演示了如何在基于类的模型中编写 SignalR 绑定:

首先,定义派生自 ServerlessHub 类的中心:

[SignalRConnection("AzureSignalRConnectionString")]
public class Functions : ServerlessHub
{
    private const string HubName = nameof(Functions); // Used by SignalR trigger only

    public Functions(IServiceProvider serviceProvider) : base(serviceProvider)
    {
    }

    [Function("negotiate")]
    public async Task<HttpResponseData> Negotiate([HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequestData req)
    {
        var negotiateResponse = await NegotiateAsync(new() { UserId = req.Headers.GetValues("userId").FirstOrDefault() });
        var response = req.CreateResponse();
        response.WriteBytes(negotiateResponse.ToArray());
        return response;
    }

    [Function("Broadcast")]
    public Task Broadcast(
    [SignalRTrigger(HubName, "messages", "broadcast", "message")] SignalRInvocationContext invocationContext, string message)
    {
        return Clients.All.SendAsync("newMessage", new NewMessage(invocationContext, message));
    }

    [Function("JoinGroup")]
    public Task JoinGroup([SignalRTrigger(HubName, "messages", "JoinGroup", "connectionId", "groupName")] SignalRInvocationContext invocationContext, string connectionId, string groupName)
    {
        return Groups.AddToGroupAsync(connectionId, groupName);
    }
}

Program.cs 文件中,注册无服务器中心:

var host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults(b => b.Services
        .AddServerlessHub<Functions>())
    .Build();

基于类的模型中的协商体验

与使用 SignalR 输入绑定 [SignalRConnectionInfoInput] 相比,基于类的模型中的协商更为灵活。 基类 ServerlessHub 具备 NegotiateAsync 方法,允许用户自定义协商选项,例如 userIdclaims 等。

Task<BinaryData> NegotiateAsync(NegotiationOptions? options = null)

在基于类的模型中发送消息和管理体验

可以通过访问基类 ServerlessHub 提供的成员来发送消息、管理组或管理客户端。

  • ServerlessHub.Clients 用于向客户端发送消息。
  • ServerlessHub.Groups 用于管理与组的连接,例如向组添加连接、从组中删除连接。
  • ServerlessHub.UserGroups 用于通过组管理用户,例如将用户添加到组、从组中删除用户。
  • ServerlessHub.ClientManager 用于检查连接是否存在、关闭连接等操作。

强类型中心

强类型中心可支持你在向客户端发送消息时使用强类型方法。 若要在基于类的模型中使用强类型中心,请将客户端方法提取到接口 T 中,并通过 ServerlessHub<T> 派生中心类。

以下代码是客户端方法的接口示例。

public interface IChatClient
{
    Task newMessage(NewMessage message);
}

然后,可以使用强类型方法,如下所示。

本文中出现的原始连接字符串仅用于演示目的。 在生产环境中,请始终保护访问密钥。 使用 Azure Key Vault 安全地管理和轮换密钥,使用 Microsoft Entra ID 保护连接字符串,并使用 Microsoft Entra ID 授权访问

[SignalRConnection("AzureSignalRConnectionString")]
public class Functions : ServerlessHub<IChatClient>
{
    private const string HubName = nameof(Functions);  // Used by SignalR trigger only

    public Functions(IServiceProvider serviceProvider) : base(serviceProvider)
    {
    }

    [Function("Broadcast")]
    public Task Broadcast(
    [SignalRTrigger(HubName, "messages", "broadcast", "message")] SignalRInvocationContext invocationContext, string message)
    {
        return Clients.All.newMessage(new NewMessage(invocationContext, message));
    }
}

注意

可以从 GitHub 获取完整的项目示例。

支持在同一处设置中心名称和连接字符串

  • 系统会自动将无服务器中心的类名设为 HubName
  • 你可能已注意到无服务器中心类上使用的 SignalRConnection 属性,如下所示:
    [SignalRConnection("AzureSignalRConnectionString")]
    public class Functions : ServerlessHub<IChatClient>
    
    该属性支持自定义无服务器中心的连接字符串的位置。 如果未指定,则使用默认值 AzureSignalRConnectionString

重要

SignalR 触发器和无服务器中心相互独立。 因此,无服务器中心和 SignalRConnection 属性的类名不会更改 SignalR 触发器的设置,即使已在无服务器中心内使用 SignalR 触发器也是如此。

客户端开发

SignalR 客户端应用程序可利用以多种语言之一编写的 SignalR 客户端 SDK 轻松连接到 Azure SignalR 服务并从中接收消息。

配置客户端连接

若要连接到 SignalR 服务,客户端必须成功完成连接协商,具体包括以下步骤:

  1. 向上述 negotiate HTTP 协商终结点发出请求,以获取有效的连接信息
  2. 使用服务终结点 URL 以及从协商negotiate终结点获取的访问令牌连接到 SignalR 服务

SignalR 客户端 SDK 已包含执行协商握手所需的逻辑。 将协商终结点的 URL(不包括 negotiate 段)传递给 SDK 的 HubConnectionBuilder。 下面是一个 JavaScript 示例:

const connection = new signalR.HubConnectionBuilder()
  .withUrl("https://my-signalr-function-app.azurewebsites.net/api")
  .build();

SDK 根据约定自动将 /negotiate 追加到 URL,然后使用该 URL 开始协商。

注意

如果在浏览器中使用 JavaScript/TypeScript SDK,则需要在函数应用中启用跨源资源共享 (CORS)

有关如何使用 SignalR 客户端 SDK 的详细信息,请参阅适用于所用语言的文档:

将消息从客户端发送到服务

如果为 SignalR 资源配置了上游,则可以使用任何 SignalR 客户端将消息从客户端发送到 Azure Functions。 下面是一个 JavaScript 示例:

connection.send("method1", "arg1", "arg2");

Azure Functions 配置

与 Azure SignalR 服务集成的 Azure 函数应用可以像任何常用 Azure 函数应用一样,使用持续部署zip 部署从包运行等技术进行部署。

但是,对于使用 SignalR 服务绑定的应用,需要注意几个特殊事项。 如果客户端在浏览器中运行,则必须启用 CORS。 如果应用需要身份验证,则你可以将协商终结点与应用服务身份验证集成。

启用 CORS

JavaScript/TypeScript 客户端向协商函数发出 HTTP 请求,以启动连接协商。 如果客户端应用程序不是托管在 Azure 函数应用所在的同一个域中,则必须在函数应用中启用跨源资源共享 (CORS),否则浏览器会阻止请求。

Localhost

在本地计算机上运行函数应用时,可将 Host 节添加到 local.settings.json 以启用 CORS。 在 Host 节中添加两个属性:

  • CORS - 输入作为客户端应用程序来源的基本 URL
  • CORSCredentials - 将其设置为 true 以允许“withCredentials”请求

示例:

{
  "IsEncrypted": false,
  "Values": {
    // values
  },
  "Host": {
    "CORS": "http://localhost:8080",
    "CORSCredentials": true
  }
}

云 - Azure Functions CORS

若要在 Azure 函数应用中启用 CORS,请在 Azure 门户中函数应用的“平台功能”选项卡下,转到 CORS 配置屏幕。

注意

CORS 配置在 Azure Functions Linux 消耗计划中尚不可用。 使用 Azure API Management 启用 CORS。

必须启用支持 Access-Control-Allow-Credentials 的 CORS 才能让 SignalR 客户端调用协商函数。 如要启用 CORS,请选中相应的复选框。

在“允许的源”部分添加一个包含 Web 应用程序源基本 URL 的条目。

配置 CORS

云 - Azure API 管理

Azure API 管理提供一个可向现有后端服务添加功能的 API 网关。 可以使用 API 管理将 CORS 添加到函数应用。 它提供按操作定价并授予每月免费额度的消耗层。

请参阅 API 管理文档来了解如何导入 Azure 函数应用。 导入后,可以添加一个入站策略来启用支持 Access-Control-Allow-Credentials 的 CORS。

<cors allow-credentials="true">
  <allowed-origins>
    <origin>https://azure-samples.github.io</origin>
  </allowed-origins>
  <allowed-methods>
    <method>GET</method>
    <method>POST</method>
  </allowed-methods>
  <allowed-headers>
    <header>*</header>
  </allowed-headers>
  <expose-headers>
    <header>*</header>
  </expose-headers>
</cors>

将 SignalR 客户端配置为使用 API 管理 URL。

使用应用服务身份验证

Azure Functions 具有内置身份验证,支持 Facebook、X、Microsoft 帐户、Google 和 Microsoft Entra ID 等常见提供程序。 此功能可与 SignalRConnectionInfo 绑定集成,以便与已使用用户 ID 进行身份验证的 Azure SignalR 服务建立连接。 应用程序可以使用 SignalR 输出绑定来发送以该用户 ID 为目标的消息。

在 Azure 门户中函数应用的“平台功能”选项卡上,打开“身份验证/授权”设置窗口。 遵循应用服务身份验证文档使用所选的标识提供者配置身份验证。

配置后,经过身份验证的 HTTP 请求会包含 x-ms-client-principal-namex-ms-client-principal-id 标头,而这些标头分别包含经过身份验证的标识的用户名和用户 ID。

可以在 SignalRConnectionInfo 绑定配置中使用这些标头来创建经过身份验证的连接。 下面是一个使用 x-ms-client-principal-id 标头的 C# 协商函数示例。

[FunctionName("negotiate")]
public static SignalRConnectionInfo Negotiate(
    [HttpTrigger(AuthorizationLevel.Anonymous)]HttpRequest req,
    [SignalRConnectionInfo
        (HubName = "chat", UserId = "{headers.x-ms-client-principal-id}")]
        SignalRConnectionInfo connectionInfo)
{
    // connectionInfo contains an access key token with a name identifier claim set to the authenticated user
    return connectionInfo;
}

然后,可以通过设置 SignalR 消息的 UserId 属性向该用户发送消息。

[FunctionName("SendMessage")]
public static Task SendMessage(
    [HttpTrigger(AuthorizationLevel.Anonymous, "post")]object message,
    [SignalR(HubName = "chat")]IAsyncCollector<SignalRMessage> signalRMessages)
{
    return signalRMessages.AddAsync(
        new SignalRMessage
        {
            // the message will only be sent to these user IDs
            UserId = "userId1",
            Target = "newMessage",
            Arguments = new [] { message }
        });
}

有关其他语言的信息,请参阅 Azure Functions 参考文章 Azure SignalR 服务绑定

后续步骤

本文介绍了如何使用 Azure Functions 开发和配置无服务器 SignalR 服务应用程序。 请尝试使用 SignalR 服务概述页上的某篇快速入门或教程自行创建应用程序。