feat(cordis): 接入上下文组合与模组事务热替换

This commit is contained in:
2026-08-16 23:20:40 +08:00
commit ad256f109b
676 changed files with 52168 additions and 0 deletions
@@ -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: