使用 .NET Core 8.0 和 SignalR 构建实时聊天服务

介绍

        在当今世界,实时通信是现代应用程序的必备功能。无论是在 Messenger、WhatsApp 还是 Microsoft Teams 等平台上聊天,实时消息传递已成为我们生活中不可或缺的一部分。这些平台不仅支持消息传递,还提供音频和视频通话等高级功能,这激发了我对实现实时通信功能的好奇心。

        本文重点介绍如何为一个实时聊天应用创建后端服务。该应用允许用户注册、创建聊天室,并分享房间 ID 或 URL 与他人进行群组对话。虽然我使用了 ReactJS(主要由 ChatGPT 生成)来测试后端,但本文的重点在于后端开发。

什么是 SignalR?

        SignalR 是一个简化向应用程序添加实时 Web 功能的库。它主要使用 WebSocket 进行通信,但必要时可以回退到较旧的传输协议。SignalR 支持“服务器推送”功能,使服务器能够调用客户端方法,这与传统 HTTP 的请求-响应模型不同。

        SignalR 应用程序可以通过内置或第三方提供程序(如 Redis 或 SQL Server)扩展到数千个客户端。

SignalR 传输选择过程

        SignalR 会根据客户端的功能自动选择最佳传输方法,并在必要时回退到旧协议(例如长轮询)。这确保了跨不同环境的可靠性。SignalR 按如下方式选择传输方法:

        • Internet Explorer 8 或更早版本:使用长轮询。
        • JSONP 已启用:使用长轮询。
        • 跨域连接:如果客户端和服务器支持,则使用WebSocket ;否则,则回退到长轮询。
        • 同域连接:如果支持,则使用WebSocket ;否则,尝试服务器发送事件,然后是永久帧,最后是长轮询。

SignalR 通信模型

        • 持久连接:用于管理消息的低级 API,适合对协议的直接控制。
        • 集线器:用于无缝双向通信的高级 API,允许客户端和服务器直接相互调用方法。

设置后端

项目设置

首先,请确保已安装以下先决条件:

        • .NET Core SDK
        • MongoDB
        • SQL 服务器
        • Redis(可选,但建议用于扩展)

        这些服务可以在本地设置(我已经这样做了),但 Docker Compose 是有效管理依赖关系的绝佳选择。

领域和基础设施

        在这个项目中,我分别使用了SQL Server和MongoDB来构建应用程序的不同部分。SQL Server 用于管理关系数据,例如用户信息、房间和会员。对于这些实体,我设计了如下模型:

        • 用户:存储用户信息,例如姓名、电子邮件和密码。
        • 房间:存储聊天室和相关数据,并提供可选密码来保护房间安全。
        • 成员:表示用户是房间的成员,具有角色和加入日期。

下面是存储在 SQL Server 中的用户实体的示例:

namespace ChatService.Domain.Users
{
    public class User
    {
        public long Id { get; private set; }
        public string Name { get; private set; }
        public string Email { get; private set; }
        public string PasswordHash { get; private set; }
        public DateTime CreatedOn { get; private set; }
    }
}

下面是存储在 SQL Server 中的Room实体的示例:

namespace ChatService.Domain.Rooms
{
    public class Room
    {
        public long Id { get; private set; }
        public string Name { get; private set; }
        public string PasswordHash { get; private set; }
        public DateTime CreatedOn { get; private set; }
        public List<Member> Members { get; private set; } = new List<Member>();
    }
}

下面是存储在 SQL Server 中的会员实体的示例:

namespace ChatService.Domain.Rooms
{
    public class Member
    {
        public long Id { get; private set; }
        public long UserId { get; private set; }
        public User User { get; private set; }  // Navigation property
        public long RoomId { get; private set; }
        public Role Role { get; private set; }
        public DateTime JoinedOn { get; private set; }
    }
}

        对于 SQL Server 实体,我们利用Entity Framework Core实现了额外的配置,以确保最佳性能和完整性。以下是关键实体的配置:User、Room和Member。

用户配置

namespace ChatService.Infrastructure.EntityConfigurations
{
    public class UserConfiguration : IEntityTypeConfiguration<User>
    {
        public void Configure(EntityTypeBuilder<User> builder)
        {
            builder.ToTable("Users");
            builder.HasKey(user => user.Id);
            builder.Property(user => user.Id)
                   .ValueGeneratedOnAdd();
            builder.Property(user => user.Name)
                   .HasMaxLength(200);
            builder.Property(user => user.Email)
                   .HasMaxLength(200);
            builder.Property(user => user.PasswordHash);
            builder.Property(user => user.CreatedOn)
                   .HasDefaultValueSql("GetUtcDate()");
            builder.HasIndex(user => user.Email).IsUnique();
        }
    }
}

