关于 signalr
面向使用 ASP.NET Core 构建或运维实时功能的 SignalR Skill,涵盖 Hub 设计、连接行为、安全、测试及扩展决策。
主要功能
- 01Hub 契约与 API 设计
- 02重连与分组处理
- 03身份验证与输入校验
- 04传输方式和故障测试
- 05多服务器扩展指导
适用场景
- 01构建聊天与通知功能
- 02排查连接或传输问题
- 03向客户端分组广播消息
- 04规划 SignalR 多服务器部署
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ managedcode/ dotnet-skills --skill signalr安装后在 Claude Code 中输入/signalr确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
manifest.jsonreferences/anti-patterns.mdpatterns.md详细介绍
SignalR
适用场景
- 构建聊天、通知、协作或实时更新功能
- 调试 Hub 生命周期、连接状态或传输问题
- 判断 SignalR 或其他传输方式是否更适合当前场景
- 实现向多个已连接客户端实时广播
- 在多台服务器间扩展 SignalR
文档
参考资料
- patterns.md - Hub 模式、流式处理、组、在线状态和高级消息传递技术的详细说明
- anti-patterns.md - 常见的 SignalR 错误及其避免方法
工作流
- 对于广播式或面向连接的实时功能,使用 SignalR;不要在 Hub 式扇出场景中强行使用 gRPC。
- 有意识地设计 Hub 合约,并保持 Hub 方法精简,将持久化工作交由其他部分处理。
- 规划重连、背压、身份验证和扇出成本,不要将实时消息传递视为无状态的请求/响应。
- 有计划地使用组、在线状态和连接元数据,以便清楚了解扩展后的行为。
- 如果涉及 Native AOT 或裁剪,请明确验证受支持的协议和序列化选项。
- 测试连接行为和故障模式,而不仅仅是顺利交付消息的情况。
当前上游说明
dotnet/aspnetcorev10.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 方法的输入 |
最佳实践
连接管理
- 启用自动重连,并采用指数退避延迟
- 明确处理重连后的重新加入组(连接 ID 会发生变化)
- 在客户端实现心跳监控,以检测失效连接
- 在多个服务器之间扩展时使用粘性会话(除非使用 Azure SignalR Service)
性能
- 使用 MessagePack 协议,以缩小消息体积并加快序列化速度
- 限制高频事件的速率,例如正在输入指示或鼠标移动
- 尽可能批量发送消息,而不是发送许多小消息
- 根据预期的消息吞吐量设置适当的缓冲区大小
安全
- 在连接时进行身份验证,通过查询字符串使用 JWT 令牌
- 使用
[Authorize]属性授权 Hub 方法 - 即使身份验证完成后,也要验证所有传入的消息
- 所有 SignalR 连接都使用 HTTPS
API 设计
- 使用强类型 Hub,以便在编译时发现客户端方法名称中的拼写错误
- 使用自定义对象参数,以便向后兼容地演进 API
- 对 Hub 名称进行版本管理(例如
ChatHubV2),以应对破坏性变更 - 保持 Hub 方法精简,并将业务逻辑委托给服务
可观测性
- 记录连接事件(连接、断开、重连)
- 跟踪每个连接使用的传输类型
- 监控消息传递延迟和失败率
- 与 Application Insights 或其他 APM 工具集成
交付内容
- 清晰的 Hub 契约和连接行为
- 符合产品场景的实时消息传递
- 对重连和授权流程进行验证
- 适用于多服务器部署的扩展策略
验证
- SignalR 是适用于该用例的传输方式
- Hub 方法仍以编排为主
- 组和身份验证行为明确且经过测试
- 正确处理重连和组成员身份
- 为多服务器场景配置 backplane
- 在 Hub 方法中实现消息验证
评论
支持 Markdown