当前位置: 首页 > article >正文

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应用在规模增长后依然保持丝滑体验。

相关文章:

13. 【Blazor全栈开发实战指南】--实时通信:SignalR集成

一、SignalR的架构与适用场景 HTTP的"请求-响应"模式对于大多数场景足够好用,但有一类需求它天然不擅长——服务器主动推送数据给客户端。想象一下实时聊天应用:用户A发送消息后,用户B的界面应该立即出现这条消息,而不是…...

前端:第七章-布局与导航组件

第七章:布局与导航组件 🎯 本章目标:开发应用主布局组件、顶部导航栏和侧边导航菜单。 7.1 布局结构设计 7.1.1 布局结构图 ┌─────────────────────────────────────────────────────────┐ │ …...

如何在Dev-C++中配置Windows API?

在Dev-C中配置Windows API的步骤如下&#xff1a;创建新项目打开Dev-C → 选择「文件」→「新建」→「项目」→ 选择「Windows Application」模板包含头文件在源代码开头添加&#xff1a;#include <windows.h>配置链接器选择「工具」→「编译选项」在「编译器」标签页勾选…...

LangChain智能体开发:使用 SDK 记录用户反馈

LangSmith 使得将反馈附加到追踪记录变得容易。这些反馈可以来自用户、标注者、自动化评估器等&#xff0c;对于监控和评估应用程序至关重要。 使用 create_feedback() / createFeedback()在这里&#xff0c;我们将逐步介绍如何使用 SDK 记录反馈。 from langsmith import tr…...

Qwen3-14B-Int4-AWQ辅助C语言学习:从语法基础到指针精讲的智能辅导

Qwen3-14B-Int4-AWQ辅助C语言学习&#xff1a;从语法基础到指针精讲的智能辅导 1. 为什么需要AI辅助学习C语言 C语言作为计算机专业的核心课程&#xff0c;一直是许多初学者的"拦路虎"。传统学习方式存在几个明显痛点&#xff1a;教材概念抽象难懂、练习缺乏即时反…...

Z-Image-GGUF多场景落地:政务宣传图生成、乡村振兴视觉素材、非遗数字化呈现

Z-Image-GGUF多场景落地&#xff1a;政务宣传图生成、乡村振兴视觉素材、非遗数字化呈现 1. 项目简介&#xff1a;当AI绘图遇见公共文化服务 想象一下&#xff0c;一个乡镇的宣传干事&#xff0c;需要在三天内为即将举办的“丰收节”制作一批宣传海报、展板素材和线上推文配图…...

Phi-3 Forest Lab多场景:产品经理需求文档生成、PRD评审要点提示

Phi-3 Forest Lab多场景&#xff1a;产品经理需求文档生成、PRD评审要点提示 1. 引言&#xff1a;当产品经理遇见森林里的AI助手 想象一下这个场景&#xff1a;你是一个产品经理&#xff0c;手头有三个需求要梳理&#xff0c;下午还要开PRD评审会。你打开文档&#xff0c;面对…...

保姆级教程:CogVideoX-2b快速体验,从启动到生成视频全流程

保姆级教程&#xff1a;CogVideoX-2b快速体验&#xff0c;从启动到生成视频全流程 1. 准备工作&#xff1a;认识你的视频创作工具 CogVideoX-2b是智谱AI开源的一款强大文字生成视频模型&#xff0c;而CSDN专用版则针对AutoDL平台进行了深度优化。这个版本最大的特点是解决了原…...

AI辅助工业设计:Qwen3-14B-AWQ根据文本描述生成Visio风格架构图草稿

AI辅助工业设计&#xff1a;Qwen3-14B-AWQ根据文本描述生成Visio风格架构图草稿 1. 工业设计中的AI新助手 想象一下这样的场景&#xff1a;你正在会议室里和团队讨论一个新系统的架构设计&#xff0c;大家七嘴八舌地提出各种想法。突然有人问&#xff1a;"能不能把这些讨…...

FireRedASR Pro多语言识别效果评测:中英日韩等语种实测

FireRedASR Pro多语言识别效果评测&#xff1a;中英日韩等语种实测 最近在折腾一个需要支持多语言语音识别的项目&#xff0c;选型时被朋友安利了FireRedASR Pro。官方宣传说它支持几十种语言&#xff0c;识别效果还很不错。说实话&#xff0c;这种“全能型”选手我见得不少&a…...

WeKnora问题解决:如何让AI严格按你给的文本回答问题