在此配置中,User实体映射到Users表。Id设置为自增主键,并且Email字段已建立索引以确保唯一性。CreatedOn字段默认为当前 UTC 日期。

房间配置

namespace ChatService.Infrastructure.EntityConfigurations
{
    public class RoomConfiguration : IEntityTypeConfiguration<Room>
    {
        public void Configure(EntityTypeBuilder<Room> builder)
        {
            builder.ToTable("Rooms");
            builder.HasKey(room => room.Id);
            builder.Property(room => room.Id)
                   .ValueGeneratedOnAdd();
            builder.Property(room => room.Name)
                   .HasMaxLength(200);
            builder.Property(room => room.PasswordHash);
            builder.Property(room => room.CreatedOn)
                   .HasDefaultValueSql("GetUtcDate()");
        }
    }
}

Room实体的配置类似,具有和等属性Name,CreatedOn配置以确保适当的长度和默认值。

会员配置

namespace ChatService.Infrastructure.EntityConfigurations
{
    public class MemberConfiguration : IEntityTypeConfiguration<Member>
    {
        public void Configure(EntityTypeBuilder<Member> builder)
        {
            builder.ToTable("Members");
            builder.HasKey(member => member.Id);
            builder.Property(member => member.Id)
                   .ValueGeneratedOnAdd();
            builder.Property(member => member.UserId);
            builder.Property(member => member.RoomId);
            builder.Property(member => member.Role);
            builder.Property(member => member.JoinedOn)
                   .HasDefaultValueSql("GetUtcDate()");
            builder.HasIndex(member => new { member.UserId, member.RoomId })
                   .IsUnique();
            builder.HasOne<User>(member => member.User)
                   .WithMany()
                   .HasForeignKey(member => member.UserId)
                   .OnDelete(DeleteBehavior.NoAction);
            builder.HasOne<Room>()
                   .WithMany(room => room.Members)
                   .HasForeignKey(member => member.RoomId)
                   .OnDelete(DeleteBehavior.NoAction);
        }
    }
}

        成员实体配置确保每个成员的UserId和RoomId组合都是唯一的,从而防止重复输入。该JoinedOn字段默认为当前 UTC 日期,导航属性 和User已Room设置相应的外键关系。

        这些配置使Entity Framework Core能够准确映射 SQL Server 实体并确保用户、房间和成员之间的引用完整性。

        另一方面,MongoDB用于管理非关系数据,例如对话和消息。这些数据是动态的,更适合 NoSQL 数据库,因为消息和对话等数据可能会频繁变化,并且需要快速检索。

        • 对话:代表一次对话,可以是私人对话,也可以是群聊。
        • 消息:存储对话中交换的消息。

以下是存储在 MongoDB 中的对话实体的示例:

namespace ChatService.Domain.Conversations
{
    public class Conversation
    {
        [BsonId]
        [BsonRepresentation(BsonType.ObjectId)]
        public string Id { get; private set; }
        public long? RoomId { get; private set; }
        public List<long> Participants { get; private set; }
        public bool IsGroup { get; private set; }
        public DateTime CreatedOn { get; private set; }
        public DateTime UpdatedOn { get; private set; }
    }
}

以下是存储在 MongoDB 中的消息实体的示例:

namespace ChatService.Domain.Conversations;
public class Message
{
    [BsonId]
    [BsonRepresentation(BsonType.ObjectId)]
    public string Id { get; private set; }
    public string ConversationId { get; private set; }
    public long SenderId { get; private set; }
    public string SenderName { get; private set; }
    public string Content { get; private set; }
    public DateTime CreatedOn { get; private set; }
}

这种方法可以有效地结合关系数据库和 NoSQL 数据库的优势,为聊天应用程序中的不同用例提供灵活性和性能。

设计 API

身份验证端点

注册:允许用户注册。

POST /api/users/register
{
    "name": "John Doe","name": "John Doe",
    "email": "john@example.com",
    "password": "securepassword"
}

登录:验证用户并提供 JWT 令牌。

POST /api/users/login
{
    "email": "john@example.com","email": "john@example.com",
    "password": "securepassword"
}

受保护的路线需要 JWT 令牌。

房间管理端点

CreateRoom:创建一个具有名称和密码的聊天室。

POST /api/rooms
{
    "name": "Project Discussion","name": "Project Discussion",
    "password": "1234"
}

JoinRoom:将用户添加到现有房间。此处密码可选

