SignalR:通知从 ASP.NET Core Web API 到 Angular 7 客户端的冗长操作的进度

Naf*_*tis 6 c# asp.net-core angular asp.net-core-signalr

编辑:见底部

我是 SignalR 的新手,并尝试使用这个库和 ASP.NET Core Web API 用 Angular7 客户端实现一个简单的场景。我所需要的只是使用 SignalR 来通知客户端 API 控制器方法中一些冗长操作的进度。

经过多次尝试,我达到了显然已建立连接的程度,但是当长任务开始运行并发送消息时,我的客户端似乎没有收到任何内容,网络套接字中也没有出现任何流量(Chrome F12 -网络 - WS)。

我在这里发布了详细信息,这可能对其他新手也有用(完整源代码位于https://1drv.ms/u/s!AsHCfliT740PkZh4cHY3r7I8f-VQiQ)。可能我只是犯了一些明显的错误,但在文档和谷歌搜索中我找不到与我的代码片段完全不同的代码片段。谁能给个提示?

服务器端我的起点是https://msdn.microsoft.com/en-us/magazine/mt846469.aspx,加上文档在https://docs.microsoft.com/en-us/aspnet/core/ signalr/hubs?view=aspnetcore-2.2。我试图用它创建一个虚拟的实验解决方案。

我以食谱形式的代码片段如下。

(A) 服务器端

1.创建一个新的 ASP.NET 核心 Web API 应用程序。没有身份验证或 Docker,只是为了保持最小化。

2.添加NuGet包Microsoft.AspNetCore.SignalR

3.在Startup.csConfigureServices

public void ConfigureServices(IServiceCollection services)
{
    // CORS
    services.AddCors(o => o.AddPolicy("CorsPolicy", builder =>
    {
        builder.AllowAnyMethod()
            .AllowAnyHeader()
            // https://github.com/aspnet/SignalR/issues/2110 for AllowCredentials
            .AllowCredentials()
            .WithOrigins("http://localhost:4200");
    }));
    // SignalR
    services.AddSignalR();
    
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
Run Code Online (Sandbox Code Playgroud)

以及相应的Configure方法:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseHsts();
    }
    
    // CORS
    app.UseCors("CorsPolicy");
    // SignalR: add to the API at route "/progress"
    app.UseSignalR(routes =>
    {
        routes.MapHub<ProgressHub>("/progress");
    });
    
    app.UseHttpsRedirection();
    app.UseMvc();
}
Run Code Online (Sandbox Code Playgroud)

4.添加一个ProgressHub类,它只是从Hub派生出来的:

public class ProgressHub : Hub
{
}
Run Code Online (Sandbox Code Playgroud)

5.TaskController添加一个方法来启动一些冗长的操作:

[Route("api/task")]
[ApiController]
public class TaskController : ControllerBase
{
    private readonly IHubContext<ProgressHub> _progressHubContext;
    
    public TaskController(IHubContext<ProgressHub> progressHubContext)
    {
        _progressHubContext = progressHubContext;
    }
    
    [HttpGet("lengthy")]
    public async Task<IActionResult> Lengthy([Bind(Prefix = "id")] string connectionId)
    {
        await _progressHubContext
            .Clients
            .Client(connectionId)
            .SendAsync("taskStarted");
            
        for (int i = 0; i < 100; i++)
        {
            Thread.Sleep(500);
            Debug.WriteLine($"progress={i}");
            await _progressHubContext
                .Clients
                .Client(connectionId)
                .SendAsync("taskProgressChanged", i);
        }
        
        await _progressHubContext
            .Clients
            .Client(connectionId)
            .SendAsync("taskEnded");
            
        return Ok();
    }
}
Run Code Online (Sandbox Code Playgroud)

(B) 客户端

1.创建一个新的 Angular7 CLI 应用程序(没有路由,只是为了简单起见)。

2. npm install @aspnet/signalr --save.

3.我的app.component代码:

import { Component, OnInit } from '@angular/core';
import { HubConnectionBuilder, HubConnection, LogLevel } from '@aspnet/signalr';
import { TaskService } from './services/task.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  private _connection: HubConnection;
  
  public messages: string[];
  
  constructor(private _taskService: TaskService) {
    this.messages = [];
  }
  
  ngOnInit(): void {
    // https://codingblast.com/asp-net-core-signalr-chat-angular/
    this._connection = new HubConnectionBuilder()
      .configureLogging(LogLevel.Debug)
      .withUrl("http://localhost:44348/signalr/progress")
      .build();
      
    this._connection.on("taskStarted", data => {
      console.log(data);
    });
    this._connection.on("taskProgressChanged", data => {
      console.log(data);
      this.messages.push(data);
    });
    this._connection.on("taskEnded", data => {
      console.log(data);
    });
    
    this._connection
      .start()
      .then(() => console.log('Connection started!'))
      .catch(err => console.error('Error while establishing connection: ' + err));
  }
  
  public startJob() {
    this.messages = [];
    this._taskService.startJob('zeus').subscribe(
      () => {
        console.log('Started');
      },
      error => {
        console.error(error);
      }
    );
  }
}
Run Code Online (Sandbox Code Playgroud)

