【问题标题】:SignalR Core two concurrent calls from same clientSignalR Core 来自同一客户端的两个并发调用
【发布时间】:2021-02-19 09:40:31
【问题描述】:

1 我有一个由 Js 客户端触发到 Asp.Net Core SignalR Hub 的长任务(300 万)

效果很好:

public class OptimizerHub : Hub, IOptimizerNotification
{
    public async Task Optimize(LightingOptimizationInput lightingOptimizationInput)
    {
        LightingOptimizer lightingOptimizer = CreateLightingOptimizer();
        Task t = lightingOptimizer.Optimize(lightingOptimizationInput);
        await t;
    }
}

2 服务器回调客户端通知进度、消息、...

Clients.Caller.SendAsync(nameof(OnProgress), progress);

到目前为止一切正常。

3 我希望通过客户端调用 Hub 方法可以取消任务

public Task Cancel()
{
    GetContextLightingOptimizer()?.Cancel();
    return Task.FromResult(0);
}

4 问题

当客户端进行调用时,我在 Chrome 开发者工具详细信息中看到它转到了服务器。 在 end long 任务结束 (3mn) 之前调用不会到达服务器!

5 我尝试了很多解决方案

就像改变我的长任务调用方法,总是失败:

    // Don't wait end of task, fails because the context disappear and can't call back the client :
    // Exception : "Cannot access a disposed object"
    
    public async Task Optimize(LightingOptimizationInput lightingOptimizationInput)
    {
        LightingOptimizer lightingOptimizer = CreateLightingOptimizer();
        Task t = lightingOptimizer.Optimize(lightingOptimizationInput);
    }

6 种可能的解决方案

我现在想的唯一解决方案是客户端在 Http 控制器中进行 Http 调用,并传递连接 ID 这可能会导致取消。

该帖子提供了有关可能解决方案的信息: Call SignalR Core Hub method from Controller

7 个问题

是否有一种简单的方法可以让客户端在处理第一个呼叫时向集线器发出第二个呼叫?

还有一篇关于并发调用的帖子: SignalR multiple concurrent calls from client

我应该从上一篇文章中推断出即使我的中心服务器方法可以多次调用客户端,它也无法处理来自客户端的任何其他调用?

【问题讨论】:

  • 也许将您的长期任务转移到Hangfire 之类的地方,让您的信号员呼叫时间缩短?
  • 您在服务器端长期运行的操作是否提供了许多取消操作的好时机?
  • 您误用了集线器。集线器应该是调解者,而不是主力,总之不要这样做。 .通过消息或直通将呼叫转移到有状态且适合运行长时间运行的任务。在该环境中创建取消令牌,使用字典并在必要时传回密钥,如果可用则通过其密钥取消该令牌
  • 感谢您的帮助,实际上,有一个单独的类处理长调用。另一个类中需要 HubContext 来通知客户端
  • @Caius 是的,有很多美好的时刻,因为我有一个包含许多迭代的循环,并且我已经在扫描 CancellationToken

标签: c# asp.net-core task signalr-hub asp.net-core-signalr


【解决方案1】:

终于有办法了

需要将 SignalR HubContext 注入自定义通知程序

它允许:

  1. 长时间工作时回调Js客户端
  2. 向客户提供某种类型的报告(回调)
  3. 从客户端取消

步骤如下

1 添加一个notifier对象,其作用是回调Js客户端

使 HubContext 被依赖注入注入

// that class can be in a business library, it is not SignalR aware
public interface IOptimizerNotification
{
    string? ConnectionId { get; set; }
    Task OnProgress(long currentMix, long totalMixes);
}

// that class has to be in the Asp.Net Core project to use IHubContext<T>
public class OptimizerNotification : IOptimizerNotification
{
  private readonly IHubContext<OptimizerHub> hubcontext;
  public string? ConnectionId { get; set; }
  
  public OptimizerNotification(IHubContext<OptimizerHub> hubcontext)
  {
    this.hubcontext = hubcontext;
  }
  #region Callbacks towards client
  public async Task OnProgress(long currentMix, long totalMixes)
  {
    int progress = (int)(currentMix * 1000 / (totalMixes - 1));
    await hubcontext.Clients.Client(ConnectionId).SendAsync(nameof(OnProgress), progress);
  }
  #endregion
}

2 在依赖注入系统中注册通知器对象

在startup.cs中

services.AddTransient<IOptimizerNotification, OptimizerNotification>();

3 获取要注入worker对象的notifier对象

public IOptimizerNotification Notification { get; set; }
public LightingOptimizer(IOptimizerNotification notification)
{
  Notification = notification;
}

4 来自工作对象的通知

await Notification.OnProgress(0, 1000);

5 Start Business 对象长期工作

使用 SignalR.ConnectionId 注册业务对象(这里是 LightingOptimizer) 以便稍后可以检索业务对象

public class OptimizerHub : Hub
{
    private static Dictionary<string, LightingOptimizer> lightingOptimizers = new Dictionary<string, LightingOptimizer>();
    
    public async void Optimize(LightingOptimizationInput lightingOptimizationInput)
    {
      // the business object is created by DI so that everyting gets injected correctly, including IOptimizerNotification 
      LightingOptimizer lightingOptimizer;
      IServiceScopeFactory factory = Context.GetHttpContext().RequestServices.GetService<IServiceScopeFactory>();
      using (IServiceScope scope = factory.CreateScope())
      {
        IServiceProvider provider = scope.ServiceProvider;
        lightingOptimizer = provider.GetRequiredService<LightingOptimizer>();
        lightingOptimizer.Notification.ConnectionId = Context.ConnectionId;
        // Register connectionId in Dictionary
        lightingOptimizers[Context.ConnectionId] = lightingOptimizer;
      }
      // Call business worker, long process method here
      await lightingOptimizer.Optimize(lightingOptimizationInput);
    }
    // ...
}

**6 在中心实现取消 **

从(当前)connectionId 中检索业务对象并对其调用 Cancel

public class OptimizerHub : Hub
{
    // ...
    public Task Cancel()
    {
      if (lightingOptimizers.TryGetValue(Context.ConnectionId, out LightingOptimizer? lightingOptimizer))
        lightingOptimizer.Cancel(); 
      return Task.FromResult(0);
    }
}

7 对业务对象中的取消做出反应

public class LightingOptimizer
{
    private CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
    private CancellationToken cancellationToken;
    
    public LightingOptimizer( IOptimizerNotification notification )
    {
        Notification = notification;
        cancellationToken = cancellationTokenSource.Token;
    }
    public void Cancel()
    {
      cancellationTokenSource.Cancel();
    }
    public async Task Optimize(LightingOptimizationInput lightingOptimizationInput)
    {
      for( int i+; i < TooMuchToBeShort ;i++)
      {
      
        if (cancellationToken.IsCancellationRequested)
            throw new TaskCanceledException();
      }
    }

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-04-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多