ASP.NET Core SignalR - 浏览器 window 保持 "loading" 120 秒

ASP.NET Core SignalR - browser window remains "loading" for 120 seconds

背景

我正在尝试构建一个 ASP.NET 核心 Web 应用程序,它需要服务器和客户端之间的双向 real-time 通信。为此,我尝试使用 ASP.NET Core port of SignalR

环境

问题

当我启动应用程序(VS2015 中的 IIS Express 按钮)时,一个新的 Firefox 选项卡会在 localhost:... 处打开,呈现我的 Views\Home\Index.cshtml。这工作正常,页面似乎在几秒钟内完全加载,但 "loading" 纺车图标持续约 123 秒。

从 Application Insights 中,我可以看到浏览器立即发出对 "Home/Index" 和“/signalr/hubs”的 GET 请求,然后在发出进一步的信号器请求之前暂停 120 秒:

在这最后三个事件的大约同一时间,我看到以下日志出现在 Firefox 的调试器中:

[11:37:43 GMT+0100 (GMT Standard Time)] SignalR: Client subscribed to hub 'communicationshub'.jquery.signalR-2.2.1.js:82:17
[11:37:43 GMT+0100 (GMT Standard Time)] SignalR: Negotiating with '/signalr/negotiate?clientProtocol=1.5&connectionData=%5B%7B%22name%22%3A%22communicationshub%22%7D%5D'.jquery.signalR-2.2.1.js:82:17
[11:37:43 GMT+0100 (GMT Standard Time)] SignalR: longPolling transport starting.jquery.signalR-2.2.1.js:82:17
[11:37:43 GMT+0100 (GMT Standard Time)] SignalR: Opening long polling request to 'http://localhost:61192/signalr/connect?transport=longPolling&clientProtocol=1.5&connectionToken=CfDJ8POyhc9yf1ZCtsJ9aOnflZgBHgcMoU0sLdQxkrNhkMLtIP%2BGWCL%2BPNY5H1RhK%2Fl92vibhhTu1PQxPpkcg%2BhpFwYw%2BNyFcTNplZ2HPBXd4QVZVOlP7QR9eIkuoCIDZMFedKEk7kzC7cXBhoF8838KJEAZnGz%2BqQGlePSxmoM6WVhW&connectionData=%5B%7B%22name%22%3A%22communicationshub%22%7D%5D'.jquery.signalR-2.2.1.js:82:17
[11:37:43 GMT+0100 (GMT Standard Time)] SignalR: Long poll complete.jquery.signalR-2.2.1.js:82:17
[11:37:43 GMT+0100 (GMT Standard Time)] SignalR: LongPolling connected.jquery.signalR-2.2.1.js:82:17
[11:37:43 GMT+0100 (GMT Standard Time)] SignalR: longPolling transport connected. Initiating start request.jquery.signalR-2.2.1.js:82:17
[11:37:43 GMT+0100 (GMT Standard Time)] SignalR: Opening long polling request to 'http://localhost:61192/signalr/poll?transport=longPolling&clientProtocol=1.5&connectionToken=CfDJ8POyhc9yf1ZCtsJ9aOnflZgBHgcMoU0sLdQxkrNhkMLtIP%2BGWCL%2BPNY5H1RhK%2Fl92vibhhTu1PQxPpkcg%2BhpFwYw%2BNyFcTNplZ2HPBXd4QVZVOlP7QR9eIkuoCIDZMFedKEk7kzC7cXBhoF8838KJEAZnGz%2BqQGlePSxmoM6WVhW&connectionData=%5B%7B%22name%22%3A%22communicationshub%22%7D%5D'.jquery.signalR-2.2.1.js:82:17
[11:37:43 GMT+0100 (GMT Standard Time)] SignalR: The start request succeeded. Transitioning to the connected state.jquery.signalR-2.2.1.js:82:17
Connected. connectionId : 9d6591af-f1b1-403f-bc42-c81a1b33b25clocalhost:61192:79:21
[11:39:34 GMT+0100 (GMT Standard Time)] SignalR: Long poll complete.jquery.signalR-2.2.1.js:82:17
[11:39:34 GMT+0100 (GMT Standard Time)] SignalR: Opening long polling request to 'http://localhost:61192/signalr/poll?transport=longPolling&clientProtocol=1.5&connectionToken=CfDJ8POyhc9yf1ZCtsJ9aOnflZgBHgcMoU0sLdQxkrNhkMLtIP%2BGWCL%2BPNY5H1RhK%2Fl92vibhhTu1PQxPpkcg%2BhpFwYw%2BNyFcTNplZ2HPBXd4QVZVOlP7QR9eIkuoCIDZMFedKEk7kzC7cXBhoF8838KJEAZnGz%2BqQGlePSxmoM6WVhW&connectionData=%5B%7B%22name%22%3A%22communicationshub%22%7D%5D'.

我的问题是:

更多详情

启动 - 配置服务:

public void ConfigureServices(IServiceCollection services)
{
    // Add framework services.
    services.AddApplicationInsightsTelemetry(Configuration);
    services.AddMvc();

    services.AddSignalR(options =>
    {
        options.Hubs.EnableDetailedErrors = true;
    });

    services.AddSingleton<CommunicationsHub>();
    services.AddScoped<ICommunicationsManager, CommunicationsManager>();
}

