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,8 @@
fileFormatVersion: 2
guid: afa48bd281d22914ea8fe82af25901b1
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,31 @@
#nullable enable
using Cysharp.Threading.Tasks;
namespace ShrinkNetwork
{
public sealed class ShrinkNetworkContext
{
public ShrinkNetworkService Service { get; }
public ShrinkNetworkSession Session { get; }
public ShrinkNetworkPacket Packet { get; }
public ShrinkNetworkContext(ShrinkNetworkService service, ShrinkNetworkSession session, ShrinkNetworkPacket packet)
{
Service = service;
Session = session;
Packet = packet;
}
public bool IsRequest => Packet.Kind == ShrinkNetworkPacketKind.Request;
public bool IsResponse => Packet.Kind == ShrinkNetworkPacketKind.Response;
public ShrinkRequestToken RequestToken => Packet.RequestToken;
public string? Route => Packet.Route;
public UniTask ReplyAsync<TResponse>(TResponse response)
where TResponse : class, IShrinkNetworkResponse
{
return Service.SendResponseAsync(Session, response, Packet.RequestToken, Packet.Route);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 41088f41c7fd6b544a1ec928650e42be
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,53 @@
using System;
namespace ShrinkNetwork
{
public static class ShrinkNetworkLogger
{
public static Action<string> InfoHandler { get; set; } = DefaultInfo;
public static Action<string> WarningHandler { get; set; } = DefaultWarning;
public static Action<string> ErrorHandler { get; set; } = DefaultError;
public static Action<Exception> ExceptionHandler { get; set; } = DefaultException;
public static void Info(string message) => InfoHandler?.Invoke(message);
public static void Warn(string message) => WarningHandler?.Invoke(message);
public static void Error(string message) => ErrorHandler?.Invoke(message);
public static void Exception(Exception ex) => ExceptionHandler?.Invoke(ex);
private static void DefaultInfo(string message)
{
#if UNITY_5_3_OR_NEWER
UnityEngine.Debug.Log(message);
#else
Console.WriteLine(message);
#endif
}
private static void DefaultWarning(string message)
{
#if UNITY_5_3_OR_NEWER
UnityEngine.Debug.LogWarning(message);
#else
Console.WriteLine("[Warn] " + message);
#endif
}
private static void DefaultError(string message)
{
#if UNITY_5_3_OR_NEWER
UnityEngine.Debug.LogError(message);
#else
Console.Error.WriteLine(message);
#endif
}
private static void DefaultException(Exception ex)
{
#if UNITY_5_3_OR_NEWER
UnityEngine.Debug.LogException(ex);
#else
Console.Error.WriteLine(ex);
#endif
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1250bb387385afa40abf0450af4616f8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,26 @@
using UnityEngine;
namespace ShrinkNetwork
{
public static class ShrinkNetworkRuntime
{
public static ShrinkNetworkService Default { get; private set; }
static ShrinkNetworkRuntime()
{
RebuildDefault();
}
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
private static void ResetOnPlayModeEnter()
{
RebuildDefault();
}
private static void RebuildDefault()
{
Default = new ShrinkNetworkService();
ShrinkNetworkGeneratedRegistry.RegisterAll(Default);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f9c281d5168894c40843e80276fe1592
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,857 @@
#nullable enable
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Cysharp.Threading.Tasks;
namespace ShrinkNetwork
{
public sealed class ShrinkNetworkService
{
private sealed class PendingRequest
{
public long SessionId;
public Type ResponseType = null!;
public UniTaskCompletionSource<object?> CompletionSource = null!;
}
private readonly ConcurrentDictionary<long, ShrinkNetworkSession> _sessions = new();
private readonly ConcurrentDictionary<ShrinkRequestToken, PendingRequest> _pendingRequests = new();
private int _requestTokenGenerator;
private IShrinkNetworkTransport? _transport;
private long _sessionsConnected;
private long _sessionsDisconnected;
private long _packetsSent;
private long _packetsReceived;
private long _bytesSent;
private long _bytesReceived;
private long _rpcStarted;
private long _rpcCompleted;
private long _rpcTimedOut;
private long _rpcCanceled;
private long _rpcFailed;
private long _protocolViolations;
private long _authRejectedCount;
private long _permissionDeniedCount;
private long _handlerExceptionCount;
private long _unknownOpcodeCount;
private long _dispatchMissCount;
private long _serializationErrorCount;
private long _dispatchQueueRejectedCount;
private IShrinkNetworkDispatchScheduler _dispatchScheduler;
public ShrinkNetworkService()
: this(new ShrinkJsonNetworkSerializer(), new ShrinkNetworkMessageRegistry(), new ShrinkNetworkRouter())
{
}
public ShrinkNetworkService(IShrinkNetworkSerializer serializer, ShrinkNetworkMessageRegistry messageRegistry,
ShrinkNetworkRouter router, IShrinkNetworkDispatchScheduler? dispatchScheduler = null)
{
Serializer = serializer ?? throw new ArgumentNullException(nameof(serializer));
MessageRegistry = messageRegistry ?? throw new ArgumentNullException(nameof(messageRegistry));
Router = router ?? throw new ArgumentNullException(nameof(router));
_dispatchScheduler = dispatchScheduler ?? ShrinkNetworkDispatchSchedulers.Inline;
}
public IShrinkNetworkSerializer Serializer { get; }
public ShrinkNetworkMessageRegistry MessageRegistry { get; }
public ShrinkNetworkRouter Router { get; }
public IReadOnlyDictionary<long, ShrinkNetworkSession> Sessions => _sessions;
public int MinProtocolVersion { get; set; } = ShrinkNetworkProtocol.CurrentProtocolVersion;
public int MaxProtocolVersion { get; set; } = ShrinkNetworkProtocol.CurrentProtocolVersion;
public int MinSchemaVersion { get; set; } = ShrinkNetworkProtocol.CurrentSchemaVersion;
public int MaxSchemaVersion { get; set; } = ShrinkNetworkProtocol.CurrentSchemaVersion;
public bool DisconnectOnProtocolViolation { get; set; } = true;
public Func<ShrinkNetworkSession, ShrinkNetworkPacket, ShrinkIncomingPacketValidationResult?>? IncomingPacketValidator { get; set; }
public IShrinkNetworkDispatchScheduler DispatchScheduler
{
get => _dispatchScheduler;
set => _dispatchScheduler = value ?? throw new ArgumentNullException(nameof(value));
}
public event Action<ShrinkNetworkSession>? OnSessionConnected;
public event Action<ShrinkNetworkSession>? OnSessionDisconnected;
public void BindTransport(IShrinkNetworkTransport? transport)
{
if (_transport != null)
_transport.OnEvent -= OnTransportEvent;
_transport = transport;
if (_transport != null)
{
_transport.OnEvent += OnTransportEvent;
if (!_transport.IsStarted)
_transport.Start();
return;
}
}
public void RegisterMessage<TMessage>(int opcode, string? route = null) where TMessage : IShrinkNetworkMessage
=> MessageRegistry.Register<TMessage>(opcode, route);
public void RegisterMessage(Type messageType, int opcode, string? route = null)
=> MessageRegistry.Register(messageType, opcode, route);
public void RegisterHandler<TMessage>(Func<ShrinkNetworkContext, TMessage, UniTask> handler,
ShrinkNetworkPermissionRequirement requirement = default)
where TMessage : IShrinkNetworkMessage
=> Router.RegisterHandler(handler, requirement);
public void RegisterHandler(Type messageType, Func<ShrinkNetworkContext, object, UniTask> handler,
ShrinkNetworkPermissionRequirement requirement = default)
=> Router.RegisterHandler(messageType, handler, requirement);
public void RegisterRequestHandler<TRequest, TResponse>(Func<ShrinkNetworkContext, TRequest, UniTask<TResponse>> handler,
ShrinkNetworkPermissionRequirement requirement = default)
where TRequest : IShrinkNetworkRequest
where TResponse : class, IShrinkNetworkResponse
=> Router.RegisterRequestHandler(handler, requirement);
public void RegisterRequestHandler(Type requestType, Type responseType,
Func<ShrinkNetworkContext, object, UniTask<object?>> handler,
ShrinkNetworkPermissionRequirement requirement = default)
=> Router.RegisterRequestHandler(requestType, responseType, handler, requirement);
public void RegisterRpc<TRequest, TResponse>(Func<ShrinkNetworkContext, TRequest, UniTask<TResponse>> handler,
ShrinkNetworkPermissionRequirement requirement = default)
where TRequest : IShrinkNetworkRequest
where TResponse : class, IShrinkNetworkResponse
=> RegisterRequestHandler(handler, requirement);
public void AutoRegisterAttributedMessages()
=> ShrinkNetworkRegHelper.RegisterAttributedMessages(this);
public void AutoRegisterStaticHandlers()
=> ShrinkNetworkRegHelper.RegisterStaticHandlers(this);
public void RegisterHandlers(object target)
=> ShrinkNetworkRegHelper.RegisterHandlers(this, target);
public void AutoRegisterAll()
{
AutoRegisterAttributedMessages();
AutoRegisterStaticHandlers();
}
public ShrinkNetworkServiceDiagnosticsSnapshot GetDiagnosticsSnapshot()
{
return new ShrinkNetworkServiceDiagnosticsSnapshot
{
CurrentSessions = _sessions.Count,
SessionsConnected = Volatile.Read(ref _sessionsConnected),
SessionsDisconnected = Volatile.Read(ref _sessionsDisconnected),
PacketsSent = Volatile.Read(ref _packetsSent),
PacketsReceived = Volatile.Read(ref _packetsReceived),
BytesSent = Volatile.Read(ref _bytesSent),
BytesReceived = Volatile.Read(ref _bytesReceived),
RpcStarted = Volatile.Read(ref _rpcStarted),
RpcCompleted = Volatile.Read(ref _rpcCompleted),
RpcTimedOut = Volatile.Read(ref _rpcTimedOut),
RpcCanceled = Volatile.Read(ref _rpcCanceled),
RpcFailed = Volatile.Read(ref _rpcFailed),
ProtocolViolations = Volatile.Read(ref _protocolViolations),
AuthRejectedCount = Volatile.Read(ref _authRejectedCount),
PermissionDeniedCount = Volatile.Read(ref _permissionDeniedCount),
HandlerExceptionCount = Volatile.Read(ref _handlerExceptionCount),
UnknownOpcodeCount = Volatile.Read(ref _unknownOpcodeCount),
DispatchMissCount = Volatile.Read(ref _dispatchMissCount),
SerializationErrorCount = Volatile.Read(ref _serializationErrorCount),
DispatchQueueRejectedCount = Volatile.Read(ref _dispatchQueueRejectedCount)
};
}
public UniTask SendAsync<TMessage>(ShrinkNetworkSession session, TMessage message, string? route = null)
where TMessage : IShrinkNetworkMessage
=> SendInternalAsync(session, message, ShrinkNetworkPacketKind.Message, ShrinkRequestToken.Default, route);
public UniTask SendAsync(ShrinkNetworkSession session, IShrinkNetworkMessage message, string? route = null)
{
if (message == null)
throw new ArgumentNullException(nameof(message));
return SendInternalAsync(session, message, message.GetType(), ShrinkNetworkPacketKind.Message, ShrinkRequestToken.Default, route);
}
public UniTask NotifyAsync<TMessage>(ShrinkNetworkSession session, TMessage message, string? route = null)
where TMessage : IShrinkNetworkMessage
=> SendAsync(session, message, route);
public UniTask NotifyAsync(ShrinkNetworkSession session, IShrinkNetworkMessage message, string? route = null)
=> SendAsync(session, message, route);
/// <summary>
/// Sends a message whose payload has already been serialized. The payload is
/// treated as immutable and can be reused for multiple sessions.
/// </summary>
public UniTask SendSerializedAsync(ShrinkNetworkSession session, Type messageType, byte[] payload,
string? route = null)
{
return SendPacketAsync(session, messageType, ShrinkNetworkPacketKind.Message,
ShrinkRequestToken.Default, route, payload);
}
public UniTask<TResponse> CallAsync<TRequest, TResponse>(ShrinkNetworkSession session, TRequest request, string? route = null)
where TRequest : IShrinkNetworkRequest
where TResponse : class, IShrinkNetworkResponse
=> CallAsync<TRequest, TResponse>(session, request, new ShrinkRpcCallOptions { RouteOverride = route });
public async UniTask<TResponse> CallAsync<TRequest, TResponse>(ShrinkNetworkSession session, TRequest request,
ShrinkRpcCallOptions options)
where TRequest : IShrinkNetworkRequest
where TResponse : class, IShrinkNetworkResponse
{
if (session == null)
throw new ArgumentNullException(nameof(session));
Interlocked.Increment(ref _rpcStarted);
var requestToken = options?.RequestTokenOverride ?? new ShrinkRequestToken(Interlocked.Increment(ref _requestTokenGenerator));
var pending = new PendingRequest
{
SessionId = session.SessionId,
ResponseType = typeof(TResponse),
CompletionSource = new UniTaskCompletionSource<object?>()
};
if (!_pendingRequests.TryAdd(requestToken, pending))
{
throw new InvalidOperationException(
$"Pending request token already exists. RequestToken={requestToken}");
}
try
{
await SendInternalAsync(session, request, ShrinkNetworkPacketKind.Request, requestToken, options?.RouteOverride);
var response = await WaitForPendingResponse<TResponse>(requestToken, pending, options);
EnsureResponseSucceeded(response);
Interlocked.Increment(ref _rpcCompleted);
return response;
}
catch
{
_pendingRequests.TryRemove(requestToken, out _);
throw;
}
}
public UniTask<TResponse> RpcAsync<TRequest, TResponse>(ShrinkNetworkSession session, TRequest request, string? route = null)
where TRequest : IShrinkNetworkRequest
where TResponse : class, IShrinkNetworkResponse
=> CallAsync<TRequest, TResponse>(session, request, route);
public UniTask<TResponse> RpcAsync<TRequest, TResponse>(ShrinkNetworkSession session, TRequest request,
ShrinkRpcCallOptions options)
where TRequest : IShrinkNetworkRequest
where TResponse : class, IShrinkNetworkResponse
=> CallAsync<TRequest, TResponse>(session, request, options);
internal UniTask SendResponseAsync<TResponse>(ShrinkNetworkSession session, TResponse response, ShrinkRequestToken requestToken, string? route = null)
where TResponse : class, IShrinkNetworkResponse
=> SendInternalAsync(session, response, typeof(TResponse), ShrinkNetworkPacketKind.Response, requestToken, route);
internal UniTask SendResponseAsync(ShrinkNetworkSession session, IShrinkNetworkResponse response, Type responseType,
ShrinkRequestToken requestToken, string? route = null)
=> SendInternalAsync(session, response, responseType, ShrinkNetworkPacketKind.Response, requestToken, route);
private UniTask SendInternalAsync<TMessage>(ShrinkNetworkSession session, TMessage message,
ShrinkNetworkPacketKind kind, ShrinkRequestToken requestToken, string? route)
where TMessage : IShrinkNetworkMessage
=> SendInternalAsync(session, message, typeof(TMessage), kind, requestToken, route);
private UniTask SendInternalAsync(ShrinkNetworkSession session, IShrinkNetworkMessage message, Type messageType,
ShrinkNetworkPacketKind kind, ShrinkRequestToken requestToken, string? route)
{
if (session == null)
throw new ArgumentNullException(nameof(session));
if (message == null)
throw new ArgumentNullException(nameof(message));
if (messageType == null)
throw new ArgumentNullException(nameof(messageType));
return SendPacketAsync(session, messageType, kind, requestToken, route, Serializer.Serialize(message));
}
private async UniTask SendPacketAsync(ShrinkNetworkSession session, Type messageType,
ShrinkNetworkPacketKind kind, ShrinkRequestToken requestToken, string? route, byte[] payload)
{
if (session == null)
throw new ArgumentNullException(nameof(session));
if (messageType == null)
throw new ArgumentNullException(nameof(messageType));
if (payload == null)
throw new ArgumentNullException(nameof(payload));
var transport = _transport ?? throw new InvalidOperationException("Transport is not bound.");
var meta = MessageRegistry.GetMeta(messageType);
var packet = new ShrinkNetworkPacket
{
Opcode = meta.Opcode,
RequestToken = requestToken,
SessionToken = session.SessionToken,
SessionTokenExpiresAtUnixTimeSeconds = session.SessionTokenExpiresAtUtc?.ToUnixTimeSeconds() ?? 0,
Route = string.IsNullOrWhiteSpace(route) ? meta.Route : route.Trim(),
Kind = kind,
Payload = payload
};
var packetData = Serializer.Serialize(packet);
Interlocked.Increment(ref _packetsSent);
Interlocked.Add(ref _bytesSent, packetData.Length);
if (transport is IShrinkNetworkAsyncTransport asyncTransport)
{
await asyncTransport.SendAsync(session.SessionId, packetData);
return;
}
transport.Send(session.SessionId, packetData);
}
private void OnTransportEvent(ShrinkNetworkTransportEvent evt)
{
if (evt == null)
return;
// 连接生命周期必须在传输回调返回前提交,否则快速退役/重激活时,
// 旧的断开事件可能晚于新的连接事件完成,观察者会读到过期会话视图。
if (evt.Type == ShrinkNetworkTransportEventType.Connected)
{
HandleConnected(evt);
return;
}
if (evt.Type == ShrinkNetworkTransportEventType.Disconnected)
{
HandleDisconnected(evt);
return;
}
ScheduleTransportEventAsync(evt).Forget();
}
private async UniTaskVoid ScheduleTransportEventAsync(ShrinkNetworkTransportEvent evt)
{
try
{
var scheduled = await DispatchScheduler.ScheduleAsync(() => HandleTransportEventAsync(evt));
if (!scheduled)
Interlocked.Increment(ref _dispatchQueueRejectedCount);
}
catch (Exception ex)
{
ShrinkNetworkLogger.Exception(ex);
ShrinkNetworkLogger.Error($"[ShrinkNetwork] Transport event dispatch failed: {ex.Message}");
}
}
private UniTask HandleTransportEventAsync(ShrinkNetworkTransportEvent evt)
{
switch (evt.Type)
{
case ShrinkNetworkTransportEventType.Connected:
HandleConnected(evt);
return UniTask.CompletedTask;
case ShrinkNetworkTransportEventType.Disconnected:
HandleDisconnected(evt);
return UniTask.CompletedTask;
case ShrinkNetworkTransportEventType.Packet:
return HandlePacketAsync(evt);
default:
return UniTask.CompletedTask;
}
}
private void HandleConnected(ShrinkNetworkTransportEvent evt)
{
var session = _sessions.AddOrUpdate(evt.SessionId,
_ => new ShrinkNetworkSession(evt.SessionId, evt.RemoteAddress, this),
(_, existing) =>
{
existing.RemoteAddress = evt.RemoteAddress;
return existing;
});
Interlocked.Increment(ref _sessionsConnected);
OnSessionConnected?.Invoke(session);
}
private void HandleDisconnected(ShrinkNetworkTransportEvent evt)
{
if (_sessions.TryRemove(evt.SessionId, out var session))
{
FailPendingRequestsForSession(evt.SessionId);
Interlocked.Increment(ref _sessionsDisconnected);
OnSessionDisconnected?.Invoke(session);
}
}
private async UniTask HandlePacketAsync(ShrinkNetworkTransportEvent evt)
{
try
{
Interlocked.Increment(ref _packetsReceived);
Interlocked.Add(ref _bytesReceived, evt.PacketData.Length);
var packet = Serializer.Deserialize<ShrinkNetworkPacket>(evt.PacketData);
if (!_sessions.TryGetValue(evt.SessionId, out var session))
{
session = _sessions.GetOrAdd(evt.SessionId,
id => new ShrinkNetworkSession(id, evt.RemoteAddress, this));
}
if (!ValidatePacketCompatibility(packet, evt.SessionId))
return;
ApplySessionTokenFromPacket(session, packet);
if (packet.Kind == ShrinkNetworkPacketKind.Response)
{
HandleResponse(packet);
return;
}
if (!ValidateIncomingPacket(session, packet))
return;
if (!MessageRegistry.TryGetMeta(packet.Opcode, out var meta))
{
Interlocked.Increment(ref _unknownOpcodeCount);
ShrinkNetworkLogger.Warn($"[ShrinkNetwork] Unregistered opcode: {packet.Opcode}");
return;
}
var resolvedMeta = meta!;
var message = Serializer.Deserialize(packet.Payload, resolvedMeta.MessageType);
if (message == null)
{
ShrinkNetworkLogger.Warn($"[ShrinkNetwork] Failed to deserialize message for opcode {packet.Opcode}.");
Interlocked.Increment(ref _serializationErrorCount);
return;
}
var context = new ShrinkNetworkContext(this, session, packet);
var handled = await Router.DispatchAsync(context, message, resolvedMeta.MessageType);
if (!handled)
{
Interlocked.Increment(ref _dispatchMissCount);
ShrinkNetworkLogger.Warn($"[ShrinkNetwork] No handler found for {resolvedMeta.MessageType.FullName}");
}
}
catch (Exception ex)
{
Interlocked.Increment(ref _serializationErrorCount);
ShrinkNetworkLogger.Exception(ex);
ShrinkNetworkLogger.Error($"[ShrinkNetwork] Packet handling failed: {ex.Message}");
}
}
private void HandleResponse(ShrinkNetworkPacket packet)
{
if (!_pendingRequests.TryRemove(packet.RequestToken, out var pending))
{
ShrinkNetworkLogger.Warn($"[ShrinkNetwork] Pending request not found. RequestToken={packet.RequestToken}");
return;
}
try
{
var response = Serializer.Deserialize(packet.Payload, pending.ResponseType);
pending.CompletionSource.TrySetResult(response);
}
catch (Exception ex)
{
pending.CompletionSource.TrySetException(ex);
}
}
private async UniTask<TResponse> WaitForPendingResponse<TResponse>(ShrinkRequestToken requestToken, PendingRequest pending,
ShrinkRpcCallOptions? options)
where TResponse : class, IShrinkNetworkResponse
{
var timeoutMs = options?.TimeoutMs ?? 10000;
var externalToken = options?.CancellationToken ?? default;
var debugLabel = string.IsNullOrWhiteSpace(options?.DebugLabel) ? string.Empty : options.DebugLabel.Trim();
using var timeoutCts = timeoutMs > 0 ? new CancellationTokenSource(timeoutMs) : null;
using var linkedCts = CreateLinkedTokenSource(externalToken, timeoutCts?.Token ?? default);
try
{
var boxed = await pending.CompletionSource.Task.AttachExternalCancellation(linkedCts?.Token ?? default);
if (boxed is not TResponse response)
throw new ShrinkRpcException(ShrinkRpcErrorCode.InvalidResponse, BuildRpcMessage("RPC returned an invalid response.", requestToken, debugLabel));
return response;
}
catch (OperationCanceledException) when (externalToken.IsCancellationRequested)
{
_pendingRequests.TryRemove(requestToken, out _);
Interlocked.Increment(ref _rpcCanceled);
Interlocked.Increment(ref _rpcFailed);
throw new ShrinkRpcException(ShrinkRpcErrorCode.Canceled, BuildRpcMessage("RPC call was canceled.", requestToken, debugLabel));
}
catch (OperationCanceledException)
{
_pendingRequests.TryRemove(requestToken, out _);
Interlocked.Increment(ref _rpcTimedOut);
Interlocked.Increment(ref _rpcFailed);
throw new ShrinkRpcException(ShrinkRpcErrorCode.Timeout, BuildRpcMessage("RPC call timed out.", requestToken, debugLabel));
}
}
private void FailPendingRequestsForSession(long sessionId)
{
var pendingEntries = _pendingRequests
.Where(pair => pair.Value.SessionId == sessionId)
.ToArray();
if (pendingEntries.Length == 0)
return;
foreach (var pendingEntry in pendingEntries)
{
if (_pendingRequests.TryRemove(pendingEntry.Key, out var pending))
{
pending.CompletionSource.TrySetException(
new ShrinkRpcException(ShrinkRpcErrorCode.ConnectionClosed,
$"RPC peer disconnected. RequestToken={pendingEntry.Key}, SessionId={sessionId}"));
}
}
}
private static string BuildRpcMessage(string message, ShrinkRequestToken requestToken, string debugLabel)
{
if (string.IsNullOrWhiteSpace(debugLabel))
return $"{message} RequestToken={requestToken}";
return $"{message} RequestToken={requestToken}, Label={debugLabel}";
}
private static CancellationTokenSource? CreateLinkedTokenSource(CancellationToken left, CancellationToken right)
{
if (left.CanBeCanceled && right.CanBeCanceled)
return CancellationTokenSource.CreateLinkedTokenSource(left, right);
if (left.CanBeCanceled)
return CancellationTokenSource.CreateLinkedTokenSource(left);
if (right.CanBeCanceled)
return CancellationTokenSource.CreateLinkedTokenSource(right);
return null;
}
private static void EnsureResponseSucceeded<TResponse>(TResponse response)
where TResponse : class, IShrinkNetworkResponse
{
if (response == null)
throw new ShrinkRpcException(ShrinkRpcErrorCode.InvalidResponse, "RPC returned a null response.");
if (response.ErrorCode != 0)
throw new ShrinkRpcException(response.ErrorCode,
string.IsNullOrWhiteSpace(response.ErrorMessage) ? "RPC call failed." : response.ErrorMessage);
}
internal void ReportPermissionDenied(Type messageType)
{
Interlocked.Increment(ref _permissionDeniedCount);
}
internal void ReportHandlerException(Type messageType, Exception ex)
{
Interlocked.Increment(ref _handlerExceptionCount);
Interlocked.Increment(ref _rpcFailed);
}
private bool ValidatePacketCompatibility(ShrinkNetworkPacket packet, long sessionId)
{
var protocolAllowed = packet.ProtocolVersion >= MinProtocolVersion && packet.ProtocolVersion <= MaxProtocolVersion;
var schemaAllowed = packet.SchemaVersion >= MinSchemaVersion && packet.SchemaVersion <= MaxSchemaVersion;
if (protocolAllowed && schemaAllowed)
return true;
Interlocked.Increment(ref _protocolViolations);
var reason =
$"protocol/schema mismatch. Protocol={packet.ProtocolVersion}, Schema={packet.SchemaVersion}, AllowedProtocol={MinProtocolVersion}-{MaxProtocolVersion}, AllowedSchema={MinSchemaVersion}-{MaxSchemaVersion}";
ShrinkNetworkLogger.Warn($"[ShrinkNetwork] Session {sessionId} rejected: {reason}");
if (DisconnectOnProtocolViolation && _transport is IShrinkNetworkSessionControlTransport sessionControl)
sessionControl.DisconnectSession(sessionId, reason);
return false;
}
private bool ValidateIncomingPacket(ShrinkNetworkSession session, ShrinkNetworkPacket packet)
{
var validator = IncomingPacketValidator;
if (validator == null)
return true;
var result = validator(session, packet);
if (result == null || result.IsAllowed)
return true;
Interlocked.Increment(ref _authRejectedCount);
var reason = string.IsNullOrWhiteSpace(result.Reason)
? "incoming packet rejected by validator."
: result.Reason.Trim();
ShrinkNetworkLogger.Warn($"[ShrinkNetwork] Session {session.SessionId} rejected: {reason}");
if (result.DisconnectSession && _transport is IShrinkNetworkSessionControlTransport sessionControl)
sessionControl.DisconnectSession(session.SessionId, reason);
return false;
}
private static void ApplySessionTokenFromPacket(ShrinkNetworkSession session, ShrinkNetworkPacket packet)
{
if (string.IsNullOrWhiteSpace(packet.SessionToken))
return;
var expiresAtUtc = packet.SessionTokenExpiresAtUnixTimeSeconds > 0
? DateTimeOffset.FromUnixTimeSeconds(packet.SessionTokenExpiresAtUnixTimeSeconds)
: (DateTimeOffset?)null;
session.SetSessionToken(packet.SessionToken, expiresAtUtc);
}
}
public interface IShrinkNetworkDispatchScheduler
{
/// <summary>
/// Schedules a transport event. The result is false when the scheduler
/// rejects or drops the work because its bounded queue is full.
/// </summary>
UniTask<bool> ScheduleAsync(Func<UniTask> callback);
}
public static class ShrinkNetworkDispatchSchedulers
{
public static IShrinkNetworkDispatchScheduler Inline { get; } =
new ShrinkNetworkInlineDispatchScheduler();
}
public sealed class ShrinkNetworkInlineDispatchScheduler : IShrinkNetworkDispatchScheduler
{
public async UniTask<bool> ScheduleAsync(Func<UniTask> callback)
{
if (callback == null)
throw new ArgumentNullException(nameof(callback));
await callback();
return true;
}
}
public enum ShrinkNetworkDispatchOverflowPolicy
{
Reject = 0,
DropNewest = 1,
DropOldest = 2
}
/// <summary>
/// A caller-pumped, bounded dispatch queue. Unity can pump it from Update
/// while a dedicated server can keep the default inline scheduler.
/// </summary>
public sealed class ShrinkNetworkDispatchQueue : IShrinkNetworkDispatchScheduler, IDisposable
{
private sealed class WorkItem
{
public Func<UniTask> Callback = null!;
public UniTaskCompletionSource<bool> Completion = null!;
}
private readonly ConcurrentQueue<WorkItem> _queue = new();
private readonly object _lifecycleLock = new();
private readonly int _capacity;
private readonly ShrinkNetworkDispatchOverflowPolicy _overflowPolicy;
private int _queuedCount;
private int _pumping;
private int _disposed;
private long _rejectedCount;
private long _droppedCount;
public ShrinkNetworkDispatchQueue(int capacity,
ShrinkNetworkDispatchOverflowPolicy overflowPolicy = ShrinkNetworkDispatchOverflowPolicy.Reject)
{
if (capacity <= 0)
throw new ArgumentOutOfRangeException(nameof(capacity));
_capacity = capacity;
_overflowPolicy = overflowPolicy;
}
public int Capacity => _capacity;
public int PendingCount => Volatile.Read(ref _queuedCount);
public long RejectedCount => Volatile.Read(ref _rejectedCount);
public long DroppedCount => Volatile.Read(ref _droppedCount);
public UniTask<bool> ScheduleAsync(Func<UniTask> callback)
{
if (callback == null)
throw new ArgumentNullException(nameof(callback));
if (Volatile.Read(ref _disposed) != 0)
return UniTask.FromException<bool>(new ObjectDisposedException(nameof(ShrinkNetworkDispatchQueue)));
var item = new WorkItem
{
Callback = callback,
Completion = new UniTaskCompletionSource<bool>()
};
while (true)
{
if (Volatile.Read(ref _queuedCount) >= _capacity)
{
switch (_overflowPolicy)
{
case ShrinkNetworkDispatchOverflowPolicy.Reject:
Interlocked.Increment(ref _rejectedCount);
return UniTask.FromResult(false);
case ShrinkNetworkDispatchOverflowPolicy.DropNewest:
Interlocked.Increment(ref _droppedCount);
return UniTask.FromResult(false);
case ShrinkNetworkDispatchOverflowPolicy.DropOldest:
if (_queue.TryDequeue(out var dropped))
{
Interlocked.Decrement(ref _queuedCount);
Interlocked.Increment(ref _droppedCount);
dropped.Completion.TrySetResult(false);
continue;
}
Thread.Yield();
continue;
default:
throw new ArgumentOutOfRangeException();
}
}
var currentCount = Volatile.Read(ref _queuedCount);
if (currentCount >= _capacity ||
Interlocked.CompareExchange(ref _queuedCount, currentCount + 1, currentCount) != currentCount)
{
continue;
}
lock (_lifecycleLock)
{
if (Volatile.Read(ref _disposed) != 0)
{
Interlocked.Decrement(ref _queuedCount);
item.Completion.TrySetResult(false);
return item.Completion.Task;
}
_queue.Enqueue(item);
return item.Completion.Task;
}
}
}
public UniTask<int> PumpAsync(int maxItems)
{
if (maxItems <= 0)
throw new ArgumentOutOfRangeException(nameof(maxItems));
if (Interlocked.Exchange(ref _pumping, 1) == 1)
return UniTask.FromResult(0);
return PumpCoreAsync(maxItems);
}
public void Dispose()
{
lock (_lifecycleLock)
{
if (Interlocked.Exchange(ref _disposed, 1) != 0)
return;
while (_queue.TryDequeue(out var item))
{
Interlocked.Decrement(ref _queuedCount);
item.Completion.TrySetResult(false);
}
}
}
private async UniTask<int> PumpCoreAsync(int maxItems)
{
var processed = 0;
try
{
while (processed < maxItems && _queue.TryDequeue(out var item))
{
Interlocked.Decrement(ref _queuedCount);
await ExecuteItemAsync(item);
processed++;
}
return processed;
}
finally
{
Volatile.Write(ref _pumping, 0);
}
}
private static async UniTask ExecuteItemAsync(WorkItem item)
{
try
{
await item.Callback();
item.Completion.TrySetResult(true);
}
catch (Exception ex)
{
item.Completion.TrySetException(ex);
}
}
}
public sealed class ShrinkNetworkServiceDiagnosticsSnapshot
{
public int CurrentSessions { get; set; }
public long SessionsConnected { get; set; }
public long SessionsDisconnected { get; set; }
public long PacketsSent { get; set; }
public long PacketsReceived { get; set; }
public long BytesSent { get; set; }
public long BytesReceived { get; set; }
public long RpcStarted { get; set; }
public long RpcCompleted { get; set; }
public long RpcTimedOut { get; set; }
public long RpcCanceled { get; set; }
public long RpcFailed { get; set; }
public long ProtocolViolations { get; set; }
public long AuthRejectedCount { get; set; }
public long PermissionDeniedCount { get; set; }
public long HandlerExceptionCount { get; set; }
public long UnknownOpcodeCount { get; set; }
public long DispatchMissCount { get; set; }
public long SerializationErrorCount { get; set; }
public long DispatchQueueRejectedCount { get; set; }
}
public sealed class ShrinkIncomingPacketValidationResult
{
public static readonly ShrinkIncomingPacketValidationResult Allow = new()
{
IsAllowed = true
};
public bool IsAllowed { get; set; }
public bool DisconnectSession { get; set; }
public string Reason { get; set; } = string.Empty;
public static ShrinkIncomingPacketValidationResult Reject(string reason, bool disconnectSession = true)
{
return new ShrinkIncomingPacketValidationResult
{
IsAllowed = false,
DisconnectSession = disconnectSession,
Reason = reason ?? string.Empty
};
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: eb6513d534c7574478bd07ffd195cf26
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,113 @@
#nullable enable
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Cysharp.Threading.Tasks;
namespace ShrinkNetwork
{
public sealed class ShrinkNetworkSession
{
private readonly ConcurrentDictionary<string, object> _items = new();
private readonly ConcurrentDictionary<string, byte> _permissions = new();
internal ShrinkNetworkSession(long sessionId, string remoteAddress, ShrinkNetworkService service)
{
SessionId = sessionId;
RemoteAddress = remoteAddress;
Service = service;
}
public long SessionId { get; }
public string RemoteAddress { get; internal set; }
public ShrinkNetworkService Service { get; }
public ShrinkNetworkPeerKind PeerKind { get; private set; } = ShrinkNetworkPeerKind.Unknown;
public string SessionToken { get; private set; } = string.Empty;
public DateTimeOffset? SessionTokenExpiresAtUtc { get; private set; }
public IDictionary<string, object> Items => _items;
public void SetPeerKind(ShrinkNetworkPeerKind peerKind)
{
PeerKind = peerKind;
}
public void SetSessionToken(string sessionToken, DateTimeOffset? expiresAtUtc = null)
{
SessionToken = string.IsNullOrWhiteSpace(sessionToken) ? string.Empty : sessionToken.Trim();
SessionTokenExpiresAtUtc = string.IsNullOrWhiteSpace(SessionToken) ? null : expiresAtUtc;
}
public void ClearSessionToken()
{
SessionToken = string.Empty;
SessionTokenExpiresAtUtc = null;
}
public void GrantPermission(string permission)
{
if (!string.IsNullOrWhiteSpace(permission))
_permissions[permission.Trim()] = 0;
}
public void RevokePermission(string permission)
{
if (!string.IsNullOrWhiteSpace(permission))
_permissions.TryRemove(permission.Trim(), out _);
}
public bool HasPermission(string permission)
{
return !string.IsNullOrWhiteSpace(permission) && _permissions.ContainsKey(permission.Trim());
}
public IReadOnlyCollection<string> GetPermissions() => _permissions.Keys.ToArray();
public UniTask SendAsync<TMessage>(TMessage message, string? route = null)
where TMessage : IShrinkNetworkMessage
=> Service.SendAsync(this, message, route);
public UniTask SendAsync(IShrinkNetworkMessage message, string? route = null)
=> Service.SendAsync(this, message, route);
public UniTask NotifyAsync<TMessage>(TMessage message, string? route = null)
where TMessage : IShrinkNetworkMessage
=> Service.SendAsync(this, message, route);
public UniTask NotifyAsync(IShrinkNetworkMessage message, string? route = null)
=> Service.SendAsync(this, message, route);
public UniTask<TResponse> CallAsync<TRequest, TResponse>(TRequest request, string? route = null)
where TRequest : IShrinkNetworkRequest
where TResponse : class, IShrinkNetworkResponse
=> Service.CallAsync<TRequest, TResponse>(this, request, route);
public UniTask<TResponse> CallAsync<TRequest, TResponse>(TRequest request, ShrinkRpcCallOptions options)
where TRequest : IShrinkNetworkRequest
where TResponse : class, IShrinkNetworkResponse
=> Service.CallAsync<TRequest, TResponse>(this, request, options);
public UniTask<TResponse> RpcAsync<TRequest, TResponse>(TRequest request, string? route = null)
where TRequest : IShrinkNetworkRequest
where TResponse : class, IShrinkNetworkResponse
=> Service.CallAsync<TRequest, TResponse>(this, request, route);
public UniTask<TResponse> RpcAsync<TRequest, TResponse>(TRequest request, ShrinkRpcCallOptions options)
where TRequest : IShrinkNetworkRequest
where TResponse : class, IShrinkNetworkResponse
=> Service.CallAsync<TRequest, TResponse>(this, request, options);
public UniTask<TResponse> RpcAsync<TRequest, TResponse>(TRequest request, int timeoutMs,
CancellationToken cancellationToken = default)
where TRequest : IShrinkNetworkRequest
where TResponse : class, IShrinkNetworkResponse
=> Service.CallAsync<TRequest, TResponse>(this, request, new ShrinkRpcCallOptions
{
TimeoutMs = timeoutMs,
CancellationToken = cancellationToken
});
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 26140534bef037a44a72d8c9eadb99b6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 56839208d6041c545bc4c52c7d1c7472
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,54 @@
#nullable enable
using System;
namespace ShrinkNetwork
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class ShrinkNetworkMessageAttribute : Attribute
{
public int Opcode { get; }
public string? Route { get; }
public ShrinkNetworkMessageAttribute(int opcode, string? route = null)
{
Opcode = opcode;
Route = string.IsNullOrWhiteSpace(route) ? null : route.Trim();
}
}
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class ShrinkNetworkSubscriberAttribute : Attribute
{
}
public enum ShrinkNetworkStateSyncRole
{
JoinRequest = 0,
JoinResponse = 1,
Command = 2,
StateDelta = 3,
LeaveNotice = 4,
Heartbeat = 5
}
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
public sealed class ShrinkNetworkStateSyncAttribute : Attribute
{
public string Group { get; }
public ShrinkNetworkStateSyncRole Role { get; }
public ShrinkNetworkStateSyncAttribute(string group, ShrinkNetworkStateSyncRole role)
{
Group = string.IsNullOrWhiteSpace(group) ? string.Empty : group.Trim();
Role = role;
}
}
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
public sealed class ShrinkNetworkSubscribeAttribute : Attribute
{
public ShrinkNetworkAuthority Authority { get; set; } = ShrinkNetworkAuthority.Any;
public string? Permission { get; set; }
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c0a58e8dd0b553646bc53a29187ee423
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,16 @@
namespace ShrinkNetwork
{
public interface IShrinkNetworkMessage
{
}
public interface IShrinkNetworkRequest : IShrinkNetworkMessage
{
}
public interface IShrinkNetworkResponse : IShrinkNetworkMessage
{
int ErrorCode { get; set; }
string ErrorMessage { get; set; }
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2183af3c779211e43933d975814eb435
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,33 @@
#nullable enable
using System;
namespace ShrinkNetwork
{
public static class ShrinkNetworkProtocol
{
public const int CurrentProtocolVersion = 1;
public const int CurrentSchemaVersion = 1;
}
public enum ShrinkNetworkPacketKind
{
Message = 0,
Request = 1,
Response = 2
}
[Serializable]
public sealed class ShrinkNetworkPacket
{
public int ProtocolVersion = ShrinkNetworkProtocol.CurrentProtocolVersion;
public int SchemaVersion = ShrinkNetworkProtocol.CurrentSchemaVersion;
public int Opcode;
public ShrinkRequestToken RequestToken;
public string SessionToken = string.Empty;
public long SessionTokenExpiresAtUnixTimeSeconds;
public string? Route;
public ShrinkNetworkPacketKind Kind;
public byte[] Payload = Array.Empty<byte>();
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d6e511da358c6c64d8c34fdb62928178
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,64 @@
#nullable enable
using System;
using System.Collections.Generic;
namespace ShrinkNetwork
{
public enum ShrinkNetworkPeerKind
{
Unknown = 0,
Client = 1,
Server = 2,
TrustedServer = 3
}
public enum ShrinkNetworkAuthority
{
Any = 0,
ClientOnly = 1,
ServerOnly = 2,
TrustedServerOnly = 3
}
public readonly struct ShrinkNetworkPermissionRequirement
{
public ShrinkNetworkAuthority Authority { get; }
public string? Permission { get; }
public ShrinkNetworkPermissionRequirement(ShrinkNetworkAuthority authority, string? permission)
{
Authority = authority;
Permission = string.IsNullOrWhiteSpace(permission) ? null : permission.Trim();
}
}
internal static class ShrinkNetworkPermissionValidator
{
public static bool IsAllowed(ShrinkNetworkSession session, ShrinkNetworkPermissionRequirement requirement)
{
if (session == null)
return false;
if (!CheckAuthority(session.PeerKind, requirement.Authority))
return false;
if (!string.IsNullOrEmpty(requirement.Permission) && !session.HasPermission(requirement.Permission))
return false;
return true;
}
private static bool CheckAuthority(ShrinkNetworkPeerKind peerKind, ShrinkNetworkAuthority authority)
{
return authority switch
{
ShrinkNetworkAuthority.Any => true,
ShrinkNetworkAuthority.ClientOnly => peerKind == ShrinkNetworkPeerKind.Client,
ShrinkNetworkAuthority.ServerOnly => peerKind == ShrinkNetworkPeerKind.Server || peerKind == ShrinkNetworkPeerKind.TrustedServer,
ShrinkNetworkAuthority.TrustedServerOnly => peerKind == ShrinkNetworkPeerKind.TrustedServer,
_ => false
};
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c6a9dc7f8d56afd41b1e47fac50c5180
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,71 @@
#nullable enable
using System;
using System.Threading;
namespace ShrinkNetwork
{
public readonly struct ShrinkRequestToken : IEquatable<ShrinkRequestToken>
{
public static readonly ShrinkRequestToken Default = new(0);
public ShrinkRequestToken(int value)
{
Value = value;
}
public int Value { get; }
public bool IsDefault => Value == 0;
public bool Equals(ShrinkRequestToken other) => Value == other.Value;
public override bool Equals(object? obj) => obj is ShrinkRequestToken other && Equals(other);
public override int GetHashCode() => Value;
public override string ToString() => Value.ToString();
public static bool operator ==(ShrinkRequestToken left, ShrinkRequestToken right) => left.Equals(right);
public static bool operator !=(ShrinkRequestToken left, ShrinkRequestToken right) => !left.Equals(right);
public static explicit operator int(ShrinkRequestToken token) => token.Value;
public static explicit operator ShrinkRequestToken(int value) => new(value);
}
public sealed class ShrinkRpcCallOptions
{
public int TimeoutMs { get; set; } = 10000;
public string? RouteOverride { get; set; }
public ShrinkRequestToken? RequestTokenOverride { get; set; }
public string? DebugLabel { get; set; }
public CancellationToken CancellationToken { get; set; } = default;
}
public sealed class ShrinkRpcException : Exception
{
public int ErrorCode { get; }
public ShrinkRpcException(int errorCode, string message)
: base(message)
{
ErrorCode = errorCode;
}
}
public static class ShrinkRpcErrorCode
{
public const int Unknown = 1;
public const int Timeout = 2;
public const int Canceled = 3;
public const int HandlerException = 4;
public const int InvalidResponse = 5;
public const int PermissionDenied = 6;
public const int ConnectionClosed = 7;
public const int ProtocolMismatch = 8;
public const int AuthenticationFailed = 9;
public const int SessionTokenExpired = 10;
}
public abstract class ShrinkRpcResponseBase : IShrinkNetworkResponse
{
public int ErrorCode { get; set; }
public string ErrorMessage { get; set; } = string.Empty;
public bool IsSuccess => ErrorCode == 0;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e430341a390625b4a90e3608f6bbc2f9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,49 @@
#nullable enable
namespace ShrinkNetwork
{
public enum ShrinkNetworkTransportEventType
{
Connected = 0,
Disconnected = 1,
Packet = 2
}
public sealed class ShrinkNetworkTransportEvent
{
public ShrinkNetworkTransportEventType Type;
public long SessionId;
public string RemoteAddress = string.Empty;
public byte[] PacketData = System.Array.Empty<byte>();
public static ShrinkNetworkTransportEvent Connected(long sessionId, string remoteAddress)
{
return new ShrinkNetworkTransportEvent
{
Type = ShrinkNetworkTransportEventType.Connected,
SessionId = sessionId,
RemoteAddress = remoteAddress
};
}
public static ShrinkNetworkTransportEvent Disconnected(long sessionId, string remoteAddress)
{
return new ShrinkNetworkTransportEvent
{
Type = ShrinkNetworkTransportEventType.Disconnected,
SessionId = sessionId,
RemoteAddress = remoteAddress
};
}
public static ShrinkNetworkTransportEvent Packet(long sessionId, byte[] packetData)
{
return new ShrinkNetworkTransportEvent
{
Type = ShrinkNetworkTransportEventType.Packet,
SessionId = sessionId,
PacketData = packetData
};
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0fbbbfca75a4ebd4c924c2cf328d549d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 050397bcd638e4e42952d69d28425c9a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,81 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace ShrinkNetwork
{
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
public sealed class ShrinkNetworkMessageRegistryAttribute : Attribute
{
public ShrinkNetworkMessageRegistryAttribute(params Type[] messageTypes)
{
MessageTypes = messageTypes ?? Array.Empty<Type>();
}
public Type[] MessageTypes { get; }
}
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
public sealed class ShrinkNetworkStaticSubscriberRegistryAttribute : Attribute
{
public ShrinkNetworkStaticSubscriberRegistryAttribute(params Type[] subscriberTypes)
{
SubscriberTypes = subscriberTypes ?? Array.Empty<Type>();
}
public Type[] SubscriberTypes { get; }
}
internal static class ShrinkNetworkGeneratedRegistry
{
public static IReadOnlyList<Type> GetAttributedMessageTypes()
=> GetAssemblyRegisteredTypes<ShrinkNetworkMessageRegistryAttribute>(attribute => attribute.MessageTypes);
public static IReadOnlyList<Type> GetStaticSubscriberTypes()
=> GetAssemblyRegisteredTypes<ShrinkNetworkStaticSubscriberRegistryAttribute>(attribute => attribute.SubscriberTypes);
public static void RegisterAll(ShrinkNetworkService service)
{
if (service == null)
throw new ArgumentNullException(nameof(service));
ShrinkNetworkRegHelper.RegisterAttributedMessages(service, GetAttributedMessageTypes());
ShrinkNetworkRegHelper.RegisterStaticHandlers(service, GetStaticSubscriberTypes());
}
private static IReadOnlyList<Type> GetAssemblyRegisteredTypes<TAttribute>(Func<TAttribute, Type[]> selector)
where TAttribute : Attribute
{
var types = new List<Type>();
var seen = new HashSet<Type>();
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
object[] attributes;
try
{
attributes = assembly.GetCustomAttributes(typeof(TAttribute), false);
}
catch
{
continue;
}
foreach (var attribute in attributes.OfType<TAttribute>())
{
foreach (var registeredType in selector(attribute) ?? Array.Empty<Type>())
{
if (registeredType == null || !seen.Add(registeredType))
continue;
types.Add(registeredType);
}
}
}
return types;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c8557e972f759fd4cac12069d5ac43e8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,60 @@
#nullable enable
using System;
using System.Collections.Generic;
namespace ShrinkNetwork
{
public sealed class ShrinkNetworkMessageRegistry
{
private readonly Dictionary<int, ShrinkNetworkMessageMeta> _opcodeToMeta = new();
private readonly Dictionary<Type, ShrinkNetworkMessageMeta> _typeToMeta = new();
public void Register<TMessage>(int opcode, string? route = null) where TMessage : IShrinkNetworkMessage
=> Register(typeof(TMessage), opcode, route);
public void Register(Type messageType, int opcode, string? route = null)
{
if (messageType == null)
throw new ArgumentNullException(nameof(messageType));
if (!typeof(IShrinkNetworkMessage).IsAssignableFrom(messageType))
throw new ArgumentException($"Type {messageType.FullName} is not a network message.", nameof(messageType));
if (_opcodeToMeta.ContainsKey(opcode))
throw new InvalidOperationException($"Opcode {opcode} is already registered.");
if (_typeToMeta.ContainsKey(messageType))
throw new InvalidOperationException($"Message type {messageType.FullName} is already registered.");
var meta = new ShrinkNetworkMessageMeta(opcode, messageType, route);
_opcodeToMeta.Add(opcode, meta);
_typeToMeta.Add(messageType, meta);
}
public bool TryGetMeta(int opcode, out ShrinkNetworkMessageMeta? meta) => _opcodeToMeta.TryGetValue(opcode, out meta);
public bool TryGetMeta(Type type, out ShrinkNetworkMessageMeta? meta) => _typeToMeta.TryGetValue(type, out meta);
public ShrinkNetworkMessageMeta GetMeta<TMessage>() where TMessage : IShrinkNetworkMessage => GetMeta(typeof(TMessage));
public ShrinkNetworkMessageMeta GetMeta(Type type)
{
if (_typeToMeta.TryGetValue(type, out var meta))
return meta;
throw new KeyNotFoundException($"Message type {type.FullName} is not registered.");
}
}
public sealed class ShrinkNetworkMessageMeta
{
public int Opcode { get; }
public Type MessageType { get; }
public string? Route { get; }
public ShrinkNetworkMessageMeta(int opcode, Type messageType, string? route)
{
Opcode = opcode;
MessageType = messageType;
Route = route;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3ba2cec4b827a0a4b86f42a16fd10393
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,210 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Reflection;
using Cysharp.Threading.Tasks;
namespace ShrinkNetwork
{
public static class ShrinkNetworkRegHelper
{
private static readonly MethodInfo AwaitUniTaskResponseMethod =
typeof(ShrinkNetworkRegHelper).GetMethod(nameof(AwaitUniTaskResponse), BindingFlags.NonPublic | BindingFlags.Static)!;
public static void RegisterAttributedMessages(ShrinkNetworkService service)
{
RegisterAttributedMessages(service, ShrinkNetworkGeneratedRegistry.GetAttributedMessageTypes());
}
public static void RegisterAttributedMessages(ShrinkNetworkService service, IEnumerable<Type> messageTypes)
{
if (service == null)
throw new ArgumentNullException(nameof(service));
if (messageTypes == null)
throw new ArgumentNullException(nameof(messageTypes));
foreach (var type in messageTypes)
{
if (type == null || !typeof(IShrinkNetworkMessage).IsAssignableFrom(type))
continue;
var attr = type.GetCustomAttribute<ShrinkNetworkMessageAttribute>(false);
if (attr == null || service.MessageRegistry.TryGetMeta(type, out _))
continue;
service.RegisterMessage(type, attr.Opcode, attr.Route);
}
}
public static void RegisterStaticHandlers(ShrinkNetworkService service)
=> RegisterStaticHandlers(service, ShrinkNetworkGeneratedRegistry.GetStaticSubscriberTypes());
public static void RegisterStaticHandlers(ShrinkNetworkService service, IEnumerable<Type> subscriberTypes)
{
if (service == null)
throw new ArgumentNullException(nameof(service));
if (subscriberTypes == null)
throw new ArgumentNullException(nameof(subscriberTypes));
foreach (var type in subscriberTypes)
{
if (type == null || type.GetCustomAttribute<ShrinkNetworkSubscriberAttribute>(false) == null)
continue;
ScanMethodsAndRegister(service, null, type,
type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic));
}
}
public static void RegisterHandlers(ShrinkNetworkService service, object target)
=> RegisterHandlersInternal(service, target);
private static void RegisterHandlersInternal(ShrinkNetworkService service, object? target)
{
if (target == null)
return;
var ownerType = target.GetType();
if (ownerType.GetCustomAttribute<ShrinkNetworkSubscriberAttribute>(false) == null)
return;
ScanMethodsAndRegister(service, target, ownerType,
ownerType.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic));
}
private static void ScanMethodsAndRegister(ShrinkNetworkService service, object? target, Type ownerType, MethodInfo[] methods)
{
foreach (var method in methods)
{
if (target != null && method.IsStatic)
continue;
var attributes = method.GetCustomAttributes(typeof(ShrinkNetworkSubscribeAttribute), false);
if (attributes.Length == 0)
continue;
var subscribeAttr = (ShrinkNetworkSubscribeAttribute)attributes[0];
var requirement = new ShrinkNetworkPermissionRequirement(subscribeAttr.Authority, subscribeAttr.Permission);
if (!TryParseHandlerSignature(method, out var hasContext, out var messageType, out var responseType))
{
ShrinkNetworkLogger.Warn($"[ShrinkNetwork] Invalid handler signature: {ownerType.FullName}.{method.Name}");
continue;
}
var resolvedMessageType = messageType!;
EnsureMessageRegistered(service, resolvedMessageType);
if (responseType != null)
EnsureMessageRegistered(service, responseType);
if (responseType == null)
{
service.RegisterHandler(resolvedMessageType, (context, message) =>
InvokeMessageHandler(target, method, hasContext, context, message), requirement);
continue;
}
var resolvedResponseType = responseType!;
service.RegisterRequestHandler(resolvedMessageType, resolvedResponseType, (context, request) =>
InvokeRequestHandler(target, method, hasContext, resolvedResponseType, context, request), requirement);
}
}
private static bool TryParseHandlerSignature(MethodInfo method, out bool hasContext, out Type? messageType, out Type? responseType)
{
hasContext = false;
messageType = null;
responseType = null;
var parameters = method.GetParameters();
if (parameters.Length == 1)
{
messageType = parameters[0].ParameterType;
}
else if (parameters.Length == 2 && parameters[0].ParameterType == typeof(ShrinkNetworkContext))
{
hasContext = true;
messageType = parameters[1].ParameterType;
}
else
{
return false;
}
if (!typeof(IShrinkNetworkMessage).IsAssignableFrom(messageType))
return false;
var returnType = method.ReturnType;
if (returnType == typeof(void) || returnType == typeof(UniTask))
return true;
if (typeof(IShrinkNetworkResponse).IsAssignableFrom(returnType))
{
responseType = returnType;
return true;
}
if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(UniTask<>))
{
var resultType = returnType.GetGenericArguments()[0];
if (typeof(IShrinkNetworkResponse).IsAssignableFrom(resultType))
{
responseType = resultType;
return true;
}
}
return false;
}
private static void EnsureMessageRegistered(ShrinkNetworkService service, Type messageType)
{
if (service.MessageRegistry.TryGetMeta(messageType, out _))
return;
var attr = messageType.GetCustomAttribute<ShrinkNetworkMessageAttribute>(false);
if (attr == null)
throw new InvalidOperationException($"Message type {messageType.FullName} must declare [ShrinkNetworkMessage].");
service.RegisterMessage(messageType, attr.Opcode, attr.Route);
}
private static async UniTask InvokeMessageHandler(object? target, MethodInfo method, bool hasContext,
ShrinkNetworkContext context, object message)
{
var args = hasContext ? new object?[] { context, message } : new object?[] { message };
var result = method.Invoke(target, args);
if (method.ReturnType == typeof(UniTask))
await (UniTask)(result ?? throw new InvalidOperationException($"Handler returned null UniTask: {method.DeclaringType?.FullName}.{method.Name}"));
}
private static async UniTask<object?> InvokeRequestHandler(object? target, MethodInfo method, bool hasContext,
Type responseType, ShrinkNetworkContext context, object request)
{
var args = hasContext ? new object?[] { context, request } : new object?[] { request };
var result = method.Invoke(target, args);
if (result == null)
return null;
if (responseType.IsInstanceOfType(result))
return result;
if (method.ReturnType.IsGenericType && method.ReturnType.GetGenericTypeDefinition() == typeof(UniTask<>))
return await AwaitUniTaskResponseObject(result, responseType);
return result;
}
private static UniTask<object?> AwaitUniTaskResponseObject(object taskObject, Type responseType)
{
return (UniTask<object?>)AwaitUniTaskResponseMethod
.MakeGenericMethod(responseType)
.Invoke(null, new[] { taskObject })!;
}
private static async UniTask<object?> AwaitUniTaskResponse<TResponse>(UniTask<TResponse> task)
where TResponse : class, IShrinkNetworkResponse
{
return await task;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fc0b4f8cf9205df4caedc2cb1dc8b6bf
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,152 @@
#nullable enable
using System;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
namespace ShrinkNetwork
{
public sealed class ShrinkNetworkRouter
{
private sealed class MessageHandlerRegistration
{
public ShrinkNetworkPermissionRequirement Requirement;
public Func<ShrinkNetworkContext, object, UniTask> Handler = null!;
}
private sealed class RequestHandlerRegistration
{
public Type ResponseType = null!;
public ShrinkNetworkPermissionRequirement Requirement;
public Func<ShrinkNetworkContext, object, UniTask<object?>> Handler = null!;
}
private readonly Dictionary<Type, MessageHandlerRegistration> _messageHandlers = new();
private readonly Dictionary<Type, RequestHandlerRegistration> _requestHandlers = new();
public void RegisterHandler<TMessage>(Func<ShrinkNetworkContext, TMessage, UniTask> handler,
ShrinkNetworkPermissionRequirement requirement = default)
where TMessage : IShrinkNetworkMessage
{
RegisterHandler(typeof(TMessage), (context, message) => handler(context, (TMessage)message), requirement);
}
public void RegisterHandler(Type messageType, Func<ShrinkNetworkContext, object, UniTask> handler,
ShrinkNetworkPermissionRequirement requirement = default)
{
if (messageType == null)
throw new ArgumentNullException(nameof(messageType));
if (handler == null)
throw new ArgumentNullException(nameof(handler));
if (_messageHandlers.ContainsKey(messageType) || _requestHandlers.ContainsKey(messageType))
throw new InvalidOperationException($"Handler already exists for {messageType.FullName}.");
_messageHandlers.Add(messageType, new MessageHandlerRegistration
{
Requirement = requirement,
Handler = handler
});
}
public void RegisterRequestHandler<TRequest, TResponse>(Func<ShrinkNetworkContext, TRequest, UniTask<TResponse>> handler,
ShrinkNetworkPermissionRequirement requirement = default)
where TRequest : IShrinkNetworkRequest
where TResponse : class, IShrinkNetworkResponse
{
RegisterRequestHandler(typeof(TRequest), typeof(TResponse),
async (context, message) => await handler(context, (TRequest)message), requirement);
}
public void RegisterRequestHandler(Type requestType, Type responseType,
Func<ShrinkNetworkContext, object, UniTask<object?>> handler,
ShrinkNetworkPermissionRequirement requirement = default)
{
if (requestType == null)
throw new ArgumentNullException(nameof(requestType));
if (responseType == null)
throw new ArgumentNullException(nameof(responseType));
if (handler == null)
throw new ArgumentNullException(nameof(handler));
if (_messageHandlers.ContainsKey(requestType) || _requestHandlers.ContainsKey(requestType))
throw new InvalidOperationException($"Handler already exists for {requestType.FullName}.");
_requestHandlers.Add(requestType, new RequestHandlerRegistration
{
ResponseType = responseType,
Requirement = requirement,
Handler = handler
});
}
public async UniTask<bool> DispatchAsync(ShrinkNetworkContext context, object message, Type messageType)
{
if (context.Packet.Kind == ShrinkNetworkPacketKind.Request &&
_requestHandlers.TryGetValue(messageType, out var requestHandler))
{
if (!ShrinkNetworkPermissionValidator.IsAllowed(context.Session, requestHandler.Requirement))
{
context.Service.ReportPermissionDenied(messageType);
var denied = CreatePermissionDeniedResponse(requestHandler.ResponseType, requestHandler.Requirement);
await context.Service.SendResponseAsync(context.Session, denied, requestHandler.ResponseType,
context.Packet.RequestToken, context.Packet.Route);
return true;
}
try
{
var response = await requestHandler.Handler(context, message);
if (response is IShrinkNetworkResponse networkResponse)
await context.Service.SendResponseAsync(context.Session, networkResponse, requestHandler.ResponseType,
context.Packet.RequestToken, context.Packet.Route);
}
catch (Exception ex)
{
context.Service.ReportHandlerException(messageType, ex);
var errorResponse = CreateErrorResponse(requestHandler.ResponseType, ex);
await context.Service.SendResponseAsync(context.Session, errorResponse, requestHandler.ResponseType,
context.Packet.RequestToken, context.Packet.Route);
}
return true;
}
if (_messageHandlers.TryGetValue(messageType, out var messageHandler))
{
if (!ShrinkNetworkPermissionValidator.IsAllowed(context.Session, messageHandler.Requirement))
{
context.Service.ReportPermissionDenied(messageType);
ShrinkNetworkLogger.Warn($"[ShrinkNetwork] Permission denied for message {messageType.FullName} on session {context.Session.SessionId}.");
return true;
}
await messageHandler.Handler(context, message);
return true;
}
return false;
}
private static IShrinkNetworkResponse CreateErrorResponse(Type responseType, Exception ex)
{
if (Activator.CreateInstance(responseType) is not IShrinkNetworkResponse response)
throw new InvalidOperationException($"Response type {responseType.FullName} cannot be instantiated.", ex);
response.ErrorCode = ShrinkRpcErrorCode.HandlerException;
response.ErrorMessage = ex.Message;
return response;
}
private static IShrinkNetworkResponse CreatePermissionDeniedResponse(Type responseType,
ShrinkNetworkPermissionRequirement requirement)
{
if (Activator.CreateInstance(responseType) is not IShrinkNetworkResponse response)
throw new InvalidOperationException($"Response type {responseType.FullName} cannot be instantiated.");
response.ErrorCode = ShrinkRpcErrorCode.PermissionDenied;
response.ErrorMessage = string.IsNullOrEmpty(requirement.Permission)
? $"Permission denied. Authority={requirement.Authority}"
: $"Permission denied. Authority={requirement.Authority}, Permission={requirement.Permission}";
return response;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 89e99852277437b439ff7891cd6aa8d5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a5ee1de0ef0ca6a4ba49cc8d900808f5
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,11 @@
using System;
namespace ShrinkNetwork
{
public interface IShrinkNetworkSerializer
{
byte[] Serialize(object value);
object Deserialize(byte[] payload, Type type);
T Deserialize<T>(byte[] payload);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8f1353960f8123d4f96a4815b3627051
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,29 @@
#nullable enable
using System;
using System.Text;
using Newtonsoft.Json;
namespace ShrinkNetwork
{
public sealed class ShrinkJsonNetworkSerializer : IShrinkNetworkSerializer
{
private static readonly JsonSerializerSettings Settings = new()
{
NullValueHandling = NullValueHandling.Ignore,
DefaultValueHandling = DefaultValueHandling.Include,
Formatting = Formatting.None
};
public byte[] Serialize(object value)
=> Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(value, Settings));
public object Deserialize(byte[] payload, Type type)
=> JsonConvert.DeserializeObject(Encoding.UTF8.GetString(payload), type, Settings)
?? throw new JsonSerializationException($"Failed to deserialize payload into {type.FullName}.");
public T Deserialize<T>(byte[] payload)
=> JsonConvert.DeserializeObject<T>(Encoding.UTF8.GetString(payload), Settings)
?? throw new JsonSerializationException($"Failed to deserialize payload into {typeof(T).FullName}.");
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f2a0f828927e72b4b9d1542cc8a24b8c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,150 @@
#nullable enable
using System;
using System.Linq;
using System.Reflection;
namespace ShrinkNetwork
{
public sealed class ShrinkMessagePackNetworkSerializer : IShrinkNetworkSerializer
{
private readonly MethodInfo _serializeMethod;
private readonly MethodInfo _deserializeMethod;
private readonly object? _serializerOptions;
public ShrinkMessagePackNetworkSerializer()
{
var serializerType = Type.GetType("MessagePack.MessagePackSerializer, MessagePack");
if (serializerType == null)
{
throw new InvalidOperationException(
"MessagePack assembly was not found. Please install MessagePack-CSharp before using ShrinkMessagePackNetworkSerializer.");
}
_serializerOptions = ResolveSerializerOptions(serializerType.Assembly);
var serializeMethod = serializerType
.GetMethods(BindingFlags.Public | BindingFlags.Static)
.FirstOrDefault(m =>
{
if (m.Name != "Serialize")
return false;
var parameters = m.GetParameters();
return parameters.Length >= 2 &&
parameters[0].ParameterType == typeof(Type) &&
parameters[1].ParameterType == typeof(object);
});
var deserializeMethod = serializerType
.GetMethods(BindingFlags.Public | BindingFlags.Static)
.FirstOrDefault(m =>
{
if (m.Name != "Deserialize")
return false;
var parameters = m.GetParameters();
return parameters.Length >= 2 &&
parameters[0].ParameterType == typeof(Type) &&
(parameters[1].ParameterType == typeof(byte[]) ||
parameters[1].ParameterType == typeof(ReadOnlyMemory<byte>));
});
if (serializeMethod == null || deserializeMethod == null)
throw new MissingMethodException("MessagePack serialize/deserialize API not found.");
_serializeMethod = serializeMethod;
_deserializeMethod = deserializeMethod;
}
public byte[] Serialize(object value)
{
if (value == null)
return Array.Empty<byte>();
var parameters = BuildParameters(_serializeMethod, value.GetType(), value, _serializerOptions);
return (byte[])_serializeMethod.Invoke(null, parameters)!;
}
public object Deserialize(byte[] payload, Type type)
{
payload ??= Array.Empty<byte>();
var parameters = BuildParameters(_deserializeMethod, type, payload, _serializerOptions);
return _deserializeMethod.Invoke(null, parameters)
?? throw new InvalidOperationException($"MessagePack returned null for type {type.FullName}.");
}
public T Deserialize<T>(byte[] payload)
{
return (T)Deserialize(payload, typeof(T));
}
private static object?[] BuildParameters(MethodInfo method, Type type, object valueOrBytes, object? serializerOptions)
{
var parameters = method.GetParameters();
var args = new object?[parameters.Length];
if (parameters.Length > 0)
args[0] = type;
if (parameters.Length > 1)
args[1] = ConvertPrimaryArgument(parameters[1].ParameterType, valueOrBytes);
for (var i = 2; i < parameters.Length; i++)
{
args[i] = ResolveAdditionalArgument(parameters[i], serializerOptions);
}
return args;
}
private static object? ResolveAdditionalArgument(ParameterInfo parameter, object? serializerOptions)
{
if (serializerOptions != null && parameter.ParameterType.IsInstanceOfType(serializerOptions))
return serializerOptions;
return parameter.HasDefaultValue
? parameter.DefaultValue
: GetDefault(parameter.ParameterType);
}
private static object ConvertPrimaryArgument(Type parameterType, object value)
{
if (parameterType == typeof(ReadOnlyMemory<byte>) && value is byte[] bytes)
return new ReadOnlyMemory<byte>(bytes);
return value;
}
private static object? ResolveSerializerOptions(Assembly serializerAssembly)
{
var contractlessResolverType = serializerAssembly.GetType("MessagePack.Resolvers.ContractlessStandardResolver");
if (contractlessResolverType != null)
{
var optionsField = contractlessResolverType.GetField("Options",
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static);
var options = optionsField?.GetValue(null);
if (options != null)
return options;
var instanceField = contractlessResolverType.GetField("Instance",
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static);
var instance = instanceField?.GetValue(null);
if (instance != null)
{
var optionsType = serializerAssembly.GetType("MessagePack.MessagePackSerializerOptions");
var standardProperty = optionsType?.GetProperty("Standard", BindingFlags.Public | BindingFlags.Static);
var standardOptions = standardProperty?.GetValue(null);
var withResolverMethod = optionsType?.GetMethod("WithResolver", BindingFlags.Public | BindingFlags.Instance);
var resolvedOptions = withResolverMethod?.Invoke(standardOptions, new[] { instance });
if (resolvedOptions != null)
return resolvedOptions;
}
}
return null;
}
private static object? GetDefault(Type type)
{
return type.IsValueType ? Activator.CreateInstance(type) : null;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 88c6239e9fc049f43b2999d7311eec1d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,16 @@
{
"name": "ShrinkNetwork.Runtime",
"rootNamespace": "ShrinkNetwork",
"references": [
"UniTask",
"Newtonsoft.Json"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": true,
"overrideReferences": false,
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 74abbc4dbafd9ac449e816fe51c954b9
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 1acf353c64b11ae49a4410edde41da6a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4eee09e255b4c294983b08e0d13eb8e1
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,9 @@
using Cysharp.Threading.Tasks;
namespace ShrinkNetwork
{
public interface IShrinkNetworkAsyncTransport : IShrinkNetworkTransport
{
UniTask SendAsync(long sessionId, byte[] packetData);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c9741e558eb992d4eae93726109250eb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -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
};
}
}
}
@@ -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;
}
}
}
@@ -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: