商城首页欢迎来到中国正版软件门户

您的位置: 首页 > 文章列表 > 编程开发 > 基于ASP.NETCoreSignalR实现实时消息提醒与聊天功能

基于ASP.NETCoreSignalR实现实时消息提醒与聊天功能

  发布于2026-07-23 阅读(0)

扫一扫,手机访问

在构建需要实时通信的应用时,WebSocket 是个绕不开的技术点。而 ASP.NET Core 生态中,SignalR 无疑是最趁手的工具之一。它把底层连接管理、协议协商、自动重连这些繁琐的事情都封装好了,让开发者能更专注于业务逻辑本身。下面就从服务端架构、客户端交互,再到一些关键优化点,把整个链路梳理一遍。

一、服务端实现(ASP.NET Core 7+)

1. 消息模型定义

要做消息通信,首先得把消息本身的数据结构定义清楚。这不仅仅是几个字段的堆砌,背后其实藏着对消息生命周期的管理思路。

public class ChatMessage
{
    public string SenderId { get; set; }
    public string ReceiverId { get; set; }
    public string Content { get; set; }
    public DateTime Timestamp { get; set; } = DateTime.UtcNow;
    public MessageStatus Status { get; set; } = MessageStatus.Sent;
}

public enum MessageStatus
{
    Sent,
    Delivered,
    Read
}

从模型能看出来,一条消息从发出到被读取,可以经历三个状态:已发送、已送达、已读。这是很多即时通讯应用的基础逻辑,也是后面做状态同步的前提。

2. SignalR Hub核心实现

Hub 是整个通信的中枢。这里最核心的工作是管理用户连接、处理消息路由和状态同步。

using Microsoft.AspNetCore.SignalR;
using System.Collections.Concurrent;

public class ChatHub : Hub
{
    private static readonly ConcurrentDictionary> _connections = 
        new ConcurrentDictionary>();

    // 用户连接管理
    public override async Task OnConnectedAsync()
    {
        var userId = Context.UserIdentifier;
        if (!_connections.TryGetValue(userId, out var connections))
        {
            connections = new List();
            _connections[userId] = connections;
        }
        connections.Add(Context.ConnectionId);
                
        // 通知好友在线状态
        await UpdateUserStatus(userId, true);
        await base.OnConnectedAsync();
    }

    public override async Task OnDisconnectedAsync(Exception? exception)
    {
        if (_connections.TryRemove(Context.UserIdentifier, out var connections))
        {
            await UpdateUserStatus(Context.UserIdentifier, false);
            connections.Remove(Context.ConnectionId);
        }
        await base.OnDisconnectedAsync(exception);
    }

    // 发送消息
    public async Task SendMessage(string receiverId, string content)
    {
        var senderId = Context.UserIdentifier;
        var message = new ChatMessage
        {
            SenderId = senderId,
            ReceiverId = receiverId,
            Content = content
        };

        // 存储消息(可选)
        await Sa veMessageToDb(message);

        // 推送消息
        await Clients.Group($"user_{receiverId}").SendAsync(
            "ReceiveMessage", 
            senderId, 
            content, 
            message.Timestamp
        );

        // 标记已送达
        await Clients.Caller.SendAsync("MessageDelivered", message.Id);
    }

    // 标记已读
    public async Task MarkAsRead(int messageId)
    {
        // 更新数据库状态
        var message = await GetMessageById(messageId);
        if (message?.ReceiverId == Context.UserIdentifier)
        {
            message.Status = MessageStatus.Read;
            await Sa veMessageToDb(message);

            // 通知发送者
            await Clients.Group($"user_{message.SenderId}").SendAsync("MessageRead", messageId);
        }
    }

    private static void UpdateUserStatus(string userId, bool isOnline)
    {
        // 更新全局状态缓存
        // 触发好友状态变更通知
        Clients.All.SendAsync("UserStatusChanged", userId, isOnline);
    }
}

这里有一个细节值得注意:为什么用 Group 而不是直接向某个 ConnectionId 发送?因为一个用户可能同时登录多个设备(比如手机和电脑),给用户所在的组发消息,能确保所有设备都收到。而 ConcurrentDictionary 用来维护用户与连接ID的映射关系,这是多用户场景下的标准做法。

二、客户端实现(Web前端)

1. HTML结构

前端的交互界面很简单,但功能点一个不少:在线用户列表、消息展示区域、输入框和发送按钮。

2. Ja vaScript连接与事件处理

连接 SignalR 的代码写起来很直接,但有几个关键点需要把握好:自动重连的配置、事件监听的处理,以及连接状态的日志输出。

const connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub")
    .withAutomaticReconnect()
    .build();

