diff --git a/CHANGELOG.md b/CHANGELOG.md index ce0e3ed..b02c353 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ 本文件记录 `ShrinkNetwork` 在当前工作区中的包内变更。 +## [0.4.4] - 2026-11-10 + +### Fixed + +- TCP 服务端:会话接收循环结束时不再读取已释放 socket 的 `RemoteEndPoint`,改用接入时记录的地址;此前客户端主动断开会抛出 `ObjectDisposedException`,`Disconnected` 事件丢失,上层仍向已断开会话发送并报 `Session N is not connected`。 +- TCP 客户端:接收循环收尾同样改用连接时记录的地址,本地 `Stop()` 导致的读取中断按正常关闭处理,不再向 Unity 日志抛出 `ObjectDisposedException`。 +- TCP 服务端:`Stop()` 后挂起的 accept 任务异常被观察,不再产生未观察的 `SocketException`。 + ## [0.2.0] - 2026-05-18 ### Changed diff --git a/DotNet~/ShrinkSDK.Network.csproj b/DotNet~/ShrinkSDK.Network.csproj index 6efa915..7525d14 100644 --- a/DotNet~/ShrinkSDK.Network.csproj +++ b/DotNet~/ShrinkSDK.Network.csproj @@ -5,7 +5,7 @@ ShrinkNetwork.Runtime ShrinkNetwork ShrinkSDK.Network - 0.4.3 + 0.4.4 ShrinkSDK messaging, RPC, permission and transport runtime. true false diff --git a/Runtime/Transport/Tcp/ShrinkTcpClientTransport.cs b/Runtime/Transport/Tcp/ShrinkTcpClientTransport.cs index 1eb1e34..fab2326 100644 --- a/Runtime/Transport/Tcp/ShrinkTcpClientTransport.cs +++ b/Runtime/Transport/Tcp/ShrinkTcpClientTransport.cs @@ -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) diff --git a/Runtime/Transport/Tcp/ShrinkTcpServerTransport.cs b/Runtime/Transport/Tcp/ShrinkTcpServerTransport.cs index 3b1bfa2..1ef523b 100644 --- a/Runtime/Transport/Tcp/ShrinkTcpServerTransport.cs +++ b/Runtime/Transport/Tcp/ShrinkTcpServerTransport.cs @@ -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 packetData, CancellationToken cancellationToken) { diff --git a/package.json b/package.json index 4b79278..b0d88c9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "com.cneicy.shrink-network", - "version": "0.4.3", + "version": "0.4.4", "displayName": "ShrinkNetwork", "description": "面向 Unity 的轻量网络框架,提供会话、消息注册、RPC、权限控制,以及 TCP/KCP 传输抽象。", "unity": "2022.3",