其极简的 HTML 模板:

<h2>Test</h2>
<button type="button" (click)="startJob()">start</button>
<div>
  <p *ngFor="let m of messages">{{m}}</p>
</div>
Run Code Online (Sandbox Code Playgroud)

上面代码中的任务服务只是一个调用HttpClient's的函数的包装器get<any>('https://localhost:44348/api/task/lengthy?id=' + id)


编辑 1

经过一些更多的实验,我带来了这些变化:

  • .withUrl('https://localhost:44348/progress')按照建议使用。似乎现在它不再触发 404。请注意更改:我替换httphttps.

  • 不要使 API 方法异步,因为它似乎await不是必需的(即将返回类型设置为IActionResult和删除asyncawait)。

通过这些更改,我现在可以在客户端(Chrome F12)上看到预期的日志消息。看着它们,似乎连接被绑定到一个生成的 ID k2Swgcy31gjumKtTWSlMLw

Utils.js:214 [2019-02-28T20:11:48.978Z] Debug: Starting HubConnection.
Utils.js:214 [2019-02-28T20:11:48.987Z] Debug: Starting connection with transfer format 'Text'.
Utils.js:214 [2019-02-28T20:11:48.988Z] Debug: Sending negotiation request: https://localhost:44348/progress/negotiate.
core.js:16828 Angular is running in the development mode. Call enableProdMode() to enable the production mode.
Utils.js:214 [2019-02-28T20:11:49.237Z] Debug: Selecting transport 'WebSockets'.
Utils.js:210 [2019-02-28T20:11:49.377Z] Information: WebSocket connected to wss://localhost:44348/progress?id=k2Swgcy31gjumKtTWSlMLw.
Utils.js:214 [2019-02-28T20:11:49.378Z] Debug: Sending handshake request.
Utils.js:210 [2019-02-28T20:11:49.380Z] Information: Using HubProtocol 'json'.
Utils.js:214 [2019-02-28T20:11:49.533Z] Debug: Server handshake complete.
app.component.ts:39 Connection started!
app.component.ts:47 Task service succeeded
Run Code Online (Sandbox Code Playgroud)

因此,可能我没有收到通知,因为我的客户端 ID 与 SignalR 分配的 ID 不匹配(从上面引用的论文中,我的印象是提供 ID 是我的职责,因为它是一个参数API 控制器)。然而,我在连接原型中看不到任何允许我检索此 ID 的可用方法或属性,以便在启动冗长的作业时将其传递给服务器。这可能是我问题的原因吗?如果是这样,应该有一种获取 ID(或从客户端设置)的方法。你怎么认为?

Naf*_*tis 6

看来我终于找到了。这个问题可能是由于ID错误引起的,所以我开始寻找解决方案。一篇文章(https://github.com/aspnet/SignalR/issues/2200)指导我使用组,这似乎是这些情况下推荐的解决方案。因此,我更改了集线器,以便它自动将当前连接 ID 分配给“进度”组:

public sealed class ProgressHub : Hub
{
    public const string GROUP_NAME = "progress";

    public override Task OnConnectedAsync()
    {
        // https://github.com/aspnet/SignalR/issues/2200
        // https://learn.microsoft.com/en-us/aspnet/signalr/overview/guide-to-the-api/working-with-groups
        return Groups.AddToGroupAsync(Context.ConnectionId, "progress");
    }
}
Run Code Online (Sandbox Code Playgroud)

现在,我的 API 控制器方法是:

[HttpGet("lengthy")]
public async Task<IActionResult> Lengthy()
{
    await _progressHubContext
        .Clients
        .Group(ProgressHub.GROUP_NAME)
        .SendAsync("taskStarted");
    for (int i = 0; i < 100; i++)
    {
        Thread.Sleep(200);
        Debug.WriteLine($"progress={i + 1}");
        await _progressHubContext
            .Clients
            .Group(ProgressHub.GROUP_NAME)
            .SendAsync("taskProgressChanged", i + 1);
    }
    await _progressHubContext
        .Clients
        .Group(ProgressHub.GROUP_NAME)
        .SendAsync("taskEnded");

    return Ok();
}
Run Code Online (Sandbox Code Playgroud)

当然,我相应地更新了客户端代码,这样在调用 API 方法时就不再需要发送 ID。

完整的演示存储库位于https://github.com/Myrmex/signalr-notify-progress