mcpField
S
signalr

signalr

github.com/managedcode开发49661 次安装1 天前更新
代码仓库

关于 signalr

面向使用 ASP.NET Core 构建或运维实时功能的 SignalR Skill,涵盖 Hub 设计、连接行为、安全、测试及扩展决策。

主要功能

  1. 01Hub 契约与 API 设计
  2. 02重连与分组处理
  3. 03身份验证与输入校验
  4. 04传输方式和故障测试
  5. 05多服务器扩展指导

适用场景

  1. 01构建聊天与通知功能
  2. 02排查连接或传输问题
  3. 03向客户端分组广播消息
  4. 04规划 SignalR 多服务器部署

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/managedcode/dotnet-skills --skill signalr

安装后在 Claude Code 中输入/signalr确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

manifest.json
references/
anti-patterns.md
patterns.md

详细介绍

SignalR

适用场景

  • 构建聊天、通知、协作或实时更新功能
  • 调试 Hub 生命周期、连接状态或传输问题
  • 判断 SignalR 或其他传输方式是否更适合当前场景
  • 实现向多个已连接客户端实时广播
  • 在多台服务器间扩展 SignalR

文档

参考资料
  • patterns.md - Hub 模式、流式处理、组、在线状态和高级消息传递技术的详细说明
  • anti-patterns.md - 常见的 SignalR 错误及其避免方法

工作流

  1. 对于广播式或面向连接的实时功能,使用 SignalR;不要在 Hub 式扇出场景中强行使用 gRPC。
  2. 有意识地设计 Hub 合约,并保持 Hub 方法精简,将持久化工作交由其他部分处理。
  3. 规划重连、背压、身份验证和扇出成本,不要将实时消息传递视为无状态的请求/响应。
  4. 有计划地使用组、在线状态和连接元数据,以便清楚了解扩展后的行为。
  5. 如果涉及 Native AOT 或裁剪,请明确验证受支持的协议和序列化选项。
  6. 测试连接行为和故障模式,而不仅仅是顺利交付消息的情况。

当前上游说明

  • dotnet/aspnetcore v10.0.11 是一个服务更新版本,不会改变 SignalR 编程模型。指南应聚焦于 Hub 合约设计、重连、传输、授权和扩展验证。
  • 2026 年 8 月的 ASP.NET Core 概述仍将 SignalR 定位为用于服务器与客户端实时通信的技术。服务更新后,至少重新运行一项重连和组广播冒烟测试,因为依赖项更新可能暴露客户端与服务器包不匹配的问题。

Hub 模式

强类型 Hub(推荐)
// Define the client interface
public interface IChatClient
{
    Task ReceiveMessage(string user, string message);
    Task UserJoined(string user);
    Task UserLeft(string user);
}

// Implement the strongly-typed hub
public class ChatHub : Hub<IChatClient>
{
    public async Task SendMessage(string user, string message)
    {
        // Compiler checks client method calls
        await Clients.All.ReceiveMessage(user, message);
    }

    public override async Task OnConnectedAsync()
    {
        await Clients.Others.UserJoined(Context.User?.Identity?.Name ?? "Anonymous");
        await base.OnConnectedAsync();
    }

    public override async Task OnDisconnectedAsync(Exception? exception)
    {
        await Clients.Others.UserLeft(Context.User?.Identity?.Name ?? "Anonymous");
        await base.OnDisconnectedAsync(exception);
    }
}
使用组进行定向消息传递
public class NotificationHub : Hub<INotificationClient>
{
    public async Task JoinGroup(string groupName)
    {
        await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
        await Clients.Group(groupName).UserJoined(Context.User?.Identity?.Name);
    }

    public async Task LeaveGroup(string groupName)
    {
        await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);
    }

    public async Task SendToGroup(string groupName, string message)
    {
        await Clients.Group(groupName).ReceiveNotification(message);
    }
}
带自定义对象参数的 Hub 方法(API 版本控制)
// Use custom objects to avoid breaking changes
public class SendMessageRequest
{
    public string Message { get; set; } = string.Empty;
    public string? Recipient { get; set; }  // Added later without breaking clients
    public int? Priority { get; set; }       // Added later without breaking clients
}

public class ChatHub : Hub<IChatClient>
{
    public async Task SendMessage(SendMessageRequest request)
    {
        // Handle both old and new clients
        if (request.Recipient != null)
        {
            await Clients.User(request.Recipient).ReceiveMessage(request.Message);
        }
        else
        {
            await Clients.All.ReceiveMessage(request.Message);
        }
    }
}

客户端模式

支持自动重连的 JavaScript 客户端
const connection = new signalR.HubConnectionBuilder()
    .withUrl("/chatHub")
    .withAutomaticReconnect([0, 2000, 5000, 10000, 30000]) // Retry delays
    .configureLogging(signalR.LogLevel.Information)
    .build();

// Handle reconnection events
connection.onreconnecting(error => {
    console.log("Reconnecting...", error);
    updateUIForReconnecting();
});

connection.onreconnected(connectionId => {
    console.log("Reconnected with ID:", connectionId);
    // Rejoin groups - reconnection does not restore group membership
    rejoinGroups();
    updateUIForConnected();
});

connection.onclose(error => {
    console.log("Connection closed", error);
    updateUIForDisconnected();
});

async function start() {
    try {
        await connection.start();
        console.log("SignalR Connected");
    } catch (err) {
        console.log(err);
        setTimeout(start, 5000);
    }
}

start();
支持重连的 .NET 客户端
var connection = new HubConnectionBuilder()
    .WithUrl("https://localhost:5001/chatHub", options =>
    {
        options.AccessTokenProvider = () => Task.FromResult(GetAccessToken());
    })
    .WithAutomaticReconnect()
    .Build();

