ASP.NET Core SignalR客户端连接的正确关闭/断开连接方式是什么?

8

我是一个新用户,正在努力从ASP.NET Core Blazor服务器页面中优雅地关闭次要的signalR客户端。

我正在设置Blazor Server Page的第一次呈现上的次要signalR客户端连接。当通过浏览器标签关闭页面时,我试图关闭此次要的signalR客户端连接。

在撰写本文时,关闭页面时似乎不会触发DisposeAsync。但是,Dispose方法确实被触发。此外,在Safari 13.0.5中,关闭浏览器选项卡时Dispose方法不会被触发?Opera、Firefox和Chrome在关闭浏览器选项卡时都会触发Dispose。通过将Safari更新到v14.0 (15610.1.28.9, 15610)来解决这个问题,该更新通过macOS Catalina v10.15.7进行。

目前,我正在从Dispose中调用DisposeAsync以关闭signalR连接。我使用以下代码关闭客户端连接:

...
Logger.LogInformation("Closing secondary signalR connection...");
await hubConnection.StopAsync();
Logger.LogInformation("Closed secondary signalR connection");
...
StopAsync 方法似乎会阻塞,即 "Closed secondary signalR connection" 没有输出任何消息。虽然我的服务器端 hub 的 OnDisconnectedAsync 处理程序显示连接已经被断开。这类似于描述在这个 问题中所述的行为。
如何在ASP.NET Core 3.1中正确处理 signalR 连接?
完整代码清单如下: 处理 signalR 连接关闭
 #region Dispose
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }


        /// <summary>
        /// Clear secondary signalR Closed event handler and stop the
        /// secondary signalR connection
        /// </summary>
        /// <remarks>
        /// ASP.NET Core Release Candidate 5 calls DisposeAsync when 
        /// navigating away from a Blazor Server page. Until the 
        /// release is stable DisposeAsync will have to be triggered from
        /// Dispose. Sadly, this means having to use GetAwaiter().GetResult()
        /// in Dispose().
        /// However, providing DisposeAsync() now makes the migration easier
        /// https://github.com/dotnet/aspnetcore/issues/26737
        /// https://github.com/dotnet/aspnetcore/issues/9960
        /// https://github.com/dotnet/aspnetcore/milestone/57?closed=1
        /// </remarks>
        protected virtual void Dispose(bool disposing)
        {
            if (disposed)
                return;

            if (disposing)
            {
                Logger.LogInformation("Index.razor page is disposing...");

                try
                {
                    if (hubConnection != null)
                    {
                        Logger.LogInformation("Removing signalR client event handlers...");
                        hubConnection.Closed -= CloseHandler;
                    }

                    // Until ASP.NET Core 5 is released in November
                    // trigger DisposeAsync(). See docstring and DiposeAsync() below.
                    // not ideal, but having to use GetAwaiter().GetResult() until
                    // forthcoming release of ASP.NET Core 5 for the introduction
                    // of triggering DisposeAsync on pages that implement IAsyncDisposable
                    DisposeAsync().GetAwaiter().GetResult();
                }
                catch (Exception exception)
                {
                    Logger.LogError($"Exception encountered while disposing Index.razor page :: {exception.Message}");
                }
            }

            disposed = true;
        }


        /// <summary>
        /// Dispose the secondary backend signalR connection
        /// </summary>
        /// <remarks>
        /// ASP.NET Core Release Candidate 5 adds DisposeAsync when 
        /// navigating away from a Blazor Server page. Until the 
        /// release is stable DisposeAsync will have to be triggered from
        /// Dispose. Sadly, this means having to use GetAwaiter().GetResult()
        /// in Dispose().
        /// However, providing DisposeAsync() now makes the migration easier
        /// https://github.com/dotnet/aspnetcore/issues/26737
        /// https://github.com/dotnet/aspnetcore/issues/9960
        /// https://github.com/dotnet/aspnetcore/milestone/57?closed=1
        /// </remarks>
        public async virtual ValueTask DisposeAsync()
        {
            try
            {
                if (hubConnection != null)
                {
                    Logger.LogInformation("Closing secondary signalR connection...");
                    await hubConnection.StopAsync();
                    Logger.LogInformation("Closed secondary signalR connection");
                }
                // Dispose(); When migrated to ASP.NET Core 5 let DisposeAsync trigger Dispose
            }
            catch (Exception exception)
            {
                Logger.LogInformation($"Exception encountered wwhile stopping secondary signalR connection :: {exception.Message}");
            }
        }
        #endregion

Blazor 服务器页面的完整代码

using System;
using System.Threading.Tasks;

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

using WebApp.Data;
using WebApp.Data.Serializers.Converters;
using WebApp.Data.Serializers.Converters.Visitors;
using WebApp.Repository.Contracts;



