#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 CompletionSource = null!; } private readonly ConcurrentDictionary _sessions = new(); private readonly ConcurrentDictionary _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 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? IncomingPacketValidator { get; set; } public IShrinkNetworkDispatchScheduler DispatchScheduler { get => _dispatchScheduler; set => _dispatchScheduler = value ?? throw new ArgumentNullException(nameof(value)); } public event Action? OnSessionConnected; public event Action? 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(int opcode, string? route = null) where TMessage : IShrinkNetworkMessage => MessageRegistry.Register(opcode, route); public void RegisterMessage(Type messageType, int opcode, string? route = null) => MessageRegistry.Register(messageType, opcode, route); public void RegisterHandler(Func handler, ShrinkNetworkPermissionRequirement requirement = default) where TMessage : IShrinkNetworkMessage => Router.RegisterHandler(handler, requirement); public void RegisterHandler(Type messageType, Func handler, ShrinkNetworkPermissionRequirement requirement = default) => Router.RegisterHandler(messageType, handler, requirement); public void RegisterRequestHandler(Func> handler, ShrinkNetworkPermissionRequirement requirement = default) where TRequest : IShrinkNetworkRequest where TResponse : class, IShrinkNetworkResponse => Router.RegisterRequestHandler(handler, requirement); public void RegisterRequestHandler(Type requestType, Type responseType, Func> handler, ShrinkNetworkPermissionRequirement requirement = default) => Router.RegisterRequestHandler(requestType, responseType, handler, requirement); public void RegisterRpc(Func> 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(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(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); /// /// Sends a message whose payload has already been serialized. The payload is /// treated as immutable and can be reused for multiple sessions. /// public UniTask SendSerializedAsync(ShrinkNetworkSession session, Type messageType, byte[] payload, string? route = null) { return SendPacketAsync(session, messageType, ShrinkNetworkPacketKind.Message, ShrinkRequestToken.Default, route, payload); } public UniTask CallAsync(ShrinkNetworkSession session, TRequest request, string? route = null) where TRequest : IShrinkNetworkRequest where TResponse : class, IShrinkNetworkResponse => CallAsync(session, request, new ShrinkRpcCallOptions { RouteOverride = route }); public async UniTask CallAsync(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() }; 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(requestToken, pending, options); EnsureResponseSucceeded(response); Interlocked.Increment(ref _rpcCompleted); return response; } catch { _pendingRequests.TryRemove(requestToken, out _); throw; } } public UniTask RpcAsync(ShrinkNetworkSession session, TRequest request, string? route = null) where TRequest : IShrinkNetworkRequest where TResponse : class, IShrinkNetworkResponse => CallAsync(session, request, route); public UniTask RpcAsync(ShrinkNetworkSession session, TRequest request, ShrinkRpcCallOptions options) where TRequest : IShrinkNetworkRequest where TResponse : class, IShrinkNetworkResponse => CallAsync(session, request, options); internal UniTask SendResponseAsync(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(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(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 WaitForPendingResponse(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 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 { /// /// Schedules a transport event. The result is false when the scheduler /// rejects or drops the work because its bounded queue is full. /// UniTask ScheduleAsync(Func callback); } public static class ShrinkNetworkDispatchSchedulers { public static IShrinkNetworkDispatchScheduler Inline { get; } = new ShrinkNetworkInlineDispatchScheduler(); } public sealed class ShrinkNetworkInlineDispatchScheduler : IShrinkNetworkDispatchScheduler { public async UniTask ScheduleAsync(Func callback) { if (callback == null) throw new ArgumentNullException(nameof(callback)); await callback(); return true; } } public enum ShrinkNetworkDispatchOverflowPolicy { Reject = 0, DropNewest = 1, DropOldest = 2 } /// /// A caller-pumped, bounded dispatch queue. Unity can pump it from Update /// while a dedicated server can keep the default inline scheduler. /// public sealed class ShrinkNetworkDispatchQueue : IShrinkNetworkDispatchScheduler, IDisposable { private sealed class WorkItem { public Func Callback = null!; public UniTaskCompletionSource Completion = null!; } private readonly ConcurrentQueue _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 ScheduleAsync(Func callback) { if (callback == null) throw new ArgumentNullException(nameof(callback)); if (Volatile.Read(ref _disposed) != 0) return UniTask.FromException(new ObjectDisposedException(nameof(ShrinkNetworkDispatchQueue))); var item = new WorkItem { Callback = callback, Completion = new UniTaskCompletionSource() }; 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 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 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 }; } } }