fix(tcp): 会话断开时不再访问已释放的 socket,确保 Disconnected 事件送达
- 服务端/客户端接收循环收尾改用连接时记录的远端地址 - 客户端本地 Stop 导致的读取中断按正常关闭处理 - 观察 Stop 后挂起的 accept 任务异常 - 版本 0.4.4
This commit is contained in:
1 parent
e36917db31
commit
401cecfc14
5 files changed
+62
-14
No files matched your search
@@ -121,9 +121,9 @@ namespace ShrinkNetwork
|
||||
return;
|
||||
}
|
||||
|
||||
var remote = _client.Client.RemoteEndPoint?.ToString() ?? $"{_host}:{_port}";
|
||||
var remote = ShrinkTcpServerTransport.TryGetRemoteAddress(_client) ?? $"{_host}:{_port}";
|
||||
OnEvent?.Invoke(ShrinkNetworkTransportEvent.Connected(_sessionId, remote));
|
||||
ReceiveLoopAsync(_client, stream, cancellationToken).Forget();
|
||||
ReceiveLoopAsync(stream, remote, cancellationToken).Forget();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -153,7 +153,7 @@ namespace ShrinkNetwork
|
||||
return sslStream;
|
||||
}
|
||||
|
||||
private async UniTaskVoid ReceiveLoopAsync(TcpClient client, Stream stream, CancellationToken cancellationToken)
|
||||
private async UniTaskVoid ReceiveLoopAsync(Stream stream, string remote, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -176,6 +176,14 @@ namespace ShrinkNetwork
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
catch (Exception) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
// Stop() cancelled the token and disposed the stream; the pending read fails with
|
||||
// ObjectDisposedException or IOException. This is a local, intentional shutdown.
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
}
|
||||
catch (IOException ex) when (IsExpectedDisconnect(ex))
|
||||
{
|
||||
ShrinkNetworkLogger.Warn($"[ShrinkNetwork] TCP receive loop ended: {ex.Message}");
|
||||
@@ -191,7 +199,8 @@ namespace ShrinkNetwork
|
||||
}
|
||||
finally
|
||||
{
|
||||
var remote = client.Client.RemoteEndPoint?.ToString() ?? $"{_host}:{_port}";
|
||||
// Do not read the socket here: Stop() may already have disposed it, and
|
||||
// Socket.RemoteEndPoint would throw ObjectDisposedException out of this async void.
|
||||
OnEvent?.Invoke(ShrinkNetworkTransportEvent.Disconnected(_sessionId, remote));
|
||||
Stop();
|
||||
}
|
||||
@@ -202,7 +211,8 @@ namespace ShrinkNetwork
|
||||
if (string.Equals(ex.Message, "Remote closed.", StringComparison.Ordinal))
|
||||
return true;
|
||||
|
||||
return ex.InnerException is SocketException socketException && IsExpectedDisconnect(socketException);
|
||||
return ex.InnerException is ObjectDisposedException ||
|
||||
ex.InnerException is SocketException socketException && IsExpectedDisconnect(socketException);
|
||||
}
|
||||
|
||||
private static bool IsExpectedDisconnect(SocketException ex)
|
||||
|
||||
@@ -150,7 +150,12 @@ namespace ShrinkNetwork
|
||||
var acceptTask = _listener.AcceptTcpClientAsync();
|
||||
var completedTask = await Task.WhenAny(acceptTask, Task.Delay(Timeout.Infinite, cancellationToken));
|
||||
if (completedTask != acceptTask)
|
||||
{
|
||||
// Listener.Stop() faults the pending accept; observe it so it is not reported as unobserved.
|
||||
_ = acceptTask.ContinueWith(static task => _ = task.Exception,
|
||||
TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously);
|
||||
break;
|
||||
}
|
||||
|
||||
client = await acceptTask;
|
||||
}
|
||||
@@ -175,9 +180,9 @@ namespace ShrinkNetwork
|
||||
_clients[sessionId] = client;
|
||||
_streams[sessionId] = stream;
|
||||
_sendLocks[sessionId] = new SemaphoreSlim(1, 1);
|
||||
var remoteAddress = client.Client.RemoteEndPoint?.ToString() ?? "unknown";
|
||||
var remoteAddress = TryGetRemoteAddress(client) ?? "unknown";
|
||||
OnEvent?.Invoke(ShrinkNetworkTransportEvent.Connected(sessionId, remoteAddress));
|
||||
_ = ReceiveLoopAsync(sessionId, client, stream, cancellationToken);
|
||||
_ = ReceiveLoopAsync(sessionId, client, stream, remoteAddress, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,7 +217,8 @@ namespace ShrinkNetwork
|
||||
_serverCertificate = new X509Certificate2(tlsOptions.ServerCertificatePath, tlsOptions.ServerCertificatePassword);
|
||||
}
|
||||
|
||||
private async Task ReceiveLoopAsync(long sessionId, TcpClient client, Stream stream, CancellationToken cancellationToken)
|
||||
private async Task ReceiveLoopAsync(long sessionId, TcpClient client, Stream stream, string remoteAddress,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -242,11 +248,19 @@ namespace ShrinkNetwork
|
||||
{
|
||||
if (_clients.TryRemove(sessionId, out var removed))
|
||||
{
|
||||
if (_streams.TryRemove(sessionId, out var ownedStream))
|
||||
ownedStream.Dispose();
|
||||
_sendLocks.TryRemove(sessionId, out _);
|
||||
// The stream owns the socket: once it is disposed, Socket.RemoteEndPoint throws
|
||||
// ObjectDisposedException. The address captured at accept time is used instead so the
|
||||
// Disconnected event is always raised.
|
||||
try
|
||||
{
|
||||
if (_streams.TryRemove(sessionId, out var ownedStream))
|
||||
ownedStream.Dispose();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
var remoteAddress = removed.Client.RemoteEndPoint?.ToString() ?? "unknown";
|
||||
_sendLocks.TryRemove(sessionId, out _);
|
||||
try
|
||||
{
|
||||
removed.Close();
|
||||
@@ -260,6 +274,22 @@ namespace ShrinkNetwork
|
||||
}
|
||||
}
|
||||
|
||||
internal static string? TryGetRemoteAddress(TcpClient client)
|
||||
{
|
||||
try
|
||||
{
|
||||
return client.Client?.RemoteEndPoint?.ToString();
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
catch (SocketException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task SendInternalAsync(Stream stream, SemaphoreSlim sendLock, ReadOnlyMemory<byte> packetData,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user