关闭/断开 ASP.NET Core signalR 客户端连接的正确方法是什么?
What Is The Correct Way of Closing / Disconnecting An ASP.NET Core signalR Client Connection?
我是一名新用户,正在努力从 ASP.NET 核心 Blazor 服务器页面优雅地关闭辅助 signalR 客户端。
我在第一次呈现 Blazor 服务器页面时设置辅助 signalR 客户端连接。当通过浏览器选项卡关闭页面时,我试图关闭此辅助 signalR 客户端连接。
在撰写本文时,DisposeAsync
似乎不会在通过浏览器选项卡关闭页面时触发。但是,Dispose
方法IS被触发了。此外,在 Safari 13.0.5 中,关闭浏览器选项卡时不会触发 Dispose
方法? Opera、Firefox 和 Chrome 都在关闭浏览器选项卡时触发 Dispose
。通过 macOS Catalina v10.15.7.
将 Safari 更新到 v14.0(15610.1.28.9、15610)解决了这个问题
目前,我正在从 Dispose
调用 DisposeAsync
以关闭 signalR 连接。我正在使用以下代码关闭客户端连接:
...
Logger.LogInformation("Closing secondary signalR connection...");
await hubConnection.StopAsync();
Logger.LogInformation("Closed secondary signalR connection");
...
StopAsync
方法出现阻塞,即没有消息输出 “已关闭的辅助信号 R 连接”。虽然,我的服务器集线器的 OnDisconnectedAsync
处理程序显示连接已断开。这类似于 behaviour described in this issue.
如何在 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 服务器集线器
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);
}
}
}
已在 ASP.NET Core Github Discussions 的帮助下修复。
在 Dispose
方法中替换了 DisposeAsync().GetAwaiter().GetResult(); to _ = DisposeAsync();
这会调用 DiposeAsync()
而不等待任务结果。
还更新了停止集线器连接的代码:
try { await hubConnection.StopAsync(); }
finally
{
await hubConnection.DisposeAsync();
}
在 DisposeAsync
内,对 HubConnection
的 StopAsync
调用不再阻塞,连接正常关闭。
我是一名新用户,正在努力从 ASP.NET 核心 Blazor 服务器页面优雅地关闭辅助 signalR 客户端。
我在第一次呈现 Blazor 服务器页面时设置辅助 signalR 客户端连接。当通过浏览器选项卡关闭页面时,我试图关闭此辅助 signalR 客户端连接。
在撰写本文时,DisposeAsync
似乎不会在通过浏览器选项卡关闭页面时触发。但是,Dispose
方法IS被触发了。此外,在 Safari 13.0.5 中,关闭浏览器选项卡时不会触发 Dispose
方法? Opera、Firefox 和 Chrome 都在关闭浏览器选项卡时触发 Dispose
。通过 macOS Catalina v10.15.7.
目前,我正在从 Dispose
调用 DisposeAsync
以关闭 signalR 连接。我正在使用以下代码关闭客户端连接:
...
Logger.LogInformation("Closing secondary signalR connection...");
await hubConnection.StopAsync();
Logger.LogInformation("Closed secondary signalR connection");
...
StopAsync
方法出现阻塞,即没有消息输出 “已关闭的辅助信号 R 连接”。虽然,我的服务器集线器的 OnDisconnectedAsync
处理程序显示连接已断开。这类似于 behaviour described in this issue.
如何在 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 服务器集线器
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);
}
}
}
已在 ASP.NET Core Github Discussions 的帮助下修复。
在 Dispose
方法中替换了 DisposeAsync().GetAwaiter().GetResult(); to _ = DisposeAsync();
这会调用 DiposeAsync()
而不等待任务结果。
还更新了停止集线器连接的代码:
try { await hubConnection.StopAsync(); }
finally
{
await hubConnection.DisposeAsync();
}
在 DisposeAsync
内,对 HubConnection
的 StopAsync
调用不再阻塞,连接正常关闭。