技术选型与核心原理

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

asp.net 网页在线即时聊天
(图片来源网络,侵删)

轮询

  • 原理:浏览器每隔几秒就向服务器发送一个请求,询问有没有新消息。
  • 优点:实现简单,兼容性好。
  • 缺点:效率极低,会产生大量无效请求,服务器压力大,延迟高。
  • 不推荐用于生产环境。

长轮询

  • 原理:浏览器向服务器发送请求后,服务器会保持这个连接打开,直到有新消息或超时才返回响应,浏览器收到响应后立即发送下一个请求。
  • 优点:比轮询高效,减少了无效请求。
  • 缺点:服务器需要维护大量长时间连接,实现逻辑比轮询复杂,延迟依然不是最低的。
  • 可以作为一种备选方案,但非最优。

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 CoreSignalR 来构建一个完整的在线聊天应用。

步骤 1:创建项目

  1. 打开 Visual Studio,创建一个新项目。
  2. 选择 "ASP.NET Core Web 应用"
  3. 给项目命名,ChatApp
  4. 在下一步中,选择 "Razor Pages" 模板(MVC 也可以,这里以 Razor Pages 为例),确保勾选了 "配置 HTTPS"
  5. 点击创建。

步骤 2:安装 SignalR 客户端库

虽然 SignalR 服务器端库已经包含在 ASP.NET Core 中,但我们需要在客户端(网页)安装它的 JavaScript 客户端库。

  1. 在解决方案资源管理器中,右键点击项目 -> "管理客户端库" (Manage Client-Side Libraries)。
  2. 在 "Provider" 中选择 unpkg
  3. 在 "Library" 中搜索 @microsoft/signalr
  4. 选择最新版本,并勾选 dist/browser/signalr.jsdist/browser/signalr.min.js
  5. 点击 "安装" 或 "接受"。

安装完成后,wwwroot/lib 文件夹下会出现 @microsoft/signalr 目录。

步骤 3:配置 SignalR 中心

“中心”是 SignalR 的核心,它处理客户端和服务器之间的通信。

asp.net 网页在线即时聊天
(图片来源网络,侵删)
  1. 在项目中创建一个新文件夹 Hubs
  2. 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:运行和测试

  1. F5 运行你的项目。
  2. 打开浏览器,访问你的聊天页面。
  3. 打开一个新的浏览器窗口或隐身窗口,访问同一个页面。
  4. 在一个窗口中输入昵称和消息,点击“发送”。
  5. 你会看到消息立即显示在另一个窗口的聊天记录中,反之亦然,恭喜,你的即时聊天应用已经成功了!

生产环境中的关键考量

上面的示例是一个基础框架,在实际应用中你还需要考虑很多:

用户身份认证

  • 问题:如何知道是谁发的消息?如何防止恶意用户?
  • 方案:将 SignalR 与 ASP.NET Core 的身份认证系统集成。
    • 在连接 Hub 时,可以通过 HttpContext.User 获取当前登录用户的身份信息。
    • 修改 ChatHub.cs,在 SendMessage 方法中,不再从前端接收 user,而是从 Context.User.Identity.Name 获取。
    • 前端发送消息时,只需要发送 message 内容即可。
// 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 是实现在线即时聊天的最佳实践,它为你提供了强大的实时通信能力,同时让你可以专注于业务逻辑,而不是底层的网络通信细节。

asp.net 网页在线即时聊天
(图片来源网络,侵删)