.NET Core中使用SignalR

news/2025/9/20 8:34:32/文章来源:https://www.cnblogs.com/yuxl01/p/18965903

基本介绍

1.什么是signalR

SignalR 是微软开发的一个开源库,它可以让服务器端代码能够即时推送内容到连接的客户端,用来简化向客户端应用程序添加实时功能的过程。

  • 大白话的意思就是微软搞了一个可以用来做服务端推送的库,并且都是帮你封装好了的,你不用操心,用就完了

特点:

概念 说明
双工通信 服务端和客户端可以互相发送数据,互不干扰,实现双向实时通信。例如,Web API 的 Controller 是单向请求-响应模式,而 SignalR 的 Hub 支持服务端主动推消息给客户端,客户端也能调用服务端方法,形成双向交互。
传输降级 SignalR 会自动选择最佳的通信方式。优先尝试 WebSocket,若不支持则依次降级为 Server-Sent Events 或长轮询等。整个过程对开发者透明,确保在各种浏览器和网络环境下都能建立连接。
简化开发 SignalR 内置了心跳检测、连接存活检查、断线自动重连等机制,开发者无需手动实现这些复杂逻辑,极大降低了实时通信功能的开发难度。
Hub 可以理解为一个“实时通信中转站”。就像你和朋友之间有一个快递站(Hub),你想发消息(寄快递),就把包裹交给 Hub;朋友也能通过同一个 Hub 寄快递给你。Hub 负责把消息准确送达对方,并通知接收方有新消息到来。它是 SignalR 的核心通信中心。

主要应用场景:

应用场景 说明 是否推荐
实时消息传递 搭建聊天室、下象棋等需要低延迟双向通信的场景,非常适合 SignalR。 ✅ 推荐
实时消息通知 向客户端推送通知(如系统提醒、订单状态更新等),是 SignalR 的典型用例。 ✅ 推荐
数据统计看板 实时将动态数据推送给前端看板(如销售数据、监控指标),体验流畅。 ✅ 推荐
服务之间通信 理论上可行,但不建议使用 SignalR 进行服务间通信。应使用 gRPC、REST、消息队列等更合适的方案。 ❌ 不建议

当然根据他的特性还能能延伸出更多应用场景,但目前在实际开发中,我使用SignalR的场景就是:
1.在业务系统内部作为站内信发送通知。
2.作为前端实时数据看板展示的业务数据,刚毕业那会,前端用js 开一堆定时器来请求后端接口刷新数据报表,每隔2天就让客户去F5一下浏览器,客户问为什么,当时的解释是电脑不行 😄。

2.WebSocket 和 SignalR

其实说到推送相关的话题,有很多实现方式,应用上使用纯WebSocket也可以实现,简单方便,最终还是得结合自己的实际需求来权衡,例如拿巴掌拍蚊子,和拿大炮打蚊子,方法上都行得通,最重要的是把握这个“度”,这里就不一一列举了.

为什么不直接用 WebSocket 说明
1. 开发效率高 SignalR 封装了底层细节,提供了开箱即用的 API,屏蔽了连接管理、序列化、异常处理等复杂逻辑,大幅提升了开发效率。虽然相比原生 WebSocket 有一定性能损耗,但换来了极高的生产力。
2. 支持传输降级 SignalR 能根据客户端环境自动选择最佳传输方式(WebSocket → Server-Sent Events → 长轮询)。在复杂网络环境或老旧浏览器中仍能保持连接,而纯 WebSocket 在不支持或被代理阻塞时会直接失败。
3. 省去基础设施开发 若使用原生 WebSocket,需自行实现心跳检测、断线重连、消息确认、集群同步等机制,开发和维护成本高。SignalR 已内置这些功能,开箱即用。
4. 已有成熟框架,何不善用? SignalR 是一个经过生产验证的成熟框架,解决了实时通信中的常见痛点。既然有稳定可靠的轮子,就没有必要重复造轮子,可以更专注于业务逻辑的实现。

image

上手实践

1.基本概念

