基本介紹
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 是一個經過生產驗證的成熟框架,解決了實時通信中的常見痛點。既然有穩定可靠的輪子,就沒有必要重復造輪子,可以更專注于業務邏輯的實現。 |

上手實踐
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.html和js測試代碼
<!-- 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 相比,有設計上的優勢,先看下面不使用強類型的截圖。

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 模式下從查詢字符串傳遞 token
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
// 如果是 SignalR 長連接,且路徑是 /chatHub,則從查詢字符串讀取 token
var 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.上面的方式在單體系統是不會有問題的,但是如果服務實例負載均衡后開啟了多個實例,會存在回話丟失的問題

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) |
| 消息一致性 | 依賴路由 | 通過中間件保證 |
| 推薦 | ? 不推薦用于生產 | ? 推薦 |
| 適用場景 | 小型項目、測試環境 | 生產環境、微服務、云部署 |
浙公網安備 33010602011771號