You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.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存在差异。

异常现象截图

Socket请求404失败示例
失败请求响应头
Socket端点成功请求响应头
成功/失败请求一致的请求头
KeepAlive消息15秒间隔正常收发验证

现有配置代码

.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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 18:12:52