角色 说明
服务端 为你提供消息推送服务的后端应用程序,负责处理连接、业务逻辑,并通过 SignalR 向客户端主动发送数据。
客户端 接收消息的一方,可以是浏览器(JavaScript)、移动应用、桌面程序等,通过连接到 Hub 来接收服务端推送的实时消息。
Hub SignalR 中客户端与服务端进行消息交换和推送的核心抽象代理。它相当于一个“通信中心”,客户端和服务端通过 Hub 进行双向方法调用和消息传递。
属性 说明
ConnectionId 获取连接的唯一 ID(在连接时 SignalR 分配)。
UserIdentifier 用户标识一般是用户id,关联连接和用户。
User 当前用户的 ClaimsPrincipal(身份信息)。
Items 一些共同的数据可以在连接建立后加载,然后存到Items,在后续不同的方法中都可以访问到,不过数据仅存在内存中,连接断开后自动销毁
ConnectionAborted 获取一个 CancellationToken,它会在客户端连接中止时发出通知,比 OnDisconnectedAsync更早触发,更及时。

Items属性举例

// 连接建立查询数据库获取用户信息
public override async Task OnConnectedAsync()
{var user = await db.GetUserAsync(Context.UserIdentifier);Context.Items["UserProfile"] = user; // 缓存用户数据
}// 在其他方法中获取Items拿到
public async Task SendMessage(string message)
{var user = (UserProfile)Context.Items["UserProfile"]; // 直接读取缓存// ... 使用用户数据
}

2.基本使用

后端代码使用.net7,客户端使用js,分为2部分,基本使用,以及更加贴合业务的实现

先铺垫一下,这一部分有条件自己试一下向个人,所有人,以及组发送消息的api,后续会分享它内部实现,也很巧妙。

1.首先注入使用SignalR需要的相关服务和配置

services.AddSignalR(options =>
{options.ClientTimeoutInterval = TimeSpan.FromMinutes(1);options.KeepAliveInterval = TimeSpan.FromSeconds(10);options.EnableDetailedErrors = true;
})

2.定义一个Hub

// Hubs/PushMsgHub.cs
using Microsoft.AspNetCore.SignalR;public class PushMsgHub : Hub
{// 连接事件public override async Task OnConnectedAsync(){await base.OnConnectedAsync();}// 断开连接事件public override async Task OnDisconnectedAsync(Exception? exception){UserManager.RemoveUser(Context.ConnectionId);await base.OnDisconnectedAsync(exception);}// 客户端调用此方法登录并注册用户信息public async Task Login(string userId, string name, string companyId, string orgId){var user = new User{ConnectionId = Context.ConnectionId,UserId = userId,Name = name,CompanyId = companyId,OrgId = orgId};UserManager.AddUser(user);await Clients.Caller.SendAsync("ReceiveMessage", "系统", $"{name}登录成功!");}// 发送消息给指定用户public async Task SendMessageToUser(string toUserId, string message){var fromUser = UserManager.GetUserByConnectionId(Context.ConnectionId);var toUser = UserManager.GetUserById(toUserId);if (toUser != null){await Clients.Client(toUser.ConnectionId).SendAsync("ReceiveMessage", $"{fromUser.Name} (私信)", message);}else{await Clients.Caller.SendAsync("ReceiveMessage", "系统", "用户不在线或不存在。");}}// 发送给组织内所有用户public async Task SendMessageToOrg(string message){var fromUser = UserManager.GetUserByConnectionId(Context.ConnectionId);var users = UserManager.GetUsersByOrg(fromUser.OrgId);foreach (var user in users){await Clients.Client(user.ConnectionId).SendAsync("ReceiveMessage", $"【组织】{fromUser.Name}", message);}}// 发送给公司内所有用户public async Task SendMessageToCompany(string message){var fromUser = UserManager.GetUserByConnectionId(Context.ConnectionId);var users = UserManager.GetUsersByCompany(fromUser.CompanyId);foreach (var user in users){await Clients.Client(user.ConnectionId).SendAsync("ReceiveMessage", $"【公司】{fromUser.Name}", message);}}
}

