前言
SignalR 是一种实时通信库,用于在客户端和服务器之间实现实时双向通信。它是由 Microsoft 开发并维护的开源项目,旨在简化构建实时 Web 应用程序的复杂性。
主要特点和用途包括:
实时通信: SignalR 提供了一种简化实时通信的方式,允许服务器主动向客户端推送数据,而不需要客户端发出请求。这使得在 Web 应用程序中实现实时功能(如聊天、通知、实时更新等)变得更加容易。
支持多种传输方式: SignalR 使用多种传输方式,包括 WebSocket、Server-Sent Events(SSE)、Long Polling 等,以确保在不同环境下都能提供最佳的实时通信性能。
跨平台和语言: SignalR 提供了多种客户端库,包括 .NET、JavaScript、Java、Python 等,使得在不同平台和语言下实现实时通信变得更加灵活。
连接持久性: SignalR 具有连接持久性,即使在网络断开或重新连接时,也能够保持通信连接。这使得实时通信在不稳定的网络环境下表现良好。
集成性: SignalR 可以与 ASP.NET Core、ASP.NET MVC 等框架集成,方便开发人员在现有项目中添加实时通信功能。
在实际应用中,SignalR 可以用于构建需要实时性的应用程序,如在线游戏、实时协作工具、监控系统等。其优点在于简化了开发人员在构建实时应用时需要处理的底层通信细节,使得开发实时功能更加高效和可维护。
后端实现
一、Nuget包下载(Microsoft.AspNetCore.SignalR)
二、SignalR注入
//添加SignalR注入配置
builder.Services.AddSignalR();
三、配置Hub中间件及前端访问路径
app.MapHub<SystemMessageHub>("/mess");
public class SystemMessageHub : Hub
{
//.netcore 定时执行 https://blog.youkuaiyun.com/sunshineGGB/article/details/121765514
public readonly AccountService _accountService;
public SystemMessageHub(AccountService adminUserService)
{
_accountService = adminUserService;
}
/// <summary>
/// 建立SignalR的客户端id与token的关系
/// </summary>
public static Dictionary<string, UserData> UserIdAndCid = new Dictionary<string, UserData>();
/// <summary>
/// 建立连接初始化方法
/// </summary>
/// <param name="token"></param>
/// <param name="userid"></param>
/// <returns></returns>
[Authorize]
public async Task Init(string token, Guid userid)
{
var login = await _accountService.GetUserInfoToMessAsync(userid);
UserData userData = new UserData()
{
ConnectionId = Context.ConnectionId,
Token = token,
UserName = login.Account,
UserTrueName = login.Name
};
UserIdAndCid.Add(Context.ConnectionId, userData);
}
/// <summary>
/// 记录当前用户的连接ID,方便后面给客户端用户推送站内信通知消息。
/// OnConnectedAsync当与集线器建立新连接时调用。
/// </summary>
/// <returns></returns>
public override async Task OnConnectedAsync()
{
//var userId = _currentUserService.GetUserId();
//await _userService.SaveWebSocketIdAsync(userId, Context.ConnectionId);
//Console.WriteLine($"新的连接:{Context.ConnectionId},userId={userId}");
处理当前用户的未读消息
//var unreadMessage = await _messageService.GetMyMessageUnReadNumberAsync(userId);
//var message = new
//{
// UnReadNumber = unreadMessage,
// MessageList = new List<MessageDTO>()
//};
//await Clients.Client(Context.ConnectionId).SendAsync(MesssageCenter.NewMessageNotify, message);
await base.OnConnectedAsync();
}
/// <summary>
/// 主要作用是,当用户的WebSocket断开连接后,清空他的连接ID。
/// OnDisconnectedAsync当与集线器的连接终止时调用。
/// </summary>
/// <param name="exception"></param>
/// <returns></returns>
public override async Task OnDisconnectedAsync(Exception? exception)
{
//var userId = _currentUserService.GetUserId();
//Console.WriteLine($"断开连接:{Context.ConnectionId},userId={userId}");
//await _userService.SaveWebSocketIdAsync(userId, string.Empty);
if (UserIdAndCid.Keys.Contains(Context.ConnectionId))
{
//UserData userData = UserIdAndCid[Context.ConnectionId];
//if (userData != null)
//{
// userData.Timer.Stop(); //停止定时器
// userData.Timer.Dispose(); //释放定时器
//}
UserIdAndCid.Remove(Context.ConnectionId);
}
await base.OnDisconnectedAsync(exception);
}
/// <summary>
/// 向客户端所有人发送消息
/// </summary>
/// <returns></returns>
public async Task SendAllMessage(SignalRMessageViewModel dto)
{
if (dto.GetUser == "all")
{
await Clients.All.SendAsync("GetAllMess", dto); //调用客户端的方法
}
}
//public async Task SendMess(string connectionId,string mess)
//{
//}
//public void StartTime(object? sender, ElapsedEventArgs e,string connectionId, Hub hub)
//{
// UserData userData = UserIdAndCid[connectionId];
// if (userData == null)
// {
// return;
// }
//}
}
public class UserData
{
public string ConnectionId { get; set; }
public string Token { get; set; }
public string UserName { get; set; }
public string UserTrueName { get; set; }
public DateTime LoginDate { get; set; }
}
其中UserData类定义可根据实际情况进行修改
三、控制器API接口层
public class MessManagerController : BaseMessConterController
{
private readonly MessageService _messageService;
/// <summary>
/// 注入消息服务
/// </summary>
/// <param name="messageService"></param>
public MessManagerController(MessageService messageService)
{
_messageService = messageService;
}
/// <summary>
/// 获取消息
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
[HttpPost]
public async Task<AppResult> getMess(MessInputModel model)
{
// 给所有客户端发送消息 message 表示给客户端传递的参数
PageResultModel<SignalRMessageViewModel> data = await _messageService.GetMessageAsync(model);
return AppResult.Status200OK(data: data);
}
/// <summary>
/// 获取未读消息
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
[HttpGet]
public async Task<AppResult> getUnreadMess(Guid userId)
{
// 获取指定用户未读消息列表
PageResultModel<SignalRMessageViewModel> data = await _messageService.GetUnreadMessageAsync(userId);
return AppResult.Status200OK(data:data);
}
/// <summary>
/// 修改消息状态
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
[HttpPost]
public async Task<AppResult> changeStatus(ChangeMessStatus model)
{
await _messageService.ChangeStatusAsync(model);
return AppResult.Status200OK();
}
/// <summary>
/// 发送消息
/// </summary>
/// <param name="message"></param>
/// <returns></returns>
[HttpPost]
public async Task<AppResult> sendMess(SignalRMessageViewModel message)
{
// 给所有客户端发送消息 message 表示给客户端传递的参数
await _messageService.SendMess(message);
return AppResult.Status200OK("消息发送成功!");
}
}
四、数据Model层
public class MessageModel : ModelBase
{
/// <summary>
/// 主键
/// </summary>
public Guid? Id { get; set; }
/// <summary>
/// 发起人
/// </summary>
public string? SendUser { get; set; }
/// <summary>
/// 接收人
/// </summary>
public string GetUser { get; set; }
/// <summary>
/// 标题
/// </summary>
public string Title { get; set; }
/// <summary>
/// 内容
/// </summary>
public string Content { get; set; }
/// <summary>
/// 发送时间
/// </summary>
public DateTime? SendDate { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreatedDate { get; set; } = DateTime.Now;
public override void ConfigureMapper(Profile profile)
{
profile.CreateMap<MessageModel, Message>();
profile.CreateMap<Message, MessageModel>()
.ForMember(d => d.Id, options => options.Ignore());
}
}
五、服务Service层(此步骤不提供具体代码 请根据实际情况编写)
六、开放跨域访问策略
// 允许任意跨域
policy.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader().AllowCredentials();
// 允许指定域名
policy.WithOrigins(AppSettings.AllowCors).AllowAnyMethod().AllowAnyHeader().AllowCredentials();
前端实现
一、npm包下载(npm install @aspnet/signalr)
二、在Store中添加SignalR及连接方法
import * as signalR from '@aspnet/signalr'
state: {
conn: null,
}
mutations: {
SET_CONN: (state, conn) => {
state.conn = conn
},
}
actions: {
...
// 建立signalR连接
signalRConnection({ commit, state }) {
// process.env.VUE_APP_BASE_Host是配置文件里写的公共的http……这个头
// 拼接建立signal连接的url 其中mess是在api端的program中注册的路径 app.MapHub<SystemMessageHub>("/mess");
const url = process.env.VUE_APP_BASE_HOST + '/mess'
// 建立signal的连接
var conn = new signalR.HubConnectionBuilder()
.withUrl(url, { skipNegotiation: true, transport: signalR.HttpTransportType.WebSockets })
// .configureLogging(signalR.LogLevel.Debug)
.build()
// 启动连接 并调用signalR的 服务端的注册登录方法 Init
conn.start().then(function() {
conn.invoke('Init', state.token, state.info.id).catch(function(err) {
return console.error(err.toString())
})
})
...
}
注意:请先在.env文件中注册路径:
VUE_APP_BASE_HOST=http://localhost:后端端口
然后在getter.js文件中添加:(即如何获取存储的状态 自行添加)
const getters = {
signalRConn: state => state.user.conn
}
export default getters
如果与后端连接不稳定 建议在.WithUrl中放开Negotiate协商
三、在页面中建立SignalR连接
推荐在基础布局文件BasicLayout.vue中使用,以保证在加载初始化时自动建立连接,并全局共享同一个连接。
computed: {
conn() {
return this.$store.getters.signalRConn
},
},
created() {
// 建立SignalR连接
if (this.conn == null) this.signalRConnection()
this.conn.on('GetAllMess', data => {
// 使用 ant-design-vue 的 Notification 通知提醒框弹出消息
this.$notification.open({
message: data.title,
description: data.content,
onClick: () => {
console.log('Notification Clicked!')
},
duration: 0
})
})
},
methods: {
...mapActions(['setSidebar', 'signalRConnection'])
}
效果测试
一、新建API接口方法
/**
* 发送消息
*
* @author chefng
* @date 2023/11/10 11:28
*/
export function sendMess(parameter) {
return axios({
url: '/MessManager/sendMess',
method: 'post',
data: parameter
})
}
二、在某个页面中添加一个按钮及方法
<a-button type="primary" @click="sendMess">发送信息</a-button>
import { sendMess } from '@/api/modular/main/signalR'
methods: {
sendMess() {
const data1 = {
getUser: 'all',
title: '主题',
content: '测试内容',
sendUser: '测试用户',
createdDate: '2023-11-10T14:07:25'
}
sendMess(data1).then(r => {
if (r.code === 200) {
this.$message('消息发送成功')
}
})
},
}
此时使用两个不同的浏览器(例如Chrome和 Edge)同时登陆网站,在其中一个浏览器中对应页面点击发送信息按钮后,两个浏览器应该都会显示对应的Notification。
当然,此处并没有针对具体用户推送消息,可以通过之前存储的UserData进行相应的配置
效果图:
上述代码部分并不完整 如有问题请自行解决或在评论区予以指出