启动-配置:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    loggerFactory.AddConsole(Configuration.GetSection("Logging"));
    loggerFactory.AddDebug();

    app.UseApplicationInsightsRequestTelemetry();

    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseBrowserLink();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
    }

    app.UseApplicationInsightsExceptionTelemetry();

    app.UseStaticFiles();
    app.UseWebSockets();
    app.UseSignalR();

    app.UseMvc(routes =>
    {
        routes.MapRoute(
            name: "default",
            template: "{controller=Home}/{action=Index}/{id?}");
    });
}

通讯中心:

public class CommunicationsHub : Hub
{
    public class TransmissionRequestedEventArgs : EventArgs
    {
        public TransmissionRequestedEventArgs(GenericMessage message)
        {
            this.Message = message;
        }

        public GenericMessage Message { get; }
    }

    private readonly IHubContext context;

    public CommunicationsHub(IConnectionManager connectionManager)
    {
        this.context = connectionManager.GetHubContext<CommunicationsHub>();
    }

    public event EventHandler<TransmissionRequestedEventArgs> MessageTransmissionRequested;

    public void OnMessageReceived(GenericMessage message)
    {
        context.Clients.All.onMessageReceived(message);
    }

    public void SendMessage(GenericMessage message)
    {
        MessageTransmissionRequested?.Invoke(this, new TransmissionRequestedEventArgs(message));
        context.Clients.All.onMessageSent(message);
    }
}

Index.cshtml:

@using <<Company>>.Communications.Core
@model IEnumerable<ICommunicationService>
@{
    ViewData["Title"] = "Home Page";
}

@section scripts
{
    <script src="../Scripts/jquery.signalR-2.2.1.js"></script>
    <script src="../signalr/hubs"></script>
    <script>
        $(function () {
            var $tbl = $("#data");
            var comms = $.connection.communicationsHub;
            comms.client.onMessageReceived = function (message) {
                console.log("Message received: " + message)
            };
            comms.client.onMessageSent = function (message) {
                console.log("Message sent: " + message)
            };

            $.connection.hub.logging = true;
            console.log($.connection.hub.logging);
            $.connection.hub.start({ transport: 'longPolling' })
                .done(function () {
                    console.log('Connected. connectionId : ' + $.connection.hub.id);
                })
                .fail(function () {
                    console.log('Could not connect!');
                });
        });
    </script>
}

Please select a communications service to test:
<ul class="nav nav-pills nav-stacked">
    @foreach (var controller in Model)
    {
        <li role="presentation">
            <span class="glyphicon glyphicon-star" aria-hidden="true"></span>@controller
        </li>
    }
</ul>

<table id="data"></table>

更新 1

事实证明这与 SignalR 无关 - 我的错。

奇怪的是,我发现即使将所有 SignalR 逻辑移动到另一个视图后,我的 "Index" 视图仍然需要很长时间才能加载。我回溯了我的 VCS 历史记录,直到找到引入问题的提交并追查到问题的根源。我的HomeControllerclass在构造的时候被注入了一个ICommunicationManager的实例,会导致我的CommunicationManager被实例化。这是 CommunicationManager:

的构造函数
public CommunicationsManager()
{
    var assemblies = from name in DependencyContext.Default.GetDefaultAssemblyNames()
                     where name.Name.StartsWith("<<Company Name>>")
                     select Assembly.Load(name);

    var configuration = new ContainerConfiguration()
        .WithAssemblies(assemblies);

    var container = configuration.CreateContainer();
    this.Candidates = container.GetExports<ICommunicationService>();
}

如果我摆脱这个逻辑并将 this.Candidates 设置为一个空列表,页面加载正常,没有 120+ 秒的纺车。奇怪的是,代码本身执行得非常快,页面加载了所有预期的导出。

我会试着弄清楚为什么会这样,然后我会关闭这个问题。任何帮助仍将不胜感激。

更新 2

我现在对问题的根源有了更好的理解。 ICommunicationManager(间接)继承了IDisposable。我在 CommunicationManager:

中实现了处理模式
#region IDisposable Support
private bool disposedValue = false; // To detect redundant calls

protected virtual void Dispose(bool disposing)
{
    if (!disposedValue)
    {
        if (disposing)
        {
            this.Candidates.ToList().ForEach(it => it.Dispose());
        }
        disposedValue = true;
    }
}

// This code added to correctly implement the disposable pattern.
public void Dispose()
{
    // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
    Dispose(true);
}
#endregion

通过断点此代码,我发现某些东西(我假设的 ASP.NET 框架)试图在返回 "Index" 视图后不久处理通信管理器 even虽然 HomeController 将其作为其私有字段之一进行维护。通过在 Dispose 的开头添加一个 return 语句(以防止实际处理任何东西),我发现加载问题消失了。

如何防止 ASP.NET 框架(或其他任何框架)自动处置 CommunicationManager?我只想在网络服务关闭时处理它。

更新 3

我决定我实际上不需要 ICommunicationManager 来实现 ICommunicationService,在这种情况下我可以放弃处置支持。这意味着我的应用程序现在可以正常工作了。但是,如果能提供有关 ICommunicationManager.Dispose() 为何自动关闭的任何信息,我将不胜感激 - 我自己粗略搜索后找不到任何信息。

您的 CommunicationsManager 正在处理,因为您使用 AddScoped 注册了它。 每个请求创建一次作用域生命周期服务,它们在作用域(即请求)的末尾处理。