.Net Core 3.1 SignalR部署Azure VM后客户端间歇接收消息故障
Azure虚拟机部署SignalR仅能接收约30%消息问题
核心疑问
Azure虚拟机是否会对SignalR消息发送进行限流?本地运行时客户端可接收全部消息,但部署到VM后客户端仅能收到约30%的消息
涉及技术栈
- 后端:.Net Core 3.1 API,采用Microsoft.AspNet.SignalR Nuget包实现SignalR能力
- 前端:VueJS单页应用
- 部署环境:搭载IIS 10的Azure Windows Server 2016虚拟机
故障现象
- 本地开发环境下SignalR运行完全正常,消息可即时稳定收发;发布到Azure VM后,即便WebSocket连接建立成功,客户端最多仅能在连接建立后的前5秒左右正常接收消息,随后停止接收。
- 测试页面验证逻辑:页面包含简单输入框与“发送”按钮,前端提交消息后调用API,后端接收到请求后将该消息通过SignalR广播给所有连接。测试发现最初几秒可正常收发消息,之后必须高频提交表单(甚至按住回车键持续提交)形成持续消息流,等待数秒后才会再次恢复接收消息,但恢复后仅能持续几秒就再次中断。
- 消息接收呈现固定周期规律:约接收到30条消息后,后续约70条消息全部丢失,数秒后再次接收到约30条消息,循环往复;若停止测试,无论等待多久,再次按住回车高频提交时,都需要等待数秒才会重新进入“接收数秒、中断数秒”的周期。若降低消息提交频率,客户端几乎无法接收到任何消息,必须持续高频发送请求才能触发消息恢复接收。
- 全链路无显式报错:发起消息的API调用无任何报错,Socket连接、消息接收环节也无错误抛出;客户端SignalR日志级别设为Trace时,可观测到每10秒一次的ping请求均正常收发,配置的15秒间隔KeepAlive消息也可按预期正常收发。
- 偶发连接异常:刷新页面时偶发WebSocket连接失败,此时会发起多个几乎完全一致的Socket端点请求,其中部分请求返回404,部分返回200。失败请求与成功请求的请求头完全一致,仅响应头中的content-length存在差异。
异常现象截图





现有配置代码
.Net端SignalR与JWT认证配置
services.AddSignalR() .AddHubOptions<StreamHub>(hubOptions => { hubOptions.EnableDetailedErrors = true; hubOptions.ClientTimeoutInterval = TimeSpan.FromHours(24); hubOptions.HandshakeTimeout = TimeSpan.FromHours(24); hubOptions.KeepAliveInterval = TimeSpan.FromSeconds(15); hubOptions.MaximumReceiveMessageSize = 1000000; }) .AddJsonProtocol(options => { options.PayloadSerializerOptions.PropertyNamingPolicy = null; }); // 添加认证服务 services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; }) // 添加JWT Bearer认证 .AddJwtBearer(options => { options.SaveToken = true; options.RequireHttpsMetadata = false; options.TokenValidationParameters = new TokenValidationParameters() { ValidateIssuer = true, ValidateAudience = true, ValidAudience = Configuration["JWT:ValidAudience"], ValidIssuer = Configuration["JWT:ValidIssuer"], IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JWT:Secret"])), ClockSkew = TimeSpan.Zero }; // 由于浏览器API限制,需要在查询字符串中传递访问令牌 // 此处代码仅限制对SignalR集线器的调用读取查询字符串中的令牌 options.Events = new JwtBearerEvents { OnMessageReceived = context => { var accessToken = context.Request.Query["access_token"]; // 若请求目标为SignalR集线器 var path = context.HttpContext.Request.Path; if (!string.IsNullOrEmpty(accessToken) && (path.StartsWithSegments("/v1/stream"))) { // 从查询字符串中读取令牌 context.Token = accessToken; } return Task.CompletedTask; } }; });
消息广播API接口代码
[HttpPost] [Route("bitcoin")] public async Task<IActionResult> SendBitcoin([FromBody] BitCoin bitcoin) { await this._hubContext.Clients.All.SendAsync("BitCoin", bitcoin.message); return Ok(bitcoin.message); }
前端SignalR连接与消息发送代码
this.connection = new signalR.HubConnectionBuilder() .configureLogging(process.env.NODE_ENV.toLowerCase() == 'development' ? signalR.LogLevel.None : signalR.LogLevel.None) .withUrl(process.env.VUE_APP_STREAM_ROOT, { accessTokenFactory: () => this.$store.state.auth.token }) .withAutomaticReconnect({ nextRetryDelayInMilliseconds: retryContext => { if(retryContext.retryReason && retryContext.retryReason.statusCode == 401) { return null } else if (retryContext.elapsedMilliseconds < 3600000) { // 若重连时长累计小于60分钟,下次重连前随机等待0-10秒 return Math.random() * 10000; } else { // 若重连时长累计超过60分钟,停止重连 return null; } } }) .build() // 连接超时设置为10分钟 this.connection.serverTimeoutInMilliseconds = 1000 * 60 * 10 this.connection.reconnectedCallbacks.push(() => { let alert = { show: true, text: 'Data connection re-established!', variant: 'success', isConnected: true, } this.$store.commit(CONNECTION_ALERT, alert) setTimeout(() => { this.$_closeConnectionAlert() }, this.$_appMessageTimeout) }) this.connection.onreconnecting((err) => { if(!!err) { console.log('reconnecting:') this.startStream() } }) this.connection.start() .then((response) => { this.startStream() }) .catch((err) => { }); startStream() { // 注册集线器调用的客户端方法 if(this.connection.connectionState.toLowerCase() == 'connected') { this.connection.methods.bitcoin = [] this.connection.on("BitCoin", (data) => { console.log('messageReceived:', data) }) } } buttonClick() { this.$_apiCall({url: 'bitcoin', method: 'POST', data: {message:this.message}}) .then(response => { // console.log('message', response.data) }) }
待确认问题
- 本地环境与Azure VM的配置存在什么差异才会导致该问题?
- 为何客户端会出现这种周期性停止/恢复接收消息的现象?
- 是否为VM配置问题导致消息被拦截?
内容的提问来源于stack exchange,提问作者jenker321
相关产品推荐
相关产品推荐

