技术选型与核心原理
传统的 Web 应用是“请求-响应”模式,浏览器主动向服务器请求,服务器返回数据后连接就断开了,要实现“即时”通信,服务器必须能主动向客户端推送消息,主要有以下几种技术方案:

(图片来源网络,侵删)
轮询
- 原理:浏览器每隔几秒就向服务器发送一个请求,询问有没有新消息。
- 优点:实现简单,兼容性好。
- 缺点:效率极低,会产生大量无效请求,服务器压力大,延迟高。
- 不推荐用于生产环境。
长轮询
- 原理:浏览器向服务器发送请求后,服务器会保持这个连接打开,直到有新消息或超时才返回响应,浏览器收到响应后立即发送下一个请求。
- 优点:比轮询高效,减少了无效请求。
- 缺点:服务器需要维护大量长时间连接,实现逻辑比轮询复杂,延迟依然不是最低的。
- 可以作为一种备选方案,但非最优。
WebSocket (推荐)
- 原理:在浏览器和服务器之间建立一个全双工、持久化的连接,一旦连接建立,双方就可以随时向对方发送数据,无需客户端主动请求。
- 优点:
- 真正的实时:延迟极低。
- 高效:只需一次握手,后续通信开销小。
- 全双工:服务器可以主动推送,客户端也可以随时发送。
- 缺点:
- 需要浏览器和服务器都支持 (现代浏览器都支持)。
- 服务器端需要额外的支持库。
- 是实现即时聊天的最佳选择。
SignalR (ASP.NET 的 WebSocket 封装)
- 原理:SignalR 是一个由微软开发的 ASP.NET 库,它极大地简化了在 Web 应用中添加实时 Web 功能的难度,它自动选择最佳的后端传输方式(优先使用 WebSocket,在不支持时自动降级到长轮询等)。
- 优点:
- 抽象简化:你不需要关心底层是 WebSocket 还是长轮询,SignalR 为你处理好了。
- 功能强大:除了简单的点对点聊天,还支持广播、组播、连接管理等高级功能。
- 与 ASP.NET 深度集成:与 ASP.NET MVC/Razor Pages/Web Forms 配合得天衣无缝。
- 对于 ASP.NET 这是最推荐、最简单、最强大的方案。
实现方案:使用 SignalR (以 ASP.NET Core Razor Pages 为例)
我们将使用 ASP.NET Core 和 SignalR 来构建一个完整的在线聊天应用。
步骤 1:创建项目
- 打开 Visual Studio,创建一个新项目。
- 选择 "ASP.NET Core Web 应用"。
- 给项目命名,
ChatApp。 - 在下一步中,选择 "Razor Pages" 模板(MVC 也可以,这里以 Razor Pages 为例),确保勾选了 "配置 HTTPS"。
- 点击创建。
步骤 2:安装 SignalR 客户端库
虽然 SignalR 服务器端库已经包含在 ASP.NET Core 中,但我们需要在客户端(网页)安装它的 JavaScript 客户端库。
- 在解决方案资源管理器中,右键点击项目 -> "管理客户端库" (Manage Client-Side Libraries)。
- 在 "Provider" 中选择
unpkg。 - 在 "Library" 中搜索
@microsoft/signalr。 - 选择最新版本,并勾选
dist/browser/signalr.js和dist/browser/signalr.min.js。 - 点击 "安装" 或 "接受"。
安装完成后,wwwroot/lib 文件夹下会出现 @microsoft/signalr 目录。
步骤 3:配置 SignalR 中心
“中心”是 SignalR 的核心,它处理客户端和服务器之间的通信。

