摘要
本文旨在系统地介绍 ASP.NET Core SignalR,从基本概念、快速入门到进阶特性(如用户管理、群组、后台服务推送、HTTPS 配置等),帮助开发者掌握这一强大的实时 Web 功能开发框架。通过丰富的代码示例,我们将一步步构建一个实时聊天室,并探索如何实现更复杂的实时交互场景。
目录
- SignalR 简介:什么是实时通信?
- 入门篇:搭建你的第一个 SignalR 应用
- 进阶篇一:Hub 详解与客户端交互
- 进阶篇二:用户管理与群组通信
- 进阶篇三:后台服务主动推送
- 实践篇:部署与 HTTPS 配置
- 总结与展望
1. SignalR 简介:什么是实时通信?
在传统的 Web 应用中,通信模式通常是"请求-响应"式的:客户端(通常是浏览器)发起一个请求到服务器,服务器处理后返回响应。客户端若想获取最新数据,只能通过轮询(Polling)的方式反复向服务器发送请求,这种方式效率低下,浪费资源。
实时 Web 应用 则要求服务器能在数据更新时主动将信息推送给客户端,实现近乎即时的数据同步。例如:
- 聊天室/IM 应用: 用户发送消息后,其他用户能立刻看到。
- 在线游戏: 玩家操作能实时反映在所有参与者的游戏界面上。
- 实时仪表盘: 数据源的变化能立即体现在监控界面上。
- 协同编辑: 多个用户同时编辑文档,更改能实时同步。
SignalR 是微软为 .NET 平台提供的一个开源库,它极大地简化了实时 Web 功能的开发。它抽象了底层的传输技术(如 WebSocket、Server-Sent Events、Long Polling 等),开发者只需专注于业务逻辑,而无需关心连接管理、故障切换等复杂细节。SignalR 会自动选择当前环境最优的传输方式,并在连接中断时尝试重连。
SignalR vs WebSocket:
- WebSocket: 是一种底层的网络协议,提供了浏览器与服务器之间的全双工通信通道。使用 WebSocket 需要开发者自己处理连接建立、心跳、重连、错误处理、消息序列化/反序列化等细节。
- SignalR: 是一个基于 WebSocket(以及其他传输技术)构建的高层应用框架 。它封装了上述所有复杂性,提供了简单的 API(如
Hub类、Clients对象),内置了连接管理、用户/群组管理、缩放等功能,大大提升了开发效率。
简而言之,SignalR 是开发 .NET 实时应用的首选框架,它让 WebSocket 的强大功能变得易于使用。
2. 入门篇:搭建你的第一个 SignalR 应用
让我们通过构建一个简单的实时聊天室来体验 SignalR 的魅力。
2.1 创建项目
首先,使用 .NET CLI 创建一个 Web API 项目:
dotnet new webapi -n MySignalRChat
cd MySignalRChat
2.2 创建 Hub 类
Hub 是 SignalR 的核心组件,它是一个服务器端类,继承自 Microsoft.AspNetCore.SignalR.Hub,定义了客户端可以调用的方法,并提供了向客户端发送消息的 API。
-
在项目根目录下创建一个名为
Hubs的文件夹。 -
在
Hubs文件夹内创建ChatHub.cs文件:using Microsoft.AspNetCore.SignalR; namespace MySignalRChat.Hubs { public class ChatHub : Hub { // 定义一个客户端可以调用的方法 public async Task SendMessage(string user, string message) { // 使用 Clients.All 向所有连接的客户端广播消息 await Clients.All.SendAsync("ReceiveMessage", user, message); } } }
2.3 配置依赖注入和路由 (Program.cs)
打开 Program.cs 文件,添加 SignalR 服务并映射 Hub。
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using MySignalRChat.Hubs; // 引入 Hubs 命名空间
var builder = WebApplication.CreateBuilder(args);
// 添加 SignalR 服务到 DI 容器
builder.Services.AddSignalR();
// 如果需要,可以配置更多 SignalR 选项
// builder.Services.AddSignalR(options =>
// {
// // 例如设置最大接收消息大小
// options.MaximumReceiveMessageSize = 1024 * 1024; // 1MB
// });
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseRouting();
// 注意:如果前端是跨域访问,必须配置 CORS
// app.UseCors("AllowSpecificOrigin");
app.UseAuthorization();
// 映射 ChatHub 到 "/chat" 路径
app.MapHub<ChatHub>("/chat");
// 如果需要提供静态文件(如 index.html)
// app.UseStaticFiles();
app.MapControllers(); // 如果有其他 API 控制器
app.Run();
2.4 创建客户端 HTML
为了方便测试,我们创建一个简单的 HTML 页面作为客户端。在项目根目录下创建 wwwroot 文件夹,然后在 wwwroot 内创建 index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SignalR Chat</title>
<!-- 引入 SignalR JavaScript 客户端库 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/8.0.7/signalr.min.js"></script>
</head>
<body>
<div id="messagesList"></div><br />
<input type="text" id="messageInput" placeholder="Type a message..." />
<input type="text" id="userInput" placeholder="Enter your name..." />
<button onclick="sendMessage()">Send Message</button>
<script>
// 创建到 ChatHub 的连接
const connection = new signalR.HubConnectionBuilder()
.withUrl("/chat") // 连接到 /chat 路径
.build();
// 启动连接
connection.start().then(function () {
console.log("SignalR Connected.");
}).catch(function (err) {
console.error(err.toString());
});
// 定义客户端方法 ReceiveMessage,用于接收服务端广播的消息
connection.on("ReceiveMessage", function (user, message) {
const messagesList = document.getElementById("messagesList");
const li = document.createElement("li");
li.textContent = `${user}: ${message}`;
messagesList.appendChild(li);
});
// 发送消息到服务端
function sendMessage() {
const user = document.getElementById("userInput").value;
const message = document.getElementById("messageInput").value;
if (user && message) {
// 调用服务端的 SendMessage 方法
connection.invoke("SendMessage", user, message).catch(function (err) {
console.error(err.toString());
});
document.getElementById("messageInput").value = ""; // 清空输入框
}
}
// 监听 Enter 键发送消息
document.getElementById("messageInput").addEventListener("keypress", function(event) {
if (event.key === "Enter") {
sendMessage();
}
});
</script>
</body>
</html>
记得在 Program.cs 中取消 app.UseStaticFiles(); 的注释,以便服务器能提供 index.html 文件。
2.5 运行与测试
- 在项目目录下运行
dotnet run。 - 打开浏览器,访问
https://localhost:5001/index.html(或根据你的端口配置)。 - 打开多个浏览器标签页,输入不同的用户名,发送消息,观察消息是否实时出现在所有标签页上。
恭喜!你已经成功创建了第一个 SignalR 应用。
3. 进阶篇一:Hub 详解与客户端交互
3.1 Hub 生命周期方法
Hub 类提供了一些生命周期方法,允许你在连接的不同阶段执行代码:
-
OnConnectedAsync(): 当新客户端连接到 Hub 时调用。 -
OnDisconnectedAsync(Exception? exception): 当客户端断开连接时调用。public class ChatHub : Hub
{
public override async Task OnConnectedAsync()
{
Console.WriteLine($"{Context.ConnectionId} connected.");
// 可以在这里做一些初始化工作,比如记录连接日志
await base.OnConnectedAsync();
}public override async Task OnDisconnectedAsync(Exception? exception) { Console.WriteLine($"{Context.ConnectionId} disconnected. Exception: {exception?.Message}"); // 可以在这里清理资源 await base.OnDisconnectedAsync(exception); } public async Task SendMessage(string user, string message) { await Clients.All.SendAsync("ReceiveMessage", user, message); }}
3.2 Hub 上下文 (Context) 与客户端对象 (Clients)
Context: 提供了关于当前连接的信息,如ConnectionId,User.Identity,Items(用于存储临时数据) 等。Clients: 提供了向客户端发送消息的方法:Clients.All: 向所有连接的客户端发送消息。Clients.Others: 向除了发送者之外的所有客户端发送消息。Clients.Caller: 向调用方法的客户端发送消息。Clients.Client(connectionId): 向指定连接 ID 的客户端发送消息。Clients.Group(groupName): 向指定群组的客户端发送消息。
3.3 客户端调用服务端方法
客户端通过 connection.invoke(hubMethodName, ...args) 调用 Hub 中定义的 public 或 public virtual 方法。
3.4 服务端调用客户端方法
服务端通过 Clients.xxx.SendAsync(clientMethodName, ...args) 调用客户端定义的 connection.on(clientMethodName, handlerFunction) 方法。
4. 进阶篇二:用户管理与群组通信
4.1 群组 (Groups)
群组是 SignalR 的核心功能之一,它允许你将用户组织起来,只向特定群组的成员发送消息。
4.1.1 群组操作
Groups.AddToGroupAsync(ConnectionId, GroupName): 将连接加入群组。Groups.RemoveFromGroupAsync(ConnectionId, GroupName): 将连接移出群组。
4.1.2 示例:创建专属用户群组
修改 ChatHub.cs,让用户连接时加入以其用户名命名的群组。
public class ChatHub : Hub
{
public override async Task OnConnectedAsync()
{
Console.WriteLine($"{Context.ConnectionId} connected.");
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception? exception)
{
Console.WriteLine($"{Context.ConnectionId} disconnected. Exception: {exception?.Message}");
// 注意:SignalR 会自动清理断开连接的用户所在的群组
await base.OnDisconnectedAsync(exception);
}
// 客户端调用此方法加入自己的专属群组
public async Task JoinUserGroup(string username)
{
if (string.IsNullOrEmpty(username)) // 简单校验
{
await Clients.Caller.SendAsync("ErrorMessage", "Username is required.");
return;
}
await Groups.AddToGroupAsync(Context.ConnectionId, username);
await Clients.Caller.SendAsync("JoinedGroup", username);
Console.WriteLine($"Client {Context.ConnectionId} joined group '{username}'");
}
// 发送消息到特定用户(通过群组)
public async Task SendPrivateMessage(string targetUser, string sender, string message)
{
await Clients.Group(targetUser).SendAsync("ReceiveMessage", sender, $"[Private] {message}");
// 可选:也向发送者确认发送成功
await Clients.Caller.SendAsync("PrivateMessageSent", targetUser, message);
}
// 广播消息
public async Task SendMessage(string user, string message)
{
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
}
4.1.3 修改客户端以加入群组
let connection;
let currentUsername = null;
function joinGroup() {
const username = document.getElementById("userInput").value.trim();
if (!username) return;
if (!connection) {
connection = new signalR.HubConnectionBuilder()
.withUrl("/chat")
.build();
connection.on("ReceiveMessage", receiveMessage);
connection.on("ErrorMessage", errorMessage);
connection.on("JoinedGroup", joinedGroup);
connection.on("PrivateMessageSent", privateMessageSent);
connection.start().then(() => {
console.log("SignalR Connected.");
connection.invoke("JoinUserGroup", username).catch(handleError);
}).catch(handleError);
} else {
// 如果已连接,直接加入群组
connection.invoke("JoinUserGroup", username).catch(handleError);
}
}
function receiveMessage(user, message) {
const li = document.createElement("li");
li.textContent = `${user}: ${message}`;
document.getElementById("messagesList").appendChild(li);
}
function errorMessage(message) {
console.error("Error from server:", message);
alert("Error: " + message);
}
function joinedGroup(username) {
console.log(`Joined group for user: ${username}`);
currentUsername = username;
document.getElementById("messageInput").disabled = false;
document.getElementById("sendButton").disabled = false;
document.getElementById("targetUserInput").disabled = false; // 新增输入框
document.getElementById("sendPrivateButton").disabled = false; // 新增按钮
}
function privateMessageSent(targetUser, message) {
console.log(`Private message sent to ${targetUser}: ${message}`);
}
function handleError(err) {
console.error(err.toString());
}
// 添加发送私信的函数和UI元素...
HTML 中需要新增目标用户输入框和发送私信按钮。
5. 进阶篇三:后台服务主动推送
有时,服务端需要在没有客户端请求的情况下主动发送消息,比如系统通知、定时数据更新等。这可以通过 IHostedService 或 BackgroundService 结合 IHubContext 来实现。
5.1 创建后台服务
创建 Services/TimedNotificationService.cs:
using Microsoft.AspNetCore.SignalR;
using MySignalRChat.Hubs;
public class TimedNotificationService : BackgroundService
{
private readonly ILogger<TimedNotificationService> _logger;
private readonly IHubContext<ChatHub> _hubContext;
public TimedNotificationService(ILogger<TimedNotificationService> logger, IHubContext<ChatHub> hubContext)
{
_logger = logger;
_hubContext = hubContext;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(30)); // 每30秒执行一次
while (!stoppingToken.IsCancellationRequested && await timer.WaitForNextTickAsync(stoppingToken))
{
try
{
_logger.LogInformation("Sending timed notification...");
await _hubContext.Clients.All.SendAsync("ReceiveMessage", "System", "This is a periodic system notification.", stoppingToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error sending timed notification.");
}
}
}
}
5.2 注册服务 (Program.cs)
在 Program.cs 中添加注册:
// ...
builder.Services.AddHostedService<TimedNotificationService>();
// ...
现在,服务端会每隔 30 秒向所有连接的客户端发送一条系统通知。
6. 实践篇:部署与 HTTPS 配置
6.1 配置 HTTPS
在生产环境中,必须使用 HTTPS 来加密 SignalR 连接。
-
配置 Kestrel: 在
appsettings.json或Program.cs中配置 HTTPS 端点。{ "Kestrel": { "Endpoints": { "Https": { "Url": "https://localhost:5001", "Certificate": { // 如果需要指定证书 "Path": "path/to/certificate.pfx", "Password": "certificate-password" } } } } } -
更新客户端连接 URL: 确保客户端连接到
wss://(WebSocket Secure) 协议的地址。const connection = new signalR.HubConnectionBuilder() .withUrl("https://yoursite.com/chat") // 使用 HTTPS/WSS .build();
6.2 配置 CORS (跨域)
如果前端和后端不在同一个域名下,必须配置 CORS。
// Program.cs
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowSpecificOrigin",
policy => policy.WithOrigins("https://yourfrontend.com") // 替换为你的前端地址
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials()); // SignalR 通常需要
});
// 在中间件管道中,UseRouting 之前使用
app.UseCors("AllowSpecificOrigin");
7. 总结与展望
SignalR 是 .NET 平台上构建实时 Web 应用的强大工具。它通过抽象底层传输细节,提供了简洁高效的 API,使得开发者能够轻松实现实时通信功能。本文介绍了从基础的 Hub 创建、客户端交互,到进阶的群组管理、后台推送,再到生产环境配置的关键步骤。
随着应用规模的增长,你可能还需要了解:
- 缩放 (Scale-out): 如何在多个服务器实例之间共享连接状态,通常使用 Redis 等背板 (Backplane)。
- 认证与授权: 如何保护 Hub 方法,确保只有经过身份验证的用户才能连接或调用特定方法。
- 错误处理与日志: 如何更好地处理连接失败、消息发送错误等情况。
- 性能优化: 如何处理大量并发连接,优化消息序列化等。