WeKnora问题解决&#xff1a;如何让AI严格按你给的文本回答问题 1. 问题根源&#xff1a;为什么AI总爱“自由发挥”&#xff1f; 你有没有这样的经历&#xff1a;给AI一段产品说明书&#xff0c;问它“电池容量是多少”&#xff0c;它却开始滔滔不绝地讲电池技术发展史&#…...

Qwen3-14b_int4_awq部署避坑:常见vLLM启动失败原因与Chainlit连接超时解决

Qwen3-14b_int4_awq部署避坑&#xff1a;常见vLLM启动失败原因与Chainlit连接超时解决 1. 模型简介 Qwen3-14b_int4_awq是基于Qwen3-14b模型的int4量化版本&#xff0c;采用AngelSlim技术进行压缩优化&#xff0c;专门用于高效文本生成任务。这个量化版本在保持较高生成质量的…...

比迪丽LoRA模型Java开发集成指南:SpringBoot后端服务调用

比迪丽LoRA模型Java开发集成指南&#xff1a;SpringBoot后端服务调用 最近在做一个内容创作平台的后台&#xff0c;需要集成AI绘画功能。团队评估了几个方案&#xff0c;最后决定用比迪丽LoRA模型&#xff0c;主要是看中它在特定风格上的生成效果比较稳定。但问题来了&#xf…...

Qwen3-TTS-12Hz-1.7B-CustomVoice与SpringBoot集成:企业级语音API服务开发

Qwen3-TTS-12Hz-1.7B-CustomVoice与SpringBoot集成&#xff1a;企业级语音API服务开发 语音合成技术正在改变我们与数字世界的交互方式&#xff0c;而将先进的TTS模型集成到企业级应用中&#xff0c;能够为业务带来全新的可能性。今天我们来聊聊如何把Qwen3-TTS-12Hz-1.7B-Cus…...

冬奥会雪花灯DIY:82颗LED单层PCB光电艺术实现

1. 项目概述“冬奥会雪花灯”是一个面向DIY爱好者与电子初学者的光电艺术装置项目&#xff0c;其设计灵感直接来源于2022年北京冬奥会开幕式中广受赞誉的巨型可编程雪花主火炬台。该项目并非对原舞台道具的功能复刻&#xff0c;而是聚焦于视觉神韵的工程化再现——以小型化、低…...

Python3.11镜像实测:快速创建独立环境,轻松复现AI实验

Python3.11镜像实测&#xff1a;快速创建独立环境&#xff0c;轻松复现AI实验 1. 引言&#xff1a;为什么你需要一个独立的Python环境&#xff1f; 如果你曾经在AI项目或数据分析工作中遇到过这样的问题&#xff0c;那你一定明白我在说什么&#xff1a; “昨天还能跑的代码&…...

Phi-3-vision-128k-instruct惊艳案例分享:128K上下文下的复杂图表深度推理

Phi-3-vision-128k-instruct惊艳案例分享&#xff1a;128K上下文下的复杂图表深度推理 1. 模型能力概览 Phi-3-Vision-128K-Instruct是目前最先进的轻量级开放多模态模型&#xff0c;专为处理复杂图文推理任务而设计。这个模型最引人注目的特点是支持长达128K的上下文窗口&am…...

lingbot-depth-pretrain-vitl-14在无人机巡检中的应用:单目航拍图像生成地形深度图

lingbot-depth-pretrain-vitl-14在无人机巡检中的应用&#xff1a;单目航拍图像生成地形深度图 1. 引言&#xff1a;当无人机“看”得更深 想象一下&#xff0c;你操控着一架无人机在山谷间飞行&#xff0c;屏幕上实时传回高清的航拍画面。你能清楚地看到山脊的轮廓、河流的走…...

InternLM2-Chat-1.8B代码助手效果实测:Python函数生成与解释

InternLM2-Chat-1.8B代码助手效果实测&#xff1a;Python函数生成与解释 最近在社区里看到不少关于InternLM2-Chat-1.8B的讨论&#xff0c;特别是它作为代码助手的能力。作为一个经常和Python打交道的开发者&#xff0c;我对这类小体量模型的实际表现特别好奇——它真的能理解…...

Ansys ACT实战指南:从零构建自定义仿真应用

1. Ansys ACT入门&#xff1a;为什么你需要自定义仿真工具 第一次打开Ansys Mechanical时&#xff0c;我就被它强大的功能震撼到了。但很快发现一个问题&#xff1a;每次做类似的项目&#xff0c;都要重复点击几十次相同的按钮。这就像每天开车上班都要重新组装方向盘——效率实…...

图神经网络实战(四)

