SignalR实时消息推送
后端
创建一个Hub类,重写连接和断连方法
ChatHub
记录下每一位登录人连接SignalR的信息至Redis中
引用的Microsoft.AspNetCore.App框架
public class ChatHub : Hub { //ConcurrentDictionary<string, HubUser> concurrentDictionary = new ConcurrentDictionary<string, HubUser>(); private ICurrentLoginInfoService _currentLoginInfoService; private IUserSignalRRedisService _userSignalRRedisService; public ChatHub(ICurrentLoginInfoService currentLoginInfoService, IUserSignalRRedisService userSignalRRedisService) { _currentLoginInfoService = currentLoginInfoService; _userSignalRRedisService = userSignalRRedisService; } /// <summary> /// 连接 /// </summary> /// <returns></returns> public override async Task OnConnectedAsync() { //var client = concurrentDictionary.TryGetValue(Context?.ConnectionId, out HubUser? hubUser); var client = await _userSignalRRedisService.GetUserConnection(_currentLoginInfoService.UserId); if (client == null) { client = new HubUser() { ConnectionID = Context.ConnectionId, UserId = _currentLoginInfoService.UserId, Name = _currentLoginInfoService.UserName }; //concurrentDictionary.GetOrAdd(newUser.ConnectionID, newUser); } else { client.ConnectionID = Context.ConnectionId; } await _userSignalRRedisService.SetUserConnection(_currentLoginInfoService.UserId, client); await base.OnConnectedAsync(); } /// <summary> /// 断开连接 /// </summary> /// <param name="exception"></param> /// <returns></returns> public override async Task OnDisconnectedAsync(Exception? exception) { //var client = concurrentDictionary.TryGetValue(Context.ConnectionId, out HubUser? hubUser); var hubUser = await _userSignalRRedisService.GetUserConnection(_currentLoginInfoService.UserId); if (hubUser != null) { await _userSignalRRedisService.RemoveUserConnection(_currentLoginInfoService.UserId); //concurrentDictionary.Remove(hubUser.ConnectionID, out HubUser? reVal); } await base.OnDisconnectedAsync(exception); } }
UserSignalRRedisService
public class UserSignalRRedisService : IUserSignalRRedisService { private IRedisServer _redisServer; public UserSignalRRedisService(IRedisServer redisServer) { _redisServer = redisServer; } /// <summary> /// 获取用户的SignalR信息 /// </summary> /// <returns></returns> public async Task<HubUser> GetUserConnection(long userId) { var key = string.Format(GlobalConstants.UserSignalRSession, userId); var obj = await _redisServer.GetAsync(key); return obj.ToObject<HubUser>(); } /// <summary> /// 设置用户的SignalR信息 /// </summary> /// <returns></returns> public async Task<bool> SetUserConnection(long userId, HubUser hubUser) { var key = string.Format(GlobalConstants.UserSignalRSession, userId); return await _redisServer.SetAsync(key, hubUser.ToJson()); } /// <summary> /// 删除用户的SignalR信息 /// </summary> /// <param name="userId"></param> /// <returns></returns> public async Task<bool> RemoveUserConnection(long userId) { return await _redisServer.RemoveAsync(userId.ToString()); } }
ICurrentLoginInfoService
当前登录人信息
Program.cs引入SignalR
app.MapHub<ChatHub>("/messageHub");
自定义一个中间件来拦截SignalR的请求,做当前登录人信息做处理
public class SignalrInterceptionMiddleware { public const string AuthorizationSchem = "Bearer"; private readonly RequestDelegate _next; private readonly IUserRedisService _redisService; private readonly JwtOption _jwtOption; private readonly ILogger<SignalrInterceptionMiddleware> _logger; public SignalrInterceptionMiddleware(RequestDelegate next, IUserRedisService redisService, IOptions<JwtOption> options, ILogger<SignalrInterceptionMiddleware> logger) { _next = next; _redisService = redisService; _jwtOption = options.Value; _logger = logger; } public async Task InvokeAsync(HttpContext context) { //if(context.WebSockets.IsWebSocketRequest && context.Request.Path.StartsWithSegments("/messageHub")) if (context.Request.Path.StartsWithSegments("/messageHub")) { var accessToken = context.Request.Query["access_token"].ToString(); if (!string.IsNullOrEmpty(accessToken)) { var token = accessToken.Replace(AuthorizationSchem, string.Empty).Trim(); var userId = GetUserId(token); var redisUser = await _redisService.GetUserSessionAsync(userId); if (redisUser == null) throw new CustomException(StatusCode.Unauthorized); if (redisUser.Token == null || !redisUser.ExpireDateTime.HasValue) throw new CustomException(StatusCode.Unauthorized); if (redisUser.Token != token) throw new CustomException(StatusCode.LoginOnOtherDevice); var identity = new ClaimsIdentity(new List<Claim> { new Claim(GlobalConstants.UserId,userId.ToString()), new Claim(GlobalConstants.UserName, redisUser.Name.ToString()), }, "Custom"); context.User = new ClaimsPrincipal(identity); context.Items.Add(GlobalConstants.UserSession, redisUser); } } await _next(context); } private long GetUserId(string? token) { try { if (token.NotNull()) { var claims = JWTHelper.ValiateToken(token, _jwtOption); if (claims != null) { var userId = claims.FirstOrDefault(t => t.Type.Equals(GlobalConstants.UserId))?.Value; if (userId.NotNull()) return Convert.ToInt64(userId); } } } catch { _logger.LogError(string.Format("解析token异常,SignalrInterceptionMiddleware=>GetUserId")); } return default(long); } }
将当前登录人的信息通过token解析到Context.item中方便ICurrentLoginInfoService使用
在 Program.cs中在引入SignalR之前引入该中间件
app.UseMiddleware<SignalrInterceptionMiddleware>(); app.MapHub<ChatHub>("/messageHub");
方式二
通过用户标识去发送消息,不需要去记录每个客服端连接的ConnectionId,通过ConnectionId来发送消息到客户端
关键点:
在中间件SignalrInterceptionMiddleware处 将用户标识写入到Context.User中,默认情况下,SignalR 使用与连接关联的 ClaimTypes.NameIdentifier 中的 ClaimsPrincipal 作为用户标识符,使用自定义标识参考官网
写入后SignalR中的Context.UserIdentifier就会写入 ClaimTypes.NameIdentifier 的值 官网参考
改变如下
public async Task InvokeAsync(HttpContext context) { //if(context.WebSockets.IsWebSocketRequest && context.Request.Path.StartsWithSegments("/messageHub")) if (context.Request.Path.StartsWithSegments("/messageHub")) { var accessToken = context.Request.Query["access_token"].ToString(); if (!string.IsNullOrEmpty(accessToken)) { var token = accessToken.Replace(AuthorizationSchem, string.Empty).Trim(); var userId = GetUserId(token); var redisUser = await _redisService.GetUserSessionAsync(userId); if (redisUser == null) throw new CustomException(ErrorCode.Unauthorized); if (redisUser.Token == null || !redisUser.ExpireDateTime.HasValue) throw new CustomException(ErrorCode.Unauthorized); if (redisUser.Token != token) throw new CustomException(ErrorCode.LoginOnOtherDevice); var identity = new ClaimsIdentity(new List<Claim> { new Claim(ClaimTypes.NameIdentifier,userId.ToString()), new Claim(GlobalConstants.UserName, redisUser.Name.ToString()), }, "Custom"); context.User = new ClaimsPrincipal(identity); context.Items.Add(GlobalConstants.UserSession, redisUser); } } await _next(context); }
通过用户标识发送消息
/// <summary> /// 发送消息 /// </summary> /// <param name="userId"></param> /// <returns></returns> public async Task SendMessage(long userId,string msg) { var res=await _userSignalRRedisService.GetUserConnection(userId); if (res != null) { await _hubContext.Clients.User(userId.ToString()).SendAsync("MessageReceived", msg); //await _hubContext.Clients.Client(res.ConnectionID).SendAsync("MessageReceived", msg); } }
总结:
方式一,通过ConnectionId去向客户端发送消息,ConnectionId的值在客户端每次重新连接都会生成一个新的值,需要服务端对该值做存储,后续取出该值对客户端发送消息,适合向精准客户端发送消息
方式二,通过用户标识去向客户端发送消息,如使用用户的UserId等系统中唯一标识,无需对标识特殊存储。此种方式适用于同一用户多设备登录,但同时都会收到消息的情况。,每一个与该用户标识相关的SignalR连接都会收到消息
前端
以vue为例
安装包@microsoft/signalr
npm install @microsoft/signalr
建立一个SignalRHelper的公共类
import type { HubConnection } from '@microsoft/signalr'
import * as signalr from '@microsoft/signalr'
export default class SingalRHelper {
connection?: HubConnection
retryCount = 0
maxRetryAttempts = 5
retryInterval = 5000
url = `${import.meta.env.VITE_BASEURL}/messageHub`
receivedMsg = 'MessageReceived'
sendMsg = 'SendMessage'
constructor() {}
/**
* 初始化SignalR
* @param token
*/
initSignalR(token: any) {
this.connection = new signalr.HubConnectionBuilder()
.withUrl(this.url, {
skipNegotiation: true,
transport: signalr.HttpTransportType.WebSockets,
// headers: { Authorization: token },
accessTokenFactory: () => token
})
.build()
this.startConnection()
this.reConnecting()
this.reConnected()
this.watchOnline()
}
/**
* 连接
*/
startConnection() {
this.connection
?.start()
.then(() => {
this.retryCount = 0
console.log('Connected to SignalR succeddfully!')
})
.catch((err: any) => {
console.log('Error connecting to SignalR:', err)
if (this.retryCount < this.maxRetryAttempts) {
setTimeout(() => {
this.retryCount++
this.startConnection()
}, this.retryInterval)
}
})
}
/**
* 重连之前调用 (只有在掉线的一瞬间,只进入一次)
*/
reConnecting() {
// 生命周期
this.connection?.onreconnecting((error: any) => {
console.log('重新连接ing', error)
console.log(
'state:',
this.connection?.state,
'Reconnecting:',
signalr.HubConnectionState.Reconnecting
)
})
}
/**
* 重连 (默认4次重连),任何一次只要回调成功,调用
*/
reConnected() {
this.connection?.onreconnected((connectionId: any) => {
console.log('reConnected 链接id', connectionId)
console.log(this.connection?.state)
console.log(this.connection?.state === signalr.HubConnectionState.Connected)
if (this.connection?.state === signalr.HubConnectionState.Connected) {
}
})
}
/**
* (默认4次重连) 全部都失败后,调用
*/
watchOnline() {
this.connection?.onclose((err: any) => {
// console.log('重新初始化连接:')
// this.intervalId = setInterval(() => {
// this.connection
// .start()
// .then(() => {
// console.log('初始化连接成功')
// clearInterval(this.intervalId)
// })
// .catch((err) => {
// console.log('初始化连接失败,5秒后重新初始化')
// })
// }, this.reconnectInterval)
console.log('重连')
if (this.retryCount < this.maxRetryAttempts) {
setTimeout(() => {
this.retryCount++
this.startConnection()
}, this.retryInterval)
} else {
console.error('重连失败,不再尝试重新连接。', err)
}
})
}
/**
* 监听服务端消息
* @param callback
*/
onMessageReceived(callback: any) {
//在连接对象上注册消息接收事件的回调函数
this.connection?.on(this.receivedMsg, callback)
}
/**
* 发送消息
* @param msg 消息内容
*/
sendMessage(msg: any) {
this.connection?.invoke(this.sendMsg, msg)
}
/**
* 断开连接
*/
stopSignalR() {
if (this.connection) {
this.connection.stop()
console.log('Disconnected from SignalR.')
}
}
}
注;SignalR在初始化时Url的目标地址要与后端在Program.cs中引入SignalR的地址保持一致“/messageHub”
页面调用
onMounted(() => { signalR.initSignalR() signalR.onMessageReceived((message: any) => { console.log('signalR:', message) }) })
Vue全局使用
使用vue的全局属性在main.ts中初始化SignalRHelper

本次是在登录系统成功后跳转Home页初始化SignalR服务
onMounted(() => { if (instance) { const global = instance.appContext.config.globalProperties var token = storage.get('token') signalR = global.signalR
if (token && !signalR.connection) { signalR.initSignalR(`Bearer ${token}`) signalR.onMessageReceived((message: any) => { console.log('signalR:', message) }) } } })

浙公网安备 33010602011771号