3.然后将Hub映射到中间件管道路由中

app.MapHub<ChatHub>("/PushMsgHub");

4.htmljs测试代码

<!-- index.html -->
<!DOCTYPE html>
<html>
<head><title>SignalR Demo</title>
</head>
<body><h2>SignalR 消息系统</h2><div><label>用户ID: <input id="userId" value="u001" /></label><label>姓名: <input id="name" value="张三" /></label><label>公司ID: <input id="companyId" value="comp001" /></label><label>组织ID: <input id="orgId" value="org001" /></label><button onclick="login()">登录</button></div><hr /><div><h3>发送私信</h3><input id="toUserId" placeholder="目标用户ID" value="u002" /><input id="privateMsg" placeholder="输入私信内容" /><button onclick="sendPrivate()">发送</button></div><div><h3>发送组织消息</h3><input id="orgMsg" placeholder="组织内广播消息" /><button onclick="sendToOrg()">发送</button></div><div><h3>发送公司消息</h3><input id="companyMsg" placeholder="公司内广播消息" /><button onclick="sendToCompany()">发送</button></div><hr /><h3>消息记录</h3><ul id="messages"></ul><script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/8.0.0/signalr.min.js"></script><script>const connection = new signalR.HubConnectionBuilder().withUrl("http://localhost:5200/PushMsgHub").build();function log(message) {const li = document.createElement("li");li.textContent = message;document.getElementById("messages").appendChild(li);}connection.on("ReceiveMessage", (user, message) => {log(`${user}: ${message}`);});connection.start().then(() => log("连接到 SignalR 服务器")).catch(err => console.error(err));async function login() {const userId = document.getElementById("userId").value;const name = document.getElementById("name").value;const companyId = document.getElementById("companyId").value;const orgId = document.getElementById("orgId").value;await connection.invoke("Login", userId, name, companyId, orgId);}async function sendPrivate() {const toUserId = document.getElementById("toUserId").value;const msg = document.getElementById("privateMsg").value;await connection.invoke("SendMessageToUser", toUserId, msg);}async function sendToOrg() {const msg = document.getElementById("orgMsg").value;await connection.invoke("SendMessageToOrg", msg);}async function sendToCompany() {const msg = document.getElementById("companyMsg").value;await connection.invoke("SendMessageToCompany", msg);}</script>
</body>
</html>

3.强类型Hub用法

与传统的直接继承 Hub 相比,有设计上的优势,先看下面不使用强类型的截图。

image

1.先定义一个接口

public interface IPushMessageHubAsync
{Task ReceiveMessage(string message);
}

2.然后优化这个Hub像这样写

public class MsgPushHub : Hub<IPushMessageHubAsync>
{public async Task SendMessage(string message){await Clients.All.ReceiveMessage(message);}
}

IPushMessageHubAsync: 用于约定具体推送的业务类型,这里的接口名就是实际推送到客户端的名字,同理如果有报表展示的需要可以定义一个为 Hub,在业务层面不需要实现它们,只是为了规范业务和参数标准化, 但是框架层面运行时会为他们创建代理.

  • 罗列的对比
对比项 Hub<IClientContract>(强类型) 直接继承 Hub(弱类型)
类型安全 ✅ 编译时检查方法名、参数 ❌ 运行时才报错(字符串魔法值)
修改 ✅ 改方法名时 IDE 自动提示,接口与实现同步更新 ❌ 手动修改所有字符串调用,易遗漏出错
代码可读性 ✅ 接口明确定义服务端可调用的客户端方法,通信契约清晰 ❌ 方法名散落在 SendAsync("MethodName") 中,不易维护
单元测试 ✅ 可轻松 Mock 客户端接口,便于测试 Hub 逻辑 ❌ 需模拟字符串发送逻辑,测试复杂且脆弱

4.鉴权

1.安装 NuGet 包(如果还没加)

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

