3 Commits
Author SHA1 Message Date
cneicy 401cecfc14 fix(tcp): 会话断开时不再访问已释放的 socket,确保 Disconnected 事件送达
Publish UPM package / publish (push) Successful in 3s
Publish NuGet packages / publish (push) Successful in 1h19m42s
- 服务端/客户端接收循环收尾改用连接时记录的远端地址
- 客户端本地 Stop 导致的读取中断按正常关闭处理
- 观察 Stop 后挂起的 accept 任务异常
- 版本 0.4.4
2026-11-10 18:36:30 +08:00
cneicy e36917db31 fix: download release SDK directly from Microsoft Blob 2026-11-06 19:26:45 +08:00
cneicy 47c978544c feat: update Godot integration and coordinated package dependencies
Publish UPM package / publish (push) Successful in 2s
Publish NuGet packages / publish (push) Failing after 54s
2026-11-06 19:13:21 +08:00
6 changed files with 69 additions and 15 deletions

No files matched your search

+8 -2
View File
@@ -5,6 +5,11 @@ on:
tags: tags:
- 'v*' - 'v*'
workflow_dispatch: workflow_dispatch:
inputs:
release_tag:
description: Existing version tag to publish
required: false
type: string
jobs: jobs:
publish: publish:
@@ -21,7 +26,7 @@ jobs:
steps: steps:
- name: Fetch exact tagged source - name: Fetch exact tagged source
env: env:
GITEA_REF: ${{ gitea.ref }} GITEA_REF: ${{ inputs.release_tag || gitea.ref }}
GITEA_REPOSITORY: ${{ gitea.repository }} GITEA_REPOSITORY: ${{ gitea.repository }}
shell: bash shell: bash
run: | run: |
@@ -63,7 +68,8 @@ jobs:
const release = metadata.releases.find(item => item.sdk?.version === sdkVersion); const release = metadata.releases.find(item => item.sdk?.version === sdkVersion);
const file = release?.sdk?.files?.find(item => item.rid === 'linux-x64' && item.name.endsWith('.tar.gz')); const file = release?.sdk?.files?.find(item => item.rid === 'linux-x64' && item.name.endsWith('.tar.gz'));
if (!file) throw new Error(`Linux x64 SDK archive not found for ${sdkVersion}`); if (!file) throw new Error(`Linux x64 SDK archive not found for ${sdkVersion}`);
const archiveResponse = await fetch(file.url); const archiveUrl = `https://dotnetcli.blob.core.windows.net/dotnet/Sdk/${sdkVersion}/dotnet-sdk-${sdkVersion}-linux-x64.tar.gz`;
const archiveResponse = await fetch(archiveUrl);
if (!archiveResponse.ok) throw new Error(`SDK download failed: ${archiveResponse.status}`); if (!archiveResponse.ok) throw new Error(`SDK download failed: ${archiveResponse.status}`);
await writeFile('/tmp/dotnet-sdk.tar.gz', new Uint8Array(await archiveResponse.arrayBuffer())); await writeFile('/tmp/dotnet-sdk.tar.gz', new Uint8Array(await archiveResponse.arrayBuffer()));
+8
View File
@@ -2,6 +2,14 @@
本文件记录 `ShrinkNetwork` 在当前工作区中的包内变更。 本文件记录 `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 ## [0.2.0] - 2026-05-18
### Changed ### Changed
+2 -2
View File
@@ -5,7 +5,7 @@
<AssemblyName>ShrinkNetwork.Runtime</AssemblyName> <AssemblyName>ShrinkNetwork.Runtime</AssemblyName>
<RootNamespace>ShrinkNetwork</RootNamespace> <RootNamespace>ShrinkNetwork</RootNamespace>
<PackageId>ShrinkSDK.Network</PackageId> <PackageId>ShrinkSDK.Network</PackageId>
<Version>0.4.2</Version> <Version>0.4.4</Version>
<Description>ShrinkSDK messaging, RPC, permission and transport runtime.</Description> <Description>ShrinkSDK messaging, RPC, permission and transport runtime.</Description>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<ShrinkCodeGenEnabled>false</ShrinkCodeGenEnabled> <ShrinkCodeGenEnabled>false</ShrinkCodeGenEnabled>
@@ -15,7 +15,7 @@
<PackageReference Include="Kcp-CSharp" Version="1.0.8" /> <PackageReference Include="Kcp-CSharp" Version="1.0.8" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="UniTask" Version="2.5.10" /> <PackageReference Include="UniTask" Version="2.5.10" />
<PackageReference Include="ShrinkSDK.Runtime.Abstractions" Version="0.1.0" /> <PackageReference Include="ShrinkSDK.Runtime.Abstractions" Version="0.2.0" />
<PackageReference Include="ShrinkSDK.CodeGen" Version="0.2.0" PrivateAssets="compile;runtime;contentfiles;native" /> <PackageReference Include="ShrinkSDK.CodeGen" Version="0.2.0" PrivateAssets="compile;runtime;contentfiles;native" />
</ItemGroup> </ItemGroup>
</Project> </Project>
@@ -121,9 +121,9 @@ namespace ShrinkNetwork
return; return;
} }
var remote = _client.Client.RemoteEndPoint?.ToString() ?? $"{_host}:{_port}"; var remote = ShrinkTcpServerTransport.TryGetRemoteAddress(_client) ?? $"{_host}:{_port}";
OnEvent?.Invoke(ShrinkNetworkTransportEvent.Connected(_sessionId, remote)); OnEvent?.Invoke(ShrinkNetworkTransportEvent.Connected(_sessionId, remote));
ReceiveLoopAsync(_client, stream, cancellationToken).Forget(); ReceiveLoopAsync(stream, remote, cancellationToken).Forget();
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -153,7 +153,7 @@ namespace ShrinkNetwork
return sslStream; return sslStream;
} }
private async UniTaskVoid ReceiveLoopAsync(TcpClient client, Stream stream, CancellationToken cancellationToken) private async UniTaskVoid ReceiveLoopAsync(Stream stream, string remote, CancellationToken cancellationToken)
{ {
try try
{ {
@@ -176,6 +176,14 @@ namespace ShrinkNetwork
catch (OperationCanceledException) 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)) catch (IOException ex) when (IsExpectedDisconnect(ex))
{ {
ShrinkNetworkLogger.Warn($"[ShrinkNetwork] TCP receive loop ended: {ex.Message}"); ShrinkNetworkLogger.Warn($"[ShrinkNetwork] TCP receive loop ended: {ex.Message}");
@@ -191,7 +199,8 @@ namespace ShrinkNetwork
} }
finally 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)); OnEvent?.Invoke(ShrinkNetworkTransportEvent.Disconnected(_sessionId, remote));
Stop(); Stop();
} }
@@ -202,7 +211,8 @@ namespace ShrinkNetwork
if (string.Equals(ex.Message, "Remote closed.", StringComparison.Ordinal)) if (string.Equals(ex.Message, "Remote closed.", StringComparison.Ordinal))
return true; 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) private static bool IsExpectedDisconnect(SocketException ex)
@@ -150,7 +150,12 @@ namespace ShrinkNetwork
var acceptTask = _listener.AcceptTcpClientAsync(); var acceptTask = _listener.AcceptTcpClientAsync();
var completedTask = await Task.WhenAny(acceptTask, Task.Delay(Timeout.Infinite, cancellationToken)); var completedTask = await Task.WhenAny(acceptTask, Task.Delay(Timeout.Infinite, cancellationToken));
if (completedTask != acceptTask) 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; break;
}
client = await acceptTask; client = await acceptTask;
} }
@@ -175,9 +180,9 @@ namespace ShrinkNetwork
_clients[sessionId] = client; _clients[sessionId] = client;
_streams[sessionId] = stream; _streams[sessionId] = stream;
_sendLocks[sessionId] = new SemaphoreSlim(1, 1); _sendLocks[sessionId] = new SemaphoreSlim(1, 1);
var remoteAddress = client.Client.RemoteEndPoint?.ToString() ?? "unknown"; var remoteAddress = TryGetRemoteAddress(client) ?? "unknown";
OnEvent?.Invoke(ShrinkNetworkTransportEvent.Connected(sessionId, remoteAddress)); 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); _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 try
{ {
@@ -241,12 +247,20 @@ namespace ShrinkNetwork
finally finally
{ {
if (_clients.TryRemove(sessionId, out var removed)) if (_clients.TryRemove(sessionId, out var removed))
{
// 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)) if (_streams.TryRemove(sessionId, out var ownedStream))
ownedStream.Dispose(); ownedStream.Dispose();
_sendLocks.TryRemove(sessionId, out _); }
catch
{
}
var remoteAddress = removed.Client.RemoteEndPoint?.ToString() ?? "unknown"; _sendLocks.TryRemove(sessionId, out _);
try try
{ {
removed.Close(); 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, private static async Task SendInternalAsync(Stream stream, SemaphoreSlim sendLock, ReadOnlyMemory<byte> packetData,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "com.cneicy.shrink-network", "name": "com.cneicy.shrink-network",
"version": "0.4.2", "version": "0.4.4",
"displayName": "ShrinkNetwork", "displayName": "ShrinkNetwork",
"description": "面向 Unity 的轻量网络框架,提供会话、消息注册、RPC、权限控制,以及 TCP/KCP 传输抽象。", "description": "面向 Unity 的轻量网络框架,提供会话、消息注册、RPC、权限控制,以及 TCP/KCP 传输抽象。",
"unity": "2022.3", "unity": "2022.3",