ASP.NET Core BlazorSignalR 指南

注意

此版本不是本文的最新版本。 对于当前版本,请参阅此文的 .NET 8 版本

重要

此信息与预发布产品相关,相应产品在商业发布之前可能会进行重大修改。 Microsoft 对此处提供的信息不提供任何明示或暗示的保证。

对于当前版本,请参阅此文的 .NET 8 版本

本文介绍如何配置和管理 Blazor 应用中的 SignalR 连接。

有关 ASP.NET Core SignalR 配置的常规指南,请参阅文档的 ASP.NET Core SignalR 概述区域中的主题,特别是 ASP.NET Core SignalR 配置

服务器端应用使用 ASP.NET Core SignalR 与浏览器进行通信。 SignalR 的托管和缩放条件适用于服务器端应用。

由于低延迟、可靠性和安全性,使用 WebSocket 作为 SignalR 传输时,Blazor 的效果最佳。 当 WebSocket 不可用时,或在将应用显式配置为使用长轮询时,SignalR 将使用长轮询。

交互式服务器组件的 WebSocket 压缩

默认情况下,交互式服务器组件:

  • WebSocket 连接启用压缩。 ConfigureWebsocketOptions 控制 WebSocket 压缩。

  • 采用设置为 'self'frame-ancestors内容安全策略 (CSP) 指令,该指令仅允许在启用压缩时或提供 WebSocket 上下文的配置时,将应用嵌入为该应用提供服务的来源的 <iframe> 中。 ContentSecurityFrameAncestorPolicy 控制 frame-ancestors CSP。

可以通过将 ConfigureWebSocketOptions 的值设置为 null 来手动移除 frame-ancestors CSP,因为你可能希望以集中的方式配置 CSP。 以集中方式管理 frame-ancestors CSP 时,每当呈现第一个文档时都必须审慎应用策略。 不建议完全移除策略,因为这可能会使应用容易受到攻击。

用法示例:

通过将 ConfigureWebSocketOptions 设为 null 来禁用压缩,这可以减少应用受攻击的风险,但可能会导致性能降低:

builder.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode(o => o.ConfigureWebSocketOptions = null)

启用压缩后,请考虑使用带有值 'none'(需要单引号)的更严格的 frame-ancestors CSP,它允许 WebSocket 压缩,但会阻止浏览器将应用嵌入任何 <iframe>

builder.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode(o => o.ContentSecurityFrameAncestorsPolicy = "'none'")

启用压缩后,通过将 ContentSecurityFrameAncestorsPolicy 设置为 null 来移除 frame-ancestors CSP。 此方案仅适用于以集中方式设置 CSP 的应用:

builder.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode(o => o.ContentSecurityFrameAncestorsPolicy = null)

重要

浏览器使用最严格的策略指令值应用来自多个 CSP 标头的 CSP 指令。 因此,开发人员无法故意或错误地添加比 'self' 更弱的 frame-ancestors 策略。

传递给 ContentSecurityFrameAncestorsPolicy 的字符串值需要单引号:

不支持的值:noneself

支持的值:'none''self'

其他选项包括指定一个或多个主机源和方案源。

有关安全影响,请参阅 ASP.NET Core Blazor 交互式服务器端呈现的威胁缓解指南。 有关 frame-ancestors 指令的详细信息,请参阅 CSP:frame-ancestors(MDN 文档)

禁用热重载的响应压缩

使用热重载时,请在 Development 环境中禁用响应压缩中间件。 无论是否使用项目模板中的默认代码,始终首先在请求处理管道中调用 UseResponseCompression

Program 文件中:

if (!app.Environment.IsDevelopment())
{
    app.UseResponseCompression();
}

用于身份验证的客户端侧 SignalR 跨源协商

本部分介绍如何将 SignalR 的基础客户端配置为发送凭据(如 cookie 或 HTTP 身份验证标头)。

使用 SetBrowserRequestCredentials 在跨源 fetch 请求中设置 Include

IncludeRequestCredentialsMessageHandler.cs

using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.WebAssembly.Http;

public class IncludeRequestCredentialsMessageHandler : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        request.SetBrowserRequestCredentials(BrowserRequestCredentials.Include);
        return base.SendAsync(request, cancellationToken);
    }
}

在构建中心连接的位置,将 HttpMessageHandler 分配给 HttpMessageHandlerFactory 选项:

private HubConnectionBuilder? hubConnection;

...

hubConnection = new HubConnectionBuilder()
    .WithUrl(new Uri(Navigation.ToAbsoluteUri("/chathub")), options =>
    {
        options.HttpMessageHandlerFactory = innerHandler => 
            new IncludeRequestCredentialsMessageHandler { InnerHandler = innerHandler };
    }).Build();

前面的示例将中心连接 URL 配置为位于 /chathub 的绝对 URI 地址。 该 URI 也可以通过字符串来设置,例如 https://signalr.example.com,或者通过配置进行设置。 Navigation 是一个注入的 NavigationManager

有关详细信息,请参阅 ASP.NET Core SignalR 配置

客户端呈现

如果已配置预呈现,则会在建立到服务器的客户端连接之前进行预呈现。 有关详细信息,请参阅 Prerender ASP.NET Core Razor 组件

如果已配置预呈现,则会在建立到服务器的客户端连接之前进行预呈现。 有关详细信息,请参阅以下文章:

预呈现状态大小和 SignalR 消息大小限制

较大的预呈现状态大小可能会超出 SignalR 线路消息大小限制,从而导致以下结果:

  • SignalR 线路初始化失败,客户端上出现错误:Circuit host not initialized.
  • 当线路发生故障时,客户端上显示重新连接对话框。 无法进行恢复。

若要解决此问题,请使用以下任一方法

  • 减少要置于预呈现状态的数据量。
  • 增加 SignalR 消息大小限制。 警告:增加限制可能会增加拒绝服务 (DoS) 攻击风险

其他客户端侧资源

将粘滞会话用于服务器端 webfarm 托管

