feat(network)!: add v2 framing, pooled buffers and bounded dispatch
This commit is contained in:
@@ -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
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user