feat(cordis): 接入上下文组合与模组事务热替换
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4eee09e255b4c294983b08e0d13eb8e1
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
using Cysharp.Threading.Tasks;
|
||||
|
||||
namespace ShrinkNetwork
|
||||
{
|
||||
public interface IShrinkNetworkAsyncTransport : IShrinkNetworkTransport
|
||||
{
|
||||
UniTask SendAsync(long sessionId, byte[] packetData);
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c9741e558eb992d4eae93726109250eb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Security.Authentication;
|
||||
|
||||
namespace ShrinkNetwork
|
||||
{
|
||||
public interface IShrinkNetworkTransport
|
||||
{
|
||||
bool IsStarted { get; }
|
||||
|
||||
event Action<ShrinkNetworkTransportEvent> OnEvent;
|
||||
|
||||
void Start();
|
||||
void Stop();
|
||||
void Send(long sessionId, byte[] packetData);
|
||||
}
|
||||
|
||||
public interface IShrinkNetworkSessionControlTransport
|
||||
{
|
||||
bool DisconnectSession(long sessionId, string? reason = null);
|
||||
}
|
||||
|
||||
public sealed class ShrinkTcpTlsOptions
|
||||
{
|
||||
public bool Enabled { get; set; }
|
||||
public string? TargetHost { get; set; }
|
||||
public bool AllowInvalidServerCertificate { get; set; }
|
||||
public string? ServerCertificatePath { get; set; }
|
||||
public string? ServerCertificatePassword { get; set; }
|
||||
public bool CheckCertificateRevocation { get; set; } = true;
|
||||
public SslProtocols EnabledProtocols { get; set; } = SslProtocols.None;
|
||||
|
||||
public ShrinkTcpTlsOptions Clone()
|
||||
{
|
||||
return new ShrinkTcpTlsOptions
|
||||
{
|
||||
Enabled = Enabled,
|
||||
TargetHost = TargetHost,
|
||||
AllowInvalidServerCertificate = AllowInvalidServerCertificate,
|
||||
ServerCertificatePath = ServerCertificatePath,
|
||||
ServerCertificatePassword = ServerCertificatePassword,
|
||||
CheckCertificateRevocation = CheckCertificateRevocation,
|
||||
EnabledProtocols = EnabledProtocols
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 427412ffd76529b418e1446e059516b5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 369ef8103f7eb084f9235125630ff1b6
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,278 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Cysharp.Threading.Tasks;
|
||||
|
||||
namespace ShrinkNetwork
|
||||
{
|
||||
public sealed class ShrinkKcpClientTransport : IShrinkNetworkAsyncTransport
|
||||
{
|
||||
private readonly string _host;
|
||||
private readonly int _port;
|
||||
private readonly long _sessionId;
|
||||
private readonly ShrinkKcpTransportOptions _options;
|
||||
private readonly object _syncRoot = new();
|
||||
|
||||
private CancellationTokenSource _cts;
|
||||
private UdpClient _udpClient;
|
||||
private ShrinkKcpPeer _peer;
|
||||
private long _handshakeNonce;
|
||||
private int _started;
|
||||
private int _connected;
|
||||
|
||||
public ShrinkKcpClientTransport(string host, int port, ShrinkKcpTransportOptions options = null, long sessionId = 1)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(host))
|
||||
throw new ArgumentException("Host cannot be empty.", nameof(host));
|
||||
if (port <= 0 || port > 65535)
|
||||
throw new ArgumentOutOfRangeException(nameof(port));
|
||||
|
||||
_host = host.Trim();
|
||||
_port = port;
|
||||
_sessionId = sessionId;
|
||||
_options = (options ?? new ShrinkKcpTransportOptions()).Clone();
|
||||
_options.Validate();
|
||||
}
|
||||
|
||||
public bool IsStarted => Volatile.Read(ref _started) == 1;
|
||||
|
||||
public event Action<ShrinkNetworkTransportEvent> OnEvent;
|
||||
|
||||
public void Start()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _started, 1) == 1)
|
||||
return;
|
||||
|
||||
_cts = new CancellationTokenSource();
|
||||
_udpClient = new UdpClient(AddressFamily.InterNetwork);
|
||||
_udpClient.Client.ReceiveBufferSize = _options.ReceiveBufferSize;
|
||||
_udpClient.Connect(_host, _port);
|
||||
|
||||
_handshakeNonce = CreateHandshakeNonce();
|
||||
ReceiveLoopAsync(_cts.Token).Forget();
|
||||
HandshakeLoopAsync(_cts.Token).Forget();
|
||||
UpdateLoopAsync(_cts.Token).Forget();
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
var remote = $"{_host}:{_port}";
|
||||
var wasConnected = Interlocked.Exchange(ref _connected, 0) == 1;
|
||||
|
||||
if (Interlocked.Exchange(ref _started, 0) == 0)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var peer = _peer;
|
||||
if (peer != null)
|
||||
SendDatagram(ShrinkKcpTransportProtocol.CreateDisconnect(peer.ConversationId));
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
_cts?.Cancel();
|
||||
|
||||
lock (_syncRoot)
|
||||
{
|
||||
_peer?.Dispose();
|
||||
_peer = null;
|
||||
|
||||
try
|
||||
{
|
||||
_udpClient?.Close();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
_udpClient = null;
|
||||
}
|
||||
|
||||
if (wasConnected)
|
||||
OnEvent?.Invoke(ShrinkNetworkTransportEvent.Disconnected(_sessionId, remote));
|
||||
}
|
||||
|
||||
public void Send(long sessionId, byte[] packetData)
|
||||
{
|
||||
SendAsync(sessionId, packetData).Forget();
|
||||
}
|
||||
|
||||
public UniTask SendAsync(long sessionId, byte[] packetData)
|
||||
{
|
||||
if (!IsStarted)
|
||||
throw new InvalidOperationException("Transport is not started.");
|
||||
if (sessionId != _sessionId)
|
||||
throw new InvalidOperationException($"Unsupported session id {sessionId}. This transport only supports {_sessionId}.");
|
||||
if (Volatile.Read(ref _connected) != 1)
|
||||
throw new InvalidOperationException("KCP client is not connected.");
|
||||
|
||||
lock (_syncRoot)
|
||||
{
|
||||
_peer?.Send(packetData ?? Array.Empty<byte>());
|
||||
}
|
||||
|
||||
return UniTask.CompletedTask;
|
||||
}
|
||||
|
||||
private async UniTaskVoid HandshakeLoopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var startedAt = DateTime.UtcNow;
|
||||
|
||||
try
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested && Volatile.Read(ref _connected) == 0)
|
||||
{
|
||||
if ((DateTime.UtcNow - startedAt).TotalMilliseconds > _options.ConnectTimeoutMs)
|
||||
throw new TimeoutException($"KCP connect timed out: {_host}:{_port}");
|
||||
|
||||
SendDatagram(ShrinkKcpTransportProtocol.CreateConnectRequest(_handshakeNonce, _options.ConversationId));
|
||||
await Task.Delay(_options.HandshakeRetryMs, cancellationToken);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShrinkNetworkLogger.Exception(ex);
|
||||
ShrinkNetworkLogger.Error($"[ShrinkNetwork] KCP connect failed: {_host}:{_port} {ex.Message}");
|
||||
Stop();
|
||||
}
|
||||
}
|
||||
|
||||
private async UniTaskVoid ReceiveLoopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
UdpReceiveResult result;
|
||||
try
|
||||
{
|
||||
result = await _udpClient.ReceiveAsync();
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (SocketException) when (!IsStarted)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
HandleDatagram(result.Buffer, result.RemoteEndPoint);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShrinkNetworkLogger.Exception(ex);
|
||||
ShrinkNetworkLogger.Warn($"[ShrinkNetwork] KCP receive loop ended: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
}
|
||||
|
||||
private async UniTaskVoid UpdateLoopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
var shouldStop = false;
|
||||
|
||||
lock (_syncRoot)
|
||||
{
|
||||
_peer?.Tick(packet => OnEvent?.Invoke(ShrinkNetworkTransportEvent.Packet(_sessionId, packet)));
|
||||
|
||||
if (_peer != null &&
|
||||
Volatile.Read(ref _connected) == 1 &&
|
||||
DateTime.UtcNow.Ticks - _peer.LastReceiveUtcTicks > TimeSpan.FromMilliseconds(_options.IdleTimeoutMs).Ticks)
|
||||
{
|
||||
ShrinkNetworkLogger.Warn($"[ShrinkNetwork] KCP idle timeout: {_host}:{_port}");
|
||||
shouldStop = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldStop)
|
||||
{
|
||||
Stop();
|
||||
return;
|
||||
}
|
||||
|
||||
await Task.Delay(_options.UpdateIntervalMs, cancellationToken);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleDatagram(byte[] datagram, IPEndPoint remoteEndPoint)
|
||||
{
|
||||
if (datagram == null || datagram.Length == 0)
|
||||
return;
|
||||
|
||||
if (ShrinkKcpTransportProtocol.TryReadConnectAccept(datagram, out var nonce, out var conversationId))
|
||||
{
|
||||
if (nonce != _handshakeNonce)
|
||||
return;
|
||||
|
||||
lock (_syncRoot)
|
||||
{
|
||||
if (_peer == null)
|
||||
{
|
||||
_peer = new ShrinkKcpPeer(conversationId, _options,
|
||||
payload => SendDatagram(ShrinkKcpTransportProtocol.CreateDataPacket(conversationId, payload)));
|
||||
}
|
||||
}
|
||||
|
||||
if (Interlocked.Exchange(ref _connected, 1) == 0)
|
||||
OnEvent?.Invoke(ShrinkNetworkTransportEvent.Connected(_sessionId, remoteEndPoint.ToString()));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (ShrinkKcpTransportProtocol.TryReadDisconnect(datagram, out var disconnectedConversationId))
|
||||
{
|
||||
if (_peer != null && _peer.ConversationId == disconnectedConversationId)
|
||||
Stop();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ShrinkKcpTransportProtocol.TryReadDataPacket(datagram, out var dataConversationId, out var payloadOffset,
|
||||
out var payloadLength))
|
||||
return;
|
||||
|
||||
lock (_syncRoot)
|
||||
{
|
||||
if (_peer == null || _peer.ConversationId != dataConversationId)
|
||||
return;
|
||||
|
||||
_peer.Input(datagram, payloadOffset, payloadLength,
|
||||
packet => OnEvent?.Invoke(ShrinkNetworkTransportEvent.Packet(_sessionId, packet)));
|
||||
}
|
||||
}
|
||||
|
||||
private void SendDatagram(byte[] datagram)
|
||||
{
|
||||
if (_udpClient == null)
|
||||
return;
|
||||
|
||||
_udpClient.Send(datagram, datagram.Length);
|
||||
}
|
||||
|
||||
private static long CreateHandshakeNonce()
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(long)];
|
||||
RandomNumberGenerator.Fill(buffer);
|
||||
return BitConverter.ToInt64(buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d3a2f267e77939e4d94652f195495f68
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,209 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using kcp;
|
||||
|
||||
namespace ShrinkNetwork
|
||||
{
|
||||
internal sealed unsafe class ShrinkKcpPeer : IDisposable
|
||||
{
|
||||
private readonly object _syncRoot = new();
|
||||
private readonly Action<byte[]> _sendDatagram;
|
||||
private readonly byte[] _receiveBuffer;
|
||||
private readonly GCHandle _selfHandle;
|
||||
private IKCPCB* _kcp;
|
||||
private bool _disposed;
|
||||
private uint _nextUpdateTime;
|
||||
|
||||
public ShrinkKcpPeer(uint conversationId, ShrinkKcpTransportOptions options, Action<byte[]> sendDatagram)
|
||||
{
|
||||
if (conversationId == 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(conversationId));
|
||||
if (options == null)
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
if (sendDatagram == null)
|
||||
throw new ArgumentNullException(nameof(sendDatagram));
|
||||
|
||||
options.Validate();
|
||||
|
||||
ConversationId = conversationId;
|
||||
_sendDatagram = sendDatagram;
|
||||
_receiveBuffer = new byte[options.MaxMessageSize];
|
||||
_selfHandle = GCHandle.Alloc(this);
|
||||
_kcp = KCP_INTERFACE.ikcp_create(conversationId, (void*)GCHandle.ToIntPtr(_selfHandle));
|
||||
if (_kcp == null)
|
||||
throw new InvalidOperationException("KCP create failed.");
|
||||
|
||||
KCP_INTERFACE.ikcp_setmtu(_kcp, options.Mtu);
|
||||
KCP_INTERFACE.ikcp_wndsize(_kcp, options.SendWindow, options.ReceiveWindow);
|
||||
KCP_INTERFACE.ikcp_nodelay(_kcp, options.NoDelay ? 1 : 0, options.Interval, options.Resend,
|
||||
options.DisableCongestionControl ? 1 : 0);
|
||||
KCP_INTERFACE.ikcp_setoutput(_kcp, &HandleOutput);
|
||||
|
||||
LastReceiveUtcTicks = DateTime.UtcNow.Ticks;
|
||||
_nextUpdateTime = GetNowMs();
|
||||
}
|
||||
|
||||
public uint ConversationId { get; }
|
||||
public long LastReceiveUtcTicks { get; private set; }
|
||||
|
||||
public void Input(byte[] datagram, int offset, int length, Action<byte[]> onPacket)
|
||||
{
|
||||
if (datagram == null)
|
||||
throw new ArgumentNullException(nameof(datagram));
|
||||
if (offset < 0 || offset > datagram.Length)
|
||||
throw new ArgumentOutOfRangeException(nameof(offset));
|
||||
if (length < 0 || offset + length > datagram.Length)
|
||||
throw new ArgumentOutOfRangeException(nameof(length));
|
||||
if (length == 0)
|
||||
return;
|
||||
if (onPacket == null)
|
||||
throw new ArgumentNullException(nameof(onPacket));
|
||||
|
||||
lock (_syncRoot)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
|
||||
fixed (byte* basePtr = datagram)
|
||||
{
|
||||
var result = KCP_INTERFACE.ikcp_input(_kcp, basePtr + offset, length);
|
||||
if (result < 0)
|
||||
throw new InvalidOperationException($"KCP input failed. Result={result}");
|
||||
}
|
||||
|
||||
LastReceiveUtcTicks = DateTime.UtcNow.Ticks;
|
||||
UpdateInternal(GetNowMs());
|
||||
DrainReceiveQueue(onPacket);
|
||||
}
|
||||
}
|
||||
|
||||
public void Send(byte[] payload)
|
||||
{
|
||||
if (payload == null)
|
||||
throw new ArgumentNullException(nameof(payload));
|
||||
if (payload.Length == 0)
|
||||
return;
|
||||
|
||||
lock (_syncRoot)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
|
||||
fixed (byte* payloadPtr = payload)
|
||||
{
|
||||
var result = KCP_INTERFACE.ikcp_send(_kcp, payloadPtr, payload.Length);
|
||||
if (result < 0)
|
||||
throw new InvalidOperationException($"KCP send failed. Result={result}");
|
||||
}
|
||||
|
||||
UpdateInternal(GetNowMs());
|
||||
}
|
||||
}
|
||||
|
||||
public void Tick(Action<byte[]> onPacket)
|
||||
{
|
||||
if (onPacket == null)
|
||||
throw new ArgumentNullException(nameof(onPacket));
|
||||
|
||||
lock (_syncRoot)
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
var now = GetNowMs();
|
||||
if (now < _nextUpdateTime)
|
||||
return;
|
||||
|
||||
UpdateInternal(now);
|
||||
DrainReceiveQueue(onPacket);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
_disposed = true;
|
||||
if (_kcp != null)
|
||||
{
|
||||
KCP_INTERFACE.ikcp_release(_kcp);
|
||||
_kcp = null;
|
||||
}
|
||||
|
||||
if (_selfHandle.IsAllocated)
|
||||
_selfHandle.Free();
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureNotDisposed()
|
||||
{
|
||||
if (_disposed || _kcp == null)
|
||||
throw new ObjectDisposedException(nameof(ShrinkKcpPeer));
|
||||
}
|
||||
|
||||
private void UpdateInternal(uint now)
|
||||
{
|
||||
KCP_INTERFACE.ikcp_update(_kcp, now);
|
||||
_nextUpdateTime = KCP_INTERFACE.ikcp_check(_kcp, now);
|
||||
}
|
||||
|
||||
private void DrainReceiveQueue(Action<byte[]> onPacket)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var peekSize = KCP_INTERFACE.ikcp_peeksize(_kcp);
|
||||
if (peekSize < 0)
|
||||
return;
|
||||
|
||||
if (peekSize > _receiveBuffer.Length)
|
||||
throw new InvalidOperationException(
|
||||
$"KCP message too large. Size={peekSize}, Buffer={_receiveBuffer.Length}");
|
||||
|
||||
var received = 0;
|
||||
fixed (byte* receivePtr = _receiveBuffer)
|
||||
{
|
||||
received = KCP_INTERFACE.ikcp_recv(_kcp, receivePtr, peekSize);
|
||||
}
|
||||
|
||||
if (received <= 0)
|
||||
return;
|
||||
|
||||
LastReceiveUtcTicks = DateTime.UtcNow.Ticks;
|
||||
var packet = new byte[received];
|
||||
Buffer.BlockCopy(_receiveBuffer, 0, packet, 0, received);
|
||||
onPacket(packet);
|
||||
}
|
||||
}
|
||||
|
||||
private static uint GetNowMs()
|
||||
{
|
||||
// KCP expects a wrapping uint32 millisecond clock; Environment.TickCount
|
||||
// matches that contract and still works on Unity's .NET Framework target.
|
||||
return unchecked((uint)Environment.TickCount);
|
||||
}
|
||||
|
||||
private int HandleOutputInternal(byte* buffer, int length)
|
||||
{
|
||||
if (length <= 0)
|
||||
return 0;
|
||||
|
||||
var datagram = new byte[length];
|
||||
Marshal.Copy((IntPtr)buffer, datagram, 0, length);
|
||||
_sendDatagram(datagram);
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static int HandleOutput(byte* buffer, int length, IKCPCB* kcp, void* user)
|
||||
{
|
||||
if (user == null)
|
||||
return -1;
|
||||
|
||||
var handle = GCHandle.FromIntPtr((IntPtr)user);
|
||||
if (handle.Target is not ShrinkKcpPeer peer)
|
||||
return -1;
|
||||
|
||||
return peer.HandleOutputInternal(buffer, length);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4b2e3e60a75261f46afb0b89e32d921c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,315 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Cysharp.Threading.Tasks;
|
||||
|
||||
namespace ShrinkNetwork
|
||||
{
|
||||
public sealed class ShrinkKcpServerTransport : IShrinkNetworkAsyncTransport, IShrinkNetworkSessionControlTransport
|
||||
{
|
||||
private sealed class SessionState : IDisposable
|
||||
{
|
||||
public long SessionId;
|
||||
public uint ConversationId;
|
||||
public IPEndPoint RemoteEndPoint = null!;
|
||||
public ShrinkKcpPeer Peer = null!;
|
||||
public readonly object SyncRoot = new();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Peer.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private readonly ConcurrentDictionary<long, SessionState> _sessions = new();
|
||||
private readonly ConcurrentDictionary<string, long> _endpointToSessionId = new();
|
||||
private readonly ConcurrentDictionary<uint, long> _conversationToSessionId = new();
|
||||
private readonly ShrinkKcpTransportOptions _options;
|
||||
private readonly IPAddress _listeningAddress;
|
||||
private readonly int _port;
|
||||
private CancellationTokenSource? _cts;
|
||||
private UdpClient? _udpClient;
|
||||
private long _sessionIdGenerator;
|
||||
private int _conversationIdGenerator;
|
||||
|
||||
public ShrinkKcpServerTransport(IPAddress listeningAddress, int port, ShrinkKcpTransportOptions? options = null)
|
||||
{
|
||||
if (port <= 0 || port > 65535)
|
||||
throw new ArgumentOutOfRangeException(nameof(port));
|
||||
|
||||
_listeningAddress = listeningAddress ?? throw new ArgumentNullException(nameof(listeningAddress));
|
||||
_port = port;
|
||||
_options = (options ?? new ShrinkKcpTransportOptions()).Clone();
|
||||
_options.Validate();
|
||||
}
|
||||
|
||||
public bool IsStarted { get; private set; }
|
||||
|
||||
public event Action<ShrinkNetworkTransportEvent>? OnEvent;
|
||||
|
||||
public void Start()
|
||||
{
|
||||
if (IsStarted)
|
||||
return;
|
||||
|
||||
_udpClient = new UdpClient(new IPEndPoint(_listeningAddress, _port));
|
||||
_udpClient.Client.ReceiveBufferSize = _options.ReceiveBufferSize;
|
||||
_cts = new CancellationTokenSource();
|
||||
IsStarted = true;
|
||||
ReceiveLoopAsync(_cts.Token).Forget();
|
||||
UpdateLoopAsync(_cts.Token).Forget();
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
if (!IsStarted)
|
||||
return;
|
||||
|
||||
IsStarted = false;
|
||||
_cts?.Cancel();
|
||||
|
||||
try
|
||||
{
|
||||
_udpClient?.Close();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
_udpClient = null;
|
||||
|
||||
foreach (var state in _sessions.Values.ToArray())
|
||||
RemoveSession(state.SessionId, true);
|
||||
|
||||
_endpointToSessionId.Clear();
|
||||
_conversationToSessionId.Clear();
|
||||
}
|
||||
|
||||
public void Send(long sessionId, byte[] packetData)
|
||||
{
|
||||
SendAsync(sessionId, packetData).Forget();
|
||||
}
|
||||
|
||||
public bool DisconnectSession(long sessionId, string? reason = null)
|
||||
{
|
||||
if (!_sessions.ContainsKey(sessionId))
|
||||
return false;
|
||||
|
||||
RemoveSession(sessionId, true);
|
||||
if (!string.IsNullOrWhiteSpace(reason))
|
||||
ShrinkNetworkLogger.Warn($"[ShrinkNetwork][KCP-Server] Disconnect session {sessionId}: {reason}");
|
||||
return true;
|
||||
}
|
||||
|
||||
public UniTask SendAsync(long sessionId, byte[] packetData)
|
||||
{
|
||||
if (!_sessions.TryGetValue(sessionId, out var session))
|
||||
throw new InvalidOperationException($"Session {sessionId} is not connected.");
|
||||
|
||||
lock (session.SyncRoot)
|
||||
{
|
||||
session.Peer.Send(packetData ?? Array.Empty<byte>());
|
||||
}
|
||||
|
||||
return UniTask.CompletedTask;
|
||||
}
|
||||
|
||||
private async UniTaskVoid ReceiveLoopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
UdpReceiveResult result;
|
||||
try
|
||||
{
|
||||
if (_udpClient == null)
|
||||
break;
|
||||
|
||||
result = await _udpClient.ReceiveAsync();
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (SocketException) when (!IsStarted)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
HandleDatagram(result.Buffer, result.RemoteEndPoint);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShrinkNetworkLogger.Exception(ex);
|
||||
ShrinkNetworkLogger.Warn($"[ShrinkNetwork] KCP server receive loop ended: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
}
|
||||
|
||||
private async UniTaskVoid UpdateLoopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
var sessions = _sessions.Values.ToArray();
|
||||
foreach (var session in sessions)
|
||||
{
|
||||
var shouldDisconnect = false;
|
||||
lock (session.SyncRoot)
|
||||
{
|
||||
session.Peer.Tick(packet => OnEvent?.Invoke(
|
||||
ShrinkNetworkTransportEvent.Packet(session.SessionId, packet)));
|
||||
|
||||
if (DateTime.UtcNow.Ticks - session.Peer.LastReceiveUtcTicks >
|
||||
TimeSpan.FromMilliseconds(_options.IdleTimeoutMs).Ticks)
|
||||
{
|
||||
shouldDisconnect = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldDisconnect)
|
||||
RemoveSession(session.SessionId, true);
|
||||
}
|
||||
|
||||
await Task.Delay(_options.UpdateIntervalMs, cancellationToken);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleDatagram(byte[] datagram, IPEndPoint remoteEndPoint)
|
||||
{
|
||||
if (datagram == null || datagram.Length == 0)
|
||||
return;
|
||||
|
||||
if (ShrinkKcpTransportProtocol.TryReadConnectRequest(datagram, out var nonce, out _))
|
||||
{
|
||||
HandleConnectRequest(remoteEndPoint, nonce);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ShrinkKcpTransportProtocol.TryReadDisconnect(datagram, out var disconnectedConversationId))
|
||||
{
|
||||
if (_conversationToSessionId.TryGetValue(disconnectedConversationId, out var disconnectedSessionId))
|
||||
{
|
||||
if (_sessions.TryGetValue(disconnectedSessionId, out var disconnectedSession) &&
|
||||
string.Equals(disconnectedSession.RemoteEndPoint.ToString(), remoteEndPoint.ToString(), StringComparison.Ordinal))
|
||||
{
|
||||
RemoveSession(disconnectedSessionId, false);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ShrinkKcpTransportProtocol.TryReadDataPacket(datagram, out var conversationId, out var payloadOffset,
|
||||
out var payloadLength))
|
||||
return;
|
||||
|
||||
if (!_conversationToSessionId.TryGetValue(conversationId, out var sessionId))
|
||||
return;
|
||||
|
||||
if (!_sessions.TryGetValue(sessionId, out var session))
|
||||
return;
|
||||
|
||||
if (!string.Equals(session.RemoteEndPoint.ToString(), remoteEndPoint.ToString(), StringComparison.Ordinal))
|
||||
return;
|
||||
|
||||
lock (session.SyncRoot)
|
||||
{
|
||||
session.Peer.Input(datagram, payloadOffset, payloadLength,
|
||||
packet => OnEvent?.Invoke(ShrinkNetworkTransportEvent.Packet(session.SessionId, packet)));
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleConnectRequest(IPEndPoint remoteEndPoint, long nonce)
|
||||
{
|
||||
var endpointKey = remoteEndPoint.ToString();
|
||||
if (_endpointToSessionId.TryGetValue(endpointKey, out var existingSessionId) &&
|
||||
_sessions.TryGetValue(existingSessionId, out var existingSession))
|
||||
{
|
||||
SendDatagram(remoteEndPoint,
|
||||
ShrinkKcpTransportProtocol.CreateConnectAccept(nonce, existingSession.ConversationId));
|
||||
return;
|
||||
}
|
||||
|
||||
var sessionId = Interlocked.Increment(ref _sessionIdGenerator);
|
||||
var conversationId = NextConversationId();
|
||||
var session = new SessionState
|
||||
{
|
||||
SessionId = sessionId,
|
||||
ConversationId = conversationId,
|
||||
RemoteEndPoint = remoteEndPoint,
|
||||
Peer = new ShrinkKcpPeer(conversationId, _options,
|
||||
payload => SendDatagram(remoteEndPoint,
|
||||
ShrinkKcpTransportProtocol.CreateDataPacket(conversationId, payload)))
|
||||
};
|
||||
|
||||
_sessions[sessionId] = session;
|
||||
_endpointToSessionId[endpointKey] = sessionId;
|
||||
_conversationToSessionId[conversationId] = sessionId;
|
||||
SendDatagram(remoteEndPoint, ShrinkKcpTransportProtocol.CreateConnectAccept(nonce, conversationId));
|
||||
OnEvent?.Invoke(ShrinkNetworkTransportEvent.Connected(sessionId, endpointKey));
|
||||
}
|
||||
|
||||
private void RemoveSession(long sessionId, bool notifyDisconnect)
|
||||
{
|
||||
if (!_sessions.TryRemove(sessionId, out var session))
|
||||
return;
|
||||
|
||||
_endpointToSessionId.TryRemove(session.RemoteEndPoint.ToString(), out _);
|
||||
_conversationToSessionId.TryRemove(session.ConversationId, out _);
|
||||
|
||||
if (notifyDisconnect)
|
||||
{
|
||||
try
|
||||
{
|
||||
SendDatagram(session.RemoteEndPoint,
|
||||
ShrinkKcpTransportProtocol.CreateDisconnect(session.ConversationId));
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
session.Dispose();
|
||||
OnEvent?.Invoke(ShrinkNetworkTransportEvent.Disconnected(sessionId, session.RemoteEndPoint.ToString()));
|
||||
}
|
||||
|
||||
private void SendDatagram(IPEndPoint remoteEndPoint, byte[] datagram)
|
||||
{
|
||||
if (_udpClient == null)
|
||||
return;
|
||||
|
||||
_udpClient.Send(datagram, datagram.Length, remoteEndPoint);
|
||||
}
|
||||
|
||||
private uint NextConversationId()
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
while (true)
|
||||
{
|
||||
RandomNumberGenerator.Fill(buffer);
|
||||
var next = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||
if (next == 0)
|
||||
continue;
|
||||
if (!_conversationToSessionId.ContainsKey(next))
|
||||
return next;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f98f0b2007fe1944bacbe69c189f034d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,69 @@
|
||||
using System;
|
||||
|
||||
namespace ShrinkNetwork
|
||||
{
|
||||
public sealed class ShrinkKcpTransportOptions
|
||||
{
|
||||
public uint ConversationId { get; set; }
|
||||
public int Mtu { get; set; } = 1200;
|
||||
public int SendWindow { get; set; } = 256;
|
||||
public int ReceiveWindow { get; set; } = 256;
|
||||
public bool NoDelay { get; set; } = true;
|
||||
public int Interval { get; set; } = 10;
|
||||
public int Resend { get; set; } = 2;
|
||||
public bool DisableCongestionControl { get; set; } = true;
|
||||
public int UpdateIntervalMs { get; set; } = 10;
|
||||
public int ConnectTimeoutMs { get; set; } = 5000;
|
||||
public int HandshakeRetryMs { get; set; } = 1000;
|
||||
public int IdleTimeoutMs { get; set; } = 15000;
|
||||
public int ReceiveBufferSize { get; set; } = 64 * 1024;
|
||||
public int MaxMessageSize { get; set; } = 64 * 1024;
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
if (Mtu < 576 || Mtu > 1400)
|
||||
throw new ArgumentOutOfRangeException(nameof(Mtu), "MTU must be between 576 and 1400.");
|
||||
if (SendWindow <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(SendWindow));
|
||||
if (ReceiveWindow <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(ReceiveWindow));
|
||||
if (Interval <= 0 || Interval > 5000)
|
||||
throw new ArgumentOutOfRangeException(nameof(Interval));
|
||||
if (Resend < 0 || Resend > 2)
|
||||
throw new ArgumentOutOfRangeException(nameof(Resend));
|
||||
if (UpdateIntervalMs <= 0 || UpdateIntervalMs > 5000)
|
||||
throw new ArgumentOutOfRangeException(nameof(UpdateIntervalMs));
|
||||
if (ConnectTimeoutMs <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(ConnectTimeoutMs));
|
||||
if (HandshakeRetryMs <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(HandshakeRetryMs));
|
||||
if (IdleTimeoutMs <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(IdleTimeoutMs));
|
||||
if (ReceiveBufferSize <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(ReceiveBufferSize));
|
||||
if (MaxMessageSize <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(MaxMessageSize));
|
||||
}
|
||||
|
||||
public ShrinkKcpTransportOptions Clone()
|
||||
{
|
||||
return new ShrinkKcpTransportOptions
|
||||
{
|
||||
ConversationId = ConversationId,
|
||||
Mtu = Mtu,
|
||||
SendWindow = SendWindow,
|
||||
ReceiveWindow = ReceiveWindow,
|
||||
NoDelay = NoDelay,
|
||||
Interval = Interval,
|
||||
Resend = Resend,
|
||||
DisableCongestionControl = DisableCongestionControl,
|
||||
UpdateIntervalMs = UpdateIntervalMs,
|
||||
ConnectTimeoutMs = ConnectTimeoutMs,
|
||||
HandshakeRetryMs = HandshakeRetryMs,
|
||||
IdleTimeoutMs = IdleTimeoutMs,
|
||||
ReceiveBufferSize = ReceiveBufferSize,
|
||||
MaxMessageSize = MaxMessageSize
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f95a6da969d7c904f94a5a2ebe17ba8e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,114 @@
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
|
||||
namespace ShrinkNetwork
|
||||
{
|
||||
internal enum ShrinkKcpEnvelopeKind : byte
|
||||
{
|
||||
ConnectRequest = 1,
|
||||
ConnectAccept = 2,
|
||||
Disconnect = 3,
|
||||
Data = 4
|
||||
}
|
||||
|
||||
internal static class ShrinkKcpTransportProtocol
|
||||
{
|
||||
private const int KindSize = 1;
|
||||
private const int ConversationSize = 4;
|
||||
private const int NonceSize = 8;
|
||||
|
||||
public static byte[] CreateConnectRequest(long nonce, uint requestedConversationId)
|
||||
{
|
||||
var buffer = new byte[KindSize + NonceSize + ConversationSize];
|
||||
buffer[0] = (byte)ShrinkKcpEnvelopeKind.ConnectRequest;
|
||||
BinaryPrimitives.WriteInt64LittleEndian(buffer.AsSpan(1, NonceSize), nonce);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(1 + NonceSize, ConversationSize), requestedConversationId);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
public static bool TryReadConnectRequest(byte[] datagram, out long nonce, out uint requestedConversationId)
|
||||
{
|
||||
nonce = default;
|
||||
requestedConversationId = default;
|
||||
if (datagram == null || datagram.Length != KindSize + NonceSize + ConversationSize)
|
||||
return false;
|
||||
if (datagram[0] != (byte)ShrinkKcpEnvelopeKind.ConnectRequest)
|
||||
return false;
|
||||
|
||||
nonce = BinaryPrimitives.ReadInt64LittleEndian(datagram.AsSpan(1, NonceSize));
|
||||
requestedConversationId = BinaryPrimitives.ReadUInt32LittleEndian(datagram.AsSpan(1 + NonceSize, ConversationSize));
|
||||
return true;
|
||||
}
|
||||
|
||||
public static byte[] CreateConnectAccept(long nonce, uint conversationId)
|
||||
{
|
||||
var buffer = new byte[KindSize + NonceSize + ConversationSize];
|
||||
buffer[0] = (byte)ShrinkKcpEnvelopeKind.ConnectAccept;
|
||||
BinaryPrimitives.WriteInt64LittleEndian(buffer.AsSpan(1, NonceSize), nonce);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(1 + NonceSize, ConversationSize), conversationId);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
public static bool TryReadConnectAccept(byte[] datagram, out long nonce, out uint conversationId)
|
||||
{
|
||||
nonce = default;
|
||||
conversationId = default;
|
||||
if (datagram == null || datagram.Length != KindSize + NonceSize + ConversationSize)
|
||||
return false;
|
||||
if (datagram[0] != (byte)ShrinkKcpEnvelopeKind.ConnectAccept)
|
||||
return false;
|
||||
|
||||
nonce = BinaryPrimitives.ReadInt64LittleEndian(datagram.AsSpan(1, NonceSize));
|
||||
conversationId = BinaryPrimitives.ReadUInt32LittleEndian(datagram.AsSpan(1 + NonceSize, ConversationSize));
|
||||
return true;
|
||||
}
|
||||
|
||||
public static byte[] CreateDisconnect(uint conversationId)
|
||||
{
|
||||
var buffer = new byte[KindSize + ConversationSize];
|
||||
buffer[0] = (byte)ShrinkKcpEnvelopeKind.Disconnect;
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(1, ConversationSize), conversationId);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
public static bool TryReadDisconnect(byte[] datagram, out uint conversationId)
|
||||
{
|
||||
conversationId = default;
|
||||
if (datagram == null || datagram.Length != KindSize + ConversationSize)
|
||||
return false;
|
||||
if (datagram[0] != (byte)ShrinkKcpEnvelopeKind.Disconnect)
|
||||
return false;
|
||||
|
||||
conversationId = BinaryPrimitives.ReadUInt32LittleEndian(datagram.AsSpan(1, ConversationSize));
|
||||
return true;
|
||||
}
|
||||
|
||||
public static byte[] CreateDataPacket(uint conversationId, byte[] payload)
|
||||
{
|
||||
payload ??= Array.Empty<byte>();
|
||||
|
||||
var buffer = new byte[KindSize + ConversationSize + payload.Length];
|
||||
buffer[0] = (byte)ShrinkKcpEnvelopeKind.Data;
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(1, ConversationSize), conversationId);
|
||||
if (payload.Length > 0)
|
||||
Buffer.BlockCopy(payload, 0, buffer, KindSize + ConversationSize, payload.Length);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
public static bool TryReadDataPacket(byte[] datagram, out uint conversationId, out int payloadOffset, out int payloadLength)
|
||||
{
|
||||
conversationId = default;
|
||||
payloadOffset = default;
|
||||
payloadLength = default;
|
||||
if (datagram == null || datagram.Length < KindSize + ConversationSize)
|
||||
return false;
|
||||
if (datagram[0] != (byte)ShrinkKcpEnvelopeKind.Data)
|
||||
return false;
|
||||
|
||||
conversationId = BinaryPrimitives.ReadUInt32LittleEndian(datagram.AsSpan(1, ConversationSize));
|
||||
payloadOffset = KindSize + ConversationSize;
|
||||
payloadLength = datagram.Length - payloadOffset;
|
||||
return payloadLength >= 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fdba195453eed5943bd86e3a1b8358f7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 88da155e143f26c44b97c25bcddc003a
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,77 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Cysharp.Threading.Tasks;
|
||||
|
||||
namespace ShrinkNetwork
|
||||
{
|
||||
public sealed class ShrinkLoopbackTransport : IShrinkNetworkAsyncTransport, IShrinkNetworkSessionControlTransport
|
||||
{
|
||||
private readonly HashSet<long> _openedSessions = new();
|
||||
private ShrinkLoopbackTransport? _peer;
|
||||
|
||||
public bool IsStarted { get; private set; }
|
||||
|
||||
public event Action<ShrinkNetworkTransportEvent>? OnEvent;
|
||||
|
||||
public void LinkPeer(ShrinkLoopbackTransport peer)
|
||||
{
|
||||
_peer = peer;
|
||||
if (peer._peer != this)
|
||||
peer.LinkPeer(this);
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
IsStarted = true;
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
IsStarted = false;
|
||||
}
|
||||
|
||||
public void OpenSession(long sessionId, string remoteAddress = "loopback")
|
||||
{
|
||||
_openedSessions.Add(sessionId);
|
||||
_peer?._openedSessions.Add(sessionId);
|
||||
OnEvent?.Invoke(ShrinkNetworkTransportEvent.Connected(sessionId, remoteAddress));
|
||||
_peer?.OnEvent?.Invoke(ShrinkNetworkTransportEvent.Connected(sessionId, remoteAddress));
|
||||
}
|
||||
|
||||
public void CloseSession(long sessionId, string remoteAddress = "loopback")
|
||||
{
|
||||
_openedSessions.Remove(sessionId);
|
||||
_peer?._openedSessions.Remove(sessionId);
|
||||
OnEvent?.Invoke(ShrinkNetworkTransportEvent.Disconnected(sessionId, remoteAddress));
|
||||
_peer?.OnEvent?.Invoke(ShrinkNetworkTransportEvent.Disconnected(sessionId, remoteAddress));
|
||||
}
|
||||
|
||||
public bool DisconnectSession(long sessionId, string? reason = null)
|
||||
{
|
||||
if (!_openedSessions.Contains(sessionId))
|
||||
return false;
|
||||
|
||||
CloseSession(sessionId);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Send(long sessionId, byte[] packetData)
|
||||
{
|
||||
SendAsync(sessionId, packetData).Forget();
|
||||
}
|
||||
|
||||
public UniTask SendAsync(long sessionId, byte[] packetData)
|
||||
{
|
||||
if (!IsStarted)
|
||||
throw new InvalidOperationException("Transport has not been started.");
|
||||
if (_peer == null)
|
||||
throw new InvalidOperationException("Peer transport is not linked.");
|
||||
if (!_openedSessions.Contains(sessionId))
|
||||
throw new InvalidOperationException($"Session {sessionId} is not open.");
|
||||
|
||||
_peer.OnEvent?.Invoke(ShrinkNetworkTransportEvent.Packet(sessionId, packetData));
|
||||
return UniTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9311f3622af8377419ba8273a873ee28
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c231f4f8533ee0c4381cc5a35929621d
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,243 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.IO;
|
||||
using System.Net.Sockets;
|
||||
using System.Net.Security;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Threading;
|
||||
using Cysharp.Threading.Tasks;
|
||||
|
||||
namespace ShrinkNetwork
|
||||
{
|
||||
public sealed class ShrinkTcpClientTransport : IShrinkNetworkAsyncTransport
|
||||
{
|
||||
private readonly string _host;
|
||||
private readonly int _port;
|
||||
private readonly long _sessionId;
|
||||
private readonly int _maxPacketSize;
|
||||
private readonly ShrinkTcpTlsOptions? _tlsOptions;
|
||||
private readonly SemaphoreSlim _sendLock = new(1, 1);
|
||||
|
||||
private TcpClient? _client;
|
||||
private Stream? _stream;
|
||||
private CancellationTokenSource? _cts;
|
||||
private int _started;
|
||||
|
||||
public ShrinkTcpClientTransport(string host, int port, long sessionId = 1, int maxPacketSize = 64 * 1024,
|
||||
ShrinkTcpTlsOptions? tlsOptions = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(host))
|
||||
throw new ArgumentException("Host cannot be empty.", nameof(host));
|
||||
if (port <= 0 || port > 65535)
|
||||
throw new ArgumentOutOfRangeException(nameof(port));
|
||||
if (maxPacketSize <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(maxPacketSize));
|
||||
|
||||
_host = host.Trim();
|
||||
_port = port;
|
||||
_sessionId = sessionId;
|
||||
_maxPacketSize = maxPacketSize;
|
||||
_tlsOptions = tlsOptions?.Clone();
|
||||
}
|
||||
|
||||
public bool IsStarted => Volatile.Read(ref _started) == 1;
|
||||
|
||||
public event Action<ShrinkNetworkTransportEvent>? OnEvent;
|
||||
|
||||
public void Start()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _started, 1) == 1)
|
||||
return;
|
||||
|
||||
_cts = new CancellationTokenSource();
|
||||
ConnectAsync(_cts.Token).Forget();
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _started, 0) == 0)
|
||||
return;
|
||||
|
||||
_cts?.Cancel();
|
||||
|
||||
try
|
||||
{
|
||||
_stream?.Dispose();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_client?.Close();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
_client = null;
|
||||
_stream = null;
|
||||
}
|
||||
|
||||
public void Send(long sessionId, byte[] packetData)
|
||||
{
|
||||
SendAsync(sessionId, packetData).Forget();
|
||||
}
|
||||
|
||||
public UniTask SendAsync(long sessionId, byte[] packetData)
|
||||
{
|
||||
if (!IsStarted)
|
||||
throw new InvalidOperationException("Transport is not started.");
|
||||
if (sessionId != _sessionId)
|
||||
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}.");
|
||||
|
||||
return SendInternalAsync(_stream, _sendLock, packetData ?? Array.Empty<byte>(),
|
||||
_cts?.Token ?? CancellationToken.None);
|
||||
}
|
||||
|
||||
private async UniTaskVoid ConnectAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
_client = new TcpClient();
|
||||
_client.NoDelay = true;
|
||||
await _client.ConnectAsync(_host, _port);
|
||||
var stream = await CreateConnectedStreamAsync(_client, cancellationToken);
|
||||
_stream = stream;
|
||||
|
||||
if (!IsStarted)
|
||||
{
|
||||
stream.Dispose();
|
||||
_client.Close();
|
||||
return;
|
||||
}
|
||||
|
||||
var remote = _client.Client.RemoteEndPoint?.ToString() ?? $"{_host}:{_port}";
|
||||
OnEvent?.Invoke(ShrinkNetworkTransportEvent.Connected(_sessionId, remote));
|
||||
ReceiveLoopAsync(_client, stream, cancellationToken).Forget();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShrinkNetworkLogger.Exception(ex);
|
||||
ShrinkNetworkLogger.Error($"[ShrinkNetwork] TCP connect failed: {_host}:{_port} {ex.Message}");
|
||||
Stop();
|
||||
}
|
||||
}
|
||||
|
||||
private async UniTask<Stream> CreateConnectedStreamAsync(TcpClient client, CancellationToken cancellationToken)
|
||||
{
|
||||
Stream stream = client.GetStream();
|
||||
var tlsOptions = _tlsOptions;
|
||||
if (tlsOptions == null || !tlsOptions.Enabled)
|
||||
return stream;
|
||||
|
||||
var targetHost = string.IsNullOrWhiteSpace(tlsOptions.TargetHost) ? _host : tlsOptions.TargetHost.Trim();
|
||||
var sslStream = new SslStream(stream, false, (_, _, _, errors) =>
|
||||
{
|
||||
if (tlsOptions.AllowInvalidServerCertificate)
|
||||
return true;
|
||||
|
||||
return errors == SslPolicyErrors.None;
|
||||
});
|
||||
await sslStream.AuthenticateAsClientAsync(targetHost, null, tlsOptions.EnabledProtocols,
|
||||
tlsOptions.CheckCertificateRevocation);
|
||||
return sslStream;
|
||||
}
|
||||
|
||||
private async UniTaskVoid ReceiveLoopAsync(TcpClient client, Stream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var lengthBuffer = new byte[4];
|
||||
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
await ReadExactlyAsync(stream, lengthBuffer, cancellationToken);
|
||||
var length = BinaryPrimitives.ReadInt32LittleEndian(lengthBuffer);
|
||||
if (length <= 0)
|
||||
throw new InvalidDataException($"Invalid packet length: {length}");
|
||||
if (length > _maxPacketSize)
|
||||
throw new InvalidDataException($"TCP packet length exceeded limit. Length={length}, Limit={_maxPacketSize}");
|
||||
|
||||
var payload = new byte[length];
|
||||
await ReadExactlyAsync(stream, payload, cancellationToken);
|
||||
OnEvent?.Invoke(ShrinkNetworkTransportEvent.Packet(_sessionId, payload));
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
catch (IOException ex) when (IsExpectedDisconnect(ex))
|
||||
{
|
||||
ShrinkNetworkLogger.Warn($"[ShrinkNetwork] TCP receive loop ended: {ex.Message}");
|
||||
}
|
||||
catch (SocketException ex) when (IsExpectedDisconnect(ex))
|
||||
{
|
||||
ShrinkNetworkLogger.Warn($"[ShrinkNetwork] TCP receive loop ended: {ex.Message}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShrinkNetworkLogger.Exception(ex);
|
||||
ShrinkNetworkLogger.Warn($"[ShrinkNetwork] TCP receive loop ended: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
var remote = client.Client.RemoteEndPoint?.ToString() ?? $"{_host}:{_port}";
|
||||
OnEvent?.Invoke(ShrinkNetworkTransportEvent.Disconnected(_sessionId, remote));
|
||||
Stop();
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsExpectedDisconnect(IOException ex)
|
||||
{
|
||||
if (string.Equals(ex.Message, "Remote closed.", StringComparison.Ordinal))
|
||||
return true;
|
||||
|
||||
return ex.InnerException is SocketException socketException && IsExpectedDisconnect(socketException);
|
||||
}
|
||||
|
||||
private static bool IsExpectedDisconnect(SocketException ex)
|
||||
{
|
||||
return ex.SocketErrorCode == SocketError.ConnectionReset ||
|
||||
ex.SocketErrorCode == SocketError.ConnectionAborted ||
|
||||
ex.SocketErrorCode == SocketError.Shutdown;
|
||||
}
|
||||
|
||||
private static async UniTask SendInternalAsync(Stream stream, SemaphoreSlim sendLock, 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);
|
||||
}
|
||||
finally
|
||||
{
|
||||
sendLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private static async UniTask ReadExactlyAsync(Stream stream, byte[] buffer, CancellationToken cancellationToken)
|
||||
{
|
||||
var offset = 0;
|
||||
while (offset < buffer.Length)
|
||||
{
|
||||
var read = await stream.ReadAsync(buffer.AsMemory(offset, buffer.Length - offset), cancellationToken);
|
||||
if (read <= 0)
|
||||
throw new IOException("Remote closed.");
|
||||
offset += read;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d7ce61d2fb059e44a8aad25fc98d6b65
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,295 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Security;
|
||||
using System.Net.Sockets;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Cysharp.Threading.Tasks;
|
||||
|
||||
namespace ShrinkNetwork
|
||||
{
|
||||
public sealed class ShrinkTcpServerTransport : IShrinkNetworkAsyncTransport, IShrinkNetworkSessionControlTransport
|
||||
{
|
||||
private readonly ConcurrentDictionary<long, TcpClient> _clients = new();
|
||||
private readonly ConcurrentDictionary<long, SemaphoreSlim> _sendLocks = new();
|
||||
private readonly ConcurrentDictionary<long, Stream> _streams = new();
|
||||
private readonly TcpListener _listener;
|
||||
private readonly int _maxPacketSize;
|
||||
private readonly ShrinkTcpTlsOptions? _tlsOptions;
|
||||
private CancellationTokenSource? _cts;
|
||||
private long _sessionIdGenerator;
|
||||
private X509Certificate2? _serverCertificate;
|
||||
|
||||
public ShrinkTcpServerTransport(IPAddress ipAddress, int port, int maxPacketSize = 64 * 1024,
|
||||
ShrinkTcpTlsOptions? tlsOptions = null)
|
||||
{
|
||||
if (port <= 0 || port > 65535)
|
||||
throw new ArgumentOutOfRangeException(nameof(port));
|
||||
if (maxPacketSize <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(maxPacketSize));
|
||||
|
||||
_listener = new TcpListener(ipAddress, port);
|
||||
_maxPacketSize = maxPacketSize;
|
||||
_tlsOptions = tlsOptions?.Clone();
|
||||
ListeningAddress = ipAddress;
|
||||
ListeningPort = port;
|
||||
}
|
||||
|
||||
public IPAddress ListeningAddress { get; }
|
||||
public int ListeningPort { get; }
|
||||
public bool IsStarted { get; private set; }
|
||||
|
||||
public event Action<ShrinkNetworkTransportEvent>? OnEvent;
|
||||
|
||||
public void Start()
|
||||
{
|
||||
if (IsStarted)
|
||||
return;
|
||||
|
||||
IsStarted = true;
|
||||
_cts = new CancellationTokenSource();
|
||||
EnsureTlsCertificateLoaded();
|
||||
_listener.Start();
|
||||
_ = AcceptLoopAsync(_cts.Token);
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
if (!IsStarted)
|
||||
return;
|
||||
|
||||
IsStarted = false;
|
||||
_cts?.Cancel();
|
||||
_listener.Stop();
|
||||
|
||||
foreach (var pair in _clients)
|
||||
{
|
||||
try
|
||||
{
|
||||
pair.Value.Close();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
_clients.Clear();
|
||||
foreach (var pair in _streams)
|
||||
{
|
||||
try
|
||||
{
|
||||
pair.Value.Dispose();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
_streams.Clear();
|
||||
|
||||
foreach (var pair in _sendLocks)
|
||||
{
|
||||
pair.Value.Dispose();
|
||||
}
|
||||
|
||||
_sendLocks.Clear();
|
||||
}
|
||||
|
||||
public void Send(long sessionId, byte[] packetData)
|
||||
{
|
||||
SendAsync(sessionId, packetData).Forget();
|
||||
}
|
||||
|
||||
public bool DisconnectSession(long sessionId, string? reason = null)
|
||||
{
|
||||
if (!_clients.TryGetValue(sessionId, out var client))
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
if (_streams.TryRemove(sessionId, out var stream))
|
||||
stream.Dispose();
|
||||
client.Close();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(reason))
|
||||
ShrinkNetworkLogger.Warn($"[ShrinkNetwork][TCP-Server] Disconnect session {sessionId}: {reason}");
|
||||
return true;
|
||||
}
|
||||
|
||||
public async UniTask SendAsync(long sessionId, byte[] packetData)
|
||||
{
|
||||
if (!_clients.TryGetValue(sessionId, out var client))
|
||||
throw new InvalidOperationException($"Session {sessionId} is not connected.");
|
||||
if (!_streams.TryGetValue(sessionId, out var stream))
|
||||
throw new InvalidOperationException($"Session {sessionId} stream is not initialized.");
|
||||
|
||||
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}.");
|
||||
|
||||
await SendInternalAsync(stream, sendLock, packetData ?? Array.Empty<byte>(), CancellationToken.None);
|
||||
}
|
||||
|
||||
private async Task AcceptLoopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
TcpClient client;
|
||||
try
|
||||
{
|
||||
var acceptTask = _listener.AcceptTcpClientAsync();
|
||||
var completedTask = await Task.WhenAny(acceptTask, Task.Delay(Timeout.Infinite, cancellationToken));
|
||||
if (completedTask != acceptTask)
|
||||
break;
|
||||
|
||||
client = await acceptTask;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (!IsStarted)
|
||||
break;
|
||||
continue;
|
||||
}
|
||||
|
||||
var sessionId = Interlocked.Increment(ref _sessionIdGenerator);
|
||||
client.NoDelay = true;
|
||||
var stream = await CreateServerStreamAsync(client, cancellationToken);
|
||||
_clients[sessionId] = client;
|
||||
_streams[sessionId] = stream;
|
||||
_sendLocks[sessionId] = new SemaphoreSlim(1, 1);
|
||||
var remoteAddress = client.Client.RemoteEndPoint?.ToString() ?? "unknown";
|
||||
OnEvent?.Invoke(ShrinkNetworkTransportEvent.Connected(sessionId, remoteAddress));
|
||||
_ = ReceiveLoopAsync(sessionId, client, stream, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Stream> CreateServerStreamAsync(TcpClient client, CancellationToken cancellationToken)
|
||||
{
|
||||
Stream stream = client.GetStream();
|
||||
var tlsOptions = _tlsOptions;
|
||||
if (tlsOptions == null || !tlsOptions.Enabled)
|
||||
return stream;
|
||||
|
||||
if (_serverCertificate == null)
|
||||
throw new InvalidOperationException("TCP TLS is enabled, but the server certificate is not loaded.");
|
||||
|
||||
var sslStream = new SslStream(stream, false);
|
||||
await sslStream.AuthenticateAsServerAsync(_serverCertificate, false, tlsOptions.EnabledProtocols,
|
||||
tlsOptions.CheckCertificateRevocation);
|
||||
return sslStream;
|
||||
}
|
||||
|
||||
private void EnsureTlsCertificateLoaded()
|
||||
{
|
||||
var tlsOptions = _tlsOptions;
|
||||
if (tlsOptions == null || !tlsOptions.Enabled)
|
||||
{
|
||||
_serverCertificate = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(tlsOptions.ServerCertificatePath))
|
||||
throw new InvalidOperationException("TCP TLS is enabled, but ServerCertificatePath is empty.");
|
||||
|
||||
_serverCertificate = new X509Certificate2(tlsOptions.ServerCertificatePath, tlsOptions.ServerCertificatePassword);
|
||||
}
|
||||
|
||||
private async Task ReceiveLoopAsync(long sessionId, TcpClient client, Stream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var lengthBuffer = new byte[4];
|
||||
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
await ReadExactlyAsync(stream, lengthBuffer, cancellationToken);
|
||||
var length = BinaryPrimitives.ReadInt32LittleEndian(lengthBuffer);
|
||||
if (length <= 0)
|
||||
throw new InvalidDataException($"Invalid packet length: {length}");
|
||||
if (length > _maxPacketSize)
|
||||
throw new InvalidDataException($"TCP packet length exceeded limit. Length={length}, Limit={_maxPacketSize}");
|
||||
|
||||
var payload = new byte[length];
|
||||
await ReadExactlyAsync(stream, payload, cancellationToken);
|
||||
OnEvent?.Invoke(ShrinkNetworkTransportEvent.Packet(sessionId, payload));
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_clients.TryRemove(sessionId, out var removed))
|
||||
{
|
||||
if (_streams.TryRemove(sessionId, out var ownedStream))
|
||||
ownedStream.Dispose();
|
||||
if (_sendLocks.TryRemove(sessionId, out var sendLock))
|
||||
sendLock.Dispose();
|
||||
|
||||
var remoteAddress = removed.Client.RemoteEndPoint?.ToString() ?? "unknown";
|
||||
try
|
||||
{
|
||||
removed.Close();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
OnEvent?.Invoke(ShrinkNetworkTransportEvent.Disconnected(sessionId, remoteAddress));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task SendInternalAsync(Stream stream, SemaphoreSlim sendLock, 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);
|
||||
}
|
||||
finally
|
||||
{
|
||||
sendLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task ReadExactlyAsync(Stream stream, byte[] buffer, CancellationToken cancellationToken)
|
||||
{
|
||||
var offset = 0;
|
||||
while (offset < buffer.Length)
|
||||
{
|
||||
var read = await stream.ReadAsync(buffer.AsMemory(offset, buffer.Length - offset), cancellationToken);
|
||||
if (read <= 0)
|
||||
throw new IOException("Remote closed.");
|
||||
offset += read;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 734d880602bc42eb8a14ae1e811e1114
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,2 @@
|
||||
// 占位文件:兼容尚未刷新的 Unity / 模板工程引用。
|
||||
// 实际定义已移动到 Runtime/Transport/Abstractions/IShrinkNetworkTransport.cs。
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 253f494145698ce49ba13db227f91603
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user