POST /api/rooms/join
{
    "roomId": 2,"roomId": 2,
    "password": ""
}

GetRoom:检索房间详细信息。

GET /api/rooms/2

对话管理端点

创建对话:

对于群聊,请指定roomId。
对于一对一聊天,请使用nullroomId 和参与者列表。

POST /api/conversations
{
    "roomId": 2,"roomId": 2,
    "participants": []
}

获取对话:获取聊天消息。

GET /api/conversations?roomId=2&page=1&pageSize=20

SignalR Hub 实现

配置 SignalR

要将 SignalR 集成到应用程序中:

安装 SignalR:

dotnet add package Microsoft.AspNetCore.SignalRadd package Microsoft.AspNetCore.SignalR

定义 Hub:创建一个ChatHub扩展 SignalR 类的Hub类。

public class ChatHub : Hub
{
}

注册中间件:在Startup.cs或中配置SignalR Program.cs。

app.MapHub<ChatHub>("/chat");"/chat");

配置 CORS:允许前端通信。

builder.Services.AddCors(options =>
{
    options.AddPolicy("ChatClient", builder =>"ChatClient", builder =>
    {
        builder.WithOrigins("http://localhost:5173")
                .AllowAnyHeader()
                .AllowAnyMethod()
                .AllowCredentials();
    });
});

app.UseCors("ChatClient");


        该类ChatHub是 SignalR 的中心,用于管理聊天应用程序中的实时通信。它支持使用 JWT 令牌进行身份验证,并提供一些关键功能,例如加入聊天室、发送消息以及通知用户输入或断开连接等活动。

该ChatHub课程的主要特点

JWT 身份验证

当用户连接时,集线器会验证用户的 JWT 令牌。
access_token在 WebSocket 握手期间,令牌作为查询参数()传递。

public override async Task OnConnectedAsync()
{
    var token = Context.GetHttpContext()?.Request.Query["access_token"].ToString();
    if (string.IsNullOrEmpty(token))
    {
        await Clients.Caller.SendAsync("Error", "Authentication failed: No token provided");
        Context.Abort();
        return;
    }
    if (!ValidateJwtToken(token))
    {
        await Clients.Caller.SendAsync("Error", "Authentication failed: Invalid token");
        Context.Abort();
        return;
    }
    await base.OnConnectedAsync();
}
private bool ValidateJwtToken(string token)
{
    try
    {
        var tokenHandler = new JwtSecurityTokenHandler();
        var key = Encoding.UTF8.GetBytes(_jwtOptions.SecretKey);
        var validationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidIssuer = _jwtOptions.Issuer,
            ValidAudience = _jwtOptions.Audience,
            IssuerSigningKey = new SymmetricSecurityKey(key)
        };
        SecurityToken validatedToken;
        var principal = tokenHandler.ValidateToken(token, validationParameters, out validatedToken);
        return principal.Identity.IsAuthenticated;
    }
    catch (Exception)
    {
        return false;
    }
}

加入聊天室

用户根据他们提供的详细信息加入特定的房间。
缓存可确保用户连接详细信息暂时保留。

public async Task JoinSpecificChatRoom(UserConnection connection)
{
    // Add the connection to the group and cache the user details
    await Groups.AddToGroupAsync(Context.ConnectionId, connection.roomId);
    await _cacheRepository.SetAsync($"connection-{Context.ConnectionId}", connection, TimeSpan.FromHours(1));
    // Notify group members about the new user
    await Clients.Group(connection.roomId)
                .SendAsync("UserJoined", "admin", $"{connection.username} has joined");
}

消息和通知

用户向特定的聊天室发送消息,并对消息进行缓存和持久化。
打字状态等通知会广播到房间。

public async Task SendMessage(string roomId, string senderId, string content)
{
    // Fetch the cached connection details
    var cachedConnection = await _cacheRepository.GetAsync<UserConnection>($"connection-{Context.ConnectionId}");
    if (cachedConnection is null)
    {
        await Clients.Caller.SendAsync("Error", "User not in any chat room.");
        return;
    }
    // Prepare the message and cache it in the room's chat history
    var message = new
    {
        Username = cachedConnection.username,
        Message = content,
        Timestamp = DateTime.UtcNow
    };
    await _cacheRepository.AddToListAsync($"room-{cachedConnection.roomId}", message);
    // Persist the message to the database
    var command = new AddMessageCommand(Convert.ToInt64(roomId), Convert.ToInt64(senderId), content);
    await _sender.Send(command);
    // Broadcast the message to the group
    await Clients.Group(cachedConnection.roomId)
                .SendAsync("ReceiveMessage", cachedConnection.username, content);
}