namespace WebApp.Pages
{
    public partial class Index : IAsyncDisposable, IDisposable
    {
        private HubConnection hubConnection;
        public bool IsConnected => hubConnection.State == HubConnectionState.Connected;
        private bool disposed = false;


        [Inject]
        public NavigationManager NavigationManager { get; set; }
        [Inject]
        public IMotionDetectionRepository Repository { get; set; }
        [Inject]
        public ILogger<MotionDetectionConverter> LoggerMotionDetection { get; set; }
        [Inject]
        public ILogger<MotionInfoConverter> LoggerMotionInfo { get; set; }
        [Inject]
        public ILogger<JsonVisitor> LoggerJsonVisitor { get; set; }
        [Inject]
        public ILogger<Index> Logger { get; set; }


        #region Dispose
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }


        /// <summary>
        /// Clear secondary signalR Closed event handler and stop the
        /// secondary signalR connection
        /// </summary>
        /// <remarks>
        /// ASP.NET Core Release Candidate 5 calls DisposeAsync when 
        /// navigating away from a Blazor Server page. Until the 
        /// release is stable DisposeAsync will have to be triggered from
        /// Dispose. Sadly, this means having to use GetAwaiter().GetResult()
        /// in Dispose().
        /// However, providing DisposeAsync() now makes the migration easier
        /// https://github.com/dotnet/aspnetcore/issues/26737
        /// https://github.com/dotnet/aspnetcore/issues/9960
        /// https://github.com/dotnet/aspnetcore/milestone/57?closed=1
        /// </remarks>
        protected virtual void Dispose(bool disposing)
        {
            if (disposed)
                return;

            if (disposing)
            {
                Logger.LogInformation("Index.razor page is disposing...");

                try
                {
                    if (hubConnection != null)
                    {
                        Logger.LogInformation("Removing signalR client event handlers...");
                        hubConnection.Closed -= CloseHandler;
                    }

                    // Until ASP.NET Core 5 is released in November
                    // trigger DisposeAsync(). See docstring and DiposeAsync() below.
                    // not ideal, but having to use GetAwaiter().GetResult() until
                    // forthcoming release of ASP.NET Core 5 for the introduction
                    // of triggering DisposeAsync on pages that implement IAsyncDisposable
                    DisposeAsync().GetAwaiter().GetResult();
                }
                catch (Exception exception)
                {
                    Logger.LogError($"Exception encountered while disposing Index.razor page :: {exception.Message}");
                }
            }

            disposed = true;
        }


        /// <summary>
        /// Dispose the secondary backend signalR connection
        /// </summary>
        /// <remarks>
        /// ASP.NET Core Release Candidate 5 adds DisposeAsync when 
        /// navigating away from a Blazor Server page. Until the 
        /// release is stable DisposeAsync will have to be triggered from
        /// Dispose. Sadly, this means having to use GetAwaiter().GetResult()
        /// in Dispose().
        /// However, providing DisposeAsync() now makes the migration easier
        /// https://github.com/dotnet/aspnetcore/issues/26737
        /// https://github.com/dotnet/aspnetcore/issues/9960
        /// https://github.com/dotnet/aspnetcore/milestone/57?closed=1
        /// </remarks>
        public async virtual ValueTask DisposeAsync()
        {
            try
            {
                if (hubConnection != null)
                {
                    Logger.LogInformation("Closing secondary signalR connection...");
                    await hubConnection.StopAsync();
                    Logger.LogInformation("Closed secondary signalR connection");
                }
                // Dispose(); When migrated to ASP.NET Core 5 let DisposeAsync trigger Dispose
            }
            catch (Exception exception)
            {
                Logger.LogInformation($"Exception encountered wwhile stopping secondary signalR connection :: {exception.Message}");
            }
        }
        #endregion


        #region ComponentBase

        /// <summary>
        /// Connect to the secondary signalR hub after rendering.
        /// Perform on the first render. 
        /// </summary>
        /// <remarks>
        /// This could have been performed in OnInitializedAsync but
        /// that method gets executed twice when server prerendering is used.
        /// </remarks>
        protected override async Task OnAfterRenderAsync(bool firstRender)
        {
            if (firstRender)
            {
                var hubUrl = NavigationManager.BaseUri.TrimEnd('/') + "/motionhub";

                try
                {
                    Logger.LogInformation("Index.razor page is performing initial render, connecting to secondary signalR hub");

                    hubConnection = new HubConnectionBuilder()
                        .WithUrl(hubUrl)
                        .ConfigureLogging(logging =>
                        {
                            logging.AddConsole();
                            logging.AddFilter("Microsoft.AspNetCore.SignalR", LogLevel.Information);
                        })
                        .AddJsonProtocol(options =>
                        {
                            options.PayloadSerializerOptions = JsonConvertersFactory.CreateDefaultJsonConverters(LoggerMotionDetection, LoggerMotionInfo, LoggerJsonVisitor);
                        })
                        .Build();

                    hubConnection.On<MotionDetection>("ReceiveMotionDetection", ReceiveMessage);
                    hubConnection.Closed += CloseHandler;

                    Logger.LogInformation("Starting HubConnection");

                    await hubConnection.StartAsync();

                    Logger.LogInformation("Index Razor Page initialised, listening on signalR hub url => " + hubUrl.ToString());
                }
                catch (Exception e)
                {
                    Logger.LogError(e, "Encountered exception => " + e);
                }
            }
        }

