尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

ASP.NET Core SignalR实时通讯系统搭建与实战解析

ASP.NET Core SignalR实时通讯系统搭建与实战解析 最近做项目的时候又一次用到了SignalR想了想决定把这套基础实时通讯系统的完整搭建过程整理出来。在ASP.NET Core的生态里SignalR是处理“服务端主动找客户端”这件事最顺手的一套框架。做聊天室、消息推送、在线状态同步、后台任务进度通知都可以用它直接落地。这篇文章我会把这套系统从选型、原理、环境搭建到核心代码实现、分组广播、身份认证接入再到常见坑位和性能调优建议全部过一遍尽量让一个没接触过SignalR的读者也能照着写出一套能跑的真实系统。先说下这篇文章的读者定位你至少用过ASP.NET Core MVC或API做过一些基础开发知道Startup或者Program.cs大概是怎么回事对前端JavaScript有基本操作能力。如果你完全没接触过实时通讯这篇文章也能帮你把“轮询、长轮询、WebSocket、SignalR”这些名词一次性理清楚。1. 为什么是SignalR实时通讯方案的横向对比1.1 轮询、长轮询与WebSocket到底差在哪很多新手一上来就写WebSocket原生代码结果一遇到代理服务器、断线重连、粘性会话就焦头烂额。其实在SignalR出现之前社区里已经有了大量实时通讯方案我们先把这些方案的逻辑捋一遍短轮询前端每2秒或5秒发一次HTTP请求问服务端“有没有新数据”。服务端不管有没有数据都会返回。这种方式最简单但空转请求特别多服务端压力大实时性也低。长轮询前端发请求后服务端把请求挂着直到有新数据再返回。下次前端再发起新请求。实时性比短轮询好很多但连接本质上还是一个一个HTTP请求频繁断开重连对服务器同样不友好。WebSocket这是真正的全双工长连接。浏览器与服务器完成一次HTTP握手后升级为TCP长连接双方可以随时互推数据。实时性没问题但原生WebSocket的坑在于消息格式要自己定义、断线重连要自己写、不同代理服务器对WebSocket支持不一致、负载均衡下要求连接粘滞。Server-Sent EventsSSE单工通道只能服务端推给客户端浏览器原生支持通过HTTP实现重连机制内置。适合服务端单向推送但不支持客户端往服务端发。所以做聊天室这种双向交互场景时它不够。1.2 SignalR的架构定位站在WebSocket肩膀上的封装层SignalR其实不是一个独立的通讯协议它是ASP.NET Core框架提供的一个实时通讯抽象层。它默认优先使用WebSocket在WebSocket不可用的时候自动降级到Server-Sent Events或长轮询。对开发者而言我只写一套Hub代码底层传输方式由框架根据浏览器能力和服务器配置自动选择。SignalR真正解决的是原生WebSocket的几大痛点连接管理框架维护连接ID与用户的映射断线自动触发事件客户端掉线后能自动重连。消息序列化内置JSON和MessagePack协议服务端Clients.Client(connId).SendAsync(method, obj)一行代码框架负责序列化、路由和调用。分组原生WebSocket想实现“给某个聊天室广播”得自己维护房间成员列表。SignalR直接提供GroupAdd、GroupRemove和Clients.Group(roomId).SendAsync(...)。粘性问题配合Redis背板或Azure SignalR Service可以在多实例部署下把消息路由到正确节点的正确连接。1.3 选型建议与适用场景我用SignalR做过在线客服系统、监控大屏数据推送、后台任务进度通知整体体验很好。但SignalR也不是万能药如果只是服务端单向推送给浏览器SSE就能搞定没必要引入SignalR如果要做桌面客户端、小程序端的极简全双工通讯可以考虑直接写WebSocket协议如果服务端要推送的对象是几十万级的长连接需要仔细规划背板方案SignalR的单节点连接数上限会受服务器资源限制。我的建议是ASP.NET Core后端 浏览器页面这种组合直接上SignalR是最省心的选择。它有官方包、文档多、社区案例丰富和ASP.NET Core的安全机制、依赖注入、认证授权能无缝集成。2. 核心概念与基础原理传输协商、Hub与连接生命周期2.1 传输方式自动协商WebSocket → SSE → 长轮询SignalR客户端连接服务器时会先发起一个/chatHub/negotiate请求服务器返回当前可用的传输方式列表客户端按照优先级选择一种。默认优先级是WebSocket优先然后是Server-Sent Events最后是长轮询。这个自动协商过程对开发者完全透明我们不需要在代码里判断浏览器是否支持WebSocket。但有一点要注意如果网站部署在HTTPS 反向代理Nginx环境下Nginx必须正确配置Upgrade和Connection请求头以支持WebSocket升级。我在后面的常见问题章节会专门讲这个坑。2.2 Hub服务端与客户端的“中间人”Hub是SignalR的顶层抽象。可以把它理解为服务端定义了一个“电话总机”。任何客户端拨电话进来通过这个总机可以呼叫别人Clients.Other.SendAsync。服务端自身也能通过这个总机主动呼叫所有或指定的客户端IHubContextT。在Hub类里Clients属性代表当前连接的客户端集合。它的常用成员包括Clients.All所有客户端。Clients.Caller调用当前方法的客户端。Clients.Others除当前调用者外的所有客户端。Clients.Client(connectionId)指定连接。Clients.Group(groupName)指定群组。Clients.Users(userIdList)指定用户列表。这里有一个容易让新手困惑的概念ConnectionId是一个连接的唯一标识User是经过认证后用户的唯一标识。一个用户可以开多个浏览器标签页产生多个ConnectionId但它们属于同一个User。发送消息时应该按User还是按ConnectionId取决于业务场景。比如私信功能就按User发确保用户所有在线设备都能收到如果只想发到当前设备就按ConnectionId发。2.3 连接生命周期与重连机制SignalR连接的生命周期包含四个阶段正在连接Connecting→已连接Connected→正在重连Reconnecting→已断开Disconnected。客户端断网时前端JS库会自动尝试重连。以microsoft/signalr浏览器客户端为例默认重连策略是禁用自动重连需要显式开启const connection new signalR.HubConnectionBuilder() .withUrl(/chatHub) .withAutomaticReconnect([0, 2000, 10000, 30000]) .build();上面的配置表示首次重连立即开始失败后依次等待2秒、10秒、30秒之后放弃。服务端的OnDisconnectedAsync方法会在连接终止时触发我们可以在这里清理连接与用户、群组的映射关系。实际项目中重连成功后的状态同步是一个容易被忽视的点。比如聊天页面在断线期间收到很多消息重连成功后服务端应该把离线期间的未读消息一次性推给客户端。这个用OnReconnected事件配合数据库增量查询就能实现。3. 环境准备与项目搭建3.1 创建项目与添加SignalR包我使用.NET 6版本的ASP.NET Core作为演示基线用.NET 8同样适用。首先创建项目dotnet new web -n SignalRChatDemo cd SignalRChatDemo然后添加SignalR服务端包dotnet add package Microsoft.AspNetCore.SignalR严格来说新建的ASP.NET Core项目默认已经包含SignalR框架引用。但如果你的项目是类库或者手动创建的就需要通过NuGet显式引用Microsoft.AspNetCore.SignalR。如果需要使用MessagePack协议以降低消息体积还要添加dotnet add package Microsoft.AspNetCore.SignalR.Protocols.MessagePack3.2 配置服务的注册与HTTP管道ASP.NET Core 6开始使用Program.cs作为统一入口配置。注册SignalR和映射Hub端点的代码如下var builder WebApplication.CreateBuilder(args); // 注册SignalR服务并指定使用JSON协议 builder.Services.AddSignalR(); var app builder.Build(); // 静态文件中间件用于输出前端页面wwwroot目录 app.UseDefaultFiles(); app.UseStaticFiles(); // 默认路由 app.MapGet(/, () SignalR Chat Demo); // 映射ChatHub到 /chatHub 端点 app.MapHubChatHub(/chatHub); app.Run();这里解释两个关键点AddSignalR()是有返回值的可以继续配置项比如设置KeepAliveInterval、ClientTimeoutInterval、MaximumReceiveMessageSize等。MapHubT指定的路径是客户端连接的URL。这个路径首先要通过WebSocket握手或HTTP请求访问所以它不需要走Controller路由但会受认证、跨域CORS等中间件影响。3.3 跨域配置前后端分离时的必备步骤如果你的SignalR服务端和前端页面不在同一个源比如前端跑在localhost:3000后端跑在localhost:5000就需要配置CORS。设置时要注意不能使用AllowAnyOrigin()因为SignalR在WebSocket握手时不允许携带Cookie的跨域请求配合任意来源。推荐写法builder.Services.AddCors(options { options.AddPolicy(AllowFrontend, policy { policy.WithOrigins(http://localhost:3000) .AllowCredentials() .AllowAnyHeader() .AllowAnyMethod(); }); }); var app builder.Build(); app.UseCors(AllowFrontend);注意UseCors必须放在MapHub之前执行。如果顺序颠倒CORS中间件不会生效前端会在请求阶段直接报跨域错误。4. 核心实战ChatHub 原生JS客户端实现双向实时通讯4.1 编写ChatHub核心逻辑这是整套系统最核心的环节。我们先创建一个功能完整的聊天室Hub包含用户加入、发送消息、离开广播、接收系统通知。using Microsoft.AspNetCore.SignalR; namespace SignalRChatDemo.Hubs { public class ChatHub : Hub { // 用户加入群组 public async Task JoinGroup(string userName, string groupName) { // 将当前连接加入指定群组 await Groups.AddToGroupAsync(Context.ConnectionId, groupName); // 广播一条系统消息给群组内的所有客户端包括加入者自己 await Clients.Group(groupName).SendAsync(ReceiveSystemMessage, ${userName} 加入了 {groupName}); } // 用户发送消息 public async Task SendMessage(string userName, string message, string groupName) { // 构造一个消息对象 var msg new MessagePayload { User userName, Content message, SentAt DateTime.Now, Group groupName }; // 广播给群组内除当前连接外的所有客户端 await Clients.Group(groupName).SendAsync(ReceiveMessage, msg); } // 用户离开群组 public async Task LeaveGroup(string userName, string groupName) { await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName); await Clients.Group(groupName).SendAsync(ReceiveSystemMessage, ${userName} 离开了 {groupName}); } // 连接断开时的清理 public override async Task OnDisconnectedAsync(Exception? exception) { // 这里可以从实际业务中获取用户名先简单打印日志 Console.WriteLine($Connection {Context.ConnectionId} disconnected. Exception: {exception?.Message}); await base.OnDisconnectedAsync(exception); } } public class MessagePayload { public string User { get; set; } public string Content { get; set; } public DateTime SentAt { get; set; } public string Group { get; set; } } }这段代码的核心逻辑是“把群组当房间”。Context.ConnectionId是当前连接的IDGroups.AddToGroupAsync是把连接塞进群组容器。之后任何对这个群组的SendAsync调用所有在这个群组内的客户端连接都能收到。这里我特别推荐使用强类型Hub接口来替代字符串方法名。先定义一个接口public interface IChatClient { Task ReceiveMessage(MessagePayload msg); Task ReceiveSystemMessage(string message); }Hub继承HubIChatClientpublic class ChatHubStrongTyped : HubIChatClient { public async Task SendMessage(string userName, string message, string groupName) { var msg new MessagePayload { User userName, Content message, SentAt DateTime.Now, Group groupName }; await Clients.Group(groupName).ReceiveMessage(msg); } }强类型的优势是编译期就能发现方法名拼写错误前端消息绑定也不需要维护魔法字符串。这在项目规模变大以后特别重要。4.2 前端原生JS接入建立连接与消息收发写一个标准的前端页面。用原生JavaScript和官方SignalR客户端库不使用任何框架。页面放在wwwroot/index.html。!DOCTYPE html html langzh-CN head meta charsetUTF-8 titleSignalR 实时聊天室/title script srchttps://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/8.0.0/signalr.min.js/script /head body h3SignalR 基础实时通讯演示/h3 div input typetext iduserName placeholder请输入昵称 / input typetext idgroupName placeholder请输入房间号 valuedev-room / button idjoinBtn加入房间/button /div div input typetext idmessageInput placeholder输入消息 / button idsendBtn发送/button /div ul idmessageList/ul script const connection new signalR.HubConnectionBuilder() .withUrl(/chatHub) .withAutomaticReconnect([0, 2000, 10000, 30000]) .configureLogging(signalR.LogLevel.Information) .build(); // 监听服务端发送来的事件 connection.on(ReceiveMessage, (msg) { const li document.createElement(li); li.textContent [${msg.group}] ${msg.user} ${new Date(msg.sentAt).toLocaleTimeString()}: ${msg.content}; document.getElementById(messageList).appendChild(li); }); connection.on(ReceiveSystemMessage, (msg) { const li document.createElement(li); li.style.color #888; li.textContent msg; document.getElementById(messageList).appendChild(li); }); document.getElementById(joinBtn).addEventListener(click, async () { const userName document.getElementById(userName).value.trim(); const groupName document.getElementById(groupName).value.trim(); if (!userName || !groupName) return; try { await connection.start(); await connection.invoke(JoinGroup, userName, groupName); } catch (err) { console.error(连接失败: , err); } }); document.getElementById(sendBtn).addEventListener(click, async () { const userName document.getElementById(userName).value.trim(); const message document.getElementById(messageInput).value.trim(); const groupName document.getElementById(groupName).value.trim(); if (!message) return; try { await connection.invoke(SendMessage, userName, message, groupName); document.getElementById(messageInput).value ; } catch (err) { console.error(消息发送失败: , err); } }); /script /body /html这段代码中connection.on(ReceiveMessage, callback)是绑定服务端事件connection.invoke(SendMessage, userName, message, groupName)是调用服务端Hub方法。两者是SignalR通讯的核心通道客户端通过invoke调用服务端服务端通过SendAsync回调客户端监控的事件。有个细节要特别说明connection.start()返回Promise只有连接成功后才能invoke。如果连接失败直接调invoke会抛异常。所以前端最好维护一个连接状态变量在onclose和onreconnected事件里同步更新UI上的连接状态。4.3 消息流转全链路分析以“用户A发送消息”为例整个实时通讯的过程可以拆成7步用户A浏览器调用connection.invoke(SendMessage, 张三, 你好, dev-room)。SignalR JS客户端将这次调用序列化为一个JSON消息通过WebSocket发送到服务端的/chatHub端点。ASP.NET Core SignalR中间件解析消息根据methodName找到ChatHub.SendMessage方法并调用。Hub方法内部执行Clients.Group(dev-room).SendAsync(ReceiveMessage, msg)。框架将ReceiveMessage这个事件名和msg对象序列化通过WebSocket推送给dev-room群组内所有连接。用户A自己的浏览器、用户B的浏览器、用户C的浏览器同时触发connection.on(ReceiveMessage)回调。回调函数更新DOM消息展示在页面上。步骤4里SendAsync是服务端主动推送msg对象会自动序列化为JSON与客户端ReceiveMessage回调里的参数类型一一对应。前后端之间只需要保证方法名相同、参数结构匹配通信就成立了。4.4 手动入群还是自动入群上面代码里用户要先点“加入房间”按钮连接成功后手动调JoinGroup。但实际项目中大多数业务希望连接一旦建立就自动进入固定的用户分组。比如在线状态系统用户登录后应自动进入“在线用户”组。自动入群的实现方式是在OnConnectedAsync方法里加逻辑public override async Task OnConnectedAsync() { // 从查询字符串或Header里拿用户身份 var userName Context.GetHttpContext()?.Request.Query[user]; var groupName ${userName}; await Groups.AddToGroupAsync(Context.ConnectionId, groupName); await base.OnConnectedAsync(); }前端连接时const connection new signalR.HubConnectionBuilder() .withUrl(/chatHub?user encodeURIComponent(currentUser)) .build();这种方案的好处是减少一次前端通联调用也避免“连接建立但还没入群”的窗口期。坏处是把用户身份直接暴露在URL查询参数里有安全隐患。如果要传敏感身份信息应该通过访问令牌accessTokenFactory带过去不要放在URL里。5. 进阶扩展分组、广播、认证与后台推送5.1 群组广播与私有消息的完整实现聊天室的核心是群组广播但真实系统里几乎所有场景都同时需要“私聊”。SignalR的群组机制根本上是基于连接的分组它和“用户私聊”的关系需要一层业务映射。实际私聊实现思路是这样的每个用户登录后以自己的用户ID作为群组名加入一个“私有群组”。服务端可以通过Context.UserIdentifier拿到经过认证的用户ID。给某用户发消息只要向Clients.User(targetUserId).SendAsync(...)或Clients.Group(targetUserId).SendAsync(...)发送即可。消息内容存数据库接收方在线与否都能在下次登录时拉取。在Hub里定义一个私聊方法public class PrivateChatHub : Hub { private readonly ILoggerPrivateChatHub _logger; public PrivateChatHub(ILoggerPrivateChatHub logger) { _logger logger; } public async Task SendPrivateMessage(string toUserId, string message) { // 当前登录用户ID需要启用身份认证 var fromUserId Context.UserIdentifier; var msg new PrivateMessage { FromUserId fromUserId, ToUserId toUserId, Content message, SentAt DateTime.Now }; // 推送给接收方 await Clients.User(toUserId).SendAsync(ReceivePrivateMessage, msg); // 回执给发送方 await Clients.Caller.SendAsync(PrivateMessageSent, msg); _logger.LogInformation(User {From} sent private message to {To}, fromUserId, toUserId); } }这里的关键是认证后的身份标识。信号器会把认证后的用户标识ClaimsPrincipal的NameIdentifier自动映射为UserIdentifier不用我们手动从连接ID去查用户表。这比传统的“连接ID到用户ID映射字典”要安全可靠得多因为连接ID会变化而用户标识是稳定的。如果私聊消息需要入库存档我会在这段代码里填上数据库写入逻辑。实际项目强烈建议先写库再推送避免消息丢失。用户不在线时推送会静默失败但消息已在数据库里面用户下次上线拉取未读消息即可。5.2 集成身份认证Cookie认证与Token认证默认情况下SignalR匿名可连这对实时通讯系统来说是危险的。开启认证的方式和普通MVC/API项目一致在认证中间件之后配置builder.Services.AddAuthentication(options { options.DefaultScheme CookieAuthenticationDefaults.AuthenticationScheme; }).AddCookie(options { options.Cookie.Name signalr.auth; }); builder.Services.AddAuthorization(); var app builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.MapHubChatHub(/chatHub).RequireAuthorization();上述代码把Hub端点要求为必须认证。客户端JS需要携带Cookie才能握手默认withUrl会使用作文档的Cookie。对于SPA项目更常见的是使用JWT Token鉴权。设置accessTokenFactory即可const connection new signalR.HubConnectionBuilder() .withUrl(/chatHub, { accessTokenFactory: () localStorage.getItem(jwtToken) }) .build();服务端需要把JwtBearerEvents.OnMessageReceived事件里的Token与access_token查询参数关联builder.Services.AddAuthentication(options ...) .AddJwtBearer(options { options.Events new JwtBearerEvents { OnMessageReceived context { var accessToken context.Request.Query[access_token]; var path context.HttpContext.Request.Path; if (!string.IsNullOrEmpty(accessToken) path.StartsWithSegments(/chatHub)) { context.Token accessToken; } return Task.CompletedTask; } }; });这里有个常识背景浏览器端的WebSocket API无法自定义请求头所以SignalR JS客户端会把Token作为查询字符串参数发送。服务端默认情况下不会从这个参数读取Token需要自己实现OnMessageReceived事件把access_token映射到context.Token。5.3 服务端后台定时推送与第三方调用推送很多时候推送方不是Hub类本身而是后台服务比如定时任务、另一个中间件。这时需要用到IHubContextT。从依赖注入容器里拿出IHubContextChatHub然后调用方法public class ScheduledPushService : BackgroundService { private readonly IHubContextChatHub _hubContext; public ScheduledPushService(IHubContextChatHub hubContext) { _hubContext hubContext; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { // 每10秒推送一个服务器时间 await _hubContext.Clients.Group(dev-room).SendAsync(ReceiveSystemMessage, $后台调度服务器时间为 {DateTime.Now:HH:mm:ss}); await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken); } } }注册后台服务builder.Services.AddHostedServiceScheduledPushService();这样即使没有任何客户端连接到ChatHub实例后台服务也能通过HubContext向所有已连接客户端推送消息。注意IHubContextT是线程安全的可以直接注入到单例服务中这是SignalR架构设计很巧妙的一点。用这种方案我们可以轻松实现监控大屏每隔几秒推送一次最新数据或者定时任务结束时通知相关用户不需要在Hub里开一个循环来等待触发。6. 常见问题与排查技巧实录6.1 高频报错与对应的解决方案我在使用SignalR过程中遇到的典型问题基本集中在这几个地方整理成一张表供速查问题现象常见原因解决方案前端一直处于connecting状态反向代理未配置WebSocket升级请求头或Hub路由与前端URL不一致Nginx配置proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade;调用invoke时报Cannot send data if the connection is not in the Connected state连接未成功就直接调用或连接已断开调用前检查connection.state signalR.HubConnectionState.Connected服务端SendAsync没有触发前端监听方法名不一致或参数序列化类型不匹配用强类型Hub接口编译期杜绝拼写错误跨域请求失败CORS未配置或AllowAnyOrigin与AllowCredentials冲突使用WithOrigins指定精确前缀 AllowCredentials部署到IIS时连接不稳定IIS应用池回收导致内存中的连接状态丢失关闭应用池空闲超时或使用Redis背板JSON消息过大MaximumReceiveMessageSize默认值为32KB在AddSignalR里调大MaximumReceiveMessageSize自动重连后收不到数据重连成功但没有重新加入群组或同步状态监听onreconnected事件重新invoke入群方法拉取离线消息第一行提到的Nginx配置值得单独强调。开发环境直接用IIS Express或dotnet run没问题但一到生产环境使用Nginx反向代理WebSocket升级就被代理吃掉了。正确配置是location /chatHub { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; }配置完一定要nginx -t检查语法然后reload。在浏览器开发者工具Network标签页里看到WebSocket的101状态码说明升级成功。6.2 性能调优背压、协议压缩与多实例背板SignalR在单节点上能支撑的连接数主要受限于内存和CPU小到几百、大到几万都有可能。要支撑更大规模先做三个层面的优化第一层减少单条消息体积。默认JSON协议下消息头里包含方法名、参数名比较冗长。改用MessagePack协议消息体积能减少50%甚至更多。服务端配置builder.Services.AddSignalR() .AddMessagePackProtocol(options { options.SerializerOptions MessagePackSerializerOptions.Standard .WithResolver(ContractlessStandardResolver.Instance); });前端JS需要添加microsoft/signalr-protocol-msgpack包然后用withHubProtocol(new MessagePackHubProtocol())启用。第二层合理设置连接和消息大小参数。builder.Services.AddSignalR(options { options.KeepAliveInterval TimeSpan.FromSeconds(15); options.ClientTimeoutInterval TimeSpan.FromSeconds(30); options.MaximumReceiveMessageSize 1024 * 1024; // 1MB options.StreamBufferCapacity 20; });KeepAliveInterval是心跳间隔防止代理/NAT清理空闲连接。ClientTimeoutInterval是客户端断开后服务端等待多久才清理。这两个值要配合调整否则会出现“时钟偏差导致误判离线”的问题。第三层多实例部署的Redis背板。当服务扩展到两个及以上实例时SignalR默认的内存消息路由会失效——用户连接到实例A实例B通过HubContext广播时消息无法到达实例A上的连接。解决方式是用Redis背板做消息分发dotnet add package Microsoft.AspNetCore.SignalR.StackExchangeRedisbuilder.Services.AddSignalR() .AddStackExchangeRedis(localhost:6379);Redis背板让所有实例通过发布订阅模式共享消息连接仍保持在各实例本地。这个方案很适合中小规模多实例部署。再往上走就是Azure SignalR Service或自建高性能消息系统但对大多数业务场景来说Redis背板已经足够。6.3 几个我踩过的真实坑位第一开发环境一切都正常一上Nginx就隔一段时间自动掉线。排了很久发现是Nginx默认的proxy_read_timeout是60秒WebSocket长连接超过60秒没有数据就会被断开。解决办法是调大这两个参数location /chatHub { proxy_read_timeout 3600s; proxy_send_timeout 3600s; }第二用户登录状态用JWT但Token刷新后SignalR连接没有重建导致服务端拿到的还是旧Token。用户的权限变更后连接仍然保持旧状态。我当时的处理是监听到Token刷新事件后主动connection.stop()再start()。第三数据库里存的消息发送时间和服务端推送时间不一致。因为服务端发送时用的是服务器本地时间DateTime.Now客户端浏览器时间和服务器时间有偏差消息展示的顺序就会乱。后来统一改成UTC时间传递展示时在前端转本地时间问题就解决了。第三点尤其值得记住任何分布式系统的跨端数据展示时间字段传输和存储都统一用UTC展示时才做本地化转换。这个原则不仅仅适用于SignalR适用于所有后端开发。很多人在SignalR的调试上卡壳是因为只盯着浏览器的Console面板看其实Network面板的WS帧View更直观能明确看到WebSocket请求是否握手成功、消息帧的具体内容。Chrome DevTools的Network标签里找到对应的WebSocket连接点击“Messages”标签就能查看实时收发帧。这个工具在排查序列化错误和消息丢失问题时效率极高。做完整套项目后再回头看我会觉得SignalR真正厉害的地方不是它有多复杂的API而是它用一个抽象层把WebSocket、SSE、长轮询这些传输细节全部收敛了起来让开发者只需要关心业务逻辑哪个用户给哪个群组发什么消息。但简单不意味着不需要理解底层原理恰恰是明白了传输协商、连接生命周期、粘性会话、背板分发这些概念才能在遇到线上故障的时候快速定位问题。希望这篇文章能让你少走一些弯路。后面我还会继续分享SignalR在真实业务场景里与数据库、消息队列结合的更多细节。
返回列表