#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 ?? new ShrinkNetworkInlineDispatchScheduler(); } 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)); } /// Reliable receive work could not be processed. Session-control transports also disconnect the peer. public event Action? OnDispatchRejected; 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, Array.Empty(), message); } private async UniTask SendPacketAsync(ShrinkNetworkSession session, Type messageType, ShrinkNetworkPacketKind kind, ShrinkRequestToken requestToken, string? route, byte[] payload, object? message = null) { 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 }; using var encoded = ShrinkPacketCodec.Encode(packet, Serializer, message); var packetData = encoded.WrittenMemory; Interlocked.Increment(ref _packetsSent); Interlocked.Add(ref _bytesSent, packetData.Length); if (transport is IShrinkNetworkMemoryTransport memoryTransport) { await memoryTransport.SendAsync(session.SessionId, packetData); return; } if (transport is IShrinkNetworkAsyncTransport asyncTransport) { await asyncTransport.SendAsync(session.SessionId, packetData.ToArray()); return; } transport.Send(session.SessionId, packetData.ToArray()); } 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; } // Responses complete pending RPCs independently of the serial handler queue, including nested calls. if (evt.Type == ShrinkNetworkTransportEventType.Packet && ShrinkPacketCodec.IsResponse(evt.PacketData)) { HandlePacketAsync(evt).Forget(); return; } ScheduleTransportEventAsync(evt).Forget(); } private async UniTaskVoid ScheduleTransportEventAsync(ShrinkNetworkTransportEvent evt) { try { var scheduler = DispatchScheduler; _sessions.TryGetValue(evt.SessionId, out var receivedSession); UniTask DispatchCurrent() { return receivedSession != null && _sessions.TryGetValue(evt.SessionId, out var current) && ReferenceEquals(current, receivedSession) ? HandleTransportEventAsync(evt) : UniTask.CompletedTask; } var scheduled = scheduler is IShrinkNetworkPacketDispatchScheduler queue ? await queue.ScheduleAsync(evt.SessionId, evt.PacketData?.Length ?? 0, DispatchCurrent) : await scheduler.ScheduleAsync(DispatchCurrent); if (!scheduled) { Interlocked.Increment(ref _dispatchQueueRejectedCount); OnDispatchRejected?.Invoke(evt.SessionId); if (_transport is IShrinkNetworkSessionControlTransport control) control.DisconnectSession(evt.SessionId, "SHRINK-NET-CONGESTION: reliable receive queue rejected work."); } } 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 = ShrinkPacketCodec.Decode(evt.PacketData); if (!_sessions.TryGetValue(evt.SessionId, out var session)) { // Queued packets from a disconnected session cannot resurrect its state. return; } if (!ValidatePacketCompatibility(packet, evt.SessionId)) return; ApplySessionTokenFromPacket(session, packet); if (packet.Kind == ShrinkNetworkPacketKind.Response) { HandleResponse(session, 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 = DeserializePayload(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 (ShrinkProtocolException ex) { Interlocked.Increment(ref _protocolViolations); if (DisconnectOnProtocolViolation && _transport is IShrinkNetworkSessionControlTransport control) control.DisconnectSession(evt.SessionId, ex.Message); } catch (Exception ex) { Interlocked.Increment(ref _serializationErrorCount); ShrinkNetworkLogger.Exception(ex); ShrinkNetworkLogger.Error($"[ShrinkNetwork] Packet handling failed: {ex.Message}"); } } private void HandleResponse(ShrinkNetworkSession session, ShrinkNetworkPacket packet) { if (!_pendingRequests.TryGetValue(packet.RequestToken, out var expected) || expected.SessionId != session.SessionId || !_pendingRequests.TryRemove(packet.RequestToken, out var pending)) { ShrinkNetworkLogger.Warn($"[ShrinkNetwork] Pending request not found. RequestToken={packet.RequestToken}"); return; } try { var response = DeserializePayload(packet.Payload, pending.ResponseType); pending.CompletionSource.TrySetResult(response); } catch (Exception ex) { pending.CompletionSource.TrySetException(ex); } } private object DeserializePayload(ReadOnlyMemory payload, Type type) => Serializer is IShrinkNetworkBufferSerializer buffered ? buffered.Deserialize(payload, type) : Serializer.Deserialize(payload.ToArray(), type); 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 => new ShrinkNetworkInlineDispatchScheduler(); } public interface IShrinkNetworkPacketDispatchScheduler : IShrinkNetworkDispatchScheduler { UniTask ScheduleAsync(long sessionId, int bytes, Func callback); } /// Automatically drains a bounded serial queue. Callbacks start on the initiating transport/continuation thread. public sealed class ShrinkNetworkInlineDispatchScheduler : IShrinkNetworkPacketDispatchScheduler, IDisposable { private readonly object _gate = new(); private readonly ShrinkNetworkWorkQueue _queue = new(4096, 64 * 1024 * 1024); private bool _draining; public ShrinkNetworkQueueDiagnostics CaptureDiagnostics() => _queue.CaptureDiagnostics(); public UniTask ScheduleAsync(Func callback) => ScheduleAsync(0, 0, callback); public async UniTask ScheduleAsync(long sessionId, int bytes, Func callback) { var completion = _queue.EnqueueAsync(sessionId, "receive", bytes, callback); var start = false; lock (_gate) { if (!_draining) { _draining = true; start = true; } } if (start) DrainAsync().Forget(); return await completion == ShrinkNetworkQueueResult.Completed; } private async UniTask DrainAsync() { while (true) { await _queue.PumpAsync(int.MaxValue); lock (_gate) { if (_queue.PendingCount == 0) { _draining = false; return; } } } } public void Dispose() => _queue.Dispose(); } public enum ShrinkNetworkDispatchOverflowPolicy { Reject = 0, DropNewest = 1, DropOldest = 2 } /// Caller-pumped serial receive queue; rejects overflow without silently dropping reliable packets. public sealed class ShrinkNetworkDispatchQueue : IShrinkNetworkPacketDispatchScheduler, IDisposable { private readonly ShrinkNetworkWorkQueue _queue; public ShrinkNetworkDispatchQueue(int capacity, ShrinkNetworkDispatchOverflowPolicy overflowPolicy = ShrinkNetworkDispatchOverflowPolicy.Reject, long byteCapacity = 64 * 1024 * 1024, int perSessionCapacity = int.MaxValue) { if (overflowPolicy != ShrinkNetworkDispatchOverflowPolicy.Reject) throw new ArgumentException("Protocol v2 reliable dispatch requires Reject. Use an explicit state key on ShrinkNetworkWorkQueue for replaceable state.", nameof(overflowPolicy)); Capacity = capacity; _queue = new ShrinkNetworkWorkQueue(capacity, byteCapacity, perSessionCapacity); } public int Capacity { get; } public int PendingCount => _queue.PendingCount; public long RejectedCount => CaptureDiagnostics().Rejected; public long DroppedCount => 0; public ShrinkNetworkQueueDiagnostics CaptureDiagnostics() => _queue.CaptureDiagnostics(); public UniTask ScheduleAsync(Func callback) => ScheduleAsync(0, 0, callback); public async UniTask ScheduleAsync(long sessionId, int bytes, Func callback) => await _queue.EnqueueAsync(sessionId, "receive", bytes, callback) == ShrinkNetworkQueueResult.Completed; public UniTask PumpAsync(int maxItems) => _queue.PumpAsync(maxItems); public UniTask PumpAsync(int maxItems, long maxBytes, TimeSpan timeBudget) => _queue.PumpAsync(maxItems, maxBytes, timeBudget); public void Dispose() => _queue.Dispose(); } 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 }; } } }