Azure Function中Serverless SignalR调用InvokeAsync成功StartAsync后挂起问题
问题背景
搭建Azure Function时配置了用于通知Web客户端的简易Serverless Hub,FunctionApp包含名为MyHub的Serverless Hub,同时内置一个客户端(最终将接收EventGrid事件并通过MyHub通知客户端)。客户端成功调用StartAsync后,调用InvokeAsync时出现挂起情况,需排查原因。
代码示例
using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR.Client; using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.Functions.Worker.Http; using Microsoft.Azure.Functions.Worker.SignalRService; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using System.Net; new HostBuilder() .ConfigureFunctionsWorkerDefaults() .ConfigureServices(services => { // 不确定是否需要,看起来加不加都没区别 //services.AddSignalR().AddAzureSignalR(); services.AddServerlessHub<MyHub>(); services.AddSingleton<Client>(); }) .Build() .Run(); public class Client { private readonly HubConnection _connection; public Client() { string? hostname = Environment.GetEnvironmentVariable("WEBSITE_HOSTNAME"); _connection = new HubConnectionBuilder() .WithUrl($"http://{hostname}/api") .Build(); _connection.StartAsync().GetAwaiter().GetResult(); } [Function("test")] public async Task Negotiate([HttpTrigger] HttpRequestData req) { await _connection.InvokeAsync("broadcast", req.ToString()); } } [SignalRConnection("AzureSignalRConnectionString")] public class MyHub : ServerlessHub { private const string HubName = nameof(MyHub); public MyHub(IServiceProvider serviceProvider) : base(serviceProvider) { } [Function("negotiate")] public static HttpResponseData Negotiate([HttpTrigger(AuthorizationLevel.Anonymous)] HttpRequestData req, [SignalRConnectionInfoInput(HubName = HubName)] string connectionInfo) { HttpResponseData response = req.CreateResponse(HttpStatusCode.OK); response.Headers.Add("Content-Type", "application/json"); response.WriteString(connectionInfo); return response; } [Function("broadcast")] public Task Broadcast( [SignalRTrigger(HubName, "messages", "broadcast", "message")] SignalRInvocationContext invocationContext, string message) { return Clients.All.SendAsync(message); } }
相关配置(local.settings.json)
{ "Host": { "CORS": "http://localhost:5002", "CORSCredentials": true }, "IsEncrypted": false, "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.AspNetCore.SignalR": "Trace" } }, "Values": { "AzureSignalRConnectionString": "Endpoint=https://xxx.service.signalr.net;AccessKey=xxx;Version=1.0;", "AzureWebJobsStorage": "UseDevelopmentStorage=true", "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated" } }
可能的问题原因及解决方案
1. 同步阻塞初始化引发死锁
Client构造函数中用_connection.StartAsync().GetAwaiter().GetResult()同步等待异步操作完成,在.NET Isolated Worker环境中,这种同步阻塞会占用关键线程,导致后续InvokeAsync因无法获取执行线程而挂起。
解决方法:
改用IHostedService延迟初始化Hub连接,避免构造阶段同步阻塞:
public class Client : IHostedService { private readonly HubConnection _connection; public Client() { string? hostname = Environment.GetEnvironmentVariable("WEBSITE_HOSTNAME") ?? "localhost:7071"; _connection = new HubConnectionBuilder() .WithUrl($"http://{hostname}/api") .Build(); } public async Task StartAsync(CancellationToken cancellationToken) { await _connection.StartAsync(cancellationToken); } public async Task StopAsync(CancellationToken cancellationToken) { await _connection.StopAsync(cancellationToken); } [Function("test")] public async Task Negotiate([HttpTrigger] HttpRequestData req) { await _connection.InvokeAsync("broadcast", req.ToString()); } }
同时更新服务注册代码:
services.AddHostedService<Client>();
2. Hub连接URL缺失本地调试 fallback
本地调试时WEBSITE_HOSTNAME环境变量未设置,导致连接URL变为http://null/api,虽然StartAsync未报错,但实际连接未成功建立,后续InvokeAsync因无效连接挂起。
解决方法:
添加本地地址 fallback:
string? hostname = Environment.GetEnvironmentVariable("WEBSITE_HOSTNAME") ?? "localhost:7071";
3. Serverless Hub调用路径不匹配
直接使用/api作为连接URL无法正确路由到MyHub,客户端调用服务端方法需要基于协商接口返回的连接信息构建连接,而非硬编码URL。
解决方法:
通过协商接口获取合法连接信息后再初始化Hub连接:
public async Task StartAsync(CancellationToken cancellationToken) { string? hostname = Environment.GetEnvironmentVariable("WEBSITE_HOSTNAME") ?? "localhost:7071"; var httpClient = new HttpClient(); var negotiateResp = await httpClient.GetStringAsync($"http://{hostname}/api/negotiate"); var connectionInfo = JsonSerializer.Deserialize<ConnectionInfo>(negotiateResp); _connection = new HubConnectionBuilder() .WithUrl(connectionInfo.Url, opts => { opts.AccessTokenProvider = () => Task.FromResult(connectionInfo.AccessToken); }) .Build(); await _connection.StartAsync(cancellationToken); }
4. CORS或权限配置问题
本地调试时CORS范围未覆盖客户端地址,或客户端连接未携带合法令牌,导致InvokeAsync请求被阻塞,进而出现挂起。
解决方法:
调试阶段可临时将local.settings.json中的CORS设置为*(生产环境需指定具体域名),同时确保客户端连接时正确使用协商接口返回的令牌。
内容的提问来源于stack exchange,提问作者Alexandre Guimond

