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