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",