SignalR Core 来自同一客户端的两个并发调用

SignalR Core two concurrent calls from same client

1 我有一个由 Js 客户端触发到 Asp.Net Core SignalR Hub

的长任务 (3mn)

它工作正常:

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 开发人员工具详细信息中看到它转到了服务器。 在结束长任务结束 (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 可以取消。

post 提供了有关可能解决方案的信息:

7 个问题

有没有一种简单的方法可以让客户端在处理第一个呼叫时对集线器进行第二次呼叫?

还有一个关于并发调用的 post : SignalR multiple concurrent calls from client

我是否应该从前面的 post 中推断出,即使我的中心服务器方法可以多次调用客户端,它也无法处理来自客户端的任何其他调用?

终于找到解决办法了

需要在自定义通知程序中注入 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 开始业务对象长期工作

使用 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();
      }
    }