feat(network)!: add v2 framing, pooled buffers and bounded dispatch
Publish UPM package / publish (push) Successful in 2s
Publish NuGet packages / publish (push) Successful in 3m5s

This commit is contained in:
2026-09-29 10:14:47 +08:00
parent c7c45b26f8
commit 93c2b8520b
36 changed files with 752 additions and 398 deletions
@@ -1,4 +1,6 @@
using Cysharp.Threading.Tasks;
using System;
using System.Threading;
namespace ShrinkNetwork
{
@@ -6,4 +8,10 @@ namespace ShrinkNetwork
{
UniTask SendAsync(long sessionId, byte[] packetData);
}
/// <summary>Borrowed memory remains owned by caller until SendAsync completes, faults or cancels.</summary>
public interface IShrinkNetworkMemoryTransport : IShrinkNetworkAsyncTransport
{
UniTask SendAsync(long sessionId, ReadOnlyMemory<byte> packetData, CancellationToken cancellationToken = default);
}
}
@@ -163,9 +163,9 @@ namespace ShrinkNetwork
{
while (!cancellationToken.IsCancellationRequested)
{
var sessions = _sessions.Values.ToArray();
foreach (var session in sessions)
foreach (var pair in _sessions)
{
var session = pair.Value;
var shouldDisconnect = false;
lock (session.SyncRoot)
{
@@ -1,11 +1,12 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Threading;
using Cysharp.Threading.Tasks;
namespace ShrinkNetwork
{
public sealed class ShrinkLoopbackTransport : IShrinkNetworkAsyncTransport, IShrinkNetworkSessionControlTransport
public sealed class ShrinkLoopbackTransport : IShrinkNetworkMemoryTransport, IShrinkNetworkSessionControlTransport
{
private readonly HashSet<long> _openedSessions = new();
private ShrinkLoopbackTransport? _peer;
@@ -73,5 +74,12 @@ namespace ShrinkNetwork
_peer.OnEvent?.Invoke(ShrinkNetworkTransportEvent.Packet(sessionId, packetData));
return UniTask.CompletedTask;
}
public UniTask SendAsync(long sessionId, ReadOnlyMemory<byte> packetData, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
// Receivers may enqueue the event beyond this call; transfer a dedicated copy.
return SendAsync(sessionId, packetData.ToArray());
}
}
}
@@ -1,6 +1,7 @@
#nullable enable
using System;
using System.Buffers.Binary;
using System.Buffers;
using System.IO;
using System.Net.Sockets;
using System.Net.Security;
@@ -10,7 +11,7 @@ using Cysharp.Threading.Tasks;
namespace ShrinkNetwork
{
public sealed class ShrinkTcpClientTransport : IShrinkNetworkAsyncTransport
public sealed class ShrinkTcpClientTransport : IShrinkNetworkMemoryTransport
{
private readonly string _host;
private readonly int _port;
@@ -86,7 +87,9 @@ namespace ShrinkNetwork
SendAsync(sessionId, packetData).Forget();
}
public UniTask SendAsync(long sessionId, byte[] packetData)
public UniTask SendAsync(long sessionId, byte[] packetData) => SendAsync(sessionId, (ReadOnlyMemory<byte>)(packetData ?? Array.Empty<byte>()));
public UniTask SendAsync(long sessionId, ReadOnlyMemory<byte> packetData, CancellationToken cancellationToken = default)
{
if (!IsStarted)
throw new InvalidOperationException("Transport is not started.");
@@ -94,11 +97,11 @@ namespace ShrinkNetwork
throw new InvalidOperationException($"Unsupported session id {sessionId}. This transport only supports {_sessionId}.");
if (_client == null || !_client.Connected || _stream == null)
throw new InvalidOperationException("TCP client is not connected.");
if ((packetData?.Length ?? 0) > _maxPacketSize)
throw new InvalidOperationException($"TCP packet is too large. Size={(packetData?.Length ?? 0)}, Limit={_maxPacketSize}.");
if (packetData.Length > _maxPacketSize)
throw new InvalidOperationException($"TCP packet is too large. Size={packetData.Length}, Limit={_maxPacketSize}.");
return SendInternalAsync(_stream, _sendLock, packetData ?? Array.Empty<byte>(),
_cts?.Token ?? CancellationToken.None);
return SendInternalAsync(_stream, _sendLock, packetData,
cancellationToken.CanBeCanceled ? cancellationToken : _cts?.Token ?? CancellationToken.None);
}
private async UniTaskVoid ConnectAsync(CancellationToken cancellationToken)
@@ -209,18 +212,20 @@ namespace ShrinkNetwork
ex.SocketErrorCode == SocketError.Shutdown;
}
private static async UniTask SendInternalAsync(Stream stream, SemaphoreSlim sendLock, byte[] packetData,
private static async UniTask SendInternalAsync(Stream stream, SemaphoreSlim sendLock, ReadOnlyMemory<byte> packetData,
CancellationToken cancellationToken)
{
await sendLock.WaitAsync(cancellationToken);
try
{
var header = new byte[4];
BinaryPrimitives.WriteInt32LittleEndian(header, packetData.Length);
await stream.WriteAsync(header, cancellationToken);
if (packetData.Length > 0)
await stream.WriteAsync(packetData, cancellationToken);
await stream.FlushAsync(cancellationToken);
var frame = ArrayPool<byte>.Shared.Rent(checked(4 + packetData.Length));
try
{
BinaryPrimitives.WriteInt32LittleEndian(frame, packetData.Length);
packetData.CopyTo(frame.AsMemory(4));
await stream.WriteAsync(frame.AsMemory(0, 4 + packetData.Length), cancellationToken);
}
finally { ArrayPool<byte>.Shared.Return(frame, clearArray: true); }
}
finally
{
@@ -1,6 +1,7 @@
#nullable enable
using System;
using System.Buffers.Binary;
using System.Buffers;
using System.Collections.Concurrent;
using System.IO;
using System.Net;
@@ -13,7 +14,7 @@ using Cysharp.Threading.Tasks;
namespace ShrinkNetwork
{
public sealed class ShrinkTcpServerTransport : IShrinkNetworkAsyncTransport, IShrinkNetworkSessionControlTransport
public sealed class ShrinkTcpServerTransport : IShrinkNetworkMemoryTransport, IShrinkNetworkSessionControlTransport
{
private readonly ConcurrentDictionary<long, TcpClient> _clients = new();
private readonly ConcurrentDictionary<long, SemaphoreSlim> _sendLocks = new();
@@ -92,11 +93,8 @@ namespace ShrinkNetwork
_streams.Clear();
foreach (var pair in _sendLocks)
{
pair.Value.Dispose();
}
// In-flight writers still release these managed semaphores after the stream closes.
// No WaitHandle is allocated; let their final users release and then collect them.
_sendLocks.Clear();
}
@@ -125,7 +123,9 @@ namespace ShrinkNetwork
return true;
}
public async UniTask SendAsync(long sessionId, byte[] packetData)
public UniTask SendAsync(long sessionId, byte[] packetData) => SendAsync(sessionId, (ReadOnlyMemory<byte>)(packetData ?? Array.Empty<byte>()));
public async UniTask SendAsync(long sessionId, ReadOnlyMemory<byte> packetData, CancellationToken cancellationToken = default)
{
if (!_clients.TryGetValue(sessionId, out var client))
throw new InvalidOperationException($"Session {sessionId} is not connected.");
@@ -134,10 +134,10 @@ namespace ShrinkNetwork
if (!_sendLocks.TryGetValue(sessionId, out var sendLock))
throw new InvalidOperationException($"Session {sessionId} send lock is not initialized.");
if ((packetData?.Length ?? 0) > _maxPacketSize)
throw new InvalidOperationException($"TCP packet is too large. Size={(packetData?.Length ?? 0)}, Limit={_maxPacketSize}.");
if (packetData.Length > _maxPacketSize)
throw new InvalidOperationException($"TCP packet is too large. Size={packetData.Length}, Limit={_maxPacketSize}.");
await SendInternalAsync(stream, sendLock, packetData ?? Array.Empty<byte>(), CancellationToken.None);
await SendInternalAsync(stream, sendLock, packetData, cancellationToken);
}
private async Task AcceptLoopAsync(CancellationToken cancellationToken)
@@ -244,8 +244,7 @@ namespace ShrinkNetwork
{
if (_streams.TryRemove(sessionId, out var ownedStream))
ownedStream.Dispose();
if (_sendLocks.TryRemove(sessionId, out var sendLock))
sendLock.Dispose();
_sendLocks.TryRemove(sessionId, out _);
var remoteAddress = removed.Client.RemoteEndPoint?.ToString() ?? "unknown";
try
@@ -261,18 +260,20 @@ namespace ShrinkNetwork
}
}
private static async Task SendInternalAsync(Stream stream, SemaphoreSlim sendLock, byte[] packetData,
private static async Task SendInternalAsync(Stream stream, SemaphoreSlim sendLock, ReadOnlyMemory<byte> packetData,
CancellationToken cancellationToken)
{
await sendLock.WaitAsync(cancellationToken);
try
{
var header = new byte[4];
BinaryPrimitives.WriteInt32LittleEndian(header, packetData.Length);
await stream.WriteAsync(header, cancellationToken);
if (packetData.Length > 0)
await stream.WriteAsync(packetData, cancellationToken);
await stream.FlushAsync(cancellationToken);
var frame = ArrayPool<byte>.Shared.Rent(checked(4 + packetData.Length));
try
{
BinaryPrimitives.WriteInt32LittleEndian(frame, packetData.Length);
packetData.CopyTo(frame.AsMemory(4));
await stream.WriteAsync(frame.AsMemory(0, 4 + packetData.Length), cancellationToken);
}
finally { ArrayPool<byte>.Shared.Return(frame, clearArray: true); }
}
finally
{