介绍
在当今世界,实时通信是现代应用程序的必备功能。无论是在 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
如果您喜欢此文章,请收藏、点赞、评论,谢谢,祝您快乐每一天。