Blazor 应用预呈现以响应第一个客户端请求,这会在服务器上创建 UI 状态。 客户端尝试创建 SignalR 连接时,“必须重新连接到同一服务器”。 使用多个后端服务器时,应用应为 SignalR 连接实现粘滞会话

说明

未在 webfarm 中启用粘滞会话的应用引发以下错误:

blazor.server.js:1 Uncaught (in promise) 错误:由于基础连接已关闭,调用被取消。

服务器端 Azure SignalR 服务

建议使用 Azure SignalR 服务进行 Microsoft Azure 中托管的服务器端开发。 该服务与应用的 Blazor 中心配合使用,以便将服务器端应用纵向扩展到大量并发 SignalR 连接。 此外,SignalR 服务的全球覆盖和高性能数据中心可帮助显著减少由于地理位置造成的延迟。

可将服务的 ServerStickyMode 选项或配置值设置为 Required,从而为 Azure SignalR 服务启用粘滞会话。 有关详细信息,请参阅托管和部署 ASP.NET Core 服务器端 Blazor 应用

服务器端线路处理程序选项

使用 CircuitOptions 配置线路。 查看引用源中的默认值。

注意

指向 .NET 参考源的文档链接通常会加载存储库的默认分支,该分支表示针对下一个 .NET 版本的当前开发。 若要为特定版本选择标记,请使用“切换分支或标记”下拉列表。 有关详细信息,请参阅如何选择 ASP.NET Core 源代码的版本标记 (dotnet/AspNetCore.Docs #26205)

使用 AddInteractiveServerComponents 的选项委托读取或设置 Program 文件中的选项。 {OPTION} 占位符代表选项,{VALUE} 占位符是值。

Program 文件中:

builder.Services.AddRazorComponents().AddInteractiveServerComponents(options =>
{
    options.{OPTION} = {VALUE};
});

使用 AddServerSideBlazor 的选项委托读取或设置 Program 文件中的选项。 {OPTION} 占位符代表选项,{VALUE} 占位符是值。

Program 文件中:

builder.Services.AddServerSideBlazor(options =>
{
    options.{OPTION} = {VALUE};
});

使用 AddServerSideBlazor 的选项委托读取或设置 Startup.ConfigureServices 中的选项。 {OPTION} 占位符代表选项,{VALUE} 占位符是值。

Startup.csStartup.ConfigureServices 中:

services.AddServerSideBlazor(options =>
{
    options.{OPTION} = {VALUE};
});

若要配置 HubConnectionContext,请结合使用 HubConnectionContextOptionsAddHubOptions。 查看引用源中的中心连接上下文选项的默认值。 有关 SignalR 文档中的选项说明,请参阅 ASP.NET Core SignalR 配置{OPTION} 占位符代表选项,{VALUE} 占位符是值。

注意

指向 .NET 参考源的文档链接通常会加载存储库的默认分支,该分支表示针对下一个 .NET 版本的当前开发。 若要为特定版本选择标记,请使用“切换分支或标记”下拉列表。 有关详细信息,请参阅如何选择 ASP.NET Core 源代码的版本标记 (dotnet/AspNetCore.Docs #26205)

Program 文件中:

builder.Services.AddRazorComponents().AddInteractiveServerComponents().AddHubOptions(options =>
{
    options.{OPTION} = {VALUE};
});

Program 文件中:

builder.Services.AddServerSideBlazor().AddHubOptions(options =>
{
    options.{OPTION} = {VALUE};
});

Startup.csStartup.ConfigureServices 中:

services.AddServerSideBlazor().AddHubOptions(options =>
{
    options.{OPTION} = {VALUE};
});

警告

MaximumReceiveMessageSize 的默认值为 32 KB。 提高此值可能会增加拒绝服务(DoS)攻击风险。

有关内存管理的信息,请参阅托管和部署 ASP.NET Core 服务器端 Blazor 应用

Blazor 中心选项

配置 MapBlazorHub 选项以控制 Blazor 中心的 HttpConnectionDispatcherOptions。 查看引用源中的中心连接调度程序选项的默认值。 {OPTION} 占位符代表选项,{VALUE} 占位符是值。

注意

指向 .NET 参考源的文档链接通常会加载存储库的默认分支,该分支表示针对下一个 .NET 版本的当前开发。 若要为特定版本选择标记,请使用“切换分支或标记”下拉列表。 有关详细信息,请参阅如何选择 ASP.NET Core 源代码的版本标记 (dotnet/AspNetCore.Docs #26205)

在应用的 Program 文件中调用 app.MapRazorComponents 之后调用 app.MapBlazorHub

app.MapBlazorHub(options =>
{
    options.{OPTION} = {VALUE};
});

向应用的 Program 文件中的 app.MapBlazorHub 提供选项:

app.MapBlazorHub(options =>
{
    options.{OPTION} = {VALUE};
});

在终结点路由配置中向 app.MapBlazorHub 提供选项:

app.UseEndpoints(endpoints =>
{
    endpoints.MapBlazorHub(options =>
    {
        options.{OPTION} = {VALUE};
    });
    ...
});

最大接收消息大小

本部分仅适用于实现 SignalR 的项目。

中心方法允许的最大传入 SignalR 消息大小限制受 HubOptions.MaximumReceiveMessageSize 限制(默认值:32 KB)。 SignalR 消息大于 MaximumReceiveMessageSize 时会引发错误。 框架对从中心到客户端的 SignalR 消息大小不施加限制。

如果未将 SignalR 日志记录设置为SignalR或跟踪,则消息大小错误仅显示在浏览器的开发人员工具控制台中:

错误:连接断开,出现错误“错误: 服务器关闭时返回错误: 连接因错误而关闭”。

SignalR 服务器端日志记录设置为调试跟踪时,服务器端日志记录会针对消息大小错误显示 InvalidDataException

appsettings.Development.json

{
  "DetailedErrors": true,
  "Logging": {
    "LogLevel": {
      ...
      "Microsoft.AspNetCore.SignalR": "Debug"
    }
  }
}

错误:

System.IO.InvalidDataException:超出了最大消息大小 32768B。 消息大小可以在 AddHubOptions 中配置。

一种方法是通过在 Program 文件中设置 MaximumReceiveMessageSize 来增加上限。 以下示例将最大接收消息大小设置为 64 KB:

builder.Services.AddRazorComponents().AddInteractiveServerComponents()
    .AddHubOptions(options => options.MaximumReceiveMessageSize = 64 * 1024);

提高 SignalR 传入消息大小限制的代价是需要使用更多的服务器资源,这将增加拒绝服务 (DoS) 攻击的风险。 此外,如果将大量内容作为字符串或字节数组读入内存中,还会导致垃圾回收器的分配工作状况不佳,从而导致额外的性能损失。

读取大型有效负载的一种更好的方法是以较小区块发送内容,并将有效负载作为 Stream 处理。 在读取大型 JavaScript (JS) 互操作 JSON 有效负载时,或者 JS 互操作数据以原始字节形式提供时,可以使用此方法。 有关演示如何使用类似于 InputFile 组件的方法在服务器端应用中发送大型二进制有效负载的示例,请参阅二进制文件提交示例应用BlazorInputLargeTextArea 组件示例

说明

指向 .NET 参考源的文档链接通常会加载存储库的默认分支,该分支表示针对下一个 .NET 版本的当前开发。 若要为特定版本选择标记,请使用“切换分支或标记”下拉列表。 有关详细信息,请参阅如何选择 ASP.NET Core 源代码的版本标记 (dotnet/AspNetCore.Docs #26205)

通过 SignalR 处理大型有效负载的表单也可以直接使用流式传输 JS 互操作。 有关详细信息,请参阅在 ASP.NET Core Blazor 中从 JavaScript 函数调用 .NET 方法。 有关将 <textarea> 数据流式传输到服务器的表单示例,请参阅排查 ASP.NET Core Blazor 表单问题

一种方法是通过在 Program 文件中设置 MaximumReceiveMessageSize 来增加上限。 以下示例将最大接收消息大小设置为 64 KB:

builder.Services.AddServerSideBlazor()
    .AddHubOptions(options => options.MaximumReceiveMessageSize = 64 * 1024);

提高 SignalR 传入消息大小限制的代价是需要使用更多的服务器资源,这将增加拒绝服务 (DoS) 攻击的风险。 此外,如果将大量内容作为字符串或字节数组读入内存中,还会导致垃圾回收器的分配工作状况不佳,从而导致额外的性能损失。

读取大型有效负载的一种更好的方法是以较小区块发送内容,并将有效负载作为 Stream 处理。 在读取大型 JavaScript (JS) 互操作 JSON 有效负载时,或者 JS 互操作数据以原始字节形式提供时,可以使用此方法。 有关演示如何使用类似于 InputFile 组件的方法在 Blazor Server 中发送大型二进制有效负载的示例,请参阅二进制文件提交示例应用BlazorInputLargeTextArea 组件示例

注意

指向 .NET 参考源的文档链接通常会加载存储库的默认分支,该分支表示针对下一个 .NET 版本的当前开发。 若要为特定版本选择标记,请使用“切换分支或标记”下拉列表。 有关详细信息,请参阅如何选择 ASP.NET Core 源代码的版本标记 (dotnet/AspNetCore.Docs #26205)

通过 SignalR 处理大型有效负载的表单也可以直接使用流式传输 JS 互操作。 有关详细信息,请参阅在 ASP.NET Core Blazor 中从 JavaScript 函数调用 .NET 方法。 有关在 Blazor Server 应用中流式传输 <textarea> 数据的表单示例,请参阅排查 ASP.NET Core Blazor 表单问题

通过在 Startup.ConfigureServices 中设置 MaximumReceiveMessageSize 来提高限制:

services.AddServerSideBlazor()
    .AddHubOptions(options => options.MaximumReceiveMessageSize = 64 * 1024);

提高 SignalR 传入消息大小限制的代价是需要使用更多的服务器资源,这将增加拒绝服务 (DoS) 攻击的风险。 此外,如果将大量内容作为字符串或字节数组读入内存中,还会导致垃圾回收器的分配工作状况不佳,从而导致额外的性能损失。

开发用于传输大量数据的代码时,请考虑以下指南:

  • 将数据切成小块,然后按顺序发送数据段,直到服务器收到所有数据。
  • 不要在 JS 和 C# 代码中分配大型对象。
  • 发送或接收数据时,请勿长时间阻止主 UI 线程。
  • 在进程完成或取消时释放已消耗的内存。
  • 为了安全起见,请强制执行以下附加要求:
    • 声明可以传递的最大文件或数据大小。
    • 声明从客户端到服务器的最低上传速率。
  • 在服务器收到数据后,数据可以:
    • 暂时存储在内存缓冲区中,直到收集完所有数据段。
    • 立即使用。 例如,在收到每个数据段时,数据可以立即存储到数据库中或写入磁盘。

Blazor 服务器端中心终结点路由配置

Program 文件中,调用 MapBlazorHub 以将 BlazorHub 映射到应用的默认路径。 Blazor 脚本 (blazor.*.js) 自动指向 MapBlazorHub 创建的终结点。

在 UI 中反映服务器端连接状态

如果客户端检测到连接已丢失,在客户端尝试重新连接时会向用户显示默认 UI。 如果重新连接失败,则会向用户提供重试选项。

若要自定义 UI,请定义一个 idcomponents-reconnect-modal 的元素。 以下示例将元素放置在 App 组件中。

App.razor

若要自定义 UI,请定义一个 idcomponents-reconnect-modal 的元素。 以下示例将元素放置在主机页中。

Pages/_Host.cshtml

若要自定义 UI,请定义一个 idcomponents-reconnect-modal 的元素。 以下示例将元素放置在布局页中。

Pages/_Layout.cshtml

若要自定义 UI,请定义一个 idcomponents-reconnect-modal 的元素。 以下示例将元素放置在主机页中。

Pages/_Host.cshtml

<div id="components-reconnect-modal">
    There was a problem with the connection!
</div>

注意

如果应用呈现了多个 idcomponents-reconnect-modal 的元素,则只有第一个呈现的元素会接收 CSS 类更改以显示或隐藏元素。

将以下 CSS 样式添加到站点的样式表中。

wwwroot/app.css

wwwroot/css/site.css

#components-reconnect-modal {
    display: none;
}

#components-reconnect-modal.components-reconnect-show, 
#components-reconnect-modal.components-reconnect-failed, 
#components-reconnect-modal.components-reconnect-rejected {
    display: block;
}

下表介绍了 Blazor 框架应用于 components-reconnect-modal 元素的 CSS 类。

CSS 类 指示…
components-reconnect-show 连接已丢失。 客户端正在尝试重新连接。 显示模式。
components-reconnect-hide 将为服务器重新建立活动连接。 隐藏模式。
components-reconnect-failed 重新连接失败,可能是由于网络故障引起的。 若要尝试重新连接,请在 JavaScript 中调用 window.Blazor.reconnect()
components-reconnect-rejected 已拒绝重新连接。 已达到服务器,但拒绝连接,服务器上的用户状态丢失。 若要重新加载应用,请在 JavaScript 中调用 location.reload()。 当出现以下情况时,可能会导致此连接状态:
  • 服务器端线路发生故障。
  • 客户端断开连接的时间足以使服务器删除用户的状态。 用户组件的实例已被处置。
  • 服务器已重启,或者应用的工作进程被回收。

通过在站点的 CSS 中为模式元素设置 transition-delay 属性,自定义重新连接显示出现之前的延迟。 以下示例将转换延迟从 500 毫秒(默认值)设置为 1000 毫秒(1 秒)。

wwwroot/app.css

wwwroot/css/site.css

#components-reconnect-modal {
    transition: visibility 0s linear 1000ms;
}

若要显示当前的重新连接尝试,请定义一个 idcomponents-reconnect-current-attempt 的元素。 若要显示最大重新连接重试次数,请定义一个 idcomponents-reconnect-max-retries 的元素。 以下示例按照上一个示例,将这些元素放置在重新连接尝试模式元素内。

<div id="components-reconnect-modal">
    There was a problem with the connection!
    (Current reconnect attempt: 
    <span id="components-reconnect-current-attempt"></span> /
    <span id="components-reconnect-max-retries"></span>)
</div>

出现自定义重新连接模式时,它会根据前面的代码呈现如下所示的内容:

There was a problem with the connection! (Current reconnect attempt: 3 / 8)

服务器端呈现

默认情况下,组件会在客户端与服务器建立连接之前在服务器上预呈现。 有关详细信息,请参阅 Prerender ASP.NET Core Razor 组件

默认情况下,组件会在客户端与服务器建立连接之前在服务器上预呈现。 有关详细信息,请参阅 ASP.NET Core 中的组件标记帮助程序

监视服务器端线路活动

使用 CircuitHandler 上的 CreateInboundActivityHandler 方法监视入站线路活动。 入站线路活动是从浏览器发送到服务器的任何活动,例如 UI 事件或 JavaScript 到 .NET 的互操作调用。

例如,可以使用线路活动处理程序来检测客户端是否处于空闲状态并记录器线路 ID(Circuit.Id):

using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.Extensions.Options;
using Timer = System.Timers.Timer;

public sealed class IdleCircuitHandler : CircuitHandler, IDisposable
{
    private Circuit? currentCircuit;
    private readonly ILogger logger;
    private readonly Timer timer;

    public IdleCircuitHandler(ILogger<IdleCircuitHandler> logger, 
        IOptions<IdleCircuitOptions> options)
    {
        timer = new Timer
        {
            Interval = options.Value.IdleTimeout.TotalMilliseconds,
            AutoReset = false
        };

        timer.Elapsed += CircuitIdle;
        this.logger = logger;
    }

    private void CircuitIdle(object? sender, System.Timers.ElapsedEventArgs e)
    {
        logger.LogInformation("{CircuitId} is idle", currentCircuit?.Id);
    }

    public override Task OnCircuitOpenedAsync(Circuit circuit, 
        CancellationToken cancellationToken)
    {
        currentCircuit = circuit;

        return Task.CompletedTask;
    }

    public override Func<CircuitInboundActivityContext, Task> CreateInboundActivityHandler(
        Func<CircuitInboundActivityContext, Task> next)
    {
        return context =>
        {
            timer.Stop();
            timer.Start();

            return next(context);
        };
    }

    public void Dispose() => timer.Dispose();
}

public class IdleCircuitOptions
{
    public TimeSpan IdleTimeout { get; set; } = TimeSpan.FromMinutes(5);
}

public static class IdleCircuitHandlerServiceCollectionExtensions
{
    public static IServiceCollection AddIdleCircuitHandler(
        this IServiceCollection services, 
        Action<IdleCircuitOptions> configureOptions)
    {
        services.Configure(configureOptions);
        services.AddIdleCircuitHandler();

        return services;
    }

    public static IServiceCollection AddIdleCircuitHandler(
        this IServiceCollection services)
    {
        services.AddScoped<CircuitHandler, IdleCircuitHandler>();

        return services;
    }
}

Program 文件中注册服务。 以下示例配置默认空闲超时为 5 分钟到 5 秒,以测试上述 IdleCircuitHandler 实现:

builder.Services.AddIdleCircuitHandler(options => 
    options.IdleTimeout = TimeSpan.FromSeconds(5));

线路活动处理程序还提供了一种从其他非 Blazor 依赖项注入 (DI) 范围访问限定范围的 Blazor 服务的方法。 有关更多信息和示例,请参阅:

Blazor 启动

在 Blazor Web 应用的 App.razor 文件中配置 Blazor 应用的 SignalR 线路的手动启动:

Pages/_Host.cshtml 文件 (Blazor Server) 中配置 Blazor 应用的 SignalR 线路的手动启动:

Pages/_Layout.cshtml 文件 (Blazor Server) 中配置 Blazor 应用的 SignalR 线路的手动启动:

Pages/_Host.cshtml 文件 (Blazor Server) 中配置 Blazor 应用的 SignalR 线路的手动启动:

  • autostart="false" 属性添加到 blazor.*.js 脚本的 <script> 标记中。
  • 放置一个在加载 Blazor 脚本后调用 Blazor.start() 的脚本,将其置于结束的 </body> 标记内。

禁用 autostart 时,应用中不依赖该回路的任何方面都能正常工作。 例如,客户端路由正常运行。 但是,在调用 Blazor.start() 之前,依赖于该回路的任何方面不会正常运行。 如果没有已建立的回路,应用行为是不可预测的。 例如,在回路断开连接时,组件方法无法执行。

有关详细信息,包括如何在文档准备就绪时初始化 Blazor,以及如何链接到 JS Promise,请参阅 ASP.NET Core Blazor 启动

在客户端上配置 SignalR 超时和 Keep-Alive

为客户端配置以下值:

  • withServerTimeout:配置服务器超时(以毫秒为单位)。 如果此超时已过但未从服务器接收任何消息,连接将终止并出现错误。 默认超时值为 30 秒。 服务器超时应至少是分配给 Keep-Alive 间隔 (withKeepAliveInterval) 的值的两倍。
  • withKeepAliveInterval:配置保持活动间隔(ping 服务器的默认间隔,以毫秒为单位)。 使用此设置,服务器可以检测强行断开连接的情况,例如客户断开其计算机的网络连接。 此 ping 的发生频率最多与服务器 ping 的频率一样。 如果服务器每 5 秒 ping 一次,则分配的值低于 5000(5 秒)时,将会每 5 秒 ping 一次。 默认值为 15 秒。 Keep-Alive 间隔应小于或等于分配给服务器超时 (withServerTimeout) 的值的一半。

下面的 App.razor 文件(Blazor Web 应用)示例显示了默认值的分配。

Blazor Web 应用:

<script src="{BLAZOR SCRIPT}" autostart="false"></script>
<script>
  Blazor.start({
    circuit: {
      configureSignalR: function (builder) {
        builder.withServerTimeout(30000).withKeepAliveInterval(15000);
      }
    }
  });
</script>

下面是 Pages/_Host.cshtml 文件(Blazor Server,除 .NET 6 中的 ASP.NET Core 外的所有版本)或 Pages/_Layout.cshtml 文件(Blazor Server,.NET 6 中的 ASP.NET Core)的示例。

Blazor Server:

<script src="{BLAZOR SCRIPT}" autostart="false"></script>
<script>
  Blazor.start({
    configureSignalR: function (builder) {
        builder.withServerTimeout(30000).withKeepAliveInterval(15000);
  });
</script>

在前面的示例中,{BLAZOR SCRIPT} 占位符是 Blazor 脚本路径和文件名。 有关该脚本的位置和要使用的路径,请参阅 ASP.NET Core Blazor 项目结构

在组件中创建中心连接时,在 HubConnectionBuilder 上设置 ServerTimeout(默认值:30 秒)和 KeepAliveInterval(默认值:15 秒)。 在生成 HubConnection 上设置 HandshakeTimeout(默认值:15 秒)。 以下示例显示了默认值的分配:

protected override async Task OnInitializedAsync()
{
    hubConnection = new HubConnectionBuilder()
        .WithUrl(Navigation.ToAbsoluteUri("/chathub"))
        .WithServerTimeout(TimeSpan.FromSeconds(30))
        .WithKeepAliveInterval(TimeSpan.FromSeconds(15))
        .Build();

    hubConnection.HandshakeTimeout = TimeSpan.FromSeconds(15);

    hubConnection.On<string, string>("ReceiveMessage", (user, message) => ...

    await hubConnection.StartAsync();
}

为客户端配置以下值:

  • serverTimeoutInMilliseconds:服务器超时(以毫秒为单位)。 如果此超时已过但未从服务器接收任何消息,连接将终止并出现错误。 默认超时值为 30 秒。 服务器超时应至少是分配给 Keep-Alive 间隔 (keepAliveIntervalInMilliseconds) 的值的两倍。
  • keepAliveIntervalInMilliseconds:ping 服务器时采用的默认间隔。 使用此设置,服务器可以检测强行断开连接的情况,例如客户断开其计算机的网络连接。 此 ping 的发生频率最多与服务器 ping 的频率一样。 如果服务器每 5 秒 ping 一次,则分配的值低于 5000(5 秒)时,将会每 5 秒 ping 一次。 默认值为 15 秒。 Keep-Alive 间隔应小于或等于分配给服务器超时 (serverTimeoutInMilliseconds) 的值的一半。

下面是 Pages/_Host.cshtml 文件(Blazor Server,除 .NET 6 中的 ASP.NET Core 外的所有版本)或 Pages/_Layout.cshtml 文件(Blazor Server,.NET 6 中的 ASP.NET Core)的示例:

<script src="{BLAZOR SCRIPT}" autostart="false"></script>
<script>
  Blazor.start({
    configureSignalR: function (builder) {
      let c = builder.build();
      c.serverTimeoutInMilliseconds = 30000;
      c.keepAliveIntervalInMilliseconds = 15000;
      builder.build = () => {
        return c;
      };
    }
  });
</script>

在前面的示例中,{BLAZOR SCRIPT} 占位符是 Blazor 脚本路径和文件名。 有关该脚本的位置和要使用的路径,请参阅 ASP.NET Core Blazor 项目结构

在组件中创建中心连接时,在生成的 HubConnection 上设置 ServerTimeout(默认值:30 秒)、HandshakeTimeout(默认值:15 秒)和 KeepAliveInterval(默认值:15 秒)。 以下示例显示了默认值的分配:

protected override async Task OnInitializedAsync()
{
    hubConnection = new HubConnectionBuilder()
        .WithUrl(Navigation.ToAbsoluteUri("/chathub"))
        .Build();

    hubConnection.ServerTimeout = TimeSpan.FromSeconds(30);
    hubConnection.HandshakeTimeout = TimeSpan.FromSeconds(15);
    hubConnection.KeepAliveInterval = TimeSpan.FromSeconds(15);

    hubConnection.On<string, string>("ReceiveMessage", (user, message) => ...

    await hubConnection.StartAsync();
}

更改服务器超时 (ServerTimeout) 或 Keep-Alive 间隔 (KeepAliveInterval) 的值时:

  • 服务器超时应至少是分配给 Keep-Alive 间隔的值的两倍。
  • Keep-Alive 间隔应小于或等于分配给服务器超时的值的一半。

有关详细信息,请参阅以下文章的“全球部署和连接失败”部分:

修改服务器端重新连接处理程序

可以针对自定义行为修改重新连接处理程序的线路连接事件,如:

  • 在连接断开时通知用户。
  • 在线路连接时(通过客户端)执行日志记录。

若要修改连接事件,请为以下连接更改注册回调:

  • 使用 onConnectionDown 删除的连接。
  • 已建立/重新建立的连接使用 onConnectionUp

必须同时指定 onConnectionDownonConnectionUp

Blazor Web 应用:

<script src="{BLAZOR SCRIPT}" autostart="false"></script>
<script>
  Blazor.start({
    circuit: {
      reconnectionHandler: {
        onConnectionDown: (options, error) => console.error(error),
        onConnectionUp: () => console.log("Up, up, and away!")
      }
    }
  });
</script>

Blazor Server:

<script src="{BLAZOR SCRIPT}" autostart="false"></script>
<script>
  Blazor.start({
    reconnectionHandler: {
      onConnectionDown: (options, error) => console.error(error),
      onConnectionUp: () => console.log("Up, up, and away!")
    }
  });
</script>

在前面的示例中,{BLAZOR SCRIPT} 占位符是 Blazor 脚本路径和文件名。 有关该脚本的位置和要使用的路径,请参阅 ASP.NET Core Blazor 项目结构

服务器端重新连接失败后自动刷新页面

默认的重新连接行为需要用户在重新连接失败后手动刷新页面。 但可使用自定义重新连接处理程序自动刷新页面:

App.razor

Pages/_Host.cshtml

<div id="reconnect-modal" style="display: none;"></div>
<script src="{BLAZOR SCRIPT}" autostart="false"></script>
<script src="boot.js"></script>

在前面的示例中,{BLAZOR SCRIPT} 占位符是 Blazor 脚本路径和文件名。 有关该脚本的位置和要使用的路径,请参阅 ASP.NET Core Blazor 项目结构

创建以下 wwwroot/boot.js 文件。

Blazor Web 应用:

(() => {
  const maximumRetryCount = 3;
  const retryIntervalMilliseconds = 5000;
  const reconnectModal = document.getElementById('reconnect-modal');

  const startReconnectionProcess = () => {
    reconnectModal.style.display = 'block';

    let isCanceled = false;

    (async () => {
      for (let i = 0; i < maximumRetryCount; i++) {
        reconnectModal.innerText = `Attempting to reconnect: ${i + 1} of ${maximumRetryCount}`;

        await new Promise(resolve => setTimeout(resolve, retryIntervalMilliseconds));

        if (isCanceled) {
          return;
        }

        try {
          const result = await Blazor.reconnect();
          if (!result) {
            // The server was reached, but the connection was rejected; reload the page.
            location.reload();
            return;
          }

          // Successfully reconnected to the server.
          return;
        } catch {
          // Didn't reach the server; try again.
        }
      }

      // Retried too many times; reload the page.
      location.reload();
    })();

    return {
      cancel: () => {
        isCanceled = true;
        reconnectModal.style.display = 'none';
      },
    };
  };

  let currentReconnectionProcess = null;

  Blazor.start({
    circuit: {
      reconnectionHandler: {
        onConnectionDown: () => currentReconnectionProcess ??= startReconnectionProcess(),
        onConnectionUp: () => {
          currentReconnectionProcess?.cancel();
          currentReconnectionProcess = null;
        }
      }
    }
  });
})();

Blazor Server:

(() => {
  const maximumRetryCount = 3;
  const retryIntervalMilliseconds = 5000;
  const reconnectModal = document.getElementById('reconnect-modal');

  const startReconnectionProcess = () => {
    reconnectModal.style.display = 'block';

    let isCanceled = false;

    (async () => {
      for (let i = 0; i < maximumRetryCount; i++) {
        reconnectModal.innerText = `Attempting to reconnect: ${i + 1} of ${maximumRetryCount}`;

        await new Promise(resolve => setTimeout(resolve, retryIntervalMilliseconds));

        if (isCanceled) {
          return;
        }

        try {
          const result = await Blazor.reconnect();
          if (!result) {
            // The server was reached, but the connection was rejected; reload the page.
            location.reload();
            return;
          }

          // Successfully reconnected to the server.
          return;
        } catch {
          // Didn't reach the server; try again.
        }
      }

      // Retried too many times; reload the page.
      location.reload();
    })();

    return {
      cancel: () => {
        isCanceled = true;
        reconnectModal.style.display = 'none';
      },
    };
  };

  let currentReconnectionProcess = null;

  Blazor.start({
    reconnectionHandler: {
      onConnectionDown: () => currentReconnectionProcess ??= startReconnectionProcess(),
      onConnectionUp: () => {
        currentReconnectionProcess?.cancel();
        currentReconnectionProcess = null;
      }
    }
  });
})();

有关 Blazor 启动的详细信息,请参阅 ASP.NET Core Blazor 启动

调整服务器端重新连接重试计数和间隔

若要调整重新连接重试次数和间隔,请设置重试次数 (maxRetries) 和允许每次重试运行的毫秒数 (retryIntervalMilliseconds)。

Blazor Web 应用:

<script src="{BLAZOR SCRIPT}" autostart="false"></script>
<script>
  Blazor.start({
    circuit: {
      reconnectionOptions: {
        maxRetries: 3,
        retryIntervalMilliseconds: 2000
      }
    }
  });
</script>

Blazor Server:

<script src="{BLAZOR SCRIPT}" autostart="false"></script>
<script>
  Blazor.start({
    reconnectionOptions: {
      maxRetries: 3,
      retryIntervalMilliseconds: 2000
    }
  });
</script>

在前面的示例中,{BLAZOR SCRIPT} 占位符是 Blazor 脚本路径和文件名。 有关该脚本的位置和要使用的路径,请参阅 ASP.NET Core Blazor 项目结构

有关 Blazor 启动的详细信息,请参阅 ASP.NET Core Blazor 启动

控制重新连接 UI 何时出现

在以下情况下,控制重新连接 UI 何时出现可能会非常有用:

  • 由于内部网络或 Internet 延迟导致的 ping 超时,所部署的应用经常显示重新连接 UI,并且你希望增加延迟。
  • 应用应向用户报告连接已更快地断开,并且你希望缩短延迟时间。

重新连接 UI 的出现时间受调整客户端上的保持连接间隔和超时的影响。 但更改设置可能需要更改其他保持连接、超时和握手设置。

作为以下指导的一般建议:

  • 保持连接间隔应在客户端和服务器配置之间相匹配。
  • 超时应至少是分配给始终连接间隔的值的两倍。

服务器配置

指定以下设置:

  • ClientTimeoutInterval(默认值:30 秒):在服务器关闭连接之前,客户端必须发送消息的时间窗口。
  • HandshakeTimeout(默认值:15 秒):服务器用于超时客户端传入握手请求的间隔。
  • KeepAliveInterval(默认值:15 秒):服务器用来向已连接的客户端发送保持连接 ping 的间隔。 请注意,客户端上还存在一个“保持连接”间隔设置,它应与服务器的值匹配。

可以增加 ClientTimeoutIntervalHandshakeTimeout,而 KeepAliveInterval 则可以保持不变。 重要的考虑事项是,如果更改该值,请确保超时至少是保持连接间隔的值的两倍,并且保持连接间隔在服务器和客户端之间匹配。 有关详细信息,请参阅在客户端部分中配置 SignalR 超时和保持活动状态

如下示例中:

  • ClientTimeoutInterval 增加到 60 秒(默认值:30 秒)。
  • HandshakeTimeout 增加到 30 秒(默认值:15 秒)。
  • KeepAliveInterval 不在开发人员代码中设置,并且使用其默认值 15 秒。 减少保持连接间隔的值会增加通信 ping 的频率,这会增加应用、服务器和网络的负载。 在降低保持连接间隔时,必须注意避免产生性能不佳的情况。

服务器项目 Program 文件中的 Blazor Web 应用(.NET 8 或更高版本):

builder.Services.AddRazorComponents().AddInteractiveServerComponents()
    .AddHubOptions(options =>
{
    options.ClientTimeoutInterval = TimeSpan.FromSeconds(60);
    options.HandshakeTimeout = TimeSpan.FromSeconds(30);
});

Program 文件中的 Blazor Server

builder.Services.AddServerSideBlazor()
    .AddHubOptions(options =>
    {
        options.ClientTimeoutInterval = TimeSpan.FromSeconds(60);
        options.HandshakeTimeout = TimeSpan.FromSeconds(30);
    });

有关详细信息,请参阅服务器端线路处理程序选项部分。

客户端配置

指定以下设置:

  • withServerTimeout(默认值:30 秒):配置线路中心连接的服务器超时(以毫秒为单位)。
  • withKeepAliveInterval(默认值:15 秒):连接发送保持连接消息的间隔(以毫秒为单位)。

可以增加服务器超时,而保持连接间隔则可以保持不变。 重要的考虑事项是,如果更改该值,请确保服务器超时至少是保持连接间隔的值的两倍,并且保持连接间隔值在服务器和客户端之间匹配。 有关详细信息,请参阅在客户端部分中配置 SignalR 超时和保持活动状态

在以下启动配置示例(Blazor 脚本的位置)中,服务器超时使用了 60 秒自定义值。 未设置保持连接间隔(withKeepAliveInterval),使用其默认值 15 秒。

Blazor Web 应用:

<script src="{BLAZOR SCRIPT}" autostart="false"></script>
<script>
  Blazor.start({
    circuit: {
      configureSignalR: function (builder) {
        builder.withServerTimeout(60000);
      }
    }
  });
</script>

Blazor Server:

<script src="{BLAZOR SCRIPT}" autostart="false"></script>
<script>
  Blazor.start({
    configureSignalR: function (builder) {
      builder.withServerTimeout(60000);
    }
  });
</script>

在组件中创建中心连接时,在 HubConnectionBuilder 上设置服务器超时(WithServerTimeout,默认值:30 秒)。 在生成 HubConnection 上设置 HandshakeTimeout(默认值:15 秒)。 确认超时至少是保持连接间隔(WithKeepAliveInterval/KeepAliveInterval)的两倍,并且保持连接值在服务器和客户端之间匹配。

以下示例基于结合使用 SignalR 和 Blazor 教程中的 Index 组件。 将服务器超时增加到 60 秒,并将握手超时增加到 30 秒。 未设置 Keep-Alive 间隔,使用其默认值 15 秒。

protected override async Task OnInitializedAsync()
{
    hubConnection = new HubConnectionBuilder()
        .WithUrl(Navigation.ToAbsoluteUri("/chathub"))
        .WithServerTimeout(TimeSpan.FromSeconds(60))
        .Build();

    hubConnection.HandshakeTimeout = TimeSpan.FromSeconds(30);

    hubConnection.On<string, string>("ReceiveMessage", (user, message) => ...

    await hubConnection.StartAsync();
}

指定以下设置:

  • serverTimeoutInMilliseconds(默认值:30 秒):配置线路中心连接的服务器超时(以毫秒为单位)。
  • keepAliveIntervalInMilliseconds(默认值:15 秒):连接发送保持连接消息的间隔(以毫秒为单位)。

可以增加服务器超时,而保持连接间隔则可以保持不变。 重要的考虑事项是,如果更改该值,请确保服务器超时至少是保持连接间隔的值的两倍,并且保持连接间隔值在服务器和客户端之间匹配。 有关详细信息,请参阅在客户端部分中配置 SignalR 超时和保持活动状态

在以下启动配置示例(Blazor 脚本的位置)中,服务器超时使用了 60 秒自定义值。 未设置 Keep-Alive 间隔(keepAliveIntervalInMilliseconds),使用其默认值 15 秒。

Pages/_Host.cshtml中:

<script src="_framework/blazor.server.js" autostart="false"></script>
<script>
  Blazor.start({
    configureSignalR: function (builder) {
      let c = builder.build();
      c.serverTimeoutInMilliseconds = 60000;
      builder.build = () => {
        return c;
      };
    }
  });
</script>

在组件中创建中心连接时,在生成的 HubConnection 上设置 ServerTimeout(默认值:30 秒)和 HandshakeTimeout(默认值:15 秒)。 确认超时至少是保持连接间隔的两倍。 确认服务器和客户端之间的保持连接间隔是否匹配。

以下示例基于结合使用 SignalR 和 Blazor 教程中的 Index 组件。 将 ServerTimeout 增加到 60 秒,并将 HandshakeTimeout 增加到 30 秒。 未设置 Keep-Alive 间隔(KeepAliveInterval),使用其默认值 15 秒。

protected override async Task OnInitializedAsync()
{
    hubConnection = new HubConnectionBuilder()
        .WithUrl(Navigation.ToAbsoluteUri("/chathub"))
        .Build();

    hubConnection.ServerTimeout = TimeSpan.FromSeconds(60);
    hubConnection.HandshakeTimeout = TimeSpan.FromSeconds(30);

    hubConnection.On<string, string>("ReceiveMessage", (user, message) => ...

    await hubConnection.StartAsync();
}

从客户端断开 Blazor 线路连接

默认情况下,触发 unload 页面事件时,Blazor 线路会断开连接。 若要断开客户端上其他方案的线路连接,请在相应的事件处理程序中调用 Blazor.disconnect。 在下面的示例中,当页面隐藏(pagehide 事件)时,线路会断开连接:

window.addEventListener('pagehide', () => {
  Blazor.disconnect();
});

有关 Blazor 启动的详细信息,请参阅 ASP.NET Core Blazor 启动

服务器端线路处理程序

可以定义线路处理程序,它允许在用户线路的状态发生更改时运行代码。 线路处理程序通过从 CircuitHandler 派生并在应用的服务容器中注册该类实现。 以下线路处理程序示例跟踪打开的 SignalR 连接。

TrackingCircuitHandler.cs

using Microsoft.AspNetCore.Components.Server.Circuits;

public class TrackingCircuitHandler : CircuitHandler
{
    private HashSet<Circuit> circuits = new();

    public override Task OnConnectionUpAsync(Circuit circuit, 
        CancellationToken cancellationToken)
    {
        circuits.Add(circuit);

        return Task.CompletedTask;
    }

    public override Task OnConnectionDownAsync(Circuit circuit, 
        CancellationToken cancellationToken)
    {
        circuits.Remove(circuit);

        return Task.CompletedTask;
    }

    public int ConnectedCircuits => circuits.Count;
}

线路处理程序使用 DI 注册。 每个线路实例都会创建区分范围的实例。 借助前面示例中的 TrackingCircuitHandler 创建单一实例服务,因为必须跟踪所有线路的状态。

Program 文件中:

builder.Services.AddSingleton<CircuitHandler, TrackingCircuitHandler>();

Startup.csStartup.ConfigureServices 中:

services.AddSingleton<CircuitHandler, TrackingCircuitHandler>();

如果自定义线路处理程序的方法引发未处理异常,则该异常会导致 线路产生严重错误。 若要容忍处理程序代码或被调用方法中的异常,请使用错误处理和日志记录将代码包装到一个或多个 try-catch 语句中。

当线路因用户断开连接而结束且框架正在清除线路状态时,框架会处置线路的 DI 范围。 处置范围时会处置所有实现 System.IDisposable 的区分线路范围的 DI 服务。 如果有任何 DI 服务在处置期间引发未处理异常,则框架会记录该异常。 有关详细信息,请参阅 ASP.NET Core Blazor 依赖项注入

用于为自定义服务捕获用户的服务器端线路处理程序

使用 CircuitHandlerAuthenticationStateProvider 捕获用户,并在服务中设置此用户。 有关详细信息和示例代码,请参阅服务器端 ASP.NET Core Blazor 其他安全方案

当没有剩余的交互式服务器组件时,关闭线路

交互式服务器组件使用与浏览器的实时连接(称为线路)来处理 Web UI 事件。 呈现根交互式服务器组件时,将创建线路及其关联状态。 当页面上没有剩余的交互式服务器组件时,线路将关闭,以释放服务器资源。

Razor 组件中的 IHttpContextAccessor/HttpContext

必须避免将 IHttpContextAccessor 与交互式呈现一起使用,因为无有效的 HttpContext 可用。

IHttpContextAccessor 可用于在服务器上静态呈现的组件。 但是,建议尽可能避免这种做法。

HttpContext 只能在常规任务的静态呈现根组件中用作级联参数,这些任务包括检查和修改 App 组件 (Components/App.razor) 中的标头或其他属性。 对于交互式呈现,值始终是 null

[CascadingParameter]
public HttpContext? HttpContext { get; set; }

对于交互式组件中需要 HttpContext 的场景,建议通过服务器的永久性组件状态传输数据。 有关详细信息,请参阅服务器端 ASP.NET Core Blazor 其他安全方案

请勿在服务器端 Blazor 应用的 Razor 组件中直接或间接使用 IHttpContextAccessor/HttpContext Blazor 应用在 ASP.NET Core 管道上下文之外运行。 既不保证 HttpContextIHttpContextAccessor 中可用,也不保证 HttpContext 会保留启动了 Blazor 应用的上下文。

建议在 Blazor 应用的初始呈现期间通过根组件参数将请求状态传递给此应用。 或者,应用可以将数据复制到根组件的初始化生命周期事件中的作用域内服务中,以便在整个应用中使用。 有关详细信息,请参阅服务器端 ASP.NET Core Blazor 其他安全方案

服务器端 Blazor 安全性的一个关键方面是,连接到给定线路的用户可能会在建立 Blazor 线路后的某个时间点得到更新,但 IHttpContextAccessor 不会更新。 有关使用自定义服务应对此情况的详细信息,请参阅服务器端 ASP.NET Core Blazor 其他安全方案

其他服务器端资源