Ant Design Pro V5 配置连接signalR,实现消息推送
前言:之前做一个业务接口的时候,需要处理的时间比较长,超过两份钟iis就自动断了连接,一直寻找iis超时的设置方式,改了几个配置后还是到2分钟就报超时,熬到很晚结果耻辱下播。后来思来想去,发现努力的方向错了,为何iis设置超时时间为2分钟,那肯定是有意为之,超过2分钟的连接便不是一个好的连接。处理时间过长的程序,到程序里应该用异步的处理方式,用户调用接口后直接告诉用户,程序在后台运行请稍等,异步执行处理程序,处理完成后消息推送给用户,这才是合理的做法。消息推送就是后端能调用前端的方法。一搜消息推送,就能看到websocket的介绍,一种全双工的连接通信方式。手上在玩的项目,前端用的ant design pro v5,后端.net6,而.net6用的实现这种全双工的通信方式,使用的是signalR。下面就介绍一下前后端如何配置使用signalR。
后端:1.创建 SignalR 中心
1 using Microsoft.AspNetCore.Authorization; 2 using Microsoft.AspNetCore.SignalR; 3 4 namespace Wood.API.Hubs 5 { 6 [Authorize] 7 public class MessageHub : Hub 8 { 9 public async Task SendMessage(string user, string message) 10 { 11 await Clients.All.SendAsync("ReceiveMessage", user, message); 12 } 13 } 14 }
2.Program中配置 SignalR,在对应的位置插入如下代码。
1 //singalR调用特定用户来发送消息,需要此设置,默认userid是ClaimTypes.NameIdentifier 2 builder.Services.TryAddSingleton(typeof(IUserIdProvider), typeof(DefaultUserIdProvider)); 3 4 builder.Services.AddSignalR(); 5 6 //这边设置了专门的认证服务器来验证登录情况,singalR认证主要是自定义一下OnMessageReceived事件,不支持header所以用地址栏参数获取 7 var Authority = Appsettings.App(new string[] { "AuthorityConfiguration", "Authority" }); 8 var ApiName = Appsettings.App(new string[] { "AuthorityConfiguration", "ApiName" }); 9 var RequireHttpsMetadata = Appsettings.App(new string[] { "AuthorityConfiguration", "RequireHttpsMetadata" }); 10 11 builder.Services.AddIdentity<User, Role>(options => configuration.GetSection(nameof(IdentityOptions)).Bind(options)) 12 .AddEntityFrameworkStores<WoodIdentityDbContext>() 13 .AddDefaultTokenProviders(); 14 15 builder.Services.AddAuthentication(options => 16 { 17 options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; 18 options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; 19 options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; 20 options.DefaultSignInScheme = JwtBearerDefaults.AuthenticationScheme; 21 options.DefaultForbidScheme = JwtBearerDefaults.AuthenticationScheme; 22 }) 23 .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => 24 { 25 options.Authority = Authority; 26 options.RequireHttpsMetadata = Convert.ToBoolean(RequireHttpsMetadata); 27 options.Audience = ApiName; 28 options.Events = new JwtBearerEvents 29 { 30 OnMessageReceived = context => 31 { 32 var accessToken = context.Request.Query["access_token"]; 33 var path = context.HttpContext.Request.Path; 34 if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/Hubs/MessageHub")) 35 { 36 context.Token = accessToken; 37 } 38 return Task.CompletedTask; 39 } 40 }; 41 }); 42 43 var app = builder.Build(); 44 45 app.MapHub<MessageHub>("/Hubs/MessageHub"); 46 app.Run();
3.在集线器外部发送消息,比如在controller里调用集线器Hub里面的方法
1 [ApiController] 2 [Route("api/[controller]/[action]")] 3 [Authorize] 4 public class CableController : Controller 5 { 6 private readonly IHubContext<MessageHub> _messageHub; 7 private string _userId = ""; 8 public CableController(IHubContext<MessageHub> messageHub) 9 { 10 _messageHub = messageHub; 11 } 12 [HttpPost] 13 public ActionResult generateData(){ 14 _userId = this.User.FindFirst(ClaimTypes.NameIdentifier).Value; 15 generateDataAsync(_userId); 16 return Ok(new { code = 200, success = true, msg = "开始运行程序,请稍候..." }); 17 } 18 private async Task<bool> generateDataAsync(string userid) 19 { 20 bool t = await Task.Run(() => 21 { 22 //...运行程序
string messageData = "运行成功"; 23 _messageHub.Clients.User(userid).SendAsync("ReceiveMessage", messageData); 24 return true; 25 }); 26 return t; 27 } 28 }
前端:1.安装singalR库
执行命令 npm install @microsoft/signalr
2.在全局初始数据中初始化连接。网上说在layouts里,可是最新的ant design pro v5里没有layouts, 那就放在app.js的getInitialState里,因为官方文档介绍那边存放的是全局初始数据,不是很笃定是否合理,前端做的不多。
1 import * as signalR from '@microsoft/signalr'; 2 3 export async function getInitialState(): Promise<{ 4 settings?: Partial<LayoutSettings>; 5 idetityUser?: Oidc.User; 6 currentUser?: API.CurrentUser; 7 connection?: any; 8 generateBtn?: boolean; 9 loading?: boolean; 10 fetchIdentityUserInfo?: () => Promise<Oidc.User | undefined>; 11 fetchUserInfo?: (options?: { [key: string]: any }) => Promise<API.CurrentUser | undefined>; 12 initConnection?: (options?: { [key: string]: any }) => any; 13 getGenerateBtn?: () => boolean | undefined; 14 }> { 15 const Mgrs = new Mgr(); 16 const fetchIdentityUserInfo = async () => { 17 const idetityUser = await Mgrs.getUser(); 18 return idetityUser; 19 }; 20 const fetchUserInfo = async (options?: { [key: string]: any }) => { 21 const response = await queryCurrentUser(options); 22 const currentUser = response?.data; 23 return currentUser; 24 }; 25 const getGenerateBtn = () => { 26 const generateBtn = false; 27 return generateBtn; 28 }; 29 const initConnection = (option?: { [key: string]: any }) => { 30 const protocol = new signalR.JsonHubProtocol(); 31 const transport = signalR.HttpTransportType.WebSockets; 32 const options = { 33 skipNegotiation: true, 34 transport: transport, 35 accessTokenFactory: () => option?.token, 36 }; 37 const connection = new signalR.HubConnectionBuilder() 38 .withUrl('/Hubs/MessageHub', options) 39 .withHubProtocol(protocol) 40 .withAutomaticReconnect() 41 .build(); 42 43 return connection; 44 }; 45 // 如果不是登录页面,执行 46 if (history.location.pathname !== loginPath) { 47 const idetityUser = await fetchIdentityUserInfo(); 48 const currentUser = await fetchUserInfo(); 49 50 const generateBtn = getGenerateBtn(); 51 const connection = initConnection({ token: idetityUser.access_token }); 52 // 开始连接 调用后台 BeginSendData 方法 成功后双方交互数据 53 connection.start().then(() => { 54 console.log('开始连接'); 55 }); 56 return { 57 idetityUser, 58 currentUser, 59 connection, 60 generateBtn, 61 fetchUserInfo, 62 fetchIdentityUserInfo, 63 initConnection, 64 getGenerateBtn, 65 settings: defaultSettings, 66 }; 67 } 68 return { 69 fetchIdentityUserInfo, 70 fetchUserInfo, 71 initConnection, 72 settings: defaultSettings, 73 }; 74 }
3.在layout中监听服务端发送过来的消息,因为是全局的并且可以setInitialState,改变全局变量。
1 export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }: any) => { 2 // 后台默认触发 3 initialState.connection.on('ReceiveMessage', (data: any) => { 4 //setBtnState(false); 5 setInitialState((s: any) => ({ ...s, generateBtn: false })); 6 const currentMSgId = localStorage.getItem('currentMSgId'); 7 if (data.id && data.id != currentMSgId) { 8 if (currentMSgId) { 9 notification.close(currentMSgId); 10 } 11 12 localStorage.setItem('currentMSgId', data.id); 13 notification.success({ 14 message: data.title, 15 description: data.desc, 16 placement: 'bottomRight', 17 duration: 0, 18 key: data.id, 19 }); 20 } 21 }); 22 23 return { 24 ... 25 } 26 }
4.代理配置 通信方式不同于http,websocket的代理配置有些许差异
4.1 本地运行
4.2 部署在iis运行,需要配置反向代理
附上大微软的官方文档:https://learn.microsoft.com/zh-cn/aspnet/core/tutorials/signalr?view=aspnetcore-6.0&tabs=visual-studio