13. 【Blazor全栈开发实战指南】--实时通信:SignalR集成
一、SignalR的架构与适用场景HTTP的请求-响应模式对于大多数场景足够好用但有一类需求它天然不擅长——服务器主动推送数据给客户端。想象一下实时聊天应用用户A发送消息后用户B的界面应该立即出现这条消息而不是等B刷新页面或发出下一个请求时才看到。传统的解决方案是轮询客户端每隔几秒发一次请求但这既浪费带宽又有延迟。SignalR是ASP.NET Core内置的实时通信库它在底层自动选择最优的传输协议优先使用WebSocket全双工连接延迟最低当WebSocket不可用时降级到Server-Sent Events最后回退到Long Polling。对开发者而言这些细节完全透明只需要编写高层的Hub调用代码。SignalR的典型应用场景包括实时聊天、在线协作编辑、实时监控仪表盘、推送通知、多人游戏等。二、构建SignalR HubHub是SignalR的服务端核心它是一个C#类客户端可以调用Hub上的方法RPC风格Hub也可以主动调用连接到它的客户端上的方法。# SignalR 是 ASP.NET Core 内置的无需额外安装# 但 Blazor WASM 前端需要安装客户端库dotnetaddpackage Microsoft.AspNetCore.SignalR.Client下面是一个支持公聊和私信的聊天Hub// Hubs/ChatHub.csusingMicrosoft.AspNetCore.SignalR;usingMicrosoft.AspNetCore.Authorization;// [Authorize] 要求客户端建立连接前必须完成认证[Authorize]publicclassChatHub:Hub{privatereadonlyIMessageService_messageService;publicChatHub(IMessageServicemessageService){_messageServicemessageService;}// 客户端连接时触发publicoverrideasyncTaskOnConnectedAsync(){varuserIdContext.UserIdentifier;// 对应JWT中的 sub claimvarusernameContext.User?.Identity?.Name??匿名;// 将当前用户的连接加入以用户ID命名的组用于精准推送awaitGroups.AddToGroupAsync(Context.ConnectionId,$user-{userId});// 广播告知所有其他连接有用户上线awaitClients.Others.SendAsync(UserConnected,new{UserIduserId,Usernameusername});awaitbase.OnConnectedAsync();}// 客户端断开时触发publicoverrideasyncTaskOnDisconnectedAsync(Exception?exception){varuserIdContext.UserIdentifier;awaitClients.Others.SendAsync(UserDisconnected,new{UserIduserId});awaitbase.OnDisconnectedAsync(exception);}// 客户端调用此方法发送公开消息// 方法名即客户端调用的命令名publicasyncTaskSendMessage(stringroomId,stringcontent){if(string.IsNullOrWhiteSpace(content)||content.Length1000)return;// 简单的服务端校验防止滥用varuserIdContext.UserIdentifier!;varusernameContext.User?.Identity?.Name??匿名;vartimestampDateTime.UtcNow;// 持久化消息到数据库await_messageService.SaveAsync(newChatMessage{RoomIdroomId,SenderIduserId,Contentcontent,CreatedAttimestamp});// 向该房间内所有连接广播新消息// ReceiveMessage 是客户端注册的接收方法名awaitClients.Group($room-{roomId}).SendAsync(ReceiveMessage,newChatMessageDto{SenderIduserId,SenderNameusername,Contentcontent,Timestamptimestamp});}// 加入聊天室publicasyncTaskJoinRoom(stringroomId){awaitGroups.AddToGroupAsync(Context.ConnectionId,$room-{roomId});// 发送给刚加入的客户端最近50条历史消息varhistoryawait_messageService.GetRecentAsync(roomId,50);awaitClients.Caller.SendAsync(LoadHistory,history);// 告知房间内其他成员有人加入awaitClients.OthersInGroup($room-{roomId}).SendAsync(UserJoinedRoom,Context.User?.Identity?.Name);}// 离开聊天室publicasyncTaskLeaveRoom(stringroomId){awaitGroups.RemoveFromGroupAsync(Context.ConnectionId,$room-{roomId});awaitClients.OthersInGroup($room-{roomId}).SendAsync(UserLeftRoom,Context.User?.Identity?.Name);}// 发送私信给指定用户publicasyncTaskSendPrivateMessage(stringtargetUserId,stringcontent){varsenderIdContext.UserIdentifier!;// 向目标用户组推送同一用户可能有多个连接如同时在PC和手机登录awaitClients.Group($user-{targetUserId}).SendAsync(ReceivePrivateMessage,new{FromUserIdsenderId,FromNameContext.User?.Identity?.Name,Contentcontent,TimestampDateTime.UtcNow});}}在Program.cs中注册和映射SignalR// Program.csAPI服务器端builder.Services.AddSignalR(options{// 心跳间隔客户端每15秒发送一次ping服务器30秒内未收到则断开options.KeepAliveIntervalTimeSpan.FromSeconds(15);options.ClientTimeoutIntervalTimeSpan.FromSeconds(30);// 单次调用允许接收的最大消息大小限制防止内存耗尽攻击options.MaximumReceiveMessageSize64*1024;// 64 KB});// 如果使用JWT认证需要配置SignalR从查询字符串获取令牌// WebSocket协议不支持自定义请求头token只能放在URL查询参数中builder.Services.AddAuthentication().AddJwtBearer(options{options.EventsnewJwtBearerEvents{OnMessageReceivedcontext{// 对于 SignalR 连接从查询字符串 ?access_tokenxxx 读取令牌varaccessTokencontext.Request.Query[access_token];varpathcontext.HttpContext.Request.Path;if(!string.IsNullOrEmpty(accessToken)path.StartsWithSegments(/hubs)){context.TokenaccessToken;}returnTask.CompletedTask;}};});// 映射 Hub 到指定路径app.MapHubChatHub(/hubs/chat);Clients对象提供了灵活的消息发送目标选项Clients.All广播给所有连接Clients.Caller只发给发起本次调用的客户端Clients.Others发给除发起者外的所有人Clients.Group(groupId)发给指定组的所有成员Clients.User(userId)发给指定用户的所有连接需要配置IUserIdProvider。这些选项的组合几乎能满足所有实时推送场景。三、Blazor客户端与SignalR Hub交互在Blazor前端通过HubConnectionBuilder创建与Hub的连接*Components/Pages/ChatRoom.razor* page/chat/{RoomId}inject IJSRuntime JS inject NavigationManager NavManager implements IAsyncDisposabledivclasschat-containerdivclassmessages-panelidmessages-panelforeach(varmsginmessages){divclassmessage (msg.SenderId currentUserId ? own : )spanclasssendermsg.SenderName/spanpclasscontentmsg.Content/pspanclasstimemsg.Timestamp.ToLocalTime().ToString(HH:mm)/span/div}/divdivclassinput-panelinputbindnewMessagebind:eventoninputonkeydownHandleKeyDownplaceholder输入消息按Enter发送...disabled(!IsConnected)/buttononclickSendMessagedisabled(!IsConnected || string.IsNullOrWhiteSpace(newMessage))发送/button/divdivclassstatus(IsConnected?已连接:连接中...)/div/divcode{[Parameter]publicstringRoomId{get;set;}string.Empty;// 从认证状态获取当前用户ID用于区分自己发的消息[CascadingParameter]privateTaskAuthenticationState?AuthState{get;set;}privateHubConnection?hubConnection;privateListChatMessageDtomessages[];privatestringnewMessagestring.Empty;privatestring?currentUserId;privateboolIsConnectedhubConnection?.StateHubConnectionState.Connected;protectedoverrideasyncTaskOnInitializedAsync(){// 获取当前用户IDif(AuthStateisnotnull){varstateawaitAuthState;currentUserIdstate.User.FindFirst(System.IdentityModel.Tokens.Jwt.JwtRegisteredClaimNames.Sub)?.Value;}// 从本地存储获取JWT令牌用于SignalR认证vartokenawaitJS.InvokeAsyncstring?(localStorage.getItem,accessToken);// 构建Hub连接hubConnectionnewHubConnectionBuilder()// SignalR要通过查询字符串传递JWT令牌.WithUrl(NavManager.ToAbsoluteUri($/hubs/chat?access_token{token}))// 自动重连在断线后按1秒、2秒、5秒、10秒的间隔重试.WithAutomaticReconnect([TimeSpan.FromSeconds(1),TimeSpan.FromSeconds(2),TimeSpan.FromSeconds(5),TimeSpan.FromSeconds(10)]).Build();// 注册从Hub接收消息的处理器// ReceiveMessage 必须与Hub中 SendAsync 的方法名完全一致hubConnection.OnChatMessageDto(ReceiveMessage,async(message){messages.Add(message);// 收到消息后通知Blazor重渲染awaitInvokeAsync(StateHasChanged);// 滚动到最新消息awaitScrollToBottom();});hubConnection.OnListChatMessageDto(LoadHistory,async(history){messageshistory;awaitInvokeAsync(StateHasChanged);awaitScrollToBottom();});hubConnection.Onstring(UserJoinedRoom,async(username){messages.Add(newChatMessageDto{SenderName系统,Content${username}加入了聊天室,TimestampDateTime.UtcNow,IsSystemtrue});awaitInvokeAsync(StateHasChanged);});// 重连成功后重新加入房间hubConnection.Reconnectedasync(_){awaithubConnection.SendAsync(JoinRoom,RoomId);awaitInvokeAsync(StateHasChanged);};// 连接状态变化时更新UIhubConnection.Closedasync(_){awaitInvokeAsync(StateHasChanged);};// 启动连接awaithubConnection.StartAsync();// 加入当前聊天室Hub会返回历史消息awaithubConnection.SendAsync(JoinRoom,RoomId);}privateasyncTaskSendMessage(){if(hubConnectionisnull||!IsConnected||string.IsNullOrWhiteSpace(newMessage))return;awaithubConnection.SendAsync(SendMessage,RoomId,newMessage);newMessagestring.Empty;}privateasyncTaskHandleKeyDown(KeyboardEventArgse){if(e.KeyEnter!e.ShiftKey){awaitSendMessage();}}privateasyncTaskScrollToBottom(){awaitJS.InvokeVoidAsync(scrollToBottom,messages-panel);}publicasyncValueTaskDisposeAsync(){if(hubConnectionisnotnull){// 离开聊天室并断开连接try{awaithubConnection.SendAsync(LeaveRoom,RoomId);}catch{/* 断线时忽略 */}awaithubConnection.DisposeAsync();}}}对应的JS辅助函数在wwwroot/app.js中// 将消息面板滚动到底部functionscrollToBottom(elementId){consteldocument.getElementById(elementId);if(el){el.scrollTopel.scrollHeight;}}WithAutomaticReconnect配置了自动重连策略覆盖了常见的网络抖动场景。重连成功后需要重新执行JoinRoom调用因为SignalR的Group成员关系存储在内存中连接断开后会丢失。hubConnection.Reconnected事件正好提供了这个时机。四、总结本章完整展示了SignalR在Blazor全栈应用中的应用服务端的Hub类以简洁的方法定义了客户端可调用的API并通过Clients.Group等方式精确控制消息推送范围JWT认证通过查询字符串传递解决了WebSocket不支持自定义Header的限制客户端的HubConnectionBuilder配合WithAutomaticReconnect构建了健壮的实时连接hubConnection.OnT注册的接收处理器配合InvokeAsync(StateHasChanged)完成了实时UI更新的闭环。至此第四章Blazor与后端API集成的全部内容已经涵盖。但拥有一个功能完备的应用还不够——当用户量增大时性能问题会接踵而至渲染海量列表卡顿、页面初始加载过慢、频繁的不必要重渲染拖慢响应。下一章我们进入性能优化专题学习如何用虚拟化、懒加载和渲染控制等武器让Blazor应用在规模增长后依然保持丝滑体验。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2418334.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!