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");
前端
以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服务
const instance = getCurrentInstance()
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) }) } } })