(图片来源网络,侵删)
- 在项目中创建一个新文件夹
Hubs。 - 在
Hubs文件夹中,创建一个 C# 文件ChatHub.cs。
// Hubs/ChatHub.cs
using Microsoft.AspNetCore.SignalR;
// ChatHub 继承自 Hub,它提供了与客户端通信的方法
public class ChatHub : Hub
{
// 当一个用户连接到聊天室时调用
public override async Task OnConnectedAsync()
{
// 将新连接的用户添加到一个名为 "ChatRoom" 的组中
// 组是实现“聊天室”功能的关键
await Groups.AddToGroupAsync(Context.ConnectionId, "ChatRoom");
Console.WriteLine($"用户 {Context.ConnectionId} 已连接。");
// 通知所有在组内的用户,有新人加入
await Clients.Group("ChatRoom").SendAsync("ReceiveMessage", "系统", "欢迎新用户加入聊天!");
}
// 当用户断开连接时调用
public override async Task OnDisconnectedAsync(Exception? exception)
{
// 从组中移除断开连接的用户
await Groups.RemoveFromGroupAsync(Context.ConnectionId, "ChatRoom");
Console.WriteLine($"用户 {Context.ConnectionId} 已断开连接。");
// 通知所有在组内的用户,有人离开了
await Clients.Group("ChatRoom").SendAsync("ReceiveMessage", "系统", "有用户离开了聊天。");
await base.OnDisconnectedAsync(exception);
}
// 客户端调用的方法,用于发送消息
// 客户端会调用这个方法,服务器接收到后,再广播给所有客户端
public async Task SendMessage(string user, string message)
{
// 调用 "ChatRoom" 组中所有客户端的 "ReceiveMessage" 方法
// 并将发送者用户名和消息内容传递过去
await Clients.Group("ChatRoom").SendAsync("ReceiveMessage", user, message);
}
}
步骤 4:配置 SignalR 中间件
要让 SignalR 工作起来,必须在 Program.cs 中注册其服务。
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// 1. 添加 SignalR 服务
builder.Services.AddSignalR();
// Add services to the container.
builder.Services.AddRazorPages();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
// 2. 将 SignalR 端点映射到我们创建的 ChatHub
// "/chatHub" 是客户端连接时使用的 URL
app.MapHub<ChatHub>("/chatHub");
app.MapRazorPages();
app.Run();
步骤 5:创建前端聊天界面
现在我们来修改 Index.cshtml 页面,让它成为一个聊天室。
@page
@model IndexModel
@{
ViewData["Title"] = "在线聊天室";
}
<div class="text-center">
<h1 class="display-4">欢迎来到在线聊天室</h1>
</div>
<!-- 聊天界面容器 -->
<div class="container mt-4">
<div class="card">
<div class="card-header">
<h5>聊天记录</h5>
</div>
<div class="card-body" id="messageBox" style="height: 400px; overflow-y: scroll;">
<!-- 消息将在这里动态显示 -->
</div>
<div class="card-footer">
<div class="input-group">
<input type="text" id="userInput" class="form-control" placeholder="您的昵称" value="用户@DateTime.Now.Millisecond">
<input type="text" id="messageInput" class="form-control" placeholder="输入消息...">
<button class="btn btn-primary" id="sendButton">发送</button>
</div>
</div>
</div>
</div>
<!-- 引入 SignalR 客户端库 -->
<script src="lib/@microsoft/signalr/dist/browser/signalr.min.js"></script>
<script>
// 1. 创建与 SignalR Hub 的连接
// "/chatHub" 必须与 Program.cs 中注册的路径一致
const connection = new signalR.HubConnectionBuilder()
.withUrl("/chatHub")
.configureLogging(signalR.LogLevel.Information)
.build();
// 2. 定义一个客户端方法,用于接收服务器推送的消息
// "ReceiveMessage" 必须与 ChatHub.cs 中 SendAsync 的第一个参数完全一致
connection.on("ReceiveMessage", (user, message) => {
const msg = document.createElement("div");
// 使用模板字符串格式化消息,防止 XSS 攻击 (虽然这里简单示例,但生产环境务必注意)
// 实际项目中应该对 user 和 message 进行 HTML 编码
msg.textContent = `${user}: ${message}`;
document.getElementById("messageBox").appendChild(msg);
// 滚动到底部
document.getElementById("messageBox").scrollTop = document.getElementById("messageBox").scrollHeight;
});
// 3. 启动连接
async function start() {
try {
await connection.start();
console.log("SignalR Connected.");
} catch (err) {
console.log(err);
setTimeout(start, 5000); // 如果连接失败,5秒后重试
}
};
connection.onclose(async () => {
await start();
});
// 启动连接
start();
// 4. 绑定发送按钮的点击事件
document.getElementById("sendButton").addEventListener("click", () => {
const user = document.getElementById("userInput").value;
const message = document.getElementById("messageInput").value;
if (message) {
// 调用服务器端的 "SendMessage" 方法
// "SendMessage" 必须与 ChatHub.cs 中的方法名一致
connection.invoke("SendMessage", user, message)
.then(() => {
document.getElementById("messageInput").value = ""; // 清空输入框
})
.catch(err => console.error(err));
}
});
// 绑定回车键发送
document.getElementById("messageInput").addEventListener("keypress", (event) => {
if (event.key === "Enter") {
document.getElementById("sendButton").click();
}
});
</script>
步骤 6:运行和测试
- 按
F5运行你的项目。 - 打开浏览器,访问你的聊天页面。
- 打开一个新的浏览器窗口或隐身窗口,访问同一个页面。
- 在一个窗口中输入昵称和消息,点击“发送”。
- 你会看到消息立即显示在另一个窗口的聊天记录中,反之亦然,恭喜,你的即时聊天应用已经成功了!
生产环境中的关键考量
上面的示例是一个基础框架,在实际应用中你还需要考虑很多:
用户身份认证
- 问题:如何知道是谁发的消息?如何防止恶意用户?
- 方案:将 SignalR 与 ASP.NET Core 的身份认证系统集成。
- 在连接 Hub 时,可以通过
HttpContext.User获取当前登录用户的身份信息。 - 修改
ChatHub.cs,在SendMessage方法中,不再从前端接收user,而是从Context.User.Identity.Name获取。 - 前端发送消息时,只需要发送
message内容即可。
- 在连接 Hub 时,可以通过
// ChatHub.cs 中的修改
public async Task SendMessage(string message)
{
var currentUser = Context.User.Identity.Name; // 获取已登录的用户名
if (!string.IsNullOrEmpty(currentUser))
{
await Clients.Group("ChatRoom").SendAsync("ReceiveMessage", currentUser, message);
}
}
持久化聊天记录
- 问题:用户刷新页面后,之前的聊天记录就没了。
- 方案:将聊天记录保存到数据库中(如 SQL Server, MySQL, PostgreSQL)。
- 创建一个
Messages表(Id,User,Content,Timestamp)。 - 当
ChatHub收到消息后,除了广播,还要将消息写入数据库。 - 当用户首次进入聊天室时,先从数据库加载最近的 N 条记录显示在界面上。
- 创建一个
部署
- 问题:SignalR 连接是长连接,IIS 默认的某些设置可能会影响它。
- 方案:
- 推荐使用 Kestrel 服务器:ASP.NET Core 默认的 Kestrel 服务器对长连接支持最好。
- 如果必须使用 IIS,请安装 ASP.NET Core Module,并确保在
web.config中正确配置。 - 对于高并发场景,考虑使用 Azure SignalR Service,这是一个完全托管的 SignalR 服务,它解决了 SignalR 在服务器扩展性、WebSocket 穿透和移动端推送方面的难题,让你的后端应用可以轻松扩展到成千上万的并发连接。
安全性
- XSS 防护:在将用户消息显示到页面前,必须进行 HTML 编码,防止跨站脚本攻击,在 JavaScript 中,可以使用
textContent而不是innerHTML,或者使用专门的编码库。 - CSRF 防护:SignalR 默认会处理 ASP.NET Core 的防伪令牌,通常不需要额外配置。
| 方案 | 优点 | 缺点 | 推荐度 |
|---|---|---|---|
| SignalR | 抽象了底层协议,开发简单,功能强大,与 ASP.NET 深度集成 | 依赖微软生态,需要服务器支持 | ★★★★★ (首选) |
| 原生 WebSocket | 性能最高,控制最灵活 | 开发复杂,需要自己处理连接管理、降级策略等 | ★★★☆☆ (高级需求) |
| 长轮询 | 兼容性好 | 效率低,延迟高,服务器压力大 | ★☆☆☆☆ (不推荐) |
对于绝大多数 ASP.NET 使用 SignalR 是实现在线即时聊天的最佳实践,它为你提供了强大的实时通信能力,同时让你可以专注于业务逻辑,而不是底层的网络通信细节。

(图片来源网络,侵删)