原文&#xff1a;zh.annas-archive.org/md5/aa0f9b9d5919ff9efe42c7ab05a87a0b 译者&#xff1a;飞龙 协议&#xff1a;CC BY-NC-SA 4.0 附录 B 安装和配置 PyTorch Geometric B.1 安装 PyTorch Geometric PyTorch Geometric (PyG) 是一个基于 PyTorch 构建的库&#xff0c;用…...

解码大脑因果网络:BrainEC-LLM如何用多尺度混合大模型革新有效连接估计

1. 当大语言模型遇见脑科学&#xff1a;BrainEC-LLM的跨界革命 想象一下&#xff0c;如果让ChatGPT去解读你的脑部扫描数据会怎样&#xff1f;这个看似科幻的场景正在成为现实。BrainEC-LLM就像一位精通多国语言的神经科医生&#xff0c;它把大语言模型&#xff08;LLM&#xf…...

DIY智能无极调速风扇:基于EspHome固件与Home Assistant的完美融合

1. 从普通风扇到智能无极调速的华丽变身 去年夏天我被家里那台老旧风扇折磨得不轻——要么全速运转吵得人心烦&#xff0c;要么完全关闭热得睡不着。直到发现用EspHome和Home Assistant改造风扇的方法&#xff0c;才真正体会到什么叫"科技改变生活"。现在我的风扇能根…...

六合一工业通讯调试盒:单USB-C集成CAN/RS485/以太网等6类接口

1. 项目概述“六合一工业通讯调试盒”是一款面向工业现场调试与协议验证场景的多功能接口集成设备。其核心设计目标是解决工程师在产线调试、PLC通信测试、CAN总线分析、RS485组网验证及嵌入式固件烧录等多任务并行时&#xff0c;频繁插拔各类USB转接器导致的接口冲突、线缆杂乱…...

从AddMvc到UseEndpoints:.NetCore3.1升级中的路由配置避坑指南

从AddMvc到UseEndpoints&#xff1a;.NetCore3.1升级中的路由配置避坑指南 如果你正在将项目从.NetCore2.2升级到3.1版本&#xff0c;路由配置的变化可能是最让你头疼的部分之一。旧版的AddMvc和UseMvc方法在新版本中虽然还能用&#xff0c;但已经不再是推荐做法。本文将带你深…...

UNIT-00:Berserk Interface在STM32嵌入式开发中的应用指南

UNIT-00&#xff1a;Berserk Interface在STM32嵌入式开发中的应用指南 最近和几个做嵌入式开发的朋友聊天&#xff0c;大家普遍有个感觉&#xff1a;项目周期越来越紧&#xff0c;但代码量却越来越大。特别是用STM32这种MCU做项目&#xff0c;从看数据手册、写初始化代码&…...

避坑指南:Trainer自定义数据顺序的两种解决方案对比(RandomSampler vs SequentialSampler)

深度解析&#xff1a;如何精准控制Transformer训练数据顺序的两种核心策略 在大型语言模型&#xff08;LLM&#xff09;的监督微调&#xff08;SFT&#xff09;过程中&#xff0c;数据输入顺序的控制往往被忽视&#xff0c;却可能对模型收敛速度和最终性能产生微妙影响。当我们…...

RK3566嵌入式Linux全栈开发:从MIPI点亮到字符驱动实战

1. 项目概述本项目以RK3566 SoC为核心&#xff0c;基于泰山派开发板构建一款具备完整Linux嵌入式系统能力的智能小手机原型平台。该平台并非面向消费级终端产品&#xff0c;而是定位为嵌入式Linux系统级开发的学习载体&#xff0c;聚焦于从硬件底层到用户空间的全栈技术贯通。其…...

零基础部署MedGemma-X:5分钟搭建你的AI影像诊断助手

零基础部署MedGemma-X&#xff1a;5分钟搭建你的AI影像诊断助手 1. 为什么选择MedGemma-X&#xff1f; 1.1 传统影像诊断工具的局限性 在医疗影像诊断领域&#xff0c;医生们长期面临着效率与准确性的双重挑战。传统计算机辅助诊断&#xff08;CAD&#xff09;系统往往只能提…...

RK3566平台MIPI DSI转RGB显示方案设计与驱动实现

1. 项目概述本项目实现了一款基于RK3566主控平台的嵌入式平板终端硬件方案&#xff0c;核心目标是在保留泰山派开发板完整可编程能力的前提下&#xff0c;集成7英寸RGB接口液晶显示屏与电容式触摸功能&#xff0c;构建一个兼具开发调试与人机交互能力的紧凑型显示终端。该设计并…...