        protected override async Task OnInitializedAsync()
        {
            await Task.CompletedTask;
        }
        #endregion


        #region signalR

        /// <summary>Log signalR connection closing</summary>
        /// <param name="exception">
        /// If an exception occurred while closing then this argument describes the exception
        /// If the signaR connection was closed intentionally by client or server, then this
        /// argument is null
        /// </param>
        private Task CloseHandler(Exception exception)
        {
            if (exception == null)
            {
                Logger.LogInformation("signalR client connection closed");
            }
            else
            {
                Logger.LogInformation($"signalR client closed due to error => {exception.Message}");
            }

            return Task.CompletedTask;
        }

        /// <summary>
        /// Add motion detection notification to repository
        /// </summary>
        /// <param name="message">Motion detection received via signalR</param>
        private void ReceiveMessage(MotionDetection message)
        {
            try
            {
                Logger.LogInformation("Motion detection message received");

                Repository.AddItem(message);

                StateHasChanged();
            }
            catch (Exception ex)
            {
                Logger.LogError(ex, "An exception was encountered => " + ex.ToString());
            }
        }
        #endregion
    }
}

signalR 服务器端 Hub

using System;
using System.Threading.Tasks;

using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging;


namespace WebApp.Realtime.SignalR
{
    /// <summary>
    /// This represents endpoints available on the server, available for the
    /// clients to call
    /// </summary>
    public class MotionHub : Hub<IMotion>
    {
        private bool _disposed = false;
        public ILogger<MotionHub> Logger { get; set; }

        public MotionHub(ILogger<MotionHub> logger) : base()
        {
            Logger = logger;
        }


        public override async Task OnConnectedAsync()
        {
            Logger.LogInformation($"OnConnectedAsync => Connection ID={Context.ConnectionId} : User={Context.User.Identity.Name}");
            await base.OnConnectedAsync();
        }

        public override async Task OnDisconnectedAsync(Exception exception)
        {
            if (exception != null)
            {
                Logger.LogInformation($"OnDisconnectedAsync => Connection ID={Context.ConnectionId} : User={Context.User.Identity.Name} : Exception={exception.Message}");
            }
            else
            {
                Logger.LogInformation($"OnDisconnectedAsync => Connection ID={Context.ConnectionId} : User={Context.User.Identity.Name}");
            }

            await base.OnDisconnectedAsync(exception);
        }

        // Protected implementation of Dispose pattern.
        protected override void Dispose(bool disposing)
        {
            if (_disposed)
            {
                return;
            }

            _disposed = true;

            // Call base class implementation.
            base.Dispose(disposing);
        }
    }
}

1
尝试使用try-catch-finally语法来关闭/释放连接。像这样:try{ await hubConnection.StopAsync(); } catch (Exception exception) { Logger.LogInformation(exception.Message);} finally { await hubConnection.DisposeAsync();} - Zhi Lv
1
我猜可能是因为CloseAsync是在Dispose内部调用的,当到达时状态已经设置为disposed,所以它可能会抛出异常,请查看Blazor wasm SignalR does not close the connectionBest practice to dispose SignalR HubConnection in .NET Core - Zhi Lv
感谢 @zhi-liv 的好建议。 我尝试在 hubConnection.StopAsync() 周围放置异常处理块。 没有抛出异常。 看起来它在 StopAsync 上被阻塞。 在Github Discussions上 提问 后,在 Dispose 方法内将 DisposeAsync().GetAwaiter().GetResult(); 更改为 _ = DisposeAsync() ,它可以工作了。 不确定为什么? 等待在GitHub上的跟进答案。 - anon_dcs3spp
1个回答

6

ASP.NET Core Github Discussions的帮助下解决了问题。

Dispose方法中,将DisposeAsync().GetAwaiter().GetResult();替换为_ = DisposeAsync();。这样调用DiposeAsync()而不需要等待任务结果。

还更新了停止hub连接的代码:

  try { await hubConnection.StopAsync(); }
  finally
  {
    await hubConnection.DisposeAsync();
  }

DisposeAsync 中,对于 HubConnectionStopAsync 调用不再阻塞,并且连接会正常关闭。

1
很高兴听到它帮助解决了问题。我建议您在可以标记时尝试将自己的答案标记为已接受的答案。这可以帮助未来遇到类似问题的其他社区成员。感谢您的理解。 - Zhi Lv

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接