本系列专栏基于杨中科老师的《ASP.NET Core技术内幕与项目 实战》,本人记录梳理的学习笔记,有部分的增补和省略。更全面系统的讲解,请看杨老师的视频课:【.NET教程,.Net Core视频教程,杨中科主讲】。
一、服务器向客户端推送数据
1. 传统方案:长轮询
- 原理:客户端发起请求→服务器阻塞请求,直到有数据 / 超时→客户端收到响应后立即发起新请求,循环往复。
- 缺点:频繁 HTTP 请求 / 响应,开销大、延迟高、并发能力弱,仅作为降级方案。
2. WebSocket:现代实时通信标准
- 基于TCP 协议 ,支持二进制 + 文本 双工通信,客户端和服务器可双向主动发送数据。
- 性能优势:一次握手,持久连接,开销远小于长轮询,并发能力大幅提升。
- 部署特点:独立于 HTTP 协议,但复用 HTTP 服务器端口 + 借助 HTTP 完成初始握手(无需单独端口,兼容性更强)。
- 局限:原生 API 繁琐,需手动处理连接、重连、序列化、兼容降级等问题。
3. ASP.NET Core SignalR:WebSocket 高级封装
- 定位:.NET Core 平台下实时通信的一站式解决方案,自动适配通信方式(优先 WebSocket,降级 SSE / 长轮询)。
- 核心概念:Hub(集线器) → 实时通信的数据交换中心,统一管理客户端连接、消息分发、组管理。
- 优势:简化 API、自动重连、依赖注入友好、支持分布式部署、集成身份认证。
二、SignalR 项目搭建
核心流程
创建 Web API 服务端 → 配置 SignalR 与跨域 → 编写 Hub 集线器 → Vue 前端接入 → 实现全员消息推送。
1. 服务端开发
(1)环境准备
创建**ASP.NET Core Web API** 项目,无需额外安装基础 NuGet 包(ASP.NET Core 内置 SignalR)。
(2)编写 Hub 集线器
Hub 是消息处理核心,继承自Hub基类,定义客户端可调用的方法:
cs
/// <summary>
/// 聊天室集线器,客户端通过此Hub与服务端通信
/// </summary>
public class ChatRoomHub : Hub
{
/// <summary>
/// 发送公共消息(全员接收)
/// 客户端通过Invoke调用此方法
/// </summary>
/// <param name="message">客户端发送的消息内容</param>
public Task SendPublicMessage(string message)
{
// 获取当前客户端的唯一连接ID
string connId = this.Context.ConnectionId;
string msg = $"{connId} {DateTime.Now}: {message}";
// 向所有连接的客户端推送消息(客户端监听ReceivePublicMessage事件)
return Clients.All.SendAsync("ReceivePublicMessage", msg);
}
}
(3)注册 SignalR 与跨域配置
SignalR 基于浏览器跨域策略,必须配置 CORS,否则前端无法连接:
cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
// 1. 注册SignalR服务
builder.Services.AddSignalR();
// 2. 配置跨域策略(允许前端地址访问)
string[] urls = new[] { "http://localhost:3000" }; // 前端Vue运行地址
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
{
policy.WithOrigins(urls) // 允许的源
.AllowAnyMethod() // 允许所有请求方法
.AllowAnyHeader() // 允许所有请求头
.AllowCredentials(); // 允许携带凭据(Cookie/Token,必选!)
});
});
var app = builder.Build();
// 启用中间件(顺序不能错)
app.UseCors(); // 启用跨域
app.UseHttpsRedirection();
app.UseAuthorization();
// 3. 映射Hub端点(客户端通过此地址连接SignalR)
app.MapHub<ChatRoomHub>("/Hubs/ChatRoomHub");
app.MapControllers();
app.Run();
2. 前端开发(Vue3)
(1)安装客户端 SDK
bash
npm install @microsoft/signalr
(2)编写实时聊天组件
javascript
<template>
<div>
<!-- 消息输入框(回车发送) -->
<input
type="text"
v-model="state.userMessage"
@keypress="handleKeyPress"
placeholder="输入消息,回车发送"
/>
<!-- 消息列表 -->
<ul>
<li v-for="(msg, index) in state.messages" :key="index">
{{ msg }}
</li>
</ul>
</div>
</template>
<script setup>
import { reactive, onMounted } from 'vue';
import * as signalR from '@microsoft/signalr';
// 响应式数据
const state = reactive({
userMessage: '', // 输入框消息
messages: [] // 消息列表
});
let connection; // SignalR连接对象
// 页面挂载后建立SignalR连接
onMounted(async () => {
// 1. 配置连接
connection = new signalR.HubConnectionBuilder()
.withUrl('https://localhost:7112/Hubs/ChatRoomHub') // 服务端Hub地址
.withAutomaticReconnect() // 自动重连(断线自动恢复)
.build();
// 2. 监听服务端推送的公共消息
connection.on('ReceivePublicMessage', (msg) => {
state.messages.push(msg);
});
// 3. 启动连接
try {
await connection.start();
console.log('SignalR连接成功');
} catch (err) {
console.error('连接失败:', err);
}
});
// 回车发送消息
const handleKeyPress = async (e) => {
if (e.keyCode !== 13) return; // 非回车直接返回
if (!state.userMessage.trim()) return;
// 调用服务端Hub的SendPublicMessage方法
await connection.invoke('SendPublicMessage', state.userMessage);
state.userMessage = ''; // 清空输入框
};
</script>
三、集群部署
1. 协议协商问题
当 SignalR 部署在服务器集群 时,会出现协议协商失败:
- 第一步:客户端发起协商请求,被服务器 A 处理;
- 第二步:后续 WebSocket 请求被负载均衡转发到服务器 B;
- 结果:连接丢失,通信失败。
2. 解决方案
方案 1:粘性会话
- 原理:负载均衡配置会话绑定 ,同一客户端的所有请求强制转发到同一台服务器。
- 缺点:公网 IP 共享会导致负载不均;集群扩容适应性差,无法灵活弹性伸缩。
方案 2:禁用协商(推荐)
直接强制使用 WebSocket 协议,跳过协商步骤,一次连接绑定单台服务器,无需依赖负载均衡配置。
- 优点:简单高效,无粘性会话缺陷;
- 缺点:无法降级到 SSE / 长轮询(现代浏览器均支持 WebSocket,几乎无影响)。
前端修改代码:
cs
// 配置:跳过协商 + 仅使用WebSocket
const transportOptions = {
skipNegotiation: true,
transport: signalR.HttpTransportType.WebSockets
};
connection = new signalR.HubConnectionBuilder()
.withUrl('https://localhost:7047/Hubs/ChatRoomHub', transportOptions)
.withAutomaticReconnect()
.build();
四、分布式部署
1. 跨服务器消息同步问题
集群中 4 个客户端分别连接服务器 A、B,A 服务器的消息无法推送给连接 B 的客户端(消息仅在单服务器内广播)。
2. 解决方案:Redis 背板
所有服务器连接同一个 Redis,通过 Redis 做消息中转,实现跨服务器消息同步。
实施步骤
-
安装 NuGet 包
bashInstall-Package Microsoft.AspNetCore.SignalR.StackExchangeRedis -
注册 Redis 背板
csbuilder.Services.AddSignalR() // 配置Redis连接字符串 + 通道前缀(区分项目) .AddStackExchangeRedis("127.0.0.1:6379", options => { options.Configuration.ChannelPrefix = "MyChatApp_"; }); -
**原理:**服务端发送消息→写入 Redis→所有服务器订阅 Redis 消息→分发给自身连接的客户端,实现全局广播。
五、身份认证
1. JWT集成问题
默认 SignalR 无权限控制,任何人可连接 Hub,需集成 JWT 实现登录后访问。
2. 服务端配置
(1)定义 JWT 配置类
cs
/// <summary>
/// JWT配置参数(从appsettings.json读取)
/// </summary>
public class JWTOptions
{
public string SigningKey { get; set; } // 密钥
public int ExpireSeconds { get; set; } // 过期时间
}
(2)appsettings.json 配置
"JWT": {
"SigningKey": "YourSecretKey1234567890123456", // 长度≥16位
"ExpireSeconds": 86400 // 1天过期
}
(3)注册 JWT 认证
cs
// 绑定JWT配置
services.Configure<JWTOptions>(builder.Configuration.GetSection("JWT"));
// 注册JWT认证
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(x =>
{
var jwtOpt = builder.Configuration.GetSection("JWT").Get<JWTOptions>();
var keyBytes = Encoding.UTF8.GetBytes(jwtOpt.SigningKey);
var secKey = new SymmetricSecurityKey(keyBytes);
// Token验证规则
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = secKey
};
// 关键:SignalR通过URL参数传递Token(WebSocket不支持请求头)
x.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
// 从URL的access_token参数获取Token
var accessToken = context.Request.Query["access_token"];
var path = context.HttpContext.Request.Path;
// 匹配Hub地址则赋值Token
if (!string.IsNullOrEmpty(accessToken) &&
path.StartsWithSegments("/Hubs/ChatRoomHub"))
{
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
(4)启用认证中间件
cs
// 顺序:认证 → 授权
app.UseAuthentication(); // 新增
app.UseAuthorization();
(5)Hub 添加权限校验
在 Hub 类上添加[Authorize]特性,未登录用户无法连接:
cs
[Authorize] // 必须登录才能访问此Hub
public class ChatRoomHub : Hub
{
// ...
}
(6)编写登录接口
cs
[ApiController]
[Route("api/[controller]")]
public class AccountController : ControllerBase
{
private readonly IOptions<JWTOptions> _jwtOptions;
public AccountController(IOptions<JWTOptions> jwtOptions)
{
_jwtOptions = jwtOptions;
}
[HttpPost("login")]
public IActionResult Login(string username, string password)
{
// 省略用户名密码校验
var userId = "1001"; // 实际从数据库获取
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, userId), // 用户唯一ID(定向推送用)
new Claim(ClaimTypes.Name, username) // 用户名
};
// 生成Token
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtOptions.Value.SigningKey));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
expires: DateTime.Now.AddSeconds(_jwtOptions.Value.ExpireSeconds),
signingCredentials: creds,
claims: claims
);
return Ok(new { Token = new JwtSecurityTokenHandler().WriteToken(token) });
}
}
3. 前端携带 Token 连接
javascript
// 登录后获取Token
const startConnection = async (token) => {
const options = {
skipNegotiation: true,
transport: signalR.HttpTransportType.WebSockets,
accessTokenFactory: () => token // 自动携带Token到URL
};
connection = new signalR.HubConnectionBuilder()
.withUrl('https://localhost:7047/Hubs/ChatRoomHub', options)
.withAutomaticReconnect()
.build();
await connection.start();
};
六、定向推送
SignalR 支持三种筛选方式,满足私聊、分组、全员推送场景:
ConnectionId:单个客户端连接 ID;Groups:客户端分组(如房间、部门);UserId:用户唯一 ID(对应ClaimTypes.NameIdentifier)。
实战:用户私聊
1. 服务端 Hub 方法
cs
/// <summary>
/// 发送私信(指定用户接收)
/// </summary>
/// <param name="destUserName">目标用户名</param>
/// <param name="message">消息内容</param>
public async Task<string> SendPrivateMessage(string destUserName, string message)
{
// 1. 根据用户名查询目标用户ID(实际从数据库获取)
string destUserId = await _userService.GetUserIdByNameAsync(destUserName);
// 2. 获取当前登录用户信息
string srcUserName = this.Context.User.FindFirst(ClaimTypes.Name)!.Value;
string time = DateTime.Now.ToShortTimeString();
// 3. 向指定用户ID推送消息
await Clients.User(destUserId).SendAsync("ReceivePrivateMessage",
srcUserName, time, message);
return "发送成功";
}
2. 前端调用与监听
javascript
// 发送私信
const sendPrivate = async (destUserName, msg) => {
await connection.invoke("SendPrivateMessage", destUserName, msg);
};
// 监听私信
connection.on('ReceivePrivateMessage', (srcUser, time, msg) => {
state.messages.push(`${srcUser} ${time} 私信:${msg}`);
});
核心 API
| 服务端 API | 作用 |
|---|---|
Clients.All |
全员推送 |
Clients.User(userId) |
指定用户推送 |
Clients.Client(connId) |
指定连接推送 |
Clients.Others |
排除自己,全员推送 |
Groups.AddToGroupAsync |
加入分组 |
Groups.RemoveFromGroupAsync |
退出分组 |
Clients.Group(groupName) |
分组推送 |
总结
- SignalR:封装 WebSocket 细节,自动适配通信方式,零成本实现实时推送;Hub 为核心,配置跨域 + 映射端点,前端通过 SDK 连接;支持用户、连接、分组三种维度,满足私聊 / 分组 / 全员场景。
- 集群 / 分布式:禁用协商解决协议问题,Redis 背板实现跨服务器消息同步。
- 安全控制 :JWT 集成,通过 URL 参数传递 Token,
[Authorize]校验权限。