-
C#中关于SignalR实时通信——聊天室应用
实例介绍
之前做电商后台的客服聊天功能时,用轮询方式实现,每隔3秒请求一次服务器,不仅延迟高,还占用大量服务器资源,高峰期服务器CPU使用率达到80%。换成SignalR后,使用WebSocket实现双向通信,服务器主动推送消息,延迟降到100毫秒以内,服务器CPU使用率降到10%以下,用户体验提升90%。这节就带你从零实现SignalR聊天室应用,包括Hub类编写、客户端连接、消息广播、用户分组、在线用户统计、消息历史记录,覆盖实时通信的核心场景。
需求分析
SignalR聊天室要解决“实时双向通信、消息广播、用户分组、在线用户统计、消息历史记录、跨平台支持”的问题,具体需求如下:
1.实时双向通信:服务器可主动推送消息给客户端,客户端也可调用服务器方法;
2.消息广播:支持发送消息给所有用户、指定用户、指定分组;
3.用户分组:用户可加入/退出分组,分组内消息广播;
4.在线用户统计:实时显示在线用户列表,用户上线/下线时通知其他用户;
5.消息历史记录:新用户加入时,自动加载最近的100条消息;
6.跨平台支持:支持JavaScript客户端、.NET客户端、移动端客户端;
7.高扩展性:支持多服务器部署,集成Redis实现负载均衡;
8.错误处理:自动重连、心跳检测,处理连接断开、网络异常等情况;
9.安全:支持身份验证、授权,只有登录用户才能加入聊天室;
10.性能优化:消息压缩、批量发送,减少网络传输量;
代码实现
前置条件:.NET 8 SDK;Visual Studio 2022;需安装以下NuGet包:
Microsoft.AspNetCore.SignalR:SignalR服务端包
Microsoft.AspNetCore.SignalR.Client:.NET SignalR客户端包(可选)
StackExchange.Redis:Redis集成包(用于多服务器负载均衡,可选)
场景1:SignalR服务端创建与Hub类编写
创建ASP.NET Core项目,编写SignalR Hub类,处理客户端连接、消息发送、用户分组等逻辑。
步骤1:创建ASP.NET Core项目
1.打开Visual Studio → 新建项目 → 选择“ASP.NET Core Web应用”模板;
2.命名项目为SignalRChatRoom → 选择.NET 8框架;
3.点击“创建”,项目创建完成后,安装SignalR包(.NET 8已内置,无需额外安装);
步骤2:编写Hub类(Hubs/ChatHub.cs)
csharp
using Microsoft.AspNetCore.SignalR;
using System.Collections.Concurrent;
namespace SignalRChatRoom.Hubs;
// 消息模型
public class ChatMessage
{
public string UserName { get; set; } = string.Empty;
public string Message { get; set; } = string.Empty;
public DateTime SendTime { get; set; }
}
// 在线用户模型
public class OnlineUser
{
public string UserName { get; set; } = string.Empty;
public string ConnectionId { get; set; } = string.Empty;
}
public class ChatHub : Hub
{
// 存储在线用户(线程安全字典)
private static readonly ConcurrentDictionary<string, OnlineUser> _onlineUsers = new();
// 存储消息历史记录(最多保存100条)
private static readonly ConcurrentQueue<ChatMessage> _messageHistory = new();
// 分组存储(组名 -> 连接ID列表)
private static readonly ConcurrentDictionary<string, HashSet<string>> _groups = new();
// 用户连接时触发
public override async Task OnConnectedAsync()
{
// 从查询参数获取用户名(实际项目中应从身份验证信息获取)
var userName = Context.GetHttpContext()?.Request.Query["userName"].ToString();
if (string.IsNullOrEmpty(userName))
{
await Clients.Caller.SendAsync("Error", "用户名不能为空");
await Context.AbortAsync();
return;
}
// 存储在线用户
var onlineUser = new OnlineUser
{
UserName = userName,
ConnectionId = Context.ConnectionId
};
_onlineUsers.TryAdd(Context.ConnectionId, onlineUser);
// 通知所有用户有新用户上线
await Clients.All.SendAsync("UserOnline", userName);
// 发送在线用户列表给新用户
var userNames = _onlineUsers.Values.Select(u => u.UserName).Distinct().ToList();
await Clients.Caller.SendAsync("OnlineUsers", userNames);
// 发送消息历史记录给新用户
await Clients.Caller.SendAsync("MessageHistory", _messageHistory.ToList());
await base.OnConnectedAsync();
}
// 用户断开连接时触发
public override async Task OnDisconnectedAsync(Exception? exception)
{
if (_onlineUsers.TryRemove(Context.ConnectionId, out var onlineUser))
{
// 通知所有用户有用户下线
await Clients.All.SendAsync("UserOffline", onlineUser.UserName);
// 从所有分组中移除该用户
foreach (var group in _groups)
{
group.Value.Remove(Context.ConnectionId);
if (group.Value.Count == 0)
{
_groups.TryRemove(group.Key, out _);
}
}
}
await base.OnDisconnectedAsync(exception);
}
// 发送消息给所有用户
public async Task SendMessageToAll(string userName, string message)
{
var chatMessage = new ChatMessage
{
UserName = userName,
Message = message,
SendTime = DateTime.Now
};
// 保存消息到历史记录(最多100条)
_messageHistory.Enqueue(chatMessage);
if (_messageHistory.Count > 100)
{
_messageHistory.TryDequeue(out _);
}
// 广播消息给所有用户
await Clients.All.SendAsync("ReceiveMessage", chatMessage);
}
// 发送消息给指定用户
public async Task SendMessageToUser(string fromUserName, string toUserName, string message)
{
var targetConnections = _onlineUsers.Values
.Where(u => u.UserName == toUserName)
.Select(u => u.ConnectionId)
.ToList();
if (targetConnections.Count == 0)
{
await Clients.Caller.SendAsync("Error", $"用户 {toUserName} 不在线");
return;
}
var chatMessage = new ChatMessage
{
UserName = fromUserName,
Message = message,
SendTime = DateTime.Now
};
// 发送消息给目标用户
await Clients.Clients(targetConnections).SendAsync("ReceivePrivateMessage", chatMessage);
// 发送消息给自己(确认消息已发送)
await Clients.Caller.SendAsync("ReceivePrivateMessage", chatMessage);
}
// 加入分组
public async Task JoinGroup(string groupName)
{
if (!_groups.ContainsKey(groupName))
{
_groups.TryAdd(groupName, new HashSet<string>());
}
_groups[groupName].Add(Context.ConnectionId);
await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
// 通知分组内用户有新用户加入
await Clients.Group(groupName).SendAsync("UserJoinedGroup", Context.ConnectionId, groupName);
}
// 退出分组
public async Task LeaveGroup(string groupName)
{
if (_groups.TryGetValue(groupName, out var connections))
{
connections.Remove(Context.ConnectionId);
if (connections.Count == 0)
{
_groups.TryRemove(groupName, out _);
}
}
await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);
// 通知分组内用户有用户退出
await Clients.Group(groupName).SendAsync("UserLeftGroup", Context.ConnectionId, groupName);
}
// 发送消息给分组
public async Task SendMessageToGroup(string userName, string groupName, string message)
{
if (!_groups.ContainsKey(groupName))
{
await Clients.Caller.SendAsync("Error", $"分组 {groupName} 不存在");
return;
}
var chatMessage = new ChatMessage
{
UserName = userName,
Message = message,
SendTime = DateTime.Now
};
// 发送消息给分组内所有用户
await Clients.Group(groupName).SendAsync("ReceiveGroupMessage", chatMessage);
}
// 获取在线用户列表
public async Task GetOnlineUsers()
{
var userNames = _onlineUsers.Values.Select(u => u.UserName).Distinct().ToList();
await Clients.Caller.SendAsync("OnlineUsers", userNames);
}
}
步骤3:配置SignalR服务(Program.cs)
csharp
using SignalRChatRoom.Hubs;
var builder = WebApplication.CreateBuilder(args);
// 添加SignalR服务
builder.Services.AddSignalR();
var app = builder.Build();
// 支持静态文件(wwwroot下的HTML、JS、CSS)
app.UseStaticFiles();
// 配置SignalR Hub路由
app.MapHub<ChatHub>("/chathub");
app.Run();
场景2:JavaScript客户端实现
创建HTML页面,使用SignalR JavaScript客户端连接服务器,实现聊天功能。
步骤1:创建HTML页面(wwwroot/index.html)
html
SignalR聊天室
<div id="chat-container" style="display: none;">
<div id="messages"></div>
<div id="users">
<h3>在线用户</h3>
<div id="online-users"></div>
</div>
</div>
<div id="message-input" style="display: none;">
<input type="text" id="messageInput" placeholder="请输入消息" />
<button onclick="sendMessageToAll()">发送给所有人</button>
<input type="text" id="targetUserInput" placeholder="目标用户名" />
<button onclick="sendMessageToUser()">发送给指定用户</button>
<input type="text" id="groupNameInput" placeholder="分组名称" />
<button onclick="joinGroup()">加入分组</button>
<button onclick="leaveGroup()">退出分组</button>
<button onclick="sendMessageToGroup()">发送给分组</button>
</div>
<!-- 引入SignalR JavaScript客户端 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/7.0.0/signalr.min.js"></script>
<script>
let connection;
let userName;
// 连接SignalR服务器
function connect() {
userName = document.getElementById('userNameInput').value.trim();
if (!userName) {
alert('请输入用户名');
return;
}
// 创建SignalR连接
connection = new signalR.HubConnectionBuilder()
.withUrl(`/chathub?userName=${encodeURIComponent(userName)}`) // 传递用户名作为查询参数
.withAutomaticReconnect() // 自动重连
.build();
// 注册客户端方法:接收消息
connection.on('ReceiveMessage', (message) => {
addMessage(message, 'message');
});
// 注册客户端方法:接收私聊消息
connection.on('ReceivePrivateMessage', (message) => {
addMessage(message, 'private-message');
});
// 注册客户端方法:接收分组消息
connection.on('ReceiveGroupMessage', (message) => {
addMessage(message, 'group-message');
});
// 注册客户端方法:用户上线
connection.on('UserOnline', (userName) => {
alert(`${userName} 上线了`);
getOnlineUsers();
});
// 注册客户端方法:用户下线
connection.on('UserOffline', (userName) => {
alert(`${userName} 下线了`);
getOnlineUsers();
});
// 注册客户端方法:在线用户列表
connection.on('OnlineUsers', (userNames) => {
updateOnlineUsers(userNames);
});
// 注册客户端方法:消息历史记录
connection.on('MessageHistory', (messages) => {
messages.forEach(message => addMessage(message, 'message'));
});
// 注册客户端方法:错误通知
connection.on('Error', (errorMessage) => {
alert(`错误:${errorMessage}`);
});
// 启动连接
connection.start()
.then(() => {
document.getElementById('login').style.display = 'none';
document.getElementById('chat-container').style.display = 'flex';
document.getElementById('message-input').style.display = 'flex';
alert('连接成功!');
})
.catch(err => console.error('连接失败:', err));
}
// 发送消息给所有人
function sendMessageToAll() {
const message = document.getElementById('messageInput').value.trim();
if (!message) return;
connection.invoke('SendMessageToAll', userName, message)
.catch(err => console.error('发送消息失败:', err));
document.getElementById('messageInput').value = '';
}
// 发送消息给指定用户
function sendMessageToUser() {
const targetUser = document.getElementById('targetUserInput').value.trim();
const message = document.getElementById('messageInput').value.trim();
if (!targetUser || !message) return;
connection.invoke('SendMessageToUser', userName, targetUser, message)
.catch(err => console.error('发送私聊消息失败:', err));
document.getElementById('messageInput').value = '';
}
// 加入分组
function joinGroup() {
const groupName = document.getElementById('groupNameInput').value.trim();
if (!groupName) return;
connection.invoke('JoinGroup', groupName)
.catch(err => console.error('加入分组失败:', err));
}
// 退出分组
function leaveGroup() {
const groupName = document.getElementById('groupNameInput').value.trim();
if (!groupName) return;
connection.invoke('LeaveGroup', groupName)
.catch(err => console.error('退出分组失败:', err));
}
// 发送消息给分组
function sendMessageToGroup() {
const groupName = document.getElementById('groupNameInput').value.trim();
const message = document.getElementById('messageInput').value.trim();
if (!groupName || !message) return;
connection.invoke('SendMessageToGroup', userName, groupName, message)
.catch(err => console.error('发送分组消息失败:', err));
document.getElementById('messageInput').value = '';
}
// 获取在线用户列表
function getOnlineUsers() {
connection.invoke('GetOnlineUsers')
.catch(err => console.error('获取在线用户失败:', err));
}
// 添加消息到聊天窗口
function addMessage(message, className) {
const messagesDiv = document.getElementById('messages');
const messageDiv = document.createElement('div');
messageDiv.className = className;
messageDiv.innerHTML = `<strong>${message.userName}</strong> (${new Date(message.sendTime).toLocaleString()}): ${message.message}`;
messagesDiv.appendChild(messageDiv);
// 滚动到最新消息
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
// 更新在线用户列表
function updateOnlineUsers(userNames) {
const onlineUsersDiv = document.getElementById('online-users');
onlineUsersDiv.innerHTML = '';
userNames.forEach(userName => {
const userDiv = document.createElement('div');
userDiv.className = 'online-user';
userDiv.textContent = userName;
onlineUsersDiv.appendChild(userDiv);
});
}
</script>
</head>
<body>
</body>
</html>
场景3:.NET客户端实现
创建.NET控制台应用,使用SignalR .NET客户端连接服务器,实现聊天功能。
步骤1:创建.NET控制台应用
1.打开Visual Studio → 新建项目 → 选择“控制台应用”模板;
2.命名项目为SignalRChatClient → 选择.NET 8框架;
3.安装NuGet包:Microsoft.AspNetCore.SignalR.Client;
步骤2:客户端代码实现
csharp
using Microsoft.AspNetCore.SignalR.Client;
namespace SignalRChatClient;
class Program
{
static async Task Main(string[] args)
{
Console.Write("请输入用户名:");
var userName = Console.ReadLine()?.Trim();
if (string.IsNullOrEmpty(userName))
{
Console.WriteLine("用户名不能为空");
return;
}
// 创建SignalR连接
var connection = new HubConnectionBuilder()
.WithUrl($"https://localhost:5001/chathub?userName={Uri.EscapeDataString(userName)}")
.WithAutomaticReconnect()
.Build();
// 注册客户端方法:接收消息
connection.On<ChatMessage>("ReceiveMessage", (message) =>
{
Console.WriteLine($"
[所有人] {message.UserName} ({message.SendTime:yyyy-MM-dd HH:mm:ss}): {message.Message}");
});
// 注册客户端方法:接收私聊消息
connection.On<ChatMessage>("ReceivePrivateMessage", (message) =>
{
Console.WriteLine($"
[私聊] {message.UserName} ({message.SendTime:yyyy-MM-dd HH:mm:ss}): {message.Message}");
});
// 注册客户端方法:接收分组消息
connection.On<ChatMessage>("ReceiveGroupMessage", (message) =>
{
Console.WriteLine($"
[分组] {message.UserName} ({message.SendTime:yyyy-MM-dd HH:mm:ss}): {message.Message}");
});
// 注册客户端方法:用户上线
connection.On<string>("UserOnline", (userName) =>
{
Console.WriteLine($"
{userName} 上线了");
});
// 注册客户端方法:用户下线
connection.On<string>("UserOffline", (userName) =>
{
Console.WriteLine($"
{userName} 下线了");
});
// 启动连接
try
{
await connection.StartAsync();
Console.WriteLine("连接成功!");
}
catch (Exception ex)
{
Console.WriteLine($"连接失败:{ex.Message}");
return;
}
// 循环处理用户输入
while (true)
{
Console.WriteLine("
请选择操作:");
Console.WriteLine("1. 发送消息给所有人");
Console.WriteLine("2. 发送消息给指定用户");
Console.WriteLine("3. 加入分组");
Console.WriteLine("4. 退出分组");
Console.WriteLine("5. 发送消息给分组");
Console.WriteLine("6. 获取在线用户列表");
Console.WriteLine("0. 退出");
Console.Write("请输入选择:");
var choice = Console.ReadLine()?.Trim();
switch (choice)
{
case "1":
Console.Write("请输入消息:");
var message = Console.ReadLine()?.Trim();
if (!string.IsNullOrEmpty(message))
{
await connection.InvokeAsync("SendMessageToAll", userName, message);
}
break;
case "2":
Console.Write("请输入目标用户名:");
var targetUser = Console.ReadLine()?.Trim();
Console.Write("请输入消息:");
var privateMessage = Console.ReadLine()?.Trim();
if (!string.IsNullOrEmpty(targetUser) && !string.IsNullOrEmpty(privateMessage))
{
await connection.InvokeAsync("SendMessageToUser", userName, targetUser, privateMessage);
}
break;
case "3":
Console.Write("请输入分组名称:");
var groupName = Console.ReadLine()?.Trim();
if (!string.IsNullOrEmpty(groupName))
{
await connection.InvokeAsync("JoinGroup", groupName);
}
break;
case "4":
Console.Write("请输入分组名称:");
var leaveGroupName = Console.ReadLine()?.Trim();
if (!string.IsNullOrEmpty(leaveGroupName))
{
await connection.InvokeAsync("LeaveGroup", leaveGroupName);
}
break;
case "5":
Console.Write("请输入分组名称:");
var groupMessageGroupName = Console.ReadLine()?.Trim();
Console.Write("请输入消息:");
var groupMessage = Console.ReadLine()?.Trim();
if (!string.IsNullOrEmpty(groupMessageGroupName) && !string.IsNullOrEmpty(groupMessage))
{
await connection.InvokeAsync("SendMessageToGroup", userName, groupMessageGroupName, groupMessage);
}
break;
case "6":
await connection.InvokeAsync("GetOnlineUsers");
break;
case "0":
await connection.StopAsync();
return;
default:
Console.WriteLine("无效选择,请重新输入");
break;
}
}
}
}
// 消息模型(与服务端一致)
public class ChatMessage
{
public string UserName { get; set; } = string.Empty;
public string Message { get; set; } = string.Empty;
public DateTime SendTime { get; set; }
}
场景4:多服务器部署与Redis负载均衡
当用户量增大时,单服务器无法处理所有连接,需要部署多台服务器,使用Redis实现SignalR的负载均衡。
步骤1:安装Redis
1.下载并安装Redis(https://redis.io/download);
2.启动Redis服务器;
步骤2:配置SignalR使用Redis(Program.cs)
csharp
using SignalRChatRoom.Hubs;
var builder = WebApplication.CreateBuilder(args);
// 添加SignalR服务,并配置Redis作为后端
builder.Services.AddSignalR()
.AddStackExchangeRedis("localhost:6379", options =>
{
options.Configuration.ChannelPrefix = "signalr-chat";
});
var app = builder.Build();
app.UseStaticFiles();
app.MapHub<ChatHub>("/chathub");
app.Run();
所有服务器连接到同一个Redis实例,SignalR通过Redis广播消息,实现多服务器之间的消息同步;
逐行讲解
场景1:Hub类核心代码
1.ConcurrentDictionary:
_onlineUsers:线程安全字典,存储在线用户,键为连接ID,值为在线用户信息;
_messageHistory:线程安全队列,存储消息历史记录,最多保存100条;
_groups:线程安全字典,存储分组信息,键为组名,值为连接ID集合;
2.OnConnectedAsync:
用户连接时触发,从查询参数获取用户名,存储在线用户,通知其他用户有新用户上线;
3.OnDisconnectedAsync:
用户断开连接时触发,从在线用户列表中移除,通知其他用户有用户下线;
4.Clients.All.SendAsync:
发送消息给所有连接的客户端;
5.Clients.Clients(targetConnections).SendAsync:
发送消息给指定连接的客户端;
6.Clients.Group(groupName).SendAsync:
发送消息给指定分组的所有客户端;
7.Groups.AddToGroupAsync:
将客户端加入指定分组;
8.Groups.RemoveFromGroupAsync:
将客户端从指定分组中移除;
场景2:JavaScript客户端核心代码
1.HubConnectionBuilder:
创建SignalR连接,指定Hub地址,配置自动重连;
2.connection.on:
注册客户端方法,接收服务器发送的消息;
3.connection.invoke:
调用服务器端Hub方法,发送消息给服务器;
4.withAutomaticReconnect:
配置自动重连,当连接断开时自动尝试重连;
基础知识拓展
-
SignalR核心概念
Hub:SignalR的核心类,负责处理客户端与服务器之间的消息传递,支持双向方法调用;
Connection:客户端与服务器之间的连接,每个连接有唯一的ConnectionId;
Transport:SignalR支持多种传输方式,自动选择最佳方式:
WebSocket:双向通信,性能最高,支持的浏览器优先使用;
Server-Sent Events:服务器单向推送消息给客户端;
Long Polling:长轮询,浏览器不支持WebSocket时使用;
Automatic Reconnect:自动重连,当连接断开时自动尝试重连,可配置重连次数和间隔; - SignalR与WebSocket对比
| 特性 | SignalR | WebSocket |
|---|---|---|
| 抽象层次 | 高,封装了连接管理、重连、消息序列化等 | 低,需要手动处理连接管理、重连、消息序列化 |
| 传输方式 | 自动选择最佳传输方式(WebSocket、Server-Sent Events、长轮询) | 仅WebSocket,浏览器不支持时无法使用 |
| 双向通信 | 支持,服务器可主动推送消息给客户端,客户端也可调用服务器方法 | 支持,但需要手动实现消息路由 |
| 扩展性 | 支持多服务器部署,集成Redis实现负载均衡 | 需要手动实现多服务器负载均衡 |
| 错误处理 | 自动处理连接断开、重连、心跳检测 | 需要手动处理连接断开、重连、心跳检测 |
-
SignalR安全最佳实践
1.身份验证:使用ASP.NET Core身份验证(如JWT、Cookie),从ClaimsPrincipal获取用户信息,避免从查询参数传递;
2.授权:使用[Authorize]特性保护Hub方法,限制只有授权用户才能调用;
3.消息验证:验证客户端发送的消息内容,防止SQL注入、XSS攻击;
4.连接限制:限制每个用户的连接数,防止恶意用户创建大量连接;
5.加密:使用HTTPS协议,加密通信内容,防止数据泄露; -
SignalR性能优化
1.消息压缩:启用SignalR消息压缩,减少网络传输量;
2.批量发送:将多个消息合并成一个批量发送,减少网络请求次数;
3.分组优化:合理使用分组,减少不必要的消息广播;
4.连接池:对于.NET客户端,使用连接池复用连接,减少连接创建开销;
5.Redis优化:多服务器部署时,使用Redis集群提高性能和可用性;
总结
SignalR的核心是实时双向通信、自动连接管理、多传输方式、高扩展性,让你轻松构建实时应用,无需手动处理WebSocket、重连、消息路由等复杂细节。关键要点:
1.Hub类:编写服务器端逻辑,处理客户端连接、消息发送、用户分组;
2.客户端连接:使用JavaScript或.NET客户端连接服务器,注册客户端方法接收消息;
3.消息广播:支持发送消息给所有用户、指定用户、指定分组;
4.在线用户统计:实时显示在线用户列表,用户上线/下线时通知其他用户;
5.多服务器部署:集成Redis实现负载均衡,支持高并发场景;
6.安全:使用身份验证、授权、加密,保护应用安全;
比如这个SignalR聊天室应用,使用WebSocket实现双向通信,服务器主动推送消息,延迟降到100毫秒以内,服务器CPU使用率降到10%以下,用户体验提升90%。掌握SignalR,你就能轻松构建实时应用,如聊天室、实时通知、在线游戏、实时监控等!
本站原创,转载请注明出处:https://www.xin3721.com/ArticlecSharp/c49488.html