2.标准的.netCore集成鉴权

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>{options.TokenValidationParameters = new TokenValidationParameters{//.....};// SignalR 要求在 WebSocket 模式下从查询字符串传递 tokenoptions.Events = new JwtBearerEvents{OnMessageReceived = context =>{var accessToken = context.Request.Query["access_token"];// 如果是 SignalR 长连接,且路径是 /chatHub,则从查询字符串读取 tokenvar path = context.HttpContext.Request.Path;if (!string.IsNullOrEmpty(accessToken) &&path.StartsWithSegments("/PushMsgHub")) // 你的 Hub 路径{context.Token = accessToken;}return Task.CompletedTask;}};});

3.然后再hub上使用 Authorize 标记

[Authorize]
public class MsgPushHub : Hub<IPushMessageHubAsync>
{public async Task SendMessage(string message){await Clients.All.ReceiveMessage(message);}
}

3.遇到的问题

1.上面的方式在单体系统是不会有问题的,但是如果服务实例负载均衡后开启了多个实例,会存在回话丢失的问题

image
A)

产生的原因其实根据上面的图就能理解,因为SignalR 默认使用内存状态存储连接信息,每个实例独立维护自己的客户端连接表。

解决方案:

1.启用粘性会话网关层根据ip和实例绑定,确保同一个客户端的所有请求都路由到同一个后端实例。不需要额外组件(如 Redis),配置简单,适合小规模部署。但是也存在一些缺点如下:

  • 单节点故障:如果该实例宕机,所有连接丢失。
  • 负载不均:某些实例可能连接过多。
  • 无法弹性伸缩:新增/删除实例时,部分用户会断连。
  • 违反微服务无状态原则。

2.启用底板机制横向扩展,因为启用横向扩展后,所有会话状态(连接、组、用户映射)均存储在外部,例如Redis中,而不是单机内存,但是需要引入外部依赖,增加复杂度,但是也有显著的优点如下:

  • 真正的高可用和弹性伸缩。
  • 实例宕机不影响整体服务(其他实例可接管)。
  • 支持动态扩缩容。
  • 符合云原生架构。

1.集成SignalR.StackExchangeRedis

1.先安装扩展库

dotnet add package Microsoft.AspNetCore.SignalR.StackExchangeRedis

2.在注册服务时加入扩展的代码

builder.Services.AddSignalR()
.AddStackExchangeRedis("redis-connection-string", options =>
{options.Configuration.ChannelPrefix = "SignalR"; // 可选:命名空间前缀
});
对比项 粘性会话 背板机制
架构模式 有状态 无状态
扩展性 差(受限于单实例容量) 好(可水平扩展)
可用性 差(实例宕机即断连) 好(故障转移)
部署复杂度 中(需 Redis)
消息一致性 依赖路由 通过中间件保证
推荐 ❌ 不推荐用于生产 ✅ 推荐
适用场景 小型项目、测试环境 生产环境、微服务、云部署

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/908216.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Django + Vue3 前后端分离工艺实现自动化测试平台从零到有系列 <第一章> 之 注册登录完成

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

实用指南:【保姆级教程】TEXTurePaper运行环境搭建与Stable Diffusion模型本地化

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

实用指南:修复Conda连接异常:CondaHTTPError HTTP 000 CONNECTION FAILED故障排除指南

实用指南:修复Conda连接异常:CondaHTTPError HTTP 000 CONNECTION FAILED故障排除指南pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important…

高级数据结构手册

LCA //exam:P3379 【模板】最近公共祖先(LCA) #include <iostream> #include <cstdio> #include <vector> #define int long long using namespace std; const int MAXN=5e5+5,MAXM=25; void dfs…

3634501 - [CVE-2025-42944] Insecure Deserialization vulnerability in SAP Netweaver (RMI-P4)

3634501 - [CVE-2025-42944] Insecure Deserialization vulnerability in SAP Netweaver (RMI-P4)Symptom Due to a deserialization vulnerability in SAP NetWeaver, an unauthenticated attacker could exploit the…

【无人艇协同】基于matlab面向海事安全的双体无人艇分布式协同任务规划(目标函数:总时间满意度)【含Matlab源码 14161期】博士论文 - 教程

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

实用指南:Unity 打包 iOS,Xcode 构建并上传 App Store

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

实用指南:GitHub 热榜项目 - 日榜(2025-09-09)

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

深入解析:【Fiora深度解析】手把手教你用固定公网IP搭建专属聊天系统!

深入解析:【Fiora深度解析】手把手教你用固定公网IP搭建专属聊天系统!2025-09-20 08:13 tlnshuju 阅读(0) 评论(0) 收藏 举报pre { white-space: pre !important; word-wrap: normal !important; overflow-x: au…

使用JavaScript和CSS创建动态高亮导航栏

本文详细介绍了两种实现动态高亮导航栏的技术方案:第一种使用getBoundingClientRect方法精确计算元素位置和尺寸,第二种利用新兴的View Transition API简化动画实现。文章包含完整的代码示例和实际演示,适合前端开发…

wxt 开发浏览器插件的框架

wxt 开发浏览器插件的框架wxt 开发浏览器插件的框架 支持的特性支持所有浏览器 支持mv2 以及mv3 协议 开发模式支持热更新 基于文件的entrypoints 基于ts 开发 支持自动导入 自动发布 支持vue,react,svelte 等框架说…

Gridspech 全通关

You made it to the end of Gridspech. Thank you for playing!!A1A2A3A4A5A6A7A8A9A10A11A12A13A14

20253320蒋丰任

1.我叫蒋丰任,是一个阳光开朗大男孩,因为有一首我挺喜欢的歌就叫这个,同时我的朋友和我自己都认为我是一个外向的社牛(在广东,到了北京,比起东北大哥的热情,我自愧不如)。 2.办公软件的使用(Excel),一定要谦…

又有两位智驾大牛联手入局具身智能机器人赛道创业,已完成数亿元融资!

微信视频号:sph0RgSyDYV47z6快手号:4874645212抖音号:dy0so323fq2w小红书号:95619019828B站1:UID:3546863642871878B站2:UID: 3546955410049087最新资讯,[元璟资本]投资合伙人、原[理想汽车]CTO王凯已入局具身智…

纯国产GPU性能对比,谁才是国产算力之王?

微信视频号:sph0RgSyDYV47z6快手号:4874645212抖音号:dy0so323fq2w小红书号:95619019828B站1:UID:3546863642871878B站2:UID: 3546955410049087 显存规格:存储能力大比拼在显存规格这一块,百度昆仑芯 3 代 P8…

地平线明年发布并争取量产舱驾一体芯片;比亚迪补强智舱团队,斑马智行原 CTO 加入

微信视频号:sph0RgSyDYV47z6快手号:4874645212抖音号:dy0so323fq2w小红书号:95619019828B站1:UID:3546863642871878B站2:UID: 3546955410049087 地平线舱驾一体芯片 2026 年发布与量产汽车智能芯片的竞赛还在继续…

英伟达入股英特尔,当竞争对手便成协作者,真正受益的......

微信视频号:sph0RgSyDYV47z6快手号:4874645212抖音号:dy0so323fq2w小红书号:95619019828B站1:UID:3546863642871878B站2:UID: 3546955410049087就在今天(9月18日),全球半导体行业迎来历史性时刻——英伟达宣布…

ODT/珂朵莉树 入门

主打一个看到别人学什么我学什么,反正什么也不会。 什么是 ODT 是一种数据结构 类比线段树的话,他的每一条线段(一个基本单位)记录了相同 "颜色" 的东西的信息 使用一个结构体的 \(set\),记录 区间 \([…

博客更新公告

来看看博客更新公告吧rt. 公示最新更新或发布的博客, 供大家查阅. 更新日志 Upd 2025.9.18 新随笔 Skywalk -- Words to be remembered 2025.9.18 网址: https://www.cnblogs.com/hsy8116/p/19099273.Upd 2025.9.12 新…