// 接收消息
connection.on("ReceiveMessage", (senderId, content, timestamp) => {
    const msgElement = document.createElement("div");
    msgElement.innerHTML = `
        ${senderId} (${new Date(timestamp).toLocaleTimeString()}):
        

${content}

`; document.getElementById("message-list").appendChild(msgElement); }); // 用户状态更新 connection.on("UserStatusChanged", (userId, isOnline) => { const statusElement = document.getElementById(`user-${userId}-status`); statusElement.textContent = isOnline ? "在线" : "离线"; }); // 连接建立 connection.start().then(() => { console.log("已连接到SignalR服务"); }).catch(err => console.error(err.toString())); // 发送消息 document.getElementById("send-btn").addEventListener("click", async () => { const receiverId = document.getElementById("receiverId").value; const content = document.getElementById("message-input").value; await connection.invoke("SendMessage", receiverId, content); });

用过几次 SignalR 之后就会发现,withAutomaticReconnect() 这个配置几乎是必选的。网络波动时,它会自动尝试重新连接,体验上平滑很多。而事件监听部分,则按照服务端定义好的回调一一对应即可。

三、关键功能扩展

1. 消息持久化(Entity Framework Core)

消息不能只停留在内存里,落地到数据库是刚需。用 EF Core 来操作,代码量很轻。

public class AppDbContext : DbContext
{
    public DbSet Messages { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity()
            .HasIndex(m => new { m.SenderId, m.ReceiverId })
            .IsUnique(false);
    }
}

// 保存消息到数据库
public async Task Sa veMessageToDb(ChatMessage message)
{
    using var dbContext = new AppDbContext();
    dbContext.Messages.Add(message);
    await dbContext.Sa veChangesAsync();
}

这里为 SenderIdReceiverId 建了一个联合索引,但注意它是非唯一索引。因为一个用户对会发很多条消息,这个索引的设计是为了后续做消息检索用的。

2. 实时通知增强

浏览器通知是提升用户体验的利器。当用户没在看聊天界面时,一条弹窗通知能及时抓住注意力。

// 弹窗通知(使用iziToast)
connection.on("ReceiveMessage", async (senderId, content, timestamp) => {
    iziToast.info({
        title: senderId,
        message: content,
        position: 'topRight',
        timeout: 5000
    });
});

3. 消息状态同步

消息的已读/未读状态,是即时通讯里感知最明显的功能之一。前端在用户查看消息时触发标记已读,服务端收到后通知发送方。

// 前端标记已读
async function markMessageAsRead(messageId) {
    await connection.invoke("MarkAsRead", messageId);
}

// 接收已读确认
connection.on("MessageRead", (messageId) => {
    document.querySelector(`[data-id="${messageId}"]`).classList.add("read");
});

这个双向确认的机制,玩过即时通讯的应该都不陌生。它让消息的流转变得透明,用户能清楚地知道自己的消息到底有没有被对方看到。

四、服务端配置

1. Program.cs配置

服务端的配置集中在 Program.cs 里。SignalR 的服务注册、认证中间件、路由映射,都得在这里安排好。

var builder = WebApplication.CreateBuilder(args);

// 添加SignalR服务
builder.Services.AddSignalR(hubOptions => {
    hubOptions.EnableDetailedErrors = true;
    hubOptions.KeepAliveInterval = TimeSpan.FromMinutes(2);
});

// 添加身份认证(JWT示例)
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options => {
        options.TokenValidationParameters = new TokenValidationParameters {
            ValidateIssuer = true,
            ValidateAudience = true
        };
    });

var app = builder.Build();

// 路由配置
app.UseAuthentication();
app.UseAuthorization();
app.MapHub("/chathub");

app.Run();

有个容易被忽略的点:KeepAliveInterval 的设置。如果间隔太短,会增加网络开销;太长又可能导致连接被误判为断开。2分钟是一个比较稳妥的折中方案。另外,EnableDetailedErrors 在开发阶段建议开着,但生产环境最好关掉,避免泄露内部细节。

五、性能优化方案

到生产环境,性能问题就绕不过去了。这里提几个关键方向:连接管理、消息分片和负载均衡。

  1. 连接管理 —— 使用 ConcurrentDictionary 维护用户连接池,配合心跳检测机制,能及时发现并清理僵尸连接。

  2. 消息分片 —— 大文件传输时,不能指望一次搞定。分片传输是常规做法。

    // 大文件分片传输
    public async Task SendLargeFile(IFormFile file)
    {
        var buffer = new byte[4096];
        using (var stream = file.OpenReadStream())
        {
            int bytesRead;
            while ((bytesRead = await stream.ReadAsync(buffer)) > 0)
            {
                await Clients.Caller.SendAsync("ReceiveFileChunk", 
                    file.Name, buffer, bytesRead);
            }
        }
    }
    
  3. 负载均衡 —— 单机扛不住,就得靠 Redis 来撑后端存储。SignalR 官方提供了 StackExchange.Redis 的扩展,配置起来很直接。

    builder.Services.AddSignalR().AddStackExchangeRedis("redis_connection_string");
    

这三个方向基本覆盖了从单机到集群、从小消息到大数据传输的优化路径。实际项目中,还需要根据流量特点做针对性调整。

本文转载于:https://www.jb51.net/aspnet/356170xr4.htm 如有侵犯,请联系zhengruancom@outlook.com删除。
免责声明:正软商城发布此文仅为传递信息,不代表正软商城认同其观点或证实其描述。

热门关注