处理断开连接

当用户断开连接时,将其从组中删除并清除其缓存的连接数据。

public override async Task OnDisconnectedAsync(Exception? exception)
{
    // Retrieve the cached connection information
    var cachedConnection = await _cacheRepository.GetAsync<UserConnection>($"connection-{Context.ConnectionId}");
    if (cachedConnection is not null)
    {
        // Clean up the cache and notify the group
        await _cacheRepository.RemoveAsync($"connection-{Context.ConnectionId}");
        await Groups.RemoveFromGroupAsync(Context.ConnectionId, cachedConnection.roomId);
        await Clients.Group(cachedConnection.roomId)
                    .SendAsync("UserLeft", cachedConnection.username);
    }
    await base.OnDisconnectedAsync(exception);
}

前端概述

        该应用程序的前端使用 ReactJS 构建,提供了与后端交互的接口。虽然本文主要关注后端,但我将对前端组件及其与后端的交互方式进行概述。

前端的关键组件

        • AuthContext:管理用户身份验证,存储 API 请求的 JWT 令牌。
        • 登录/注册:允许用户使用身份验证端点登录和注册。
        • SelectRoom:显示创建或加入房间的选项。
        • CreateRoom:允许用户通过调用 API 创建新的聊天室CreateRoom。
        • JoinRoom:让用户通过提供房间 ID 加入现有房间。
        • 聊天室:实时消息传递的地方。它连接到 SignalR 中心并发送/接收消息。

SignalR 客户端集成

        React 组件利用 SignalR 的 JavaScript 客户端ChatRoom与用户交互ChatHub。它管理用户的连接、消息传递和活动通知。

SignalR 连接

ChatHub使用用户的 JWT 令牌初始化连接。

const connection = new HubConnectionBuilder()
  .withUrl('http://localhost:5223/chat', { accessTokenFactory: () => token })
  .withAutomaticReconnect()
  .build();

connectionRef.current = connection;

await connection.start();

加入房间并接收消息

调用JoinSpecificChatRoom集线器上的方法加入聊天室。
监听传入消息并更新 UI。

发送消息

使用该方法向集线器发送消息SendMessage。

键入通知

当用户打字时通知聊天室中的其他用户。

关键要点

        • 使用 SignalR 轻松实现实时功能
                SignalR 利用 WebSocket 并为旧版浏览器或受限环境提供回退选项,简化了向应用程序添加实时功能的过程。其灵活性使其成为构建聊天应用程序的绝佳选择。

        • 可扩展和安全的后端设计
                实现演示了如何使用 JWT 身份验证进行安全用户访问、使用 SignalR Hubs 进行实时通信以及使用 Redis 进行缓存以支持可扩展性。

        • 实用的 API 设计
                通过构建用户身份验证、房间管理和消息传递端点,后端提供了一个干净的界面,可供前端应用程序轻松使用。

        • 前端与后端的集成
                使用 ReactJS 作为前端展示了 SignalR 如何无缝连接用户界面和后端以实现实时更新和消息传递。

未来的增强功能

高级功能

        • 使用 WebRTC 实现音频和视频通话。
        • 添加实时阅读回执、消息反应和通知,以改善聊天体验。

改进的可扩展性

        • 探索其他底板选项,例如用于分布式消息传递的 SQL Server 或 Redis。

数据库优化

        • 通过搜索功能和存档功能增强消息存储。
        • 使用数据库索引来提高聊天历史的查询性能。

增强安全性

        • 为消息添加端到端加密以确保隐私。
        • 对用户帐户实施多因素身份验证。

用户体验

        • 通过现代设计和响应式 UI 增强前端,以提高可用性。
        • 提供离线消息存储和传递,实现无缝通信。

结论

        本文介绍了使用 .NET Core 和 SignalR 构建实时聊天应用程序的基本步骤,并展示了其高效处理实时消息的能力。虽然当前的实现已经可以正常运行,但仍有令人兴奋的改进空间,如“未来增强功能”部分所述。我们非常欢迎您提出想法和反馈,这将有助于进一步完善此解决方案——欢迎随时联系我们或在评论区分享您的想法。

参考

聊天服务(后端) — https://download.youkuaiyun.com/download/hefeng_aspnet/90985964
聊天客户端(前端) — https://download.youkuaiyun.com/download/hefeng_aspnet/90985960
微软的 SignalR 简介 — https://learn.microsoft.com/en-us/aspnet/signalr/overview/getting-started/introduction-to-signalr

如果您喜欢此文章,请收藏、点赞、评论,谢谢,祝您快乐每一天。 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

csdn_aspnet

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值