connection.Reconnecting += error =>
{
    _logger.LogWarning("Connection lost. Reconnecting: {Error}", error?.Message);
    return Task.CompletedTask;
};

connection.Reconnected += connectionId =>
{
    _logger.LogInformation("Reconnected with ID: {ConnectionId}", connectionId);
    // Rejoin groups after reconnection
    return RejoinGroupsAsync();
};

connection.Closed += async error =>
{
    _logger.LogError("Connection closed: {Error}", error?.Message);
    await Task.Delay(Random.Shared.Next(0, 5) * 1000);
    await connection.StartAsync();
};

await connection.StartAsync();

服务器配置

使用身份验证注册 Hub
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSignalR(options =>
{
    options.EnableDetailedErrors = builder.Environment.IsDevelopment();
    options.MaximumReceiveMessageSize = 64 * 1024; // 64 KB
    options.StreamBufferCapacity = 10;
    options.KeepAliveInterval = TimeSpan.FromSeconds(15);
    options.ClientTimeoutInterval = TimeSpan.FromSeconds(30);
})
.AddMessagePackProtocol(); // Binary protocol for performance

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Events = new JwtBearerEvents
        {
            OnMessageReceived = context =>
            {
                // Read token from query string for WebSocket connections
                var accessToken = context.Request.Query["access_token"];
                var path = context.HttpContext.Request.Path;
                if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs"))
                {
                    context.Token = accessToken;
                }
                return Task.CompletedTask;
            }
        };
    });

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

app.MapHub<ChatHub>("/hubs/chat");
从 Hub 外部发送消息
public class NotificationService
{
    private readonly IHubContext<NotificationHub, INotificationClient> _hubContext;

    public NotificationService(IHubContext<NotificationHub, INotificationClient> hubContext)
    {
        _hubContext = hubContext;
    }

    public async Task NotifyAllAsync(string message)
    {
        await _hubContext.Clients.All.ReceiveNotification(message);
    }

    public async Task NotifyUserAsync(string userId, string message)
    {
        await _hubContext.Clients.User(userId).ReceiveNotification(message);
    }

    public async Task NotifyGroupAsync(string groupName, string message)
    {
        await _hubContext.Clients.Group(groupName).ReceiveNotification(message);
    }
}

使用 Redis Backplane 进行扩展

builder.Services.AddSignalR()
    .AddStackExchangeRedis(connectionString, options =>
    {
        options.Configuration.ChannelPrefix = RedisChannel.Literal("MyApp");
    });

应避免的反模式

反模式不足之处更好的做法
将状态存储在 Hub 属性中Hub 实例按每次方法调用创建使用 IMemoryCache、数据库或外部存储
直接实例化 Hub绕过 SignalR 基础设施使用 IHubContext<THub> 发送外部消息
未等待 SendAsync 调用完成Hub 方法完成前,消息可能尚未发送始终对异步 Hub 调用使用 await
添加方法参数时不进行版本管理会破坏现有客户端使用自定义对象参数
忽略重连时组成员身份丢失的问题客户端重连后会失去组成员身份在 OnConnectedAsync 或客户端重连处理程序中重新加入组
通过 SignalR 发送大型负载会造成内存压力和带宽问题批量数据使用 REST/gRPC,通知使用 SignalR
多服务器场景未配置 backplane消息只能到达同一服务器上的客户端使用 Redis backplane 或 Azure SignalR Service
直接公开 ORM 实体可能会序列化敏感数据使用属性明确的 DTO
未验证传入的消息初始身份验证后仍存在安全风险验证每个 Hub 方法的输入

最佳实践

连接管理
  1. 启用自动重连,并采用指数退避延迟
  2. 明确处理重连后的重新加入组(连接 ID 会发生变化)
  3. 在客户端实现心跳监控,以检测失效连接
  4. 在多个服务器之间扩展时使用粘性会话(除非使用 Azure SignalR Service)
性能
  1. 使用 MessagePack 协议,以缩小消息体积并加快序列化速度
  2. 限制高频事件的速率,例如正在输入指示或鼠标移动
  3. 尽可能批量发送消息,而不是发送许多小消息
  4. 根据预期的消息吞吐量设置适当的缓冲区大小
安全
  1. 在连接时进行身份验证,通过查询字符串使用 JWT 令牌
  2. 使用 [Authorize] 属性授权 Hub 方法
  3. 即使身份验证完成后,也要验证所有传入的消息
  4. 所有 SignalR 连接都使用 HTTPS
API 设计
  1. 使用强类型 Hub,以便在编译时发现客户端方法名称中的拼写错误
  2. 使用自定义对象参数,以便向后兼容地演进 API
  3. 对 Hub 名称进行版本管理(例如 ChatHubV2),以应对破坏性变更
  4. 保持 Hub 方法精简,并将业务逻辑委托给服务
可观测性
  1. 记录连接事件(连接、断开、重连)
  2. 跟踪每个连接使用的传输类型
  3. 监控消息传递延迟和失败率
  4. 与 Application Insights 或其他 APM 工具集成

交付内容

  • 清晰的 Hub 契约和连接行为
  • 符合产品场景的实时消息传递
  • 对重连和授权流程进行验证
  • 适用于多服务器部署的扩展策略

验证

  • SignalR 是适用于该用例的传输方式
  • Hub 方法仍以编排为主
  • 组和身份验证行为明确且经过测试
  • 正确处理重连和组成员身份
  • 为多服务器场景配置 backplane
  • 在 Hub 方法中实现消息验证

评论

支持 Markdown