Init enter game

This commit is contained in:
Naruse
2025-06-14 11:15:32 +08:00
commit 6a03b39f07
568 changed files with 92872 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
#if !NEED_POH_SHIM
using System.Buffers;
namespace KianaBH.KcpSharp.Base;
internal sealed class ArrayMemoryOwner : IMemoryOwner<byte>
{
private readonly byte[] _buffer;
public ArrayMemoryOwner(byte[] buffer)
{
_buffer = buffer ?? throw new ArgumentNullException(nameof(buffer));
}
public Memory<byte> Memory => _buffer;
public void Dispose()
{
}
}
#endif

View File

@@ -0,0 +1,112 @@
using System.Diagnostics;
using System.Threading.Tasks.Sources;
namespace KianaBH.KcpSharp.Base;
internal class AsyncAutoResetEvent<T> : IValueTaskSource<T>
{
private bool _activeWait;
private bool _isSet;
private SpinLock _lock;
private ManualResetValueTaskSourceCore<T> _rvtsc;
private bool _signaled;
private T? _value;
public AsyncAutoResetEvent()
{
_rvtsc = new ManualResetValueTaskSourceCore<T>
{
RunContinuationsAsynchronously = true
};
_lock = new SpinLock();
}
T IValueTaskSource<T>.GetResult(short token)
{
try
{
return _rvtsc.GetResult(token);
}
finally
{
_rvtsc.Reset();
var lockTaken = false;
try
{
_lock.Enter(ref lockTaken);
_activeWait = false;
_signaled = false;
}
finally
{
if (lockTaken) _lock.Exit();
}
}
}
ValueTaskSourceStatus IValueTaskSource<T>.GetStatus(short token)
{
return _rvtsc.GetStatus(token);
}
void IValueTaskSource<T>.OnCompleted(Action<object?> continuation, object? state, short token,
ValueTaskSourceOnCompletedFlags flags)
{
_rvtsc.OnCompleted(continuation, state, token, flags);
}
public ValueTask<T> WaitAsync()
{
var lockTaken = false;
try
{
_lock.Enter(ref lockTaken);
if (_activeWait)
return new ValueTask<T>(
Task.FromException<T>(new InvalidOperationException("Another thread is already waiting.")));
if (_isSet)
{
_isSet = false;
var value = _value!;
_value = default;
return new ValueTask<T>(value);
}
_activeWait = true;
Debug.Assert(!_signaled);
return new ValueTask<T>(this, _rvtsc.Version);
}
finally
{
if (lockTaken) _lock.Exit();
}
}
public void Set(T value)
{
var lockTaken = false;
try
{
_lock.Enter(ref lockTaken);
if (_activeWait && !_signaled)
{
_signaled = true;
_rvtsc.SetResult(value);
return;
}
_isSet = true;
_value = value;
}
finally
{
if (lockTaken) _lock.Exit();
}
}
}

View File

@@ -0,0 +1,11 @@
namespace KianaBH.KcpSharp.Base;
internal sealed class DefaultArrayPoolBufferAllocator : IKcpBufferPool
{
public static DefaultArrayPoolBufferAllocator Default { get; } = new();
public KcpRentedBuffer Rent(KcpBufferPoolRentOptions options)
{
return KcpRentedBuffer.FromSharedArrayPool(options.Size);
}
}

View File

@@ -0,0 +1,14 @@
namespace KianaBH.KcpSharp.Base;
/// <summary>
/// The buffer pool to rent buffers from.
/// </summary>
public interface IKcpBufferPool
{
/// <summary>
/// Rent a buffer using the specified options.
/// </summary>
/// <param name="options">The options used to rent this buffer.</param>
/// <returns></returns>
KcpRentedBuffer Rent(KcpBufferPoolRentOptions options);
}

View File

@@ -0,0 +1,25 @@
using System.Net.Sockets;
namespace KianaBH.KcpSharp.Base;
/// <summary>
/// A conversation or a channel over the transport.
/// </summary>
public interface IKcpConversation : IDisposable
{
/// <summary>
/// Put message into the receive queue of the channel.
/// </summary>
/// <param name="packet">
/// The packet content with the optional conversation ID. This buffer should not contain space for
/// pre-buffer and post-buffer.
/// </param>
/// <param name="cancellationToken">The token to cancel this operation.</param>
/// <returns>A <see cref="ValueTask" /> that completes when the packet is put into the receive queue.</returns>
ValueTask InputPakcetAsync(UdpReceiveResult packet, CancellationToken cancellationToken);
/// <summary>
/// Mark the underlying transport as closed. Abort all active send or receive operations.
/// </summary>
void SetTransportClosed();
}

View File

@@ -0,0 +1,7 @@
namespace KianaBH.KcpSharp.Base;
internal interface IKcpConversationUpdateNotificationSource
{
ReadOnlyMemory<byte> Packet { get; }
void Release();
}

View File

@@ -0,0 +1,16 @@
namespace KianaBH.KcpSharp.Base;
/// <summary>
/// An instance that can produce exceptions in background jobs.
/// </summary>
/// <typeparam name="T">The type of the instance.</typeparam>
public interface IKcpExceptionProducer<out T>
{
/// <summary>
/// Set the handler to invoke when exception is thrown. Return true in the handler to ignore the error and continue
/// running. Return false in the handler to abort the operation.
/// </summary>
/// <param name="handler">The exception handler.</param>
/// <param name="state">The state object to pass into the exception handler.</param>
void SetExceptionHandler(Func<Exception, T, object?, bool> handler, object? state);
}

View File

@@ -0,0 +1,56 @@
using System.Net;
namespace KianaBH.KcpSharp.Base;
/// <summary>
/// Multiplex many channels or conversations over the same transport.
/// </summary>
public interface IKcpMultiplexConnection : IDisposable
{
/// <summary>
/// Determine whether the multiplex connection contains a conversation with the specified id.
/// </summary>
/// <param name="id">The conversation ID.</param>
/// <returns>True if the multiplex connection contains the specified conversation. Otherwise false.</returns>
bool Contains(long id);
/// <summary>
/// Create a raw channel with the specified conversation ID.
/// </summary>
/// <param name="id">The conversation ID.</param>
/// <param name="remoteEndpoint">The remote endpoint</param>
/// <param name="options">The options of the <see cref="KcpRawChannel" />.</param>
/// <returns>The raw channel created.</returns>
/// <exception cref="ObjectDisposedException">The current instance is disposed.</exception>
/// <exception cref="InvalidOperationException">Another channel or conversation with the same ID was already registered.</exception>
KcpRawChannel CreateRawChannel(long id, IPEndPoint remoteEndpoint, KcpRawChannelOptions? options = null);
/// <summary>
/// Create a conversation with the specified conversation ID.
/// </summary>
/// <param name="id">The conversation ID.</param>
/// <param name="remoteEndpoint">The remote endpoint</param>
/// <param name="options">The options of the <see cref="KcpConversation" />.</param>
/// <returns>The KCP conversation created.</returns>
/// <exception cref="ObjectDisposedException">The current instance is disposed.</exception>
/// <exception cref="InvalidOperationException">Another channel or conversation with the same ID was already registered.</exception>
KcpConversation CreateConversation(long id, IPEndPoint remoteEndpoint, KcpConversationOptions? options = null);
/// <summary>
/// Register a conversation or channel with the specified conversation ID and user state.
/// </summary>
/// <param name="conversation">The conversation or channel to register.</param>
/// <param name="id">The conversation ID.</param>
/// <exception cref="ArgumentNullException"><paramref name="conversation" /> is not provided.</exception>
/// <exception cref="ObjectDisposedException">The current instance is disposed.</exception>
/// <exception cref="InvalidOperationException">Another channel or conversation with the same ID was already registered.</exception>
void RegisterConversation(IKcpConversation conversation, long id);
/// <summary>
/// Unregister a conversation or channel with the specified conversation ID.
/// </summary>
/// <param name="id">The conversation ID.</param>
/// <returns>The conversation unregistered. Returns null when the conversation with the specified ID is not found.</returns>
IKcpConversation? UnregisterConversation(long id);
}

View File

@@ -0,0 +1,56 @@
using System.Net;
namespace KianaBH.KcpSharp.Base;
/// <summary>
/// Multiplex many channels or conversations over the same transport.
/// </summary>
public interface IKcpMultiplexConnection<T> : IKcpMultiplexConnection
{
/// <summary>
/// Create a raw channel with the specified conversation ID.
/// </summary>
/// <param name="id">The conversation ID.</param>
/// <param name="remoteEndpoint">The remote Endpoint</param>
/// <param name="state">The user state of this channel.</param>
/// <param name="options">The options of the <see cref="KcpRawChannel" />.</param>
/// <returns>The raw channel created.</returns>
/// <exception cref="ObjectDisposedException">The current instance is disposed.</exception>
/// <exception cref="InvalidOperationException">Another channel or conversation with the same ID was already registered.</exception>
KcpRawChannel CreateRawChannel(long id, IPEndPoint remoteEndpoint, T state, KcpRawChannelOptions? options = null);
/// <summary>
/// Create a conversation with the specified conversation ID.
/// </summary>
/// <param name="id">The conversation ID.</param>
/// <param name="remoteEndpoint">The remote Endpoint</param>
/// <param name="state">The user state of this conversation.</param>
/// <param name="options">The options of the <see cref="KcpConversation" />.</param>
/// <returns>The KCP conversation created.</returns>
/// <exception cref="ObjectDisposedException">The current instance is disposed.</exception>
/// <exception cref="InvalidOperationException">Another channel or conversation with the same ID was already registered.</exception>
KcpConversation CreateConversation(long id, IPEndPoint remoteEndpoint, T state,
KcpConversationOptions? options = null);
/// <summary>
/// Register a conversation or channel with the specified conversation ID and user state.
/// </summary>
/// <param name="conversation">The conversation or channel to register.</param>
/// <param name="id">The conversation ID.</param>
/// <param name="state">The user state</param>
/// <exception cref="ArgumentNullException"><paramref name="conversation" /> is not provided.</exception>
/// <exception cref="ObjectDisposedException">The current instance is disposed.</exception>
/// <exception cref="InvalidOperationException">Another channel or conversation with the same ID was already registered.</exception>
void RegisterConversation(IKcpConversation conversation, long id, T? state);
/// <summary>
/// Unregister a conversation or channel with the specified conversation ID.
/// </summary>
/// <param name="id">The conversation ID.</param>
/// <param name="state">The user state.</param>
/// <returns>
/// The conversation unregistered with the user state. Returns default when the conversation with the specified ID
/// is not found.
/// </returns>
IKcpConversation? UnregisterConversation(long id, out T? state);
}

View File

@@ -0,0 +1,18 @@
using System.Net;
namespace KianaBH.KcpSharp.Base;
/// <summary>
/// A transport to send and receive packets.
/// </summary>
public interface IKcpTransport
{
/// <summary>
/// Send a packet into the transport.
/// </summary>
/// <param name="packet">The content of the packet.</param>
/// <param name="remoteEndpoint">The remote endpoint</param>
/// <param name="cancellationToken">A token to cancel this operation.</param>
/// <returns>A <see cref="ValueTask" /> that completes when the packet is sent.</returns>
ValueTask SendPacketAsync(Memory<byte> packet, IPEndPoint remoteEndpoint, CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,22 @@
namespace KianaBH.KcpSharp.Base;
/// <summary>
/// A transport instance for upper-level connections.
/// </summary>
/// <typeparam name="T">The type of the upper-level connection.</typeparam>
public interface IKcpTransport<out T> : IKcpTransport, IKcpExceptionProducer<IKcpTransport<T>>, IDisposable
{
/// <summary>
/// Get the upper-level connection instace. If Start is not called or the transport is closed,
/// <see cref="InvalidOperationException" /> will be thrown.
/// </summary>
/// <exception cref="InvalidOperationException">Start is not called or the transport is closed.</exception>
T Connection { get; }
/// <summary>
/// Create the upper-level connection and start pumping packets from the socket to the upper-level connection.
/// </summary>
/// <exception cref="ObjectDisposedException">The current instance is disposed.</exception>
/// <exception cref="InvalidOperationException"><see cref="Start" /> has been called before.</exception>
void Start();
}

View File

@@ -0,0 +1,93 @@
using System.Runtime.CompilerServices;
namespace KianaBH.KcpSharp.Base;
internal sealed class KcpAcknowledgeList
{
private readonly KcpSendQueue _sendQueue;
private (uint SerialNumber, uint Timestamp)[] _array;
private int _count;
private SpinLock _lock;
public KcpAcknowledgeList(KcpSendQueue sendQueue, int windowSize)
{
_array = new (uint SerialNumber, uint Timestamp)[windowSize];
_count = 0;
_lock = new SpinLock();
_sendQueue = sendQueue;
}
public bool TryGetAt(int index, out uint serialNumber, out uint timestamp)
{
var lockTaken = false;
try
{
_lock.Enter(ref lockTaken);
if ((uint)index >= (uint)_count)
{
serialNumber = default;
timestamp = default;
return false;
}
(serialNumber, timestamp) = _array[index];
return true;
}
finally
{
if (lockTaken) _lock.Exit();
}
}
public void Clear()
{
var lockTaken = false;
try
{
_lock.Enter(ref lockTaken);
_count = 0;
}
finally
{
if (lockTaken) _lock.Exit();
}
_sendQueue.NotifyAckListChanged(false);
}
public void Add(uint serialNumber, uint timestamp)
{
var lockTaken = false;
try
{
_lock.Enter(ref lockTaken);
EnsureCapacity();
_array[_count++] = (serialNumber, timestamp);
}
finally
{
if (lockTaken) _lock.Exit();
}
_sendQueue.NotifyAckListChanged(true);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void EnsureCapacity()
{
if (_count == _array.Length) Expand();
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void Expand()
{
var capacity = _count + 1;
capacity = Math.Max(capacity + capacity / 2, 16);
var newArray = new (uint SerialNumber, uint Timestamp)[capacity];
_array.AsSpan(0, _count).CopyTo(newArray);
_array = newArray;
}
}

View File

@@ -0,0 +1,51 @@
using System.Diagnostics;
namespace KianaBH.KcpSharp.Base;
internal readonly struct KcpBuffer
{
private readonly object? _owner;
private readonly Memory<byte> _memory;
public ReadOnlyMemory<byte> DataRegion => _memory.Slice(0, Length);
public int Length { get; }
private KcpBuffer(object? owner, Memory<byte> memory, int length)
{
_owner = owner;
_memory = memory;
Length = length;
}
public static KcpBuffer CreateFromSpan(KcpRentedBuffer buffer, ReadOnlySpan<byte> dataSource)
{
var memory = buffer.Memory;
if (dataSource.Length > memory.Length) ThrowRentedBufferTooSmall();
dataSource.CopyTo(memory.Span);
return new KcpBuffer(buffer.Owner, memory, dataSource.Length);
}
public KcpBuffer AppendData(ReadOnlySpan<byte> data)
{
if (Length + data.Length > _memory.Length) ThrowRentedBufferTooSmall();
data.CopyTo(_memory.Span.Slice(Length));
return new KcpBuffer(_owner, _memory, Length + data.Length);
}
public KcpBuffer Consume(int length)
{
Debug.Assert((uint)length <= (uint)Length);
return new KcpBuffer(_owner, _memory.Slice(length), Length - length);
}
public void Release()
{
new KcpRentedBuffer(_owner, _memory).Dispose();
}
private static void ThrowRentedBufferTooSmall()
{
throw new InvalidOperationException("The rented buffer is not large enough to hold the data.");
}
}

View File

@@ -0,0 +1,50 @@
namespace KianaBH.KcpSharp.Base;
/// <summary>
/// The options to use when renting buffers from the pool.
/// </summary>
public readonly struct KcpBufferPoolRentOptions : IEquatable<KcpBufferPoolRentOptions>
{
/// <summary>
/// The minimum size of the buffer.
/// </summary>
public int Size { get; }
/// <summary>
/// True if the buffer may be passed to the outside of KcpSharp. False if the buffer is only used internally in
/// KcpSharp.
/// </summary>
public bool IsOutbound { get; }
/// <summary>
/// Create a <see cref="KcpBufferPoolRentOptions" /> with the specified parameters.
/// </summary>
/// <param name="size">The minimum size of the buffer.</param>
/// <param name="isOutbound">
/// True if the buffer may be passed to the outside of KcpSharp. False if the buffer is only used
/// internally in KcpSharp.
/// </param>
public KcpBufferPoolRentOptions(int size, bool isOutbound)
{
Size = size;
IsOutbound = isOutbound;
}
/// <inheritdoc />
public bool Equals(KcpBufferPoolRentOptions other)
{
return Size == other.Size && IsOutbound == other.IsOutbound;
}
/// <inheritdoc />
public override bool Equals(object? obj)
{
return obj is KcpBufferPoolRentOptions other && Equals(other);
}
/// <inheritdoc />
public override int GetHashCode()
{
return HashCode.Combine(Size, IsOutbound);
}
}

View File

@@ -0,0 +1,9 @@
namespace KianaBH.KcpSharp.Base;
internal enum KcpCommand : byte
{
Push = 81,
Ack = 82,
WindowProbe = 83,
WindowSize = 84
}

View File

@@ -0,0 +1,272 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.ExceptionServices;
using System.Threading.Tasks.Sources;
namespace KianaBH.KcpSharp.Base;
partial class KcpConversation
{
#if NET6_0_OR_GREATER
[ThreadStatic] private static KcpConversation? s_currentObject;
private object? _flushStateMachine;
private struct KcpFlushAsyncMethodBuilder
{
private readonly KcpConversation _conversation;
private StateMachineBox? _task;
private static readonly StateMachineBox s_syncSuccessSentinel = new SyncSuccessSentinelStateMachineBox();
public KcpFlushAsyncMethodBuilder(KcpConversation conversation)
{
_conversation = conversation;
_task = null;
}
public static KcpFlushAsyncMethodBuilder Create()
{
var conversation = s_currentObject;
Debug.Assert(conversation is not null);
s_currentObject = null;
return new KcpFlushAsyncMethodBuilder(conversation);
}
#pragma warning disable CA1822 // Mark members as static
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Start<TStateMachine>(ref TStateMachine stateMachine)
where TStateMachine : IAsyncStateMachine
#pragma warning restore CA1822 // Mark members as static
{
Debug.Assert(stateMachine is not null);
stateMachine.MoveNext();
}
public ValueTask Task
{
get
{
if (ReferenceEquals(_task, s_syncSuccessSentinel)) return default;
var stateMachineBox = _task ??= CreateWeaklyTypedStateMachineBox();
return new ValueTask(stateMachineBox, stateMachineBox.Version);
}
}
#pragma warning disable CA1822 // Mark members as static
public void SetStateMachine(IAsyncStateMachine stateMachine)
#pragma warning restore CA1822 // Mark members as static
{
Debug.Fail("SetStateMachine should not be used.");
}
public void SetResult()
{
if (_task == null)
_task = s_syncSuccessSentinel;
else
_task.SetResult();
}
public void SetException(Exception exception)
{
SetException(exception, ref _task);
}
private static void SetException(Exception exception, ref StateMachineBox? boxFieldRef)
{
if (exception == null) throw new ArgumentNullException(nameof(exception));
(boxFieldRef ??= CreateWeaklyTypedStateMachineBox()).SetException(exception);
}
public void AwaitOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)
where TAwaiter : INotifyCompletion
where TStateMachine : IAsyncStateMachine
{
AwaitOnCompleted(ref awaiter, ref stateMachine, ref _task, _conversation);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter,
ref TStateMachine stateMachine)
where TAwaiter : ICriticalNotifyCompletion
where TStateMachine : IAsyncStateMachine
{
AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine, ref _task, _conversation);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter,
ref TStateMachine stateMachine, ref StateMachineBox? boxRef, KcpConversation conversation)
where TAwaiter : ICriticalNotifyCompletion where TStateMachine : IAsyncStateMachine
{
var stateMachineBox = GetStateMachineBox(ref stateMachine, ref boxRef, conversation);
AwaitUnsafeOnCompleted(ref awaiter, stateMachineBox);
}
private static void AwaitOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter,
ref TStateMachine stateMachine, ref StateMachineBox? box, KcpConversation conversation)
where TAwaiter : INotifyCompletion where TStateMachine : IAsyncStateMachine
{
try
{
awaiter.OnCompleted(GetStateMachineBox(ref stateMachine, ref box, conversation).MoveNextAction);
}
catch (Exception exception)
{
var edi = ExceptionDispatchInfo.Capture(exception);
ThreadPool.QueueUserWorkItem(static state => ((ExceptionDispatchInfo)state!).Throw(), edi);
}
}
private static void AwaitUnsafeOnCompleted<TAwaiter>(ref TAwaiter awaiter, StateMachineBox box)
where TAwaiter : ICriticalNotifyCompletion
{
try
{
awaiter.UnsafeOnCompleted(box.MoveNextAction);
}
catch (Exception exception)
{
var edi = ExceptionDispatchInfo.Capture(exception);
ThreadPool.QueueUserWorkItem(static state => ((ExceptionDispatchInfo)state!).Throw(), edi);
}
}
private static StateMachineBox CreateWeaklyTypedStateMachineBox()
{
return new StateMachineBox<IAsyncStateMachine>(null);
}
private static StateMachineBox GetStateMachineBox<TStateMachine>(ref TStateMachine stateMachine,
ref StateMachineBox? boxFieldRef, KcpConversation conversation) where TStateMachine : IAsyncStateMachine
{
var stateMachineBox = boxFieldRef as StateMachineBox<TStateMachine>;
if (stateMachineBox != null) return stateMachineBox;
var stateMachineBox2 = boxFieldRef as StateMachineBox<IAsyncStateMachine>;
if (stateMachineBox2 != null)
{
if (stateMachineBox2.StateMachine == null)
{
Debugger.NotifyOfCrossThreadDependency();
stateMachineBox2.StateMachine = stateMachine;
}
return stateMachineBox2;
}
Debugger.NotifyOfCrossThreadDependency();
var stateMachineBox3 =
(StateMachineBox<TStateMachine>)(boxFieldRef =
StateMachineBox<TStateMachine>.GetOrCreateBox(conversation));
stateMachineBox3.StateMachine = stateMachine;
return stateMachineBox3;
}
private abstract class StateMachineBox : IValueTaskSource
{
protected Action? _moveNextAction;
protected ManualResetValueTaskSourceCore<bool> _mrvtsc;
public virtual Action MoveNextAction => _moveNextAction!;
public short Version => _mrvtsc.Version;
public ValueTaskSourceStatus GetStatus(short token)
{
return _mrvtsc.GetStatus(token);
}
public void OnCompleted(Action<object?> continuation, object? state, short token,
ValueTaskSourceOnCompletedFlags flags)
{
_mrvtsc.OnCompleted(continuation, state, token, flags);
}
void IValueTaskSource.GetResult(short token)
{
throw new NotSupportedException();
}
public void SetResult()
{
_mrvtsc.SetResult(true);
}
public void SetException(Exception error)
{
_mrvtsc.SetException(error);
}
}
private sealed class SyncSuccessSentinelStateMachineBox : StateMachineBox
{
public SyncSuccessSentinelStateMachineBox()
{
SetResult();
}
}
private sealed class StateMachineBox<TStateMachine> : StateMachineBox, IValueTaskSource
where TStateMachine : IAsyncStateMachine
{
private KcpConversation? _conversation;
[MaybeNull] public TStateMachine StateMachine;
internal StateMachineBox(KcpConversation? conversation)
{
_conversation = conversation;
}
public override Action MoveNextAction => _moveNextAction ??= MoveNext;
void IValueTaskSource.GetResult(short token)
{
try
{
_mrvtsc.GetResult(token);
}
finally
{
ReturnOrDropBox();
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static StateMachineBox<TStateMachine> GetOrCreateBox(KcpConversation conversation)
{
if (conversation._flushStateMachine is StateMachineBox<TStateMachine> stateMachine)
{
stateMachine._conversation = conversation;
conversation._flushStateMachine = null;
return stateMachine;
}
return new StateMachineBox<TStateMachine>(conversation);
}
public void MoveNext()
{
if (StateMachine is not null) StateMachine.MoveNext();
}
private void ReturnOrDropBox()
{
StateMachine = default!;
_mrvtsc.Reset();
if (_conversation is not null)
{
_conversation._flushStateMachine = this;
_conversation = null;
}
}
}
}
#endif
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,98 @@
namespace KianaBH.KcpSharp.Base;
/// <summary>
/// Options used to control the behaviors of <see cref="KcpConversation" />.
/// </summary>
public class KcpConversationOptions
{
internal const int MtuDefaultValue = 1400;
internal const uint SendWindowDefaultValue = 32;
internal const uint ReceiveWindowDefaultValue = 128;
internal const uint RemoteReceiveWindowDefaultValue = 128;
internal const uint UpdateIntervalDefaultValue = 100;
internal const int SendQueueSizeDefaultValue = 32;
internal const int ReceiveQueueSizeDefaultValue = 32;
/// <summary>
/// The buffer pool to rent buffer from.
/// </summary>
public IKcpBufferPool? BufferPool { get; set; }
/// <summary>
/// The maximum packet size that can be transmitted over the underlying transport.
/// </summary>
public int Mtu { get; set; } = 1400;
/// <summary>
/// The number of packets in the send window.
/// </summary>
public int SendWindow { get; set; } = 32;
/// <summary>
/// The number of packets in the receive window.
/// </summary>
public int ReceiveWindow { get; set; } = 128;
/// <summary>
/// The nuber of packets in the receive window of the remote host.
/// </summary>
public int RemoteReceiveWindow { get; set; } = 128;
/// <summary>
/// The interval in milliseconds to update the internal state of <see cref="KcpConversation" />.
/// </summary>
public int UpdateInterval { get; set; } = 100;
/// <summary>
/// Wether no-delay mode is enabled.
/// </summary>
public bool NoDelay { get; set; }
/// <summary>
/// The number of ACK packet skipped before a resend is triggered.
/// </summary>
public int FastResend { get; set; }
/// <summary>
/// Whether congestion control is disabled.
/// </summary>
public bool DisableCongestionControl { get; set; }
/// <summary>
/// Whether stream mode is enabled.
/// </summary>
public bool StreamMode { get; set; }
/// <summary>
/// The number of packets in the send queue.
/// </summary>
public int SendQueueSize { get; set; }
/// <summary>
/// The number of packets in the receive queue.
/// </summary>
public int ReceiveQueueSize { get; set; }
/// <summary>
/// The number of bytes to reserve at the start of buffer passed into the underlying transport. The transport should
/// fill this reserved space.
/// </summary>
public int PreBufferSize { get; set; }
/// <summary>
/// The number of bytes to reserve at the end of buffer passed into the underlying transport. The transport should fill
/// this reserved space.
/// </summary>
public int PostBufferSize { get; set; }
/// <summary>
/// Options for customized keep-alive functionality.
/// </summary>
public KcpKeepAliveOptions? KeepAliveOptions { get; set; }
/// <summary>
/// Options for receive window size notification functionality.
/// </summary>
public KcpReceiveWindowNotificationOptions? ReceiveWindowNotificationOptions { get; set; }
}

View File

@@ -0,0 +1,77 @@
using System.Globalization;
namespace KianaBH.KcpSharp.Base;
/// <summary>
/// The result of a receive or peek operation.
/// </summary>
public readonly struct KcpConversationReceiveResult : IEquatable<KcpConversationReceiveResult>
{
private readonly bool _connectionAlive;
/// <summary>
/// The number of bytes received.
/// </summary>
public int BytesReceived { get; }
/// <summary>
/// Whether the underlying transport is marked as closed.
/// </summary>
public bool TransportClosed => !_connectionAlive;
/// <summary>
/// Construct a <see cref="KcpConversationReceiveResult" /> with the specified number of bytes received.
/// </summary>
/// <param name="bytesReceived">The number of bytes received.</param>
public KcpConversationReceiveResult(int bytesReceived)
{
BytesReceived = bytesReceived;
_connectionAlive = true;
}
/// <summary>
/// Checks whether the two instance is equal.
/// </summary>
/// <param name="left">The one instance.</param>
/// <param name="right">The other instance.</param>
/// <returns>Whether the two instance is equal</returns>
public static bool operator ==(KcpConversationReceiveResult left, KcpConversationReceiveResult right)
{
return left.Equals(right);
}
/// <summary>
/// Checks whether the two instance is not equal.
/// </summary>
/// <param name="left">The one instance.</param>
/// <param name="right">The other instance.</param>
/// <returns>Whether the two instance is not equal</returns>
public static bool operator !=(KcpConversationReceiveResult left, KcpConversationReceiveResult right)
{
return !left.Equals(right);
}
/// <inheritdoc />
public bool Equals(KcpConversationReceiveResult other)
{
return BytesReceived == other.BytesReceived && TransportClosed == other.TransportClosed;
}
/// <inheritdoc />
public override bool Equals(object? obj)
{
return obj is KcpConversationReceiveResult other && Equals(other);
}
/// <inheritdoc />
public override int GetHashCode()
{
return HashCode.Combine(BytesReceived, TransportClosed);
}
/// <inheritdoc />
public override string ToString()
{
return _connectionAlive ? BytesReceived.ToString(CultureInfo.InvariantCulture) : "Transport is closed.";
}
}

View File

@@ -0,0 +1,474 @@
using System.Diagnostics;
using System.Threading.Tasks.Sources;
namespace KianaBH.KcpSharp.Base;
internal sealed class KcpConversationUpdateActivation : IValueTaskSource<KcpConversationUpdateNotification>, IDisposable
{
private readonly Timer _timer;
private readonly WaitList _waitList;
private bool _activeWait;
private CancellationTokenRegistration _cancellationRegistration;
private CancellationToken _cancellationToken;
private bool _disposed;
private ManualResetValueTaskSourceCore<KcpConversationUpdateNotification> _mrvtsc;
private bool _notificationPending;
private bool _signaled;
public KcpConversationUpdateActivation(int interval)
{
_timer = new Timer(state =>
{
var reference = (WeakReference<KcpConversationUpdateActivation>?)state!;
if (reference.TryGetTarget(out var target)) target.Notify();
}, new WeakReference<KcpConversationUpdateActivation>(this), interval, interval);
_mrvtsc = new ManualResetValueTaskSourceCore<KcpConversationUpdateNotification>
{ RunContinuationsAsynchronously = true };
_waitList = new WaitList(this);
}
public void Dispose()
{
lock (this)
{
if (_disposed) return;
_disposed = true;
if (_activeWait && !_signaled)
{
_signaled = true;
_cancellationToken = default;
_mrvtsc.SetResult(default);
}
}
_timer.Dispose();
_waitList.Dispose();
}
ValueTaskSourceStatus IValueTaskSource<KcpConversationUpdateNotification>.GetStatus(short token)
{
return _mrvtsc.GetStatus(token);
}
void IValueTaskSource<KcpConversationUpdateNotification>.OnCompleted(Action<object?> continuation, object? state,
short token, ValueTaskSourceOnCompletedFlags flags)
{
_mrvtsc.OnCompleted(continuation, state, token, flags);
}
KcpConversationUpdateNotification IValueTaskSource<KcpConversationUpdateNotification>.GetResult(short token)
{
_cancellationRegistration.Dispose();
try
{
return _mrvtsc.GetResult(token);
}
finally
{
_mrvtsc.Reset();
lock (this)
{
_signaled = false;
_activeWait = false;
_cancellationRegistration = default;
}
}
}
public void Notify()
{
if (_disposed) return;
lock (this)
{
if (_disposed || _notificationPending) return;
if (_activeWait && !_signaled)
{
_signaled = true;
_cancellationToken = default;
_mrvtsc.SetResult(default);
}
else
{
_notificationPending = true;
}
}
}
private void NotifyPacketReceived()
{
lock (this)
{
if (_disposed) return;
if (_activeWait && !_signaled)
if (_waitList.Occupy(out var notification))
{
_signaled = true;
_cancellationToken = default;
var timerNotification = _notificationPending;
_notificationPending = false;
_mrvtsc.SetResult(notification.WithTimerNotification(timerNotification));
}
}
}
public ValueTask<KcpConversationUpdateNotification> WaitAsync(CancellationToken cancellationToken)
{
short token;
lock (this)
{
if (_disposed) return default;
if (cancellationToken.IsCancellationRequested)
return new ValueTask<KcpConversationUpdateNotification>(
Task.FromCanceled<KcpConversationUpdateNotification>(cancellationToken));
if (_activeWait) throw new InvalidOperationException();
if (_waitList.Occupy(out var notification))
{
var timerNotification = _notificationPending;
_notificationPending = false;
return new ValueTask<KcpConversationUpdateNotification>(
notification.WithTimerNotification(timerNotification));
}
if (_notificationPending)
{
_notificationPending = false;
return default;
}
_activeWait = true;
Debug.Assert(!_signaled);
_cancellationToken = cancellationToken;
token = _mrvtsc.Version;
}
_cancellationRegistration =
cancellationToken.UnsafeRegister(state => ((KcpConversationUpdateActivation?)state)!.CancelWaiting(), this);
return new ValueTask<KcpConversationUpdateNotification>(this, token);
}
private void CancelWaiting()
{
lock (this)
{
if (_activeWait && !_signaled)
{
var cancellationToken = _cancellationToken;
_signaled = true;
_cancellationToken = default;
_mrvtsc.SetException(new OperationCanceledException(cancellationToken));
}
}
}
public ValueTask InputPacketAsync(ReadOnlyMemory<byte> packet, CancellationToken cancellationToken)
{
if (_disposed) return default;
return _waitList.InputPacketAsync(packet, cancellationToken);
}
private class WaitList : IValueTaskSource, IKcpConversationUpdateNotificationSource, IDisposable
{
private readonly KcpConversationUpdateActivation _parent;
private bool _available; // activeWait
private CancellationTokenRegistration _cancellationRegistration;
private CancellationToken _cancellationToken;
private bool _disposed;
private LinkedList<WaitItem>? _list;
private ManualResetValueTaskSourceCore<bool> _mrvtsc;
private bool _occupied;
private ReadOnlyMemory<byte> _packet;
private bool _signaled;
public WaitList(KcpConversationUpdateActivation parent)
{
_parent = parent;
_mrvtsc = new ManualResetValueTaskSourceCore<bool> { RunContinuationsAsynchronously = true };
}
public void Dispose()
{
if (_disposed) return;
lock (this)
{
_disposed = true;
if (_available && !_occupied && !_signaled)
{
_signaled = true;
_packet = default;
_cancellationToken = default;
_mrvtsc.SetResult(false);
}
var list = _list;
if (list is not null)
{
_list = null;
var node = list.First;
var next = node?.Next;
while (node is not null)
{
node.Value.Release();
list.Remove(node);
node = next;
next = node?.Next;
}
}
}
}
public ReadOnlyMemory<byte> Packet
{
get
{
lock (this)
{
if (_available && _occupied && !_signaled) return _packet;
}
return default;
}
}
public void Release()
{
lock (this)
{
if (_available && _occupied && !_signaled)
{
_signaled = true;
_packet = default;
_cancellationToken = default;
_mrvtsc.SetResult(true);
}
}
}
ValueTaskSourceStatus IValueTaskSource.GetStatus(short token)
{
return _mrvtsc.GetStatus(token);
}
void IValueTaskSource.OnCompleted(Action<object?> continuation, object? state, short token,
ValueTaskSourceOnCompletedFlags flags)
{
_mrvtsc.OnCompleted(continuation, state, token, flags);
}
void IValueTaskSource.GetResult(short token)
{
_cancellationRegistration.Dispose();
try
{
_mrvtsc.GetResult(token);
}
finally
{
_mrvtsc.Reset();
lock (this)
{
_available = false;
_occupied = false;
_signaled = false;
_cancellationRegistration = default;
}
}
}
public ValueTask InputPacketAsync(ReadOnlyMemory<byte> packet, CancellationToken cancellationToken)
{
WaitItem? waitItem = null;
short token = 0;
lock (this)
{
if (_disposed) return default;
if (cancellationToken.IsCancellationRequested)
return new ValueTask(Task.FromCanceled(cancellationToken));
if (_available)
{
waitItem = new WaitItem(this, packet, cancellationToken);
_list ??= new LinkedList<WaitItem>();
_list.AddLast(waitItem.Node);
}
else
{
token = _mrvtsc.Version;
_available = true;
Debug.Assert(!_occupied);
Debug.Assert(!_signaled);
_packet = packet;
_cancellationToken = cancellationToken;
}
}
ValueTask task;
if (waitItem is null)
{
_cancellationRegistration =
cancellationToken.UnsafeRegister(state => ((WaitList?)state)!.CancelWaiting(), this);
task = new ValueTask(this, token);
}
else
{
waitItem.RegisterCancellationToken();
task = new ValueTask(waitItem.Task);
}
_parent.NotifyPacketReceived();
return task;
}
private void CancelWaiting()
{
lock (this)
{
if (_available && !_occupied && !_signaled)
{
_signaled = true;
var cancellationToken = _cancellationToken;
_packet = default;
_cancellationToken = default;
_mrvtsc.SetException(new OperationCanceledException(cancellationToken));
}
}
}
public bool Occupy(out KcpConversationUpdateNotification notification)
{
lock (this)
{
if (_disposed)
{
notification = default;
return false;
}
if (_available && !_occupied && !_signaled)
{
_occupied = true;
notification = new KcpConversationUpdateNotification(this, true);
return true;
}
if (_list is null)
{
notification = default;
return false;
}
var node = _list.First;
if (node is not null)
{
_list.Remove(node);
notification = new KcpConversationUpdateNotification(node.Value, true);
return true;
}
}
notification = default;
return false;
}
internal bool TryRemove(WaitItem item)
{
lock (this)
{
var list = _list;
if (list is null) return false;
var node = item.Node;
if (node.Previous is null && node.Next is null) return false;
list.Remove(node);
return true;
}
}
}
private class WaitItem : TaskCompletionSource, IKcpConversationUpdateNotificationSource
{
private readonly WaitList _parent;
private CancellationTokenRegistration _cancellationRegistration;
private CancellationToken _cancellationToken;
private ReadOnlyMemory<byte> _packet;
private bool _released;
public WaitItem(WaitList parent, ReadOnlyMemory<byte> packet, CancellationToken cancellationToken)
{
_parent = parent;
_packet = packet;
_cancellationToken = cancellationToken;
Node = new LinkedListNode<WaitItem>(this);
}
public LinkedListNode<WaitItem> Node { get; }
public ReadOnlyMemory<byte> Packet
{
get
{
lock (this)
{
if (!_released) return _packet;
}
return default;
}
}
public void Release()
{
CancellationTokenRegistration cancellationRegistration;
lock (this)
{
_released = true;
cancellationRegistration = _cancellationRegistration;
_packet = default;
_cancellationToken = default;
_cancellationRegistration = default;
}
TrySetResult();
cancellationRegistration.Dispose();
}
public void RegisterCancellationToken()
{
_cancellationRegistration =
_cancellationToken.UnsafeRegister(state => ((WaitItem?)state)!.CancelWaiting(), this);
}
private void CancelWaiting()
{
CancellationTokenRegistration cancellationRegistration;
if (_parent.TryRemove(this))
{
CancellationToken cancellationToken;
lock (this)
{
_released = true;
cancellationToken = _cancellationToken;
cancellationRegistration = _cancellationRegistration;
_packet = default;
_cancellationToken = default;
_cancellationRegistration = default;
}
TrySetCanceled(cancellationToken);
}
_cancellationRegistration.Dispose();
}
}
}

View File

@@ -0,0 +1,27 @@
namespace KianaBH.KcpSharp.Base;
internal readonly struct KcpConversationUpdateNotification : IDisposable
{
private readonly IKcpConversationUpdateNotificationSource? _source;
private readonly bool _skipTimerNotification;
public ReadOnlyMemory<byte> Packet => _source?.Packet ?? default;
public bool TimerNotification => !_skipTimerNotification;
public KcpConversationUpdateNotification(IKcpConversationUpdateNotificationSource? source,
bool skipTimerNotification)
{
_source = source;
_skipTimerNotification = skipTimerNotification;
}
public KcpConversationUpdateNotification WithTimerNotification(bool timerNotification)
{
return new KcpConversationUpdateNotification(_source, !_skipTimerNotification | timerNotification);
}
public void Dispose()
{
if (_source is not null) _source.Release();
}
}

View File

@@ -0,0 +1,106 @@
namespace KianaBH.KcpSharp.Base;
/// <summary>
/// Helper methods for <see cref="IKcpExceptionProducer{T}" />.
/// </summary>
public static class KcpExceptionProducerExtensions
{
/// <summary>
/// Set the handler to invoke when exception is thrown. Return true in the handler to ignore the error and continue
/// running. Return false in the handler to abort the operation.
/// </summary>
/// <param name="producer">The producer instance.</param>
/// <param name="handler">The exception handler.</param>
public static void SetExceptionHandler<T>(this IKcpExceptionProducer<T> producer, Func<Exception, T, bool> handler)
{
if (producer is null) throw new ArgumentNullException(nameof(producer));
if (handler is null) throw new ArgumentNullException(nameof(handler));
producer.SetExceptionHandler(
(ex, conv, state) => ((Func<Exception, T, bool>?)state)!.Invoke(ex, conv),
handler
);
}
/// <summary>
/// Set the handler to invoke when exception is thrown. Return true in the handler to ignore the error and continue
/// running. Return false in the handler to abort the operation.
/// </summary>
/// <param name="producer">The producer instance.</param>
/// <param name="handler">The exception handler.</param>
public static void SetExceptionHandler<T>(this IKcpExceptionProducer<T> producer, Func<Exception, bool> handler)
{
if (producer is null) throw new ArgumentNullException(nameof(producer));
if (handler is null) throw new ArgumentNullException(nameof(handler));
producer.SetExceptionHandler(
(ex, conv, state) => ((Func<Exception, bool>?)state)!.Invoke(ex),
handler
);
}
/// <summary>
/// Set the handler to invoke when exception is thrown.
/// </summary>
/// <param name="producer">The producer instance.</param>
/// <param name="handler">The exception handler.</param>
/// <param name="state">The state object to pass into the exception handler.</param>
public static void SetExceptionHandler<T>(this IKcpExceptionProducer<T> producer,
Action<Exception, T, object?> handler, object? state)
{
if (producer is null) throw new ArgumentNullException(nameof(producer));
if (handler is null) throw new ArgumentNullException(nameof(handler));
producer.SetExceptionHandler(
(ex, conv, state) =>
{
var tuple = (Tuple<Action<Exception, T, object?>, object?>)state!;
tuple.Item1.Invoke(ex, conv, tuple.Item2);
return false;
},
Tuple.Create(handler, state)
);
}
/// <summary>
/// Set the handler to invoke when exception is thrown.
/// </summary>
/// <param name="producer">The producer instance.</param>
/// <param name="handler">The exception handler.</param>
public static void SetExceptionHandler<T>(this IKcpExceptionProducer<T> producer, Action<Exception, T> handler)
{
if (producer is null) throw new ArgumentNullException(nameof(producer));
if (handler is null) throw new ArgumentNullException(nameof(handler));
producer.SetExceptionHandler(
(ex, conv, state) =>
{
var handler = (Action<Exception, T>)state!;
handler.Invoke(ex, conv);
return false;
},
handler
);
}
/// <summary>
/// Set the handler to invoke when exception is thrown.
/// </summary>
/// <param name="producer">The producer instance.</param>
/// <param name="handler">The exception handler.</param>
public static void SetExceptionHandler<T>(this IKcpExceptionProducer<T> producer, Action<Exception> handler)
{
if (producer is null) throw new ArgumentNullException(nameof(producer));
if (handler is null) throw new ArgumentNullException(nameof(handler));
producer.SetExceptionHandler(
(ex, conv, state) =>
{
var handler = (Action<Exception>)state!;
handler.Invoke(ex);
return false;
},
handler
);
}
}

View File

@@ -0,0 +1,13 @@
namespace KianaBH.KcpSharp.Base;
internal static class KcpGlobalVars
{
#if !CONVID32
public const ushort CONVID_LENGTH = 8;
public const ushort HEADER_LENGTH_WITH_CONVID = 28;
public const ushort HEADER_LENGTH_WITHOUT_CONVID = 20;
#else
public const ushort HEADER_LENGTH_WITH_CONVID = 24;
public const ushort HEADER_LENGTH_WITHOUT_CONVID = 20;
#endif
}

View File

@@ -0,0 +1,26 @@
namespace KianaBH.KcpSharp.Base;
/// <summary>
/// Options for customized keep-alive functionality.
/// </summary>
public sealed class KcpKeepAliveOptions
{
/// <summary>
/// Create an instance of option object for customized keep-alive functionality.
/// </summary>
/// <param name="sendInterval">The minimum interval in milliseconds between sending keep-alive messages.</param>
/// <param name="gracePeriod">
/// When no packets are received during this period (in milliseconds), the transport is
/// considered to be closed.
/// </param>
public KcpKeepAliveOptions(int sendInterval, int gracePeriod)
{
if (sendInterval <= 0) throw new ArgumentOutOfRangeException(nameof(sendInterval));
if (gracePeriod <= 0) throw new ArgumentOutOfRangeException(nameof(gracePeriod));
SendInterval = sendInterval;
GracePeriod = gracePeriod;
}
internal int SendInterval { get; }
internal int GracePeriod { get; }
}

View File

@@ -0,0 +1,281 @@
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Net;
using System.Net.Sockets;
namespace KianaBH.KcpSharp.Base;
/// <summary>
/// Multiplex many channels or conversations over the same transport.
/// </summary>
/// <typeparam name="T">The state of the channel.</typeparam>
public sealed class KcpMultiplexConnection<T> : IKcpTransport, IKcpConversation, IKcpMultiplexConnection<T>
{
private readonly ConcurrentDictionary<long, (IKcpConversation Conversation, T? State)> _conversations = new();
private readonly Action<T?>? _disposeAction;
private readonly IKcpTransport _transport;
private bool _disposed;
private bool _transportClosed;
/// <summary>
/// Construct a multiplexed connection over a transport.
/// </summary>
/// <param name="transport">The underlying transport.</param>
public KcpMultiplexConnection(IKcpTransport transport)
{
_transport = transport ?? throw new ArgumentNullException(nameof(transport));
_disposeAction = null;
}
/// <summary>
/// Construct a multiplexed connection over a transport.
/// </summary>
/// <param name="transport">The underlying transport.</param>
/// <param name="disposeAction">The action to invoke when state object is removed.</param>
public KcpMultiplexConnection(IKcpTransport transport, Action<T?>? disposeAction)
{
_transport = transport ?? throw new ArgumentNullException(nameof(transport));
_disposeAction = disposeAction;
}
/// <summary>
/// Process a newly received packet from the transport.
/// </summary>
/// <param name="packet">The content of the packet with conversation ID.</param>
/// <param name="cancellationToken">A token to cancel this operation.</param>
/// <returns>
/// A <see cref="ValueTask" /> that completes when the packet is handled by the corresponding channel or
/// conversation.
/// </returns>
public ValueTask InputPakcetAsync(UdpReceiveResult packet, CancellationToken cancellationToken = default)
{
ReadOnlySpan<byte> span = packet.Buffer.AsSpan();
if (span.Length < KcpGlobalVars.CONVID_LENGTH) return default;
if (_transportClosed || _disposed) return default;
var id = BinaryPrimitives.ReadInt64BigEndian(span);
if (_conversations.TryGetValue(id, out var value))
return value.Conversation.InputPakcetAsync(packet, cancellationToken);
return default;
}
/// <inheritdoc />
public void SetTransportClosed()
{
_transportClosed = true;
foreach (var (conversation, _) in _conversations.Values) conversation.SetTransportClosed();
}
/// <inheritdoc />
public void Dispose()
{
if (_disposed) return;
_transportClosed = true;
_disposed = true;
while (!_conversations.IsEmpty)
foreach (var id in _conversations.Keys)
if (_conversations.TryRemove(id, out var value))
{
value.Conversation.Dispose();
if (_disposeAction is not null) _disposeAction.Invoke(value.State);
}
}
/// <summary>
/// Determine whether the multiplex connection contains a conversation with the specified id.
/// </summary>
/// <param name="id">The conversation ID.</param>
/// <returns>True if the multiplex connection contains the specified conversation. Otherwise false.</returns>
public bool Contains(long id)
{
CheckDispose();
return _conversations.ContainsKey(id);
}
/// <summary>
/// Create a raw channel with the specified conversation ID.
/// </summary>
/// <param name="id">The conversation ID.</param>
/// <param name="remoteEndpoint">The remote Endpoint</param>
/// <param name="options">The options of the <see cref="KcpRawChannel" />.</param>
/// <returns>The raw channel created.</returns>
/// <exception cref="ObjectDisposedException">The current instance is disposed.</exception>
/// <exception cref="InvalidOperationException">Another channel or conversation with the same ID was already registered.</exception>
public KcpRawChannel CreateRawChannel(long id, IPEndPoint remoteEndpoint, KcpRawChannelOptions? options = null)
{
KcpRawChannel? channel = new(remoteEndpoint, this, id, options);
try
{
RegisterConversation(channel, id, default);
if (_transportClosed) channel.SetTransportClosed();
return Interlocked.Exchange(ref channel, null)!;
}
finally
{
if (channel is not null) channel.Dispose();
}
}
/// <summary>
/// Create a raw channel with the specified conversation ID.
/// </summary>
/// <param name="id">The conversation ID.</param>
/// <param name="remoteEndpoint">The remote Endpoint</param>
/// <param name="state">The user state of this channel.</param>
/// <param name="options">The options of the <see cref="KcpRawChannel" />.</param>
/// <returns>The raw channel created.</returns>
/// <exception cref="ObjectDisposedException">The current instance is disposed.</exception>
/// <exception cref="InvalidOperationException">Another channel or conversation with the same ID was already registered.</exception>
public KcpRawChannel CreateRawChannel(long id, IPEndPoint remoteEndpoint, T state,
KcpRawChannelOptions? options = null)
{
KcpRawChannel? channel = new(remoteEndpoint, this, id, options);
try
{
RegisterConversation(channel, id, state);
if (_transportClosed) channel.SetTransportClosed();
return Interlocked.Exchange(ref channel, null)!;
}
finally
{
if (channel is not null) channel.Dispose();
}
}
/// <summary>
/// Create a conversation with the specified conversation ID.
/// </summary>
/// <param name="id">The conversation ID.</param>
/// <param name="remoteEndpoint">The remote Endpoint</param>
/// <param name="options">The options of the <see cref="KcpConversation" />.</param>
/// <returns>The KCP conversation created.</returns>
/// <exception cref="ObjectDisposedException">The current instance is disposed.</exception>
/// <exception cref="InvalidOperationException">Another channel or conversation with the same ID was already registered.</exception>
public KcpConversation CreateConversation(long id, IPEndPoint remoteEndpoint,
KcpConversationOptions? options = null)
{
KcpConversation? conversation = new(remoteEndpoint, this, id, options);
try
{
RegisterConversation(conversation, id, default);
if (_transportClosed) conversation.SetTransportClosed();
return Interlocked.Exchange(ref conversation, null)!;
}
finally
{
if (conversation is not null) conversation.Dispose();
}
}
/// <summary>
/// Create a conversation with the specified conversation ID.
/// </summary>
/// <param name="id">The conversation ID.</param>
/// <param name="remoteEndpoint">The remote Endpoint</param>
/// <param name="state">The user state of this conversation.</param>
/// <param name="options">The options of the <see cref="KcpConversation" />.</param>
/// <returns>The KCP conversation created.</returns>
/// <exception cref="ObjectDisposedException">The current instance is disposed.</exception>
/// <exception cref="InvalidOperationException">Another channel or conversation with the same ID was already registered.</exception>
public KcpConversation CreateConversation(long id, IPEndPoint remoteEndpoint, T state,
KcpConversationOptions? options = null)
{
KcpConversation? conversation = new(remoteEndpoint, this, id, options);
try
{
RegisterConversation(conversation, id, state);
if (_transportClosed) conversation.SetTransportClosed();
return Interlocked.Exchange(ref conversation, null)!;
}
finally
{
if (conversation is not null) conversation.Dispose();
}
}
/// <summary>
/// Register a conversation or channel with the specified conversation ID and user state.
/// </summary>
/// <param name="conversation">The conversation or channel to register.</param>
/// <param name="id">The conversation ID.</param>
/// <exception cref="ArgumentNullException"><paramref name="conversation" /> is not provided.</exception>
/// <exception cref="ObjectDisposedException">The current instance is disposed.</exception>
/// <exception cref="InvalidOperationException">Another channel or conversation with the same ID was already registered.</exception>
public void RegisterConversation(IKcpConversation conversation, long id)
{
RegisterConversation(conversation, id, default);
}
/// <summary>
/// Register a conversation or channel with the specified conversation ID and user state.
/// </summary>
/// <param name="conversation">The conversation or channel to register.</param>
/// <param name="id">The conversation ID.</param>
/// <param name="state">The user state</param>
/// <exception cref="ArgumentNullException"><paramref name="conversation" /> is not provided.</exception>
/// <exception cref="ObjectDisposedException">The current instance is disposed.</exception>
/// <exception cref="InvalidOperationException">Another channel or conversation with the same ID was already registered.</exception>
public void RegisterConversation(IKcpConversation conversation, long id, T? state)
{
if (conversation is null) throw new ArgumentNullException(nameof(conversation));
CheckDispose();
var (addedConversation, _) = _conversations.GetOrAdd(id, (conversation, state));
if (!ReferenceEquals(addedConversation, conversation))
throw new InvalidOperationException("Duplicated conversation.");
if (_disposed)
{
if (_conversations.TryRemove(id, out var value) && _disposeAction is not null)
_disposeAction.Invoke(value.State);
ThrowObjectDisposedException();
}
}
/// <summary>
/// Unregister a conversation or channel with the specified conversation ID.
/// </summary>
/// <param name="id">The conversation ID.</param>
/// <returns>The conversation unregistered. Returns null when the conversation with the specified ID is not found.</returns>
public IKcpConversation? UnregisterConversation(long id)
{
return UnregisterConversation(id, out _);
}
/// <summary>
/// Unregister a conversation or channel with the specified conversation ID.
/// </summary>
/// <param name="id">The conversation ID.</param>
/// <param name="state">The user state.</param>
/// <returns>The conversation unregistered. Returns null when the conversation with the specified ID is not found.</returns>
public IKcpConversation? UnregisterConversation(long id, out T? state)
{
if (!_transportClosed && !_disposed && _conversations.TryRemove(id, out var value))
{
value.Conversation.SetTransportClosed();
state = value.State;
if (_disposeAction is not null) _disposeAction.Invoke(state);
return value.Conversation;
}
state = default;
return default;
}
/// <inheritdoc />
public ValueTask SendPacketAsync(Memory<byte> packet, IPEndPoint remoteEndpoint,
CancellationToken cancellationToken = default)
{
if (_transportClosed || _disposed) return default;
return _transport.SendPacketAsync(packet, remoteEndpoint, cancellationToken);
}
private void CheckDispose()
{
if (_disposed) ThrowObjectDisposedException();
}
private static void ThrowObjectDisposedException()
{
throw new ObjectDisposedException(nameof(KcpMultiplexConnection<T>));
}
}

View File

@@ -0,0 +1,89 @@
using System.Buffers.Binary;
using System.Diagnostics;
namespace KianaBH.KcpSharp.Base;
internal readonly struct KcpPacketHeader : IEquatable<KcpPacketHeader>
{
public KcpPacketHeader(KcpCommand command, byte fragment, ushort windowSize, uint timestamp, uint serialNumber,
uint unacknowledged)
{
Command = command;
Fragment = fragment;
WindowSize = windowSize;
Timestamp = timestamp;
SerialNumber = serialNumber;
Unacknowledged = unacknowledged;
}
internal KcpPacketHeader(byte fragment)
{
Command = 0;
Fragment = fragment;
WindowSize = 0;
Timestamp = 0;
SerialNumber = 0;
Unacknowledged = 0;
}
public KcpCommand Command { get; }
public byte Fragment { get; }
public ushort WindowSize { get; }
public uint Timestamp { get; }
public uint SerialNumber { get; }
public uint Unacknowledged { get; }
public bool Equals(KcpPacketHeader other)
{
return Command == other.Command && Fragment == other.Fragment && WindowSize == other.WindowSize &&
Timestamp == other.Timestamp && SerialNumber == other.SerialNumber &&
Unacknowledged == other.Unacknowledged;
}
public override bool Equals(object? obj)
{
return obj is KcpPacketHeader other && Equals(other);
}
public override int GetHashCode()
{
return HashCode.Combine(Command, Fragment, WindowSize, Timestamp, SerialNumber, Unacknowledged);
}
public static KcpPacketHeader Parse(ReadOnlySpan<byte> buffer)
{
Debug.Assert(buffer.Length >= 16);
return new KcpPacketHeader(
(KcpCommand)buffer[0],
buffer[1],
BinaryPrimitives.ReadUInt16LittleEndian(buffer.Slice(2)),
BinaryPrimitives.ReadUInt32LittleEndian(buffer.Slice(4)),
BinaryPrimitives.ReadUInt32LittleEndian(buffer.Slice(8)),
BinaryPrimitives.ReadUInt32LittleEndian(buffer.Slice(12))
);
}
internal void EncodeHeader(ulong? conversationId, int payloadLength, Span<byte> destination, out int bytesWritten)
{
Debug.Assert(destination.Length >= 20);
if (conversationId.HasValue)
{
BinaryPrimitives.WriteUInt64BigEndian(destination, conversationId.GetValueOrDefault());
destination = destination.Slice(8);
bytesWritten = 28;
}
else
{
bytesWritten = 20;
}
Debug.Assert(destination.Length >= 20);
destination[1] = Fragment;
destination[0] = (byte)Command;
BinaryPrimitives.WriteUInt16LittleEndian(destination.Slice(2), WindowSize);
BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(4), Timestamp);
BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(8), SerialNumber);
BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(12), Unacknowledged);
BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(16), (uint)payloadLength);
}
}

View File

@@ -0,0 +1,9 @@
namespace KianaBH.KcpSharp.Base;
[Flags]
internal enum KcpProbeType
{
None = 0,
AskSend = 1,
AskTell = 2
}

View File

@@ -0,0 +1,400 @@
using System.Buffers.Binary;
using System.Net;
using System.Net.Sockets;
namespace KianaBH.KcpSharp.Base;
/// <summary>
/// An unreliable channel with a conversation ID.
/// </summary>
public sealed class KcpRawChannel : IKcpConversation, IKcpExceptionProducer<KcpRawChannel>
{
private readonly IKcpBufferPool _bufferPool;
private readonly ulong? _id;
private readonly int _mtu;
private readonly int _postBufferSize;
private readonly int _preBufferSize;
private readonly KcpRawReceiveQueue _receiveQueue;
private readonly IPEndPoint _remoteEndPoint;
private readonly AsyncAutoResetEvent<int> _sendNotification;
private readonly KcpRawSendOperation _sendOperation;
private readonly IKcpTransport _transport;
private Func<Exception, KcpRawChannel, object?, bool>? _exceptionHandler;
private object? _exceptionHandlerState;
private CancellationTokenSource? _sendLoopCts;
/// <summary>
/// Construct a unreliable channel with a conversation ID.
/// </summary>
/// <param name="remoteEndPoint">The remote Endpoint</param>
/// <param name="transport">The underlying transport.</param>
/// <param name="options">The options of the <see cref="KcpRawChannel" />.</param>
public KcpRawChannel(IPEndPoint remoteEndPoint, IKcpTransport transport, KcpRawChannelOptions? options)
: this(remoteEndPoint, transport, null, options)
{
}
/// <summary>
/// Construct a unreliable channel with a conversation ID.
/// </summary>
/// <param name="remoteEndPoint">The remote Endpoint</param>
/// <param name="transport">The underlying transport.</param>
/// <param name="conversationId">The conversation ID.</param>
/// <param name="options">The options of the <see cref="KcpRawChannel" />.</param>
public KcpRawChannel(IPEndPoint remoteEndPoint, IKcpTransport transport, long conversationId,
KcpRawChannelOptions? options)
: this(remoteEndPoint, transport, (ulong)conversationId, options)
{
}
private KcpRawChannel(IPEndPoint remoteEndPoint, IKcpTransport transport, ulong? conversationId,
KcpRawChannelOptions? options)
{
_bufferPool = options?.BufferPool ?? DefaultArrayPoolBufferAllocator.Default;
_remoteEndPoint = remoteEndPoint;
_transport = transport;
_id = conversationId;
if (options is null)
_mtu = KcpConversationOptions.MtuDefaultValue;
else if (options.Mtu < 50)
throw new ArgumentException("MTU must be at least 50.", nameof(options));
else
_mtu = options.Mtu;
_preBufferSize = options?.PreBufferSize ?? 0;
_postBufferSize = options?.PostBufferSize ?? 0;
if (_preBufferSize < 0)
throw new ArgumentException("PreBufferSize must be a non-negative integer.", nameof(options));
if (_postBufferSize < 0)
throw new ArgumentException("PostBufferSize must be a non-negative integer.", nameof(options));
if ((uint)(_preBufferSize + _postBufferSize) >= (uint)_mtu)
throw new ArgumentException("The sum of PreBufferSize and PostBufferSize must be less than MTU.",
nameof(options));
if (conversationId.HasValue && (uint)(_preBufferSize + _postBufferSize) >= (uint)(_mtu - 4))
throw new ArgumentException(
"The sum of PreBufferSize and PostBufferSize is too large. There is not enough space in the packet for the conversation ID.",
nameof(options));
var queueSize = options?.ReceiveQueueSize ?? 32;
if (queueSize < 1) throw new ArgumentException("QueueSize must be a positive integer.", nameof(options));
_sendLoopCts = new CancellationTokenSource();
_sendNotification = new AsyncAutoResetEvent<int>();
_receiveQueue = new KcpRawReceiveQueue(_bufferPool, queueSize);
_sendOperation = new KcpRawSendOperation(_sendNotification);
RunSendLoop();
}
/// <summary>
/// Get the ID of the current conversation.
/// </summary>
public long? ConversationId => (long?)_id;
/// <summary>
/// Get whether the transport is marked as closed.
/// </summary>
public bool TransportClosed => _sendLoopCts is null;
/// <inheritdoc />
public ValueTask InputPakcetAsync(UdpReceiveResult packet, CancellationToken cancellationToken = default)
{
ReadOnlySpan<byte> span = packet.Buffer.AsSpan();
var overhead = _id.HasValue ? KcpGlobalVars.CONVID_LENGTH : 0;
if (span.Length < overhead || span.Length > _mtu) return default;
if (_id.HasValue)
{
if (BinaryPrimitives.ReadUInt64BigEndian(span) != _id.GetValueOrDefault()) return default;
span = span.Slice(8);
}
_receiveQueue.Enqueue(span);
return default;
}
/// <inheritdoc />
public void SetTransportClosed()
{
var cts = Interlocked.Exchange(ref _sendLoopCts, null);
if (cts is not null)
{
cts.Cancel();
cts.Dispose();
}
_receiveQueue.SetTransportClosed();
_sendOperation.SetTransportClosed();
_sendNotification.Set(0);
}
/// <inheritdoc />
public void Dispose()
{
SetTransportClosed();
_receiveQueue.Dispose();
_sendOperation.Dispose();
}
/// <summary>
/// Set the handler to invoke when exception is thrown during flushing packets to the transport. Return true in the
/// handler to ignore the error and continue running. Return false in the handler to abort the operation and mark the
/// transport as closed.
/// </summary>
/// <param name="handler">The exception handler.</param>
/// <param name="state">The state object to pass into the exception handler.</param>
public void SetExceptionHandler(Func<Exception, KcpRawChannel, object?, bool> handler, object? state)
{
if (handler is null) throw new ArgumentNullException(nameof(handler));
_exceptionHandler = handler;
_exceptionHandlerState = state;
}
/// <summary>
/// Send message to the underlying transport.
/// </summary>
/// <param name="buffer">The content of the message</param>
/// <param name="cancellationToken">The token to cancel this operation.</param>
/// <exception cref="ArgumentException">The size of the message is larger than mtu, thus it can not be sent.</exception>
/// <exception cref="OperationCanceledException">
/// The <paramref name="cancellationToken" /> is fired before send operation
/// is completed.
/// </exception>
/// <exception cref="InvalidOperationException">The send operation is initiated concurrently.</exception>
/// <exception cref="ObjectDisposedException">The <see cref="KcpConversation" /> instance is disposed.</exception>
/// <returns>
/// A <see cref="ValueTask{Boolean}" /> that completes when the entire message is put into the queue. The result
/// of the task is false when the transport is closed.
/// </returns>
public ValueTask<bool> SendAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
{
return _sendOperation.SendAsync(buffer, cancellationToken);
}
/// <summary>
/// Cancel the current send operation or flush operation.
/// </summary>
/// <returns>True if the current operation is canceled. False if there is no active send operation.</returns>
public bool CancelPendingSend()
{
return _sendOperation.CancelPendingOperation(null, default);
}
/// <summary>
/// Cancel the current send operation or flush operation.
/// </summary>
/// <param name="innerException">
/// The inner exception of the <see cref="OperationCanceledException" /> thrown by the
/// <see cref="SendAsync(ReadOnlyMemory{byte}, CancellationToken)" /> method.
/// </param>
/// <param name="cancellationToken">
/// The <see cref="CancellationToken" /> in the <see cref="OperationCanceledException" />
/// thrown by the <see cref="SendAsync(ReadOnlyMemory{byte}, CancellationToken)" /> method.
/// </param>
/// <returns>True if the current operation is canceled. False if there is no active send operation.</returns>
public bool CancelPendingSend(Exception? innerException, CancellationToken cancellationToken)
{
return _sendOperation.CancelPendingOperation(innerException, cancellationToken);
}
private async void RunSendLoop()
{
var cancellationToken = _sendLoopCts?.Token ?? new CancellationToken(true);
var sendOperation = _sendOperation;
var ev = _sendNotification;
var mss = _mtu - _preBufferSize - _postBufferSize;
if (_id.HasValue) mss -= 8;
try
{
while (!cancellationToken.IsCancellationRequested)
{
var payloadSize = await ev.WaitAsync().ConfigureAwait(false);
if (cancellationToken.IsCancellationRequested) break;
if (payloadSize < 0 || payloadSize > mss)
{
_ = sendOperation.TryConsume(default, out _);
continue;
}
var overhead = _preBufferSize + _postBufferSize;
if (_id.HasValue) overhead += 8;
{
using var owner = _bufferPool.Rent(new KcpBufferPoolRentOptions(payloadSize + overhead, true));
var memory = owner.Memory;
// Fill the buffer
if (_preBufferSize != 0)
{
memory.Span.Slice(0, _preBufferSize).Clear();
memory = memory.Slice(_preBufferSize);
}
if (_id.HasValue)
{
BinaryPrimitives.WriteUInt64LittleEndian(memory.Span, _id.GetValueOrDefault());
memory = memory.Slice(8);
}
if (!sendOperation.TryConsume(memory, out var bytesWritten)) continue;
payloadSize = Math.Min(payloadSize, bytesWritten);
memory = memory.Slice(payloadSize);
if (_postBufferSize != 0) memory.Span.Slice(0, _postBufferSize).Clear();
// Send the buffer
try
{
await _transport.SendPacketAsync(owner.Memory.Slice(0, payloadSize + overhead), _remoteEndPoint,
cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
if (!HandleFlushException(ex)) break;
}
}
}
}
catch (OperationCanceledException)
{
// Do nothing
}
catch (Exception ex)
{
HandleFlushException(ex);
}
}
private bool HandleFlushException(Exception ex)
{
var handler = _exceptionHandler;
var state = _exceptionHandlerState;
var result = false;
if (handler is not null)
try
{
result = handler.Invoke(ex, this, state);
}
catch
{
result = false;
}
if (!result) SetTransportClosed();
return result;
}
/// <summary>
/// Get the size of the next available message in the receive queue.
/// </summary>
/// <param name="result">The transport state and the size of the next available message.</param>
/// <exception cref="InvalidOperationException">The receive or peek operation is initiated concurrently.</exception>
/// <returns>
/// True if the receive queue contains at least one message. False if the receive queue is empty or the transport
/// is closed.
/// </returns>
public bool TryPeek(out KcpConversationReceiveResult result)
{
return _receiveQueue.TryPeek(out result);
}
/// <summary>
/// Remove the next available message in the receive queue and copy its content into <paramref name="buffer" />.
/// </summary>
/// <param name="buffer">The buffer to receive message.</param>
/// <param name="result">The transport state and the count of bytes moved into <paramref name="buffer" />.</param>
/// <exception cref="ArgumentException">
/// The size of the next available message is larger than the size of
/// <paramref name="buffer" />.
/// </exception>
/// <exception cref="InvalidOperationException">The receive or peek operation is initiated concurrently.</exception>
/// <returns>
/// True if the next available message is moved into <paramref name="buffer" />. False if the receive queue is
/// empty or the transport is closed.
/// </returns>
public bool TryReceive(Span<byte> buffer, out KcpConversationReceiveResult result)
{
return _receiveQueue.TryReceive(buffer, out result);
}
/// <summary>
/// Wait until the receive queue contains at least one message.
/// </summary>
/// <param name="cancellationToken">The token to cancel this operation.</param>
/// <exception cref="OperationCanceledException">
/// The <paramref name="cancellationToken" /> is fired before receive
/// operation is completed.
/// </exception>
/// <exception cref="InvalidOperationException">The receive or peek operation is initiated concurrently.</exception>
/// <returns>
/// A <see cref="ValueTask{KcpConversationReceiveResult}" /> that completes when the receive queue contains at
/// least one full message, or at least one byte in stream mode. Its result contains the transport state and the size
/// of the available message.
/// </returns>
public ValueTask<KcpConversationReceiveResult> WaitToReceiveAsync(CancellationToken cancellationToken)
{
return _receiveQueue.WaitToReceiveAsync(cancellationToken);
}
/// <summary>
/// Wait for the next full message to arrive if the receive queue is empty. Remove the next available message in the
/// receive queue and copy its content into <paramref name="buffer" />.
/// </summary>
/// <param name="buffer">The buffer to receive message.</param>
/// <param name="cancellationToken">The token to cancel this operation.</param>
/// <exception cref="ArgumentException">
/// The size of the next available message is larger than the size of
/// <paramref name="buffer" />.
/// </exception>
/// <exception cref="OperationCanceledException">
/// The <paramref name="cancellationToken" /> is fired before send operation
/// is completed.
/// </exception>
/// <exception cref="InvalidOperationException">The receive or peek operation is initiated concurrently.</exception>
/// <returns>
/// A <see cref="ValueTask{KcpConversationReceiveResult}" /> that completes when a message is moved into
/// <paramref name="buffer" /> or the transport is closed. Its result contains the transport state and the count of
/// bytes written into <paramref name="buffer" />.
/// </returns>
public ValueTask<KcpConversationReceiveResult> ReceiveAsync(Memory<byte> buffer,
CancellationToken cancellationToken = default)
{
return _receiveQueue.ReceiveAsync(buffer, cancellationToken);
}
/// <summary>
/// Cancel the current receive operation.
/// </summary>
/// <returns>True if the current operation is canceled. False if there is no active send operation.</returns>
public bool CancelPendingReceive()
{
return _receiveQueue.CancelPendingOperation(null, default);
}
/// <summary>
/// Cancel the current send operation or flush operation.
/// </summary>
/// <param name="innerException">
/// The inner exception of the <see cref="OperationCanceledException" /> thrown by the
/// <see cref="ReceiveAsync(Memory{byte}, CancellationToken)" /> method or
/// <see cref="WaitToReceiveAsync(CancellationToken)" /> method.
/// </param>
/// <param name="cancellationToken">
/// The <see cref="CancellationToken" /> in the <see cref="OperationCanceledException" />
/// thrown by the <see cref="ReceiveAsync(Memory{byte}, CancellationToken)" /> method or
/// <see cref="WaitToReceiveAsync(CancellationToken)" /> method.
/// </param>
/// <returns>True if the current operation is canceled. False if there is no active send operation.</returns>
public bool CancelPendingReceive(Exception? innerException, CancellationToken cancellationToken)
{
return _receiveQueue.CancelPendingOperation(innerException, cancellationToken);
}
}

View File

@@ -0,0 +1,34 @@
namespace KianaBH.KcpSharp.Base;
/// <summary>
/// Options used to control the behaviors of <see cref="KcpRawChannelOptions" />.
/// </summary>
public sealed class KcpRawChannelOptions
{
/// <summary>
/// The buffer pool to rent buffer from.
/// </summary>
public IKcpBufferPool? BufferPool { get; set; }
/// <summary>
/// The maximum packet size that can be transmitted over the underlying transport.
/// </summary>
public int Mtu { get; set; } = 1400;
/// <summary>
/// The number of packets in the receive queue.
/// </summary>
public int ReceiveQueueSize { get; set; } = 32;
/// <summary>
/// The number of bytes to reserve at the start of buffer passed into the underlying transport. The transport should
/// fill this reserved space.
/// </summary>
public int PreBufferSize { get; set; }
/// <summary>
/// The number of bytes to reserve at the end of buffer passed into the underlying transport. The transport should fill
/// this reserved space.
/// </summary>
public int PostBufferSize { get; set; }
}

View File

@@ -0,0 +1,343 @@
#if NEED_LINKEDLIST_SHIM
using LinkedListOfQueueItem = KcpSharp.NetstandardShim.LinkedList<KcpSharp.KcpBuffer>;
using LinkedListNodeOfQueueItem = KcpSharp.NetstandardShim.LinkedListNode<KcpSharp.KcpBuffer>;
#else
using LinkedListOfQueueItem = System.Collections.Generic.LinkedList<KianaBH.KcpSharp.Base.KcpBuffer>;
using LinkedListNodeOfQueueItem =
System.Collections.Generic.LinkedListNode<KianaBH.KcpSharp.Base.KcpBuffer>;
#endif
using System.Diagnostics;
using System.Threading.Tasks.Sources;
namespace KianaBH.KcpSharp.Base;
internal sealed class KcpRawReceiveQueue : IValueTaskSource<KcpConversationReceiveResult>, IDisposable
{
private readonly IKcpBufferPool _bufferPool;
private readonly int _capacity;
private readonly LinkedListOfQueueItem _queue;
private readonly LinkedListOfQueueItem _recycled;
private bool _activeWait;
private Memory<byte> _buffer;
private bool _bufferProvided;
private CancellationTokenRegistration _cancellationRegistration;
private CancellationToken _cancellationToken;
private bool _disposed;
private ManualResetValueTaskSourceCore<KcpConversationReceiveResult> _mrvtsc;
private bool _signaled;
private bool _transportClosed;
public KcpRawReceiveQueue(IKcpBufferPool bufferPool, int capacity)
{
_bufferPool = bufferPool;
_capacity = capacity;
_queue = new LinkedListOfQueueItem();
_recycled = new LinkedListOfQueueItem();
}
public void Dispose()
{
lock (_queue)
{
if (_disposed) return;
if (_activeWait && !_signaled)
{
ClearPreviousOperation();
_mrvtsc.SetResult(default);
}
var node = _queue.First;
while (node is not null)
{
node.ValueRef.Release();
node = node.Next;
}
_queue.Clear();
_recycled.Clear();
_disposed = true;
_transportClosed = true;
}
}
KcpConversationReceiveResult IValueTaskSource<KcpConversationReceiveResult>.GetResult(short token)
{
_cancellationRegistration.Dispose();
try
{
return _mrvtsc.GetResult(token);
}
finally
{
_mrvtsc.Reset();
lock (_queue)
{
_activeWait = false;
_signaled = false;
_cancellationRegistration = default;
}
}
}
ValueTaskSourceStatus IValueTaskSource<KcpConversationReceiveResult>.GetStatus(short token)
{
return _mrvtsc.GetStatus(token);
}
void IValueTaskSource<KcpConversationReceiveResult>.OnCompleted(Action<object?> continuation, object? state,
short token, ValueTaskSourceOnCompletedFlags flags)
{
_mrvtsc.OnCompleted(continuation, state, token, flags);
}
public bool TryPeek(out KcpConversationReceiveResult result)
{
lock (_queue)
{
if (_disposed || _transportClosed)
{
result = default;
return false;
}
if (_activeWait) ThrowHelper.ThrowConcurrentReceiveException();
var first = _queue.First;
if (first is null)
{
result = new KcpConversationReceiveResult(0);
return false;
}
result = new KcpConversationReceiveResult(first.ValueRef.Length);
return true;
}
}
public ValueTask<KcpConversationReceiveResult> WaitToReceiveAsync(CancellationToken cancellationToken)
{
short token;
lock (_queue)
{
if (_transportClosed || _disposed) return default;
if (_activeWait)
return new ValueTask<KcpConversationReceiveResult>(
Task.FromException<KcpConversationReceiveResult>(ThrowHelper.NewConcurrentReceiveException()));
if (cancellationToken.IsCancellationRequested)
return new ValueTask<KcpConversationReceiveResult>(
Task.FromCanceled<KcpConversationReceiveResult>(cancellationToken));
var first = _queue.First;
if (first is not null)
return new ValueTask<KcpConversationReceiveResult>(
new KcpConversationReceiveResult(first.ValueRef.Length));
_activeWait = true;
Debug.Assert(!_signaled);
_bufferProvided = false;
_buffer = default;
_cancellationToken = cancellationToken;
token = _mrvtsc.Version;
}
_cancellationRegistration =
cancellationToken.UnsafeRegister(state => ((KcpRawReceiveQueue?)state)!.SetCanceled(), this);
return new ValueTask<KcpConversationReceiveResult>(this, token);
}
public bool TryReceive(Span<byte> buffer, out KcpConversationReceiveResult result)
{
lock (_queue)
{
if (_disposed || _transportClosed)
{
result = default;
return false;
}
if (_activeWait) ThrowHelper.ThrowConcurrentReceiveException();
var first = _queue.First;
if (first is null)
{
result = new KcpConversationReceiveResult(0);
return false;
}
ref var source = ref first.ValueRef;
if (buffer.Length < source.Length) ThrowHelper.ThrowBufferTooSmall();
source.DataRegion.Span.CopyTo(buffer);
result = new KcpConversationReceiveResult(source.Length);
_queue.RemoveFirst();
source.Release();
source = default;
_recycled.AddLast(first);
return true;
}
}
public ValueTask<KcpConversationReceiveResult> ReceiveAsync(Memory<byte> buffer,
CancellationToken cancellationToken = default)
{
short token;
lock (_queue)
{
if (_transportClosed || _disposed) return default;
if (_activeWait)
return new ValueTask<KcpConversationReceiveResult>(
Task.FromException<KcpConversationReceiveResult>(ThrowHelper.NewConcurrentReceiveException()));
if (cancellationToken.IsCancellationRequested)
return new ValueTask<KcpConversationReceiveResult>(
Task.FromCanceled<KcpConversationReceiveResult>(cancellationToken));
var first = _queue.First;
if (first is not null)
{
ref var source = ref first.ValueRef;
var length = source.Length;
if (buffer.Length < source.Length)
return new ValueTask<KcpConversationReceiveResult>(
Task.FromException<KcpConversationReceiveResult>(
ThrowHelper.NewBufferTooSmallForBufferArgument()));
_queue.Remove(first);
source.DataRegion.CopyTo(buffer);
source.Release();
source = default;
_recycled.AddLast(first);
return new ValueTask<KcpConversationReceiveResult>(new KcpConversationReceiveResult(length));
}
_activeWait = true;
Debug.Assert(!_signaled);
_bufferProvided = true;
_buffer = buffer;
_cancellationToken = cancellationToken;
token = _mrvtsc.Version;
}
_cancellationRegistration =
cancellationToken.UnsafeRegister(state => ((KcpRawReceiveQueue?)state)!.SetCanceled(), this);
return new ValueTask<KcpConversationReceiveResult>(this, token);
}
public bool CancelPendingOperation(Exception? innerException, CancellationToken cancellationToken)
{
lock (_queue)
{
if (_activeWait && !_signaled)
{
ClearPreviousOperation();
_mrvtsc.SetException(
ThrowHelper.NewOperationCanceledExceptionForCancelPendingReceive(innerException,
cancellationToken));
return true;
}
}
return false;
}
private void SetCanceled()
{
lock (_queue)
{
if (_activeWait && !_signaled)
{
var cancellationToken = _cancellationToken;
ClearPreviousOperation();
_mrvtsc.SetException(new OperationCanceledException(cancellationToken));
}
}
}
private void ClearPreviousOperation()
{
_signaled = true;
_bufferProvided = false;
_buffer = default;
_cancellationToken = default;
}
public void Enqueue(ReadOnlySpan<byte> buffer)
{
lock (_queue)
{
if (_transportClosed || _disposed) return;
var queueSize = _queue.Count;
if (queueSize > 0 || !_activeWait)
{
if (queueSize >= _capacity) return;
var owner = _bufferPool.Rent(new KcpBufferPoolRentOptions(buffer.Length, false));
_queue.AddLast(AllocateNode(KcpBuffer.CreateFromSpan(owner, buffer)));
return;
}
if (!_bufferProvided)
{
var owner = _bufferPool.Rent(new KcpBufferPoolRentOptions(buffer.Length, false));
_queue.AddLast(AllocateNode(KcpBuffer.CreateFromSpan(owner, buffer)));
ClearPreviousOperation();
_mrvtsc.SetResult(new KcpConversationReceiveResult(buffer.Length));
return;
}
if (buffer.Length > _buffer.Length)
{
var owner = _bufferPool.Rent(new KcpBufferPoolRentOptions(buffer.Length, false));
_queue.AddLast(AllocateNode(KcpBuffer.CreateFromSpan(owner, buffer)));
ClearPreviousOperation();
_mrvtsc.SetException(ThrowHelper.NewBufferTooSmallForBufferArgument());
return;
}
buffer.CopyTo(_buffer.Span);
ClearPreviousOperation();
_mrvtsc.SetResult(new KcpConversationReceiveResult(buffer.Length));
}
}
private LinkedListNodeOfQueueItem AllocateNode(KcpBuffer buffer)
{
var node = _recycled.First;
if (node is null)
{
node = new LinkedListNodeOfQueueItem(buffer);
}
else
{
node.ValueRef = buffer;
_recycled.Remove(node);
}
return node;
}
public void SetTransportClosed()
{
lock (_queue)
{
if (_transportClosed || _disposed) return;
if (_activeWait && !_signaled)
{
ClearPreviousOperation();
_mrvtsc.SetResult(default);
}
_recycled.Clear();
_transportClosed = true;
}
}
}

View File

@@ -0,0 +1,184 @@
using System.Diagnostics;
using System.Threading.Tasks.Sources;
namespace KianaBH.KcpSharp.Base;
internal sealed class KcpRawSendOperation : IValueTaskSource<bool>, IDisposable
{
private readonly AsyncAutoResetEvent<int> _notification;
private bool _activeWait;
private ReadOnlyMemory<byte> _buffer;
private CancellationTokenRegistration _cancellationRegistration;
private CancellationToken _cancellationToken;
private bool _disposed;
private ManualResetValueTaskSourceCore<bool> _mrvtsc;
private bool _signaled;
private bool _transportClosed;
public KcpRawSendOperation(AsyncAutoResetEvent<int> notification)
{
_notification = notification;
_mrvtsc = new ManualResetValueTaskSourceCore<bool>
{
RunContinuationsAsynchronously = true
};
}
public void Dispose()
{
lock (this)
{
if (_disposed) return;
if (_activeWait && !_signaled)
{
ClearPreviousOperation();
_mrvtsc.SetResult(false);
}
_disposed = true;
_transportClosed = true;
}
}
bool IValueTaskSource<bool>.GetResult(short token)
{
_cancellationRegistration.Dispose();
try
{
return _mrvtsc.GetResult(token);
}
finally
{
_mrvtsc.Reset();
lock (this)
{
_activeWait = false;
_signaled = false;
_cancellationRegistration = default;
}
}
}
ValueTaskSourceStatus IValueTaskSource<bool>.GetStatus(short token)
{
return _mrvtsc.GetStatus(token);
}
void IValueTaskSource<bool>.OnCompleted(Action<object?> continuation, object? state, short token,
ValueTaskSourceOnCompletedFlags flags)
{
_mrvtsc.OnCompleted(continuation, state, token, flags);
}
public ValueTask<bool> SendAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
{
short token;
lock (this)
{
if (_transportClosed || _disposed) return new ValueTask<bool>(false);
if (_activeWait)
return new ValueTask<bool>(Task.FromException<bool>(ThrowHelper.NewConcurrentSendException()));
if (cancellationToken.IsCancellationRequested)
return new ValueTask<bool>(Task.FromCanceled<bool>(cancellationToken));
_activeWait = true;
Debug.Assert(!_signaled);
_buffer = buffer;
_cancellationToken = cancellationToken;
token = _mrvtsc.Version;
}
_cancellationRegistration =
cancellationToken.UnsafeRegister(state => ((KcpRawSendOperation?)state)!.SetCanceled(), this);
_notification.Set(buffer.Length);
return new ValueTask<bool>(this, token);
}
public bool CancelPendingOperation(Exception? innerException, CancellationToken cancellationToken)
{
lock (this)
{
if (_activeWait && !_signaled)
{
ClearPreviousOperation();
_mrvtsc.SetException(
ThrowHelper.NewOperationCanceledExceptionForCancelPendingSend(innerException, cancellationToken));
return true;
}
}
return false;
}
private void SetCanceled()
{
lock (this)
{
if (_activeWait && !_signaled)
{
var cancellationToken = _cancellationToken;
ClearPreviousOperation();
_mrvtsc.SetException(new OperationCanceledException(cancellationToken));
}
}
}
private void ClearPreviousOperation()
{
_signaled = true;
_buffer = default;
_cancellationToken = default;
}
public bool TryConsume(Memory<byte> buffer, out int bytesWritten)
{
lock (this)
{
if (_transportClosed || _disposed)
{
bytesWritten = 0;
return false;
}
if (!_activeWait)
{
bytesWritten = 0;
return false;
}
var source = _buffer;
if (source.Length > buffer.Length)
{
ClearPreviousOperation();
_mrvtsc.SetException(ThrowHelper.NewMessageTooLargeForBufferArgument());
bytesWritten = 0;
return false;
}
source.CopyTo(buffer);
bytesWritten = source.Length;
ClearPreviousOperation();
_mrvtsc.SetResult(true);
return true;
}
}
public void SetTransportClosed()
{
lock (this)
{
if (_transportClosed || _disposed) return;
if (_activeWait && !_signaled)
{
ClearPreviousOperation();
_mrvtsc.SetResult(false);
}
_transportClosed = true;
}
}
}

View File

@@ -0,0 +1,638 @@
#if NEED_LINKEDLIST_SHIM
using LinkedListOfQueueItem = KcpSharp.NetstandardShim.LinkedList<(KcpSharp.KcpBuffer Data, byte Fragment)>;
using LinkedListNodeOfQueueItem = KcpSharp.NetstandardShim.LinkedListNode<(KcpSharp.KcpBuffer Data, byte Fragment)>;
#else
using LinkedListOfQueueItem =
System.Collections.Generic.LinkedList<(KianaBH.KcpSharp.Base.KcpBuffer Data, byte Fragment)>;
using LinkedListNodeOfQueueItem =
System.Collections.Generic.LinkedListNode<(KianaBH.KcpSharp.Base.KcpBuffer Data, byte Fragment
)>;
#endif
using System.Diagnostics;
using System.Threading.Tasks.Sources;
namespace KianaBH.KcpSharp.Base;
internal sealed class KcpReceiveQueue : IValueTaskSource<KcpConversationReceiveResult>, IValueTaskSource<int>,
IValueTaskSource<bool>, IDisposable
{
private readonly KcpSendReceiveQueueItemCache _cache;
private readonly LinkedListOfQueueItem _queue;
private readonly int _queueSize;
private readonly bool _stream;
private bool _activeWait;
private Memory<byte> _buffer;
private CancellationTokenRegistration _cancellationRegistration;
private CancellationToken _cancellationToken;
private int _completedPacketsCount;
private bool _disposed;
private int _minimumBytes;
private int _minimumSegments;
private ManualResetValueTaskSourceCore<KcpConversationReceiveResult> _mrvtsc;
private byte _operationMode; // 0-receive 1-wait for message 2-wait for available data
private bool _signaled;
private bool _transportClosed;
public KcpReceiveQueue(bool stream, int queueSize, KcpSendReceiveQueueItemCache cache)
{
_mrvtsc = new ManualResetValueTaskSourceCore<KcpConversationReceiveResult>
{
RunContinuationsAsynchronously = true
};
_queue = new LinkedListOfQueueItem();
_stream = stream;
_queueSize = queueSize;
_cache = cache;
}
public void Dispose()
{
lock (_queue)
{
if (_disposed) return;
if (_activeWait && !_signaled)
{
ClearPreviousOperation(true);
_mrvtsc.SetResult(default);
}
var node = _queue.First;
while (node is not null)
{
node.ValueRef.Data.Release();
node = node.Next;
}
_queue.Clear();
_disposed = true;
_transportClosed = true;
}
}
bool IValueTaskSource<bool>.GetResult(short token)
{
_cancellationRegistration.Dispose();
try
{
return !_mrvtsc.GetResult(token).TransportClosed;
}
finally
{
_mrvtsc.Reset();
lock (_queue)
{
_activeWait = false;
_signaled = false;
_cancellationRegistration = default;
}
}
}
int IValueTaskSource<int>.GetResult(short token)
{
_cancellationRegistration.Dispose();
try
{
return _mrvtsc.GetResult(token).BytesReceived;
}
finally
{
_mrvtsc.Reset();
lock (_queue)
{
_activeWait = false;
_signaled = false;
_cancellationRegistration = default;
}
}
}
public ValueTaskSourceStatus GetStatus(short token)
{
return _mrvtsc.GetStatus(token);
}
public void OnCompleted(Action<object?> continuation, object? state, short token,
ValueTaskSourceOnCompletedFlags flags)
{
_mrvtsc.OnCompleted(continuation, state, token, flags);
}
KcpConversationReceiveResult IValueTaskSource<KcpConversationReceiveResult>.GetResult(short token)
{
_cancellationRegistration.Dispose();
try
{
return _mrvtsc.GetResult(token);
}
finally
{
_mrvtsc.Reset();
lock (_queue)
{
_activeWait = false;
_signaled = false;
_cancellationRegistration = default;
}
}
}
public bool TryPeek(out KcpConversationReceiveResult result)
{
lock (_queue)
{
if (_disposed || _transportClosed)
{
result = default;
return false;
}
if (_activeWait) ThrowHelper.ThrowConcurrentReceiveException();
if (_completedPacketsCount == 0)
{
result = new KcpConversationReceiveResult(0);
return false;
}
var node = _queue.First;
if (node is null)
{
result = new KcpConversationReceiveResult(0);
return false;
}
if (CalculatePacketSize(node, out var packetSize))
{
result = new KcpConversationReceiveResult(packetSize);
return true;
}
result = default;
return false;
}
}
public ValueTask<KcpConversationReceiveResult> WaitToReceiveAsync(CancellationToken cancellationToken)
{
short token;
lock (_queue)
{
if (_transportClosed || _disposed) return default;
if (_activeWait)
return new ValueTask<KcpConversationReceiveResult>(
Task.FromException<KcpConversationReceiveResult>(ThrowHelper.NewConcurrentReceiveException()));
if (cancellationToken.IsCancellationRequested)
return new ValueTask<KcpConversationReceiveResult>(
Task.FromCanceled<KcpConversationReceiveResult>(cancellationToken));
_operationMode = 1;
_buffer = default;
_minimumBytes = 0;
_minimumSegments = 0;
token = _mrvtsc.Version;
if (_completedPacketsCount > 0)
{
ConsumePacket(_buffer.Span, out var result, out var bufferTooSmall);
ClearPreviousOperation(false);
if (bufferTooSmall)
{
Debug.Assert(false, "This should never be reached.");
return new ValueTask<KcpConversationReceiveResult>(
Task.FromException<KcpConversationReceiveResult>(
ThrowHelper.NewBufferTooSmallForBufferArgument()));
}
return new ValueTask<KcpConversationReceiveResult>(result);
}
_activeWait = true;
Debug.Assert(!_signaled);
_cancellationToken = cancellationToken;
}
_cancellationRegistration =
cancellationToken.UnsafeRegister(state => ((KcpReceiveQueue?)state)!.SetCanceled(), this);
return new ValueTask<KcpConversationReceiveResult>(this, token);
}
public ValueTask<bool> WaitForAvailableDataAsync(int minimumBytes, int minimumSegments,
CancellationToken cancellationToken)
{
if (minimumBytes < 0)
return new ValueTask<bool>(
Task.FromException<bool>(ThrowHelper.NewArgumentOutOfRangeException(nameof(minimumBytes))));
if (minimumSegments < 0)
return new ValueTask<bool>(
Task.FromException<bool>(ThrowHelper.NewArgumentOutOfRangeException(nameof(minimumSegments))));
short token;
lock (_queue)
{
if (_transportClosed || _disposed) return default;
if (_activeWait)
return new ValueTask<bool>(Task.FromException<bool>(ThrowHelper.NewConcurrentReceiveException()));
if (cancellationToken.IsCancellationRequested)
return new ValueTask<bool>(Task.FromCanceled<bool>(cancellationToken));
if (CheckQueeuSize(_queue, minimumBytes, minimumSegments, _stream)) return new ValueTask<bool>(true);
_activeWait = true;
Debug.Assert(!_signaled);
_operationMode = 2;
_buffer = default;
_minimumBytes = minimumBytes;
_minimumSegments = minimumSegments;
_cancellationToken = cancellationToken;
token = _mrvtsc.Version;
}
_cancellationRegistration =
cancellationToken.UnsafeRegister(state => ((KcpReceiveQueue?)state)!.SetCanceled(), this);
return new ValueTask<bool>(this, token);
}
public bool TryReceive(Span<byte> buffer, out KcpConversationReceiveResult result)
{
lock (_queue)
{
if (_disposed || _transportClosed)
{
result = default;
return false;
}
if (_activeWait) ThrowHelper.ThrowConcurrentReceiveException();
if (_completedPacketsCount == 0)
{
result = new KcpConversationReceiveResult(0);
return false;
}
Debug.Assert(!_signaled);
_operationMode = 0;
ConsumePacket(buffer, out result, out var bufferTooSmall);
ClearPreviousOperation(false);
if (bufferTooSmall) ThrowHelper.ThrowBufferTooSmall();
return true;
}
}
public ValueTask<KcpConversationReceiveResult> ReceiveAsync(Memory<byte> buffer,
CancellationToken cancellationToken)
{
short token;
lock (_queue)
{
if (_transportClosed || _disposed) return default;
if (_activeWait)
return new ValueTask<KcpConversationReceiveResult>(
Task.FromException<KcpConversationReceiveResult>(ThrowHelper.NewConcurrentReceiveException()));
if (cancellationToken.IsCancellationRequested)
return new ValueTask<KcpConversationReceiveResult>(
Task.FromCanceled<KcpConversationReceiveResult>(cancellationToken));
_operationMode = 0;
_buffer = buffer;
token = _mrvtsc.Version;
if (_completedPacketsCount > 0)
{
ConsumePacket(_buffer.Span, out var result, out var bufferTooSmall);
ClearPreviousOperation(false);
if (bufferTooSmall)
return new ValueTask<KcpConversationReceiveResult>(
Task.FromException<KcpConversationReceiveResult>(
ThrowHelper.NewBufferTooSmallForBufferArgument()));
return new ValueTask<KcpConversationReceiveResult>(result);
}
_activeWait = true;
Debug.Assert(!_signaled);
_cancellationToken = cancellationToken;
}
_cancellationRegistration =
cancellationToken.UnsafeRegister(state => ((KcpReceiveQueue?)state)!.SetCanceled(), this);
return new ValueTask<KcpConversationReceiveResult>(this, token);
}
public ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken)
{
short token;
lock (_queue)
{
if (_transportClosed || _disposed)
return new ValueTask<int>(Task.FromException<int>(ThrowHelper.NewTransportClosedForStreamException()));
if (_activeWait)
return new ValueTask<int>(Task.FromException<int>(ThrowHelper.NewConcurrentReceiveException()));
if (cancellationToken.IsCancellationRequested)
return new ValueTask<int>(Task.FromCanceled<int>(cancellationToken));
_operationMode = 0;
_buffer = buffer;
token = _mrvtsc.Version;
if (_completedPacketsCount > 0)
{
ConsumePacket(_buffer.Span, out var result, out var bufferTooSmall);
ClearPreviousOperation(false);
if (bufferTooSmall)
return new ValueTask<int>(
Task.FromException<int>(ThrowHelper.NewBufferTooSmallForBufferArgument()));
return new ValueTask<int>(result.BytesReceived);
}
_activeWait = true;
Debug.Assert(!_signaled);
_cancellationToken = cancellationToken;
}
_cancellationRegistration =
cancellationToken.UnsafeRegister(state => ((KcpReceiveQueue?)state)!.SetCanceled(), this);
return new ValueTask<int>(this, token);
}
public bool CancelPendingOperation(Exception? innerException, CancellationToken cancellationToken)
{
lock (_queue)
{
if (_activeWait && !_signaled)
{
ClearPreviousOperation(true);
_mrvtsc.SetException(
ThrowHelper.NewOperationCanceledExceptionForCancelPendingReceive(innerException,
cancellationToken));
return true;
}
}
return false;
}
private void SetCanceled()
{
lock (_queue)
{
if (_activeWait && !_signaled)
{
var cancellationToken = _cancellationToken;
ClearPreviousOperation(true);
_mrvtsc.SetException(new OperationCanceledException(cancellationToken));
}
}
}
private void ClearPreviousOperation(bool signaled)
{
_signaled = signaled;
_operationMode = 0;
_buffer = default;
_minimumBytes = default;
_minimumSegments = default;
_cancellationToken = default;
}
public void Enqueue(KcpBuffer buffer, byte fragment)
{
lock (_queue)
{
if (_transportClosed || _disposed) return;
if (_stream)
{
if (buffer.Length == 0) return;
fragment = 0;
_queue.AddLast(_cache.Rent(buffer, 0));
}
else
{
var lastNode = _queue.Last;
if (lastNode is null || lastNode.ValueRef.Fragment == 0 || lastNode.ValueRef.Fragment - 1 == fragment)
{
_queue.AddLast(_cache.Rent(buffer, fragment));
}
else
{
fragment = 0;
_queue.AddLast(_cache.Rent(buffer, 0));
}
}
if (fragment == 0)
{
_completedPacketsCount++;
if (_activeWait && !_signaled)
{
TryCompleteReceive();
TryCompleteWaitForData();
}
}
}
}
private void TryCompleteReceive()
{
Debug.Assert(_activeWait && !_signaled);
if (_operationMode <= 1)
{
Debug.Assert(_operationMode == 0 || _operationMode == 1);
ConsumePacket(_buffer.Span, out var result, out var bufferTooSmall);
ClearPreviousOperation(true);
if (bufferTooSmall)
_mrvtsc.SetException(ThrowHelper.NewBufferTooSmallForBufferArgument());
else
_mrvtsc.SetResult(result);
}
}
private void TryCompleteWaitForData()
{
if (_operationMode == 2)
if (CheckQueeuSize(_queue, _minimumBytes, _minimumSegments, _stream))
{
ClearPreviousOperation(true);
_mrvtsc.SetResult(new KcpConversationReceiveResult(0));
}
}
private void ConsumePacket(Span<byte> buffer, out KcpConversationReceiveResult result, out bool bufferTooSmall)
{
var node = _queue.First;
if (node is null)
{
result = default;
bufferTooSmall = false;
return;
}
// peek
if (_operationMode == 1)
{
if (CalculatePacketSize(node, out var bytesRecevied))
result = new KcpConversationReceiveResult(bytesRecevied);
else
result = default;
bufferTooSmall = false;
return;
}
Debug.Assert(_operationMode == 0);
// ensure buffer is big enough
var bytesInPacket = 0;
if (!_stream)
{
while (node is not null)
{
bytesInPacket += node.ValueRef.Data.Length;
if (node.ValueRef.Fragment == 0) break;
node = node.Next;
}
if (node is null)
{
// incomplete packet
result = default;
bufferTooSmall = false;
return;
}
if (bytesInPacket > buffer.Length)
{
result = default;
bufferTooSmall = true;
return;
}
}
var anyDataReceived = false;
bytesInPacket = 0;
node = _queue.First;
LinkedListNodeOfQueueItem? next;
while (node is not null)
{
next = node.Next;
var fragment = node.ValueRef.Fragment;
ref var data = ref node.ValueRef.Data;
var sizeToCopy = Math.Min(data.Length, buffer.Length);
data.DataRegion.Span.Slice(0, sizeToCopy).CopyTo(buffer);
buffer = buffer.Slice(sizeToCopy);
bytesInPacket += sizeToCopy;
anyDataReceived = true;
if (sizeToCopy != data.Length)
{
// partial data is received.
node.ValueRef = (data.Consume(sizeToCopy), node.ValueRef.Fragment);
}
else
{
// full fragment is consumed
data.Release();
_queue.Remove(node);
_cache.Return(node);
if (fragment == 0) _completedPacketsCount--;
}
if (!_stream && fragment == 0) break;
if (sizeToCopy == 0) break;
node = next;
}
if (!anyDataReceived)
{
result = default;
bufferTooSmall = false;
}
else
{
result = new KcpConversationReceiveResult(bytesInPacket);
bufferTooSmall = false;
}
}
private static bool CalculatePacketSize(LinkedListNodeOfQueueItem first, out int packetSize)
{
var bytesRecevied = first.ValueRef.Data.Length;
if (first.ValueRef.Fragment == 0)
{
packetSize = bytesRecevied;
return true;
}
var node = first.Next;
while (node is not null)
{
bytesRecevied += node.ValueRef.Data.Length;
if (node.ValueRef.Fragment == 0)
{
packetSize = bytesRecevied;
return true;
}
node = node.Next;
}
// deadlink
packetSize = 0;
return false;
}
private static bool CheckQueeuSize(LinkedListOfQueueItem queue, int minimumBytes, int minimumSegments, bool stream)
{
var node = queue.First;
while (node is not null)
{
ref var buffer = ref node.ValueRef.Data;
minimumBytes = Math.Max(minimumBytes - buffer.Length, 0);
if (stream || node.ValueRef.Fragment == 0) minimumSegments = Math.Max(minimumSegments - 1, 0);
if (minimumBytes == 0 && minimumSegments == 0) return true;
node = node.Next;
}
return minimumBytes == 0 && minimumSegments == 0;
}
public void SetTransportClosed()
{
lock (_queue)
{
if (_transportClosed || _disposed) return;
if (_activeWait && !_signaled)
{
ClearPreviousOperation(true);
_mrvtsc.SetResult(default);
}
_transportClosed = true;
}
}
public int GetQueueSize()
{
int count;
lock (_queue)
{
count = _queue.Count;
}
return Math.Max(_queue.Count - _queueSize, 0);
}
}

View File

@@ -0,0 +1,30 @@
namespace KianaBH.KcpSharp.Base;
/// <summary>
/// Options for sending receive window size notification.
/// </summary>
public sealed class KcpReceiveWindowNotificationOptions
{
/// <summary>
/// Create an instance of option object for receive window size notification functionality.
/// </summary>
/// <param name="initialInterval">The initial interval in milliseconds of sending window size notification.</param>
/// <param name="maximumInterval">The maximum interval in milliseconds of sending window size notification.</param>
public KcpReceiveWindowNotificationOptions(int initialInterval, int maximumInterval)
{
if (initialInterval <= 0) throw new ArgumentOutOfRangeException(nameof(initialInterval));
if (maximumInterval < initialInterval) throw new ArgumentOutOfRangeException(nameof(maximumInterval));
InitialInterval = initialInterval;
MaximumInterval = maximumInterval;
}
/// <summary>
/// The initial interval in milliseconds of sending window size notification.
/// </summary>
public int InitialInterval { get; }
/// <summary>
/// The maximum interval in milliseconds of sending window size notification.
/// </summary>
public int MaximumInterval { get; }
}

View File

@@ -0,0 +1,195 @@
using System.Buffers;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace KianaBH.KcpSharp.Base;
/// <summary>
/// The buffer rented and owned by KcpSharp.
/// </summary>
public readonly struct KcpRentedBuffer : IEquatable<KcpRentedBuffer>, IDisposable
{
private readonly Memory<byte> _memory;
internal object? Owner { get; }
/// <summary>
/// The rented buffer.
/// </summary>
public Memory<byte> Memory => _memory;
/// <summary>
/// The rented buffer.
/// </summary>
public Span<byte> Span => _memory.Span;
/// <summary>
/// Whether this struct contains buffer rented from the pool.
/// </summary>
public bool IsAllocated => Owner is not null;
/// <summary>
/// Whether this buffer contains no data.
/// </summary>
public bool IsEmpry => _memory.IsEmpty;
internal KcpRentedBuffer(object? owner, Memory<byte> buffer)
{
Owner = owner;
_memory = buffer;
}
/// <summary>
/// Create the buffer from the specified <see cref="Memory{T}" />.
/// </summary>
/// <param name="memory">The memory region of this buffer.</param>
/// <returns>The rented buffer.</returns>
public static KcpRentedBuffer FromMemory(Memory<byte> memory)
{
return new KcpRentedBuffer(null, memory);
}
/// <summary>
/// Create the buffer from the shared array pool.
/// </summary>
/// <param name="size">The minimum size of the buffer required.</param>
/// <returns>The rented buffer.</returns>
public static KcpRentedBuffer FromSharedArrayPool(int size)
{
if (size < 0) throw new ArgumentOutOfRangeException(nameof(size));
var buffer = ArrayPool<byte>.Shared.Rent(size);
return new KcpRentedBuffer(ArrayPool<byte>.Shared, buffer);
}
/// <summary>
/// Create the buffer from the specified array pool.
/// </summary>
/// <param name="pool">The array pool to use.</param>
/// <param name="buffer">The byte array rented from the specified pool.</param>
/// <returns>The rented buffer.</returns>
public static KcpRentedBuffer FromArrayPool(ArrayPool<byte> pool, byte[] buffer)
{
if (pool is null) throw new ArgumentNullException(nameof(pool));
if (buffer is null) throw new ArgumentNullException(nameof(buffer));
return new KcpRentedBuffer(pool, buffer);
}
/// <summary>
/// Create the buffer from the specified array pool.
/// </summary>
/// <param name="pool">The array pool to use.</param>
/// <param name="arraySegment">The byte array segment rented from the specified pool.</param>
/// <returns>The rented buffer.</returns>
public static KcpRentedBuffer FromArrayPool(ArrayPool<byte> pool, ArraySegment<byte> arraySegment)
{
if (pool is null) throw new ArgumentNullException(nameof(pool));
return new KcpRentedBuffer(pool, arraySegment);
}
/// <summary>
/// Create the buffer from the specified array pool.
/// </summary>
/// <param name="pool">The array pool to use.</param>
/// <param name="size">The minimum size of the buffer required.</param>
/// <returns>The rented buffer.</returns>
public static KcpRentedBuffer FromArrayPool(ArrayPool<byte> pool, int size)
{
if (pool is null) throw new ArgumentNullException(nameof(pool));
if (size < 0) throw new ArgumentOutOfRangeException(nameof(size));
return new KcpRentedBuffer(pool, pool.Rent(size));
}
/// <summary>
/// Create the buffer from the memory owner.
/// </summary>
/// <param name="memoryOwner">The owner of this memory region.</param>
/// <returns>The rented buffer.</returns>
public static KcpRentedBuffer FromMemoryOwner(IMemoryOwner<byte> memoryOwner)
{
if (memoryOwner is null) throw new ArgumentNullException(nameof(memoryOwner));
return new KcpRentedBuffer(memoryOwner, memoryOwner.Memory);
}
/// <summary>
/// Create the buffer from the memory owner.
/// </summary>
/// <param name="memoryOwner">The owner of this memory region.</param>
/// <param name="memory">The memory region of the buffer.</param>
/// <returns>The rented buffer.</returns>
public static KcpRentedBuffer FromMemoryOwner(IDisposable memoryOwner, Memory<byte> memory)
{
if (memoryOwner is null) throw new ArgumentNullException(nameof(memoryOwner));
return new KcpRentedBuffer(memoryOwner, memory);
}
/// <summary>
/// Forms a slice out of the current buffer that begins at a specified index.
/// </summary>
/// <param name="start">The index at which to begin the slice.</param>
/// <returns>An object that contains all elements of the current instance from start to the end of the instance.</returns>
public KcpRentedBuffer Slice(int start)
{
var memory = _memory;
if ((uint)start > (uint)memory.Length) ThrowHelper.ThrowArgumentOutOfRangeException(nameof(start));
return new KcpRentedBuffer(Owner, memory.Slice(start));
}
/// <summary>
/// Forms a slice out of the current memory starting at a specified index for a specified length.
/// </summary>
/// <param name="start">The index at which to begin the slice.</param>
/// <param name="length">The number of elements to include in the slice.</param>
/// <returns>
/// An object that contains <paramref name="length" /> elements from the current instance starting at
/// <paramref name="start" />.
/// </returns>
public KcpRentedBuffer Slice(int start, int length)
{
var memory = _memory;
if ((uint)start > (uint)memory.Length) ThrowHelper.ThrowArgumentOutOfRangeException(nameof(start));
if ((uint)length > (uint)(memory.Length - start)) ThrowHelper.ThrowArgumentOutOfRangeException(nameof(length));
return new KcpRentedBuffer(Owner, memory.Slice(start, length));
}
/// <inheritdoc />
public void Dispose()
{
Debug.Assert(Owner is null || Owner is ArrayPool<byte> || Owner is IDisposable);
if (Owner is null) return;
if (Owner is ArrayPool<byte> arrayPool)
if (MemoryMarshal.TryGetArray(_memory, out ArraySegment<byte> arraySegment))
{
arrayPool.Return(arraySegment.Array!);
return;
}
if (Owner is IDisposable disposable) disposable.Dispose();
}
/// <inheritdoc />
public bool Equals(KcpRentedBuffer other)
{
return ReferenceEquals(Owner, other.Owner) && _memory.Equals(other._memory);
}
/// <inheritdoc />
public override bool Equals(object? obj)
{
return obj is KcpRentedBuffer other && Equals(other);
}
/// <inheritdoc />
public override int GetHashCode()
{
return Owner is null ? _memory.GetHashCode() : HashCode.Combine(RuntimeHelpers.GetHashCode(Owner), _memory);
}
/// <inheritdoc />
public override string ToString()
{
return $"KcpSharp.KcpRentedBuffer[{_memory.Length}]";
}
}

View File

@@ -0,0 +1,653 @@
#if NEED_LINKEDLIST_SHIM
using LinkedListOfQueueItem = KcpSharp.NetstandardShim.LinkedList<(KcpSharp.KcpBuffer Data, byte Fragment)>;
using LinkedListNodeOfQueueItem = KcpSharp.NetstandardShim.LinkedListNode<(KcpSharp.KcpBuffer Data, byte Fragment)>;
#else
using LinkedListOfQueueItem =
System.Collections.Generic.LinkedList<(KianaBH.KcpSharp.Base.KcpBuffer Data, byte Fragment)>;
#endif
using System.Diagnostics;
using System.Threading.Tasks.Sources;
namespace KianaBH.KcpSharp.Base;
internal sealed class KcpSendQueue : IValueTaskSource<bool>, IValueTaskSource, IDisposable
{
private readonly IKcpBufferPool _bufferPool;
private readonly KcpSendReceiveQueueItemCache _cache;
private readonly int _capacity;
private readonly int _mss;
private readonly LinkedListOfQueueItem _queue;
private readonly bool _stream;
private readonly KcpConversationUpdateActivation _updateActivation;
private bool _ackListNotEmpty;
private bool _activeWait;
private ReadOnlyMemory<byte> _buffer;
private CancellationTokenRegistration _cancellationRegistration;
private CancellationToken _cancellationToken;
private bool _disposed;
private bool _forStream;
private ManualResetValueTaskSourceCore<bool> _mrvtsc;
private byte _operationMode; // 0-send 1-flush 2-wait for space
private bool _signled;
private bool _transportClosed;
private long _unflushedBytes;
private int _waitForByteCount;
private int _waitForSegmentCount;
public KcpSendQueue(IKcpBufferPool bufferPool, KcpConversationUpdateActivation updateActivation, bool stream,
int capacity, int mss, KcpSendReceiveQueueItemCache cache)
{
_bufferPool = bufferPool;
_updateActivation = updateActivation;
_stream = stream;
_capacity = capacity;
_mss = mss;
_cache = cache;
_mrvtsc = new ManualResetValueTaskSourceCore<bool>
{
RunContinuationsAsynchronously = true
};
_queue = new LinkedListOfQueueItem();
}
public void Dispose()
{
lock (_queue)
{
if (_disposed) return;
if (_activeWait && !_signled)
{
if (_forStream)
{
ClearPreviousOperation();
_mrvtsc.SetException(ThrowHelper.NewTransportClosedForStreamException());
}
else
{
ClearPreviousOperation();
_mrvtsc.SetResult(false);
}
}
var node = _queue.First;
while (node is not null)
{
node.ValueRef.Data.Release();
node = node.Next;
}
_queue.Clear();
_disposed = true;
_transportClosed = true;
}
}
void IValueTaskSource.GetResult(short token)
{
try
{
_mrvtsc.GetResult(token);
}
finally
{
_mrvtsc.Reset();
lock (_queue)
{
_activeWait = false;
_signled = false;
_cancellationRegistration = default;
}
}
}
public ValueTaskSourceStatus GetStatus(short token)
{
return _mrvtsc.GetStatus(token);
}
public void OnCompleted(Action<object?> continuation, object? state, short token,
ValueTaskSourceOnCompletedFlags flags)
{
_mrvtsc.OnCompleted(continuation, state, token, flags);
}
bool IValueTaskSource<bool>.GetResult(short token)
{
_cancellationRegistration.Dispose();
try
{
return _mrvtsc.GetResult(token);
}
finally
{
_mrvtsc.Reset();
lock (_queue)
{
_activeWait = false;
_signled = false;
_cancellationRegistration = default;
}
}
}
public bool TryGetAvailableSpace(out int byteCount, out int segmentCount)
{
lock (_queue)
{
if (_transportClosed || _disposed)
{
byteCount = 0;
segmentCount = 0;
return false;
}
if (_activeWait && _operationMode == 0)
{
byteCount = 0;
segmentCount = 0;
return true;
}
GetAvailableSpaceCore(out byteCount, out segmentCount);
return true;
}
}
private void GetAvailableSpaceCore(out int byteCount, out int segmentCount)
{
var mss = _mss;
var availableFragments = _capacity - _queue.Count;
if (availableFragments < 0)
{
byteCount = 0;
segmentCount = 0;
return;
}
var availableBytes = availableFragments * mss;
if (_stream)
{
var last = _queue.Last;
if (last is not null) availableBytes += _mss - last.ValueRef.Data.Length;
}
byteCount = availableBytes;
segmentCount = availableFragments;
}
public ValueTask<bool> WaitForAvailableSpaceAsync(int minimumBytes, int minimumSegments,
CancellationToken cancellationToken)
{
short token;
lock (_queue)
{
if (_transportClosed || _disposed)
{
minimumBytes = 0;
minimumSegments = 0;
return default;
}
if ((uint)minimumBytes > (uint)(_mss * _capacity))
return new ValueTask<bool>(
Task.FromException<bool>(ThrowHelper.NewArgumentOutOfRangeException(nameof(minimumBytes))));
if ((uint)minimumSegments > (uint)_capacity)
return new ValueTask<bool>(
Task.FromException<bool>(ThrowHelper.NewArgumentOutOfRangeException(nameof(minimumSegments))));
if (_activeWait)
return new ValueTask<bool>(Task.FromException<bool>(ThrowHelper.NewConcurrentSendException()));
if (cancellationToken.IsCancellationRequested)
return new ValueTask<bool>(Task.FromCanceled<bool>(cancellationToken));
GetAvailableSpaceCore(out var currentByteCount, out var currentSegmentCount);
if (currentByteCount >= minimumBytes && currentSegmentCount >= minimumSegments)
return new ValueTask<bool>(true);
_activeWait = true;
Debug.Assert(!_signled);
_forStream = false;
_operationMode = 2;
_waitForByteCount = minimumBytes;
_waitForSegmentCount = minimumSegments;
_cancellationToken = cancellationToken;
token = _mrvtsc.Version;
}
_cancellationRegistration =
cancellationToken.UnsafeRegister(state => ((KcpSendQueue?)state)!.SetCanceled(), this);
return new ValueTask<bool>(this, token);
}
public bool TrySend(ReadOnlySpan<byte> buffer, bool allowPartialSend, out int bytesWritten)
{
lock (_queue)
{
if (allowPartialSend && !_stream) ThrowHelper.ThrowAllowPartialSendArgumentException();
if (_transportClosed || _disposed)
{
bytesWritten = 0;
return false;
}
var mss = _mss;
// Make sure there is enough space.
if (!allowPartialSend)
{
var spaceAvailable = mss * (_capacity - _queue.Count);
if (spaceAvailable < 0)
{
bytesWritten = 0;
return false;
}
if (_stream)
{
var last = _queue.Last;
if (last is not null) spaceAvailable += mss - last.ValueRef.Data.Length;
}
if (buffer.Length > spaceAvailable)
{
bytesWritten = 0;
return false;
}
}
// Copy buffer content.
bytesWritten = 0;
if (_stream)
{
var node = _queue.Last;
if (node is not null)
{
ref var data = ref node.ValueRef.Data;
var expand = mss - data.Length;
expand = Math.Min(expand, buffer.Length);
if (expand > 0)
{
data = data.AppendData(buffer.Slice(0, expand));
buffer = buffer.Slice(expand);
Interlocked.Add(ref _unflushedBytes, expand);
bytesWritten = expand;
}
}
if (buffer.IsEmpty) return true;
}
var anySegmentAdded = false;
var count = buffer.Length <= mss ? 1 : (buffer.Length + mss - 1) / mss;
Debug.Assert(count >= 1);
while (count > 0 && _queue.Count < _capacity)
{
var fragment = --count;
var size = buffer.Length > mss ? mss : buffer.Length;
var owner = _bufferPool.Rent(new KcpBufferPoolRentOptions(mss, false));
var kcpBuffer = KcpBuffer.CreateFromSpan(owner, buffer.Slice(0, size));
buffer = buffer.Slice(size);
_queue.AddLast(_cache.Rent(kcpBuffer, _stream ? (byte)0 : (byte)fragment));
Interlocked.Add(ref _unflushedBytes, size);
bytesWritten += size;
anySegmentAdded = true;
}
if (anySegmentAdded) _updateActivation.Notify();
return anySegmentAdded;
}
}
public ValueTask<bool> SendAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken)
{
short token;
lock (_queue)
{
if (_transportClosed || _disposed) return new ValueTask<bool>(false);
if (_activeWait)
return new ValueTask<bool>(Task.FromException<bool>(ThrowHelper.NewConcurrentSendException()));
if (cancellationToken.IsCancellationRequested)
return new ValueTask<bool>(Task.FromCanceled<bool>(cancellationToken));
var mss = _mss;
if (_stream)
{
var node = _queue.Last;
if (node is not null)
{
ref var data = ref node.ValueRef.Data;
var expand = mss - data.Length;
expand = Math.Min(expand, buffer.Length);
if (expand > 0)
{
data = data.AppendData(buffer.Span.Slice(0, expand));
buffer = buffer.Slice(expand);
Interlocked.Add(ref _unflushedBytes, expand);
}
}
if (buffer.IsEmpty) return new ValueTask<bool>(true);
}
var count = buffer.Length <= mss ? 1 : (buffer.Length + mss - 1) / mss;
Debug.Assert(count >= 1);
if (!_stream && count > 256)
return new ValueTask<bool>(Task.FromException<bool>(ThrowHelper.NewMessageTooLargeForBufferArgument()));
// synchronously put fragments into queue.
while (count > 0 && _queue.Count < _capacity)
{
var fragment = --count;
var size = buffer.Length > mss ? mss : buffer.Length;
var owner = _bufferPool.Rent(new KcpBufferPoolRentOptions(mss, false));
var kcpBuffer = KcpBuffer.CreateFromSpan(owner, buffer.Span.Slice(0, size));
buffer = buffer.Slice(size);
_queue.AddLast(_cache.Rent(kcpBuffer, _stream ? (byte)0 : (byte)fragment));
Interlocked.Add(ref _unflushedBytes, size);
}
_updateActivation.Notify();
if (count == 0) return new ValueTask<bool>(true);
_activeWait = true;
Debug.Assert(!_signled);
_forStream = false;
_operationMode = 0;
_buffer = buffer;
_cancellationToken = cancellationToken;
token = _mrvtsc.Version;
}
_cancellationRegistration =
cancellationToken.UnsafeRegister(state => ((KcpSendQueue?)state)!.SetCanceled(), this);
return new ValueTask<bool>(this, token);
}
public ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken)
{
short token;
lock (_queue)
{
if (_transportClosed || _disposed)
return new ValueTask(Task.FromException(ThrowHelper.NewTransportClosedForStreamException()));
if (_activeWait) return new ValueTask(Task.FromException(ThrowHelper.NewConcurrentSendException()));
if (cancellationToken.IsCancellationRequested) return new ValueTask(Task.FromCanceled(cancellationToken));
var mss = _mss;
if (_stream)
{
var node = _queue.Last;
if (node is not null)
{
ref var data = ref node.ValueRef.Data;
var expand = mss - data.Length;
expand = Math.Min(expand, buffer.Length);
if (expand > 0)
{
data = data.AppendData(buffer.Span.Slice(0, expand));
buffer = buffer.Slice(expand);
Interlocked.Add(ref _unflushedBytes, expand);
}
}
if (buffer.IsEmpty) return default;
}
var count = buffer.Length <= mss ? 1 : (buffer.Length + mss - 1) / mss;
Debug.Assert(count >= 1);
Debug.Assert(_stream);
// synchronously put fragments into queue.
while (count > 0 && _queue.Count < _capacity)
{
var size = buffer.Length > mss ? mss : buffer.Length;
var owner = _bufferPool.Rent(new KcpBufferPoolRentOptions(mss, false));
var kcpBuffer = KcpBuffer.CreateFromSpan(owner, buffer.Span.Slice(0, size));
buffer = buffer.Slice(size);
_queue.AddLast(_cache.Rent(kcpBuffer, 0));
Interlocked.Add(ref _unflushedBytes, size);
}
_updateActivation.Notify();
if (count == 0) return default;
_activeWait = true;
Debug.Assert(!_signled);
_forStream = true;
_operationMode = 0;
_buffer = buffer;
_cancellationToken = cancellationToken;
token = _mrvtsc.Version;
}
_cancellationRegistration =
cancellationToken.UnsafeRegister(state => ((KcpSendQueue?)state)!.SetCanceled(), this);
return new ValueTask(this, token);
}
public ValueTask<bool> FlushAsync(CancellationToken cancellationToken)
{
short token;
lock (_queue)
{
if (_transportClosed || _disposed) return new ValueTask<bool>(false);
if (_activeWait)
return new ValueTask<bool>(Task.FromException<bool>(ThrowHelper.NewConcurrentSendException()));
if (cancellationToken.IsCancellationRequested)
return new ValueTask<bool>(Task.FromCanceled<bool>(cancellationToken));
_activeWait = true;
Debug.Assert(!_signled);
_forStream = false;
_operationMode = 1;
_buffer = default;
_cancellationToken = cancellationToken;
token = _mrvtsc.Version;
}
_cancellationRegistration =
cancellationToken.UnsafeRegister(state => ((KcpSendQueue?)state)!.SetCanceled(), this);
return new ValueTask<bool>(this, token);
}
public ValueTask FlushForStreamAsync(CancellationToken cancellationToken)
{
short token;
lock (_queue)
{
if (_transportClosed || _disposed)
return new ValueTask(Task.FromException(ThrowHelper.NewTransportClosedForStreamException()));
if (_activeWait) return new ValueTask(Task.FromException(ThrowHelper.NewConcurrentSendException()));
if (cancellationToken.IsCancellationRequested) return new ValueTask(Task.FromCanceled(cancellationToken));
_activeWait = true;
Debug.Assert(!_signled);
_forStream = true;
_operationMode = 1;
_buffer = default;
_cancellationToken = cancellationToken;
token = _mrvtsc.Version;
}
_cancellationRegistration =
cancellationToken.UnsafeRegister(state => ((KcpSendQueue?)state)!.SetCanceled(), this);
return new ValueTask(this, token);
}
public bool CancelPendingOperation(Exception? innerException, CancellationToken cancellationToken)
{
lock (_queue)
{
if (_activeWait && !_signled)
{
ClearPreviousOperation();
_mrvtsc.SetException(
ThrowHelper.NewOperationCanceledExceptionForCancelPendingSend(innerException, cancellationToken));
return true;
}
}
return false;
}
private void SetCanceled()
{
lock (_queue)
{
if (_activeWait && !_signled)
{
var cancellationToken = _cancellationToken;
ClearPreviousOperation();
_mrvtsc.SetException(new OperationCanceledException(cancellationToken));
}
}
}
private void ClearPreviousOperation()
{
_signled = true;
_forStream = false;
_operationMode = 0;
_buffer = default;
_waitForByteCount = default;
_waitForSegmentCount = default;
_cancellationToken = default;
}
public bool TryDequeue(out KcpBuffer data, out byte fragment)
{
lock (_queue)
{
var node = _queue.First;
if (node is null)
{
data = default;
fragment = default;
return false;
}
(data, fragment) = node.ValueRef;
_queue.RemoveFirst();
node.ValueRef = default;
_cache.Return(node);
MoveOneSegmentIn();
CheckForAvailableSpace();
return true;
}
}
public void NotifyAckListChanged(bool itemsListNotEmpty)
{
lock (_queue)
{
if (_transportClosed || _disposed) return;
_ackListNotEmpty = itemsListNotEmpty;
TryCompleteFlush(Interlocked.Read(ref _unflushedBytes));
}
}
private void MoveOneSegmentIn()
{
if (_activeWait && !_signled && _operationMode == 0)
{
var buffer = _buffer;
var mss = _mss;
var count = buffer.Length <= mss ? 1 : (buffer.Length + mss - 1) / mss;
var size = buffer.Length > mss ? mss : buffer.Length;
var owner = _bufferPool.Rent(new KcpBufferPoolRentOptions(mss, false));
var kcpBuffer = KcpBuffer.CreateFromSpan(owner, buffer.Span.Slice(0, size));
_buffer = buffer.Slice(size);
_queue.AddLast(_cache.Rent(kcpBuffer, _stream ? (byte)0 : (byte)(count - 1)));
Interlocked.Add(ref _unflushedBytes, size);
if (count == 1)
{
ClearPreviousOperation();
_mrvtsc.SetResult(true);
}
}
}
private void CheckForAvailableSpace()
{
if (_activeWait && !_signled && _operationMode == 2)
{
GetAvailableSpaceCore(out var byteCount, out var segmentCount);
if (byteCount >= _waitForByteCount && segmentCount >= _waitForSegmentCount)
{
ClearPreviousOperation();
_mrvtsc.SetResult(true);
}
}
}
private void TryCompleteFlush(long unflushedBytes)
{
if (_activeWait && !_signled && _operationMode == 1)
if (_queue.Last is null && unflushedBytes == 0 && !_ackListNotEmpty)
{
ClearPreviousOperation();
_mrvtsc.SetResult(true);
}
}
public void SubtractUnflushedBytes(int size)
{
var unflushedBytes = Interlocked.Add(ref _unflushedBytes, -size);
if (unflushedBytes == 0)
lock (_queue)
{
TryCompleteFlush(0);
}
}
public long GetUnflushedBytes()
{
if (_transportClosed || _disposed) return 0;
return Interlocked.Read(ref _unflushedBytes);
}
public void SetTransportClosed()
{
lock (_queue)
{
if (_transportClosed || _disposed) return;
if (_activeWait && !_signled)
{
if (_forStream)
{
ClearPreviousOperation();
_mrvtsc.SetException(ThrowHelper.NewTransportClosedForStreamException());
}
else
{
ClearPreviousOperation();
_mrvtsc.SetResult(false);
}
}
_transportClosed = true;
Interlocked.Exchange(ref _unflushedBytes, 0);
}
}
}

View File

@@ -0,0 +1,8 @@
namespace KianaBH.KcpSharp.Base;
internal struct KcpSendReceiveBufferItem
{
public KcpBuffer Data;
public KcpPacketHeader Segment;
public KcpSendSegmentStats Stats;
}

View File

@@ -0,0 +1,68 @@
#if NEED_LINKEDLIST_SHIM
using LinkedListOfBufferItem = KcpSharp.NetstandardShim.LinkedList<KcpSharp.KcpSendReceiveBufferItem>;
using LinkedListNodeOfBufferItem = KcpSharp.NetstandardShim.LinkedListNode<KcpSharp.KcpSendReceiveBufferItem>;
#else
using LinkedListNodeOfBufferItem =
System.Collections.Generic.LinkedListNode<KianaBH.KcpSharp.Base.KcpSendReceiveBufferItem>;
using LinkedListOfBufferItem =
System.Collections.Generic.LinkedList<KianaBH.KcpSharp.Base.KcpSendReceiveBufferItem>;
#endif
namespace KianaBH.KcpSharp.Base;
internal struct KcpSendReceiveBufferItemCache
{
private LinkedListOfBufferItem _items;
private SpinLock _lock;
public static KcpSendReceiveBufferItemCache Create()
{
return new KcpSendReceiveBufferItemCache
{
_items = new LinkedListOfBufferItem(),
_lock = new SpinLock()
};
}
public LinkedListNodeOfBufferItem Allocate(in KcpSendReceiveBufferItem item)
{
var lockAcquired = false;
try
{
_lock.Enter(ref lockAcquired);
var node = _items.First;
if (node is null)
{
node = new LinkedListNodeOfBufferItem(item);
}
else
{
_items.Remove(node);
node.ValueRef = item;
}
return node;
}
finally
{
if (lockAcquired) _lock.Exit();
}
}
public void Return(LinkedListNodeOfBufferItem node)
{
var lockAcquired = false;
try
{
_lock.Enter(ref lockAcquired);
node.ValueRef = default;
_items.AddLast(node);
}
finally
{
if (lockAcquired) _lock.Exit();
}
}
}

View File

@@ -0,0 +1,76 @@
#if NEED_LINKEDLIST_SHIM
using LinkedListOfQueueItem = KcpSharp.NetstandardShim.LinkedList<(KcpSharp.KcpBuffer Data, byte Fragment)>;
using LinkedListNodeOfQueueItem = KcpSharp.NetstandardShim.LinkedListNode<(KcpSharp.KcpBuffer Data, byte Fragment)>;
#else
using LinkedListNodeOfQueueItem =
System.Collections.Generic.LinkedListNode<(KianaBH.KcpSharp.Base.KcpBuffer Data, byte Fragment
)>;
using LinkedListOfQueueItem =
System.Collections.Generic.LinkedList<(KianaBH.KcpSharp.Base.KcpBuffer Data, byte Fragment)>;
#endif
namespace KianaBH.KcpSharp.Base;
internal sealed class KcpSendReceiveQueueItemCache
{
private readonly LinkedListOfQueueItem _list = new();
private SpinLock _lock;
public LinkedListNodeOfQueueItem Rent(in KcpBuffer buffer, byte fragment)
{
var lockTaken = false;
try
{
_lock.Enter(ref lockTaken);
var node = _list.First;
if (node is null)
{
node = new LinkedListNodeOfQueueItem((buffer, fragment));
}
else
{
node.ValueRef = (buffer, fragment);
_list.RemoveFirst();
}
return node;
}
finally
{
if (lockTaken) _lock.Exit();
}
}
public void Return(LinkedListNodeOfQueueItem node)
{
node.ValueRef = default;
var lockTaken = false;
try
{
_lock.Enter(ref lockTaken);
_list.AddLast(node);
}
finally
{
if (lockTaken) _lock.Exit();
}
}
public void Clear()
{
var lockTaken = false;
try
{
_lock.Enter(ref lockTaken);
_list.Clear();
}
finally
{
if (lockTaken) _lock.Exit();
}
}
}

View File

@@ -0,0 +1,17 @@
namespace KianaBH.KcpSharp.Base;
internal readonly struct KcpSendSegmentStats
{
public KcpSendSegmentStats(uint resendTimestamp, uint rto, uint fastAck, uint transmitCount)
{
ResendTimestamp = resendTimestamp;
Rto = rto;
FastAck = fastAck;
TransmitCount = transmitCount;
}
public uint ResendTimestamp { get; }
public uint Rto { get; }
public uint FastAck { get; }
public uint TransmitCount { get; }
}

View File

@@ -0,0 +1,123 @@
using System.Net;
using System.Net.Sockets;
namespace KianaBH.KcpSharp.Base;
/// <summary>
/// Helper methods to create socket transports for KCP conversations.
/// </summary>
public static class KcpSocketTransport
{
/// <summary>
/// Create a socket transport for KCP covnersation.
/// </summary>
/// <param name="listener">The udp listener instance.</param>
/// <param name="endPoint">The remote endpoint.</param>
/// <param name="conversationId">The conversation ID.</param>
/// <param name="options">The options of the <see cref="KcpConversation" />.</param>
/// <returns>The created socket transport instance.</returns>
public static IKcpTransport<KcpConversation> CreateConversation(UdpClient listener, IPEndPoint endPoint,
long conversationId, KcpConversationOptions? options)
{
if (listener is null) throw new ArgumentNullException(nameof(listener));
if (endPoint is null) throw new ArgumentNullException(nameof(endPoint));
return new KcpSocketTransportForConversation(listener, endPoint, conversationId, options);
}
/// <summary>
/// Create a socket transport for KCP covnersation with no conversation ID.
/// </summary>
/// <param name="listener">The udp listener instance.</param>
/// <param name="endPoint">The remote endpoint.</param>
/// <param name="options">The options of the <see cref="KcpConversation" />.</param>
/// <returns>The created socket transport instance.</returns>
public static IKcpTransport<KcpConversation> CreateConversation(UdpClient listener, IPEndPoint endPoint,
KcpConversationOptions? options)
{
if (listener is null) throw new ArgumentNullException(nameof(listener));
if (endPoint is null) throw new ArgumentNullException(nameof(endPoint));
return new KcpSocketTransportForConversation(listener, endPoint, null, options);
}
/// <summary>
/// Create a socket transport for raw channel.
/// </summary>
/// <param name="listener">The udp listener instance.</param>
/// <param name="endPoint">The remote endpoint.</param>
/// <param name="conversationId">The conversation ID.</param>
/// <param name="options">The options of the <see cref="KcpRawChannel" />.</param>
/// <returns>The created socket transport instance.</returns>
public static IKcpTransport<KcpRawChannel> CreateRawChannel(UdpClient listener, IPEndPoint endPoint,
long conversationId, KcpRawChannelOptions? options)
{
if (listener is null) throw new ArgumentNullException(nameof(listener));
if (endPoint is null) throw new ArgumentNullException(nameof(endPoint));
return new KcpSocketTransportForRawChannel(listener, endPoint, conversationId, options);
}
/// <summary>
/// Create a socket transport for raw channel with no conversation ID.
/// </summary>
/// <param name="listener">The udp listener instance.</param>
/// <param name="endPoint">The remote endpoint.</param>
/// <param name="options">The options of the <see cref="KcpRawChannel" />.</param>
/// <returns>The created socket transport instance.</returns>
public static IKcpTransport<KcpRawChannel> CreateRawChannel(UdpClient listener, IPEndPoint endPoint,
KcpRawChannelOptions? options)
{
if (listener is null) throw new ArgumentNullException(nameof(listener));
if (endPoint is null) throw new ArgumentNullException(nameof(endPoint));
return new KcpSocketTransportForRawChannel(listener, endPoint, null, options);
}
/// <summary>
/// Create a socket transport for multiplex connection.
/// </summary>
/// <param name="listener">The udp listener instance.</param>
/// <param name="mtu">The maximum packet size that can be transmitted over the socket.</param>
/// <returns></returns>
public static IKcpTransport<IKcpMultiplexConnection> CreateMultiplexConnection(UdpClient listener, int mtu)
{
if (listener is null) throw new ArgumentNullException(nameof(listener));
return new KcpSocketTransportForMultiplexConnection<object>(listener, mtu);
}
/// <summary>
/// Create a socket transport for multiplex connection.
/// </summary>
/// <typeparam name="T">The type of the user state.</typeparam>
/// <param name="listener">The udp listener instance.</param>
/// <param name="mtu">The maximum packet size that can be transmitted over the socket.</param>
/// <returns></returns>
public static IKcpTransport<IKcpMultiplexConnection<T>> CreateMultiplexConnection<T>(UdpClient listener,
IPEndPoint endPoint, int mtu)
{
if (listener is null) throw new ArgumentNullException(nameof(listener));
if (endPoint is null) throw new ArgumentNullException(nameof(endPoint));
return new KcpSocketTransportForMultiplexConnection<T>(listener, mtu);
}
/// <summary>
/// Create a socket transport for multiplex connection.
/// </summary>
/// <typeparam name="T">The type of the user state.</typeparam>
/// <param name="listener">The udp listener instance.</param>
/// <param name="endPoint">The remote endpoint.</param>
/// <param name="mtu">The maximum packet size that can be transmitted over the socket.</param>
/// <param name="disposeAction">The action to invoke when state object is removed.</param>
/// <returns></returns>
public static IKcpTransport<IKcpMultiplexConnection<T>> CreateMultiplexConnection<T>(UdpClient listener,
EndPoint endPoint, int mtu, Action<T?>? disposeAction)
{
if (listener is null) throw new ArgumentNullException(nameof(listener));
if (endPoint is null) throw new ArgumentNullException(nameof(endPoint));
return new KcpSocketTransportForMultiplexConnection<T>(listener, mtu, disposeAction);
}
}

View File

@@ -0,0 +1,48 @@
using System.Net;
using System.Net.Sockets;
namespace KianaBH.KcpSharp.Base;
/// <summary>
/// Socket transport for KCP conversation.
/// </summary>
internal sealed class KcpSocketTransportForConversation : KcpSocketTransport<KcpConversation>,
IKcpTransport<KcpConversation>
{
private readonly long? _conversationId;
private readonly KcpConversationOptions? _options;
private readonly IPEndPoint _remoteEndPoint;
private Func<Exception, IKcpTransport<KcpConversation>, object?, bool>? _exceptionHandler;
private object? _exceptionHandlerState;
internal KcpSocketTransportForConversation(UdpClient listener, IPEndPoint endPoint, long? conversationId,
KcpConversationOptions? options)
: base(listener, options?.Mtu ?? KcpConversationOptions.MtuDefaultValue)
{
_conversationId = conversationId;
_remoteEndPoint = endPoint;
_options = options;
}
public void SetExceptionHandler(Func<Exception, IKcpTransport<KcpConversation>, object?, bool> handler,
object? state)
{
_exceptionHandler = handler;
_exceptionHandlerState = state;
}
protected override KcpConversation Activate()
{
return _conversationId.HasValue
? new KcpConversation(_remoteEndPoint, this, _conversationId.GetValueOrDefault(), _options)
: new KcpConversation(_remoteEndPoint, this, _options);
}
protected override bool HandleException(Exception ex)
{
if (_exceptionHandler is not null) return _exceptionHandler.Invoke(ex, this, _exceptionHandlerState);
return false;
}
}

View File

@@ -0,0 +1,42 @@
using System.Net.Sockets;
namespace KianaBH.KcpSharp.Base;
internal sealed class KcpSocketTransportForMultiplexConnection<T> : KcpSocketTransport<KcpMultiplexConnection<T>>,
IKcpTransport<IKcpMultiplexConnection<T>>
{
private readonly Action<T?>? _disposeAction;
private Func<Exception, IKcpTransport<IKcpMultiplexConnection<T>>, object?, bool>? _exceptionHandler;
private object? _exceptionHandlerState;
internal KcpSocketTransportForMultiplexConnection(UdpClient listener, int mtu)
: base(listener, mtu)
{
}
internal KcpSocketTransportForMultiplexConnection(UdpClient listener, int mtu, Action<T?>? disposeAction)
: base(listener, mtu)
{
_disposeAction = disposeAction;
}
IKcpMultiplexConnection<T> IKcpTransport<IKcpMultiplexConnection<T>>.Connection => Connection;
public void SetExceptionHandler(Func<Exception, IKcpTransport<IKcpMultiplexConnection<T>>, object?, bool> handler,
object? state)
{
_exceptionHandler = handler;
_exceptionHandlerState = state;
}
protected override KcpMultiplexConnection<T> Activate()
{
return new KcpMultiplexConnection<T>(this, _disposeAction);
}
protected override bool HandleException(Exception ex)
{
if (_exceptionHandler is not null) return _exceptionHandler.Invoke(ex, this, _exceptionHandlerState);
return false;
}
}

View File

@@ -0,0 +1,43 @@
using System.Net;
using System.Net.Sockets;
namespace KianaBH.KcpSharp.Base;
internal sealed class KcpSocketTransportForRawChannel : KcpSocketTransport<KcpRawChannel>, IKcpTransport<KcpRawChannel>
{
private readonly long? _conversationId;
private readonly KcpRawChannelOptions? _options;
private readonly IPEndPoint _remoteEndPoint;
private Func<Exception, IKcpTransport<KcpRawChannel>, object?, bool>? _exceptionHandler;
private object? _exceptionHandlerState;
internal KcpSocketTransportForRawChannel(UdpClient listener, IPEndPoint endPoint, long? conversationId,
KcpRawChannelOptions? options)
: base(listener, options?.Mtu ?? KcpConversationOptions.MtuDefaultValue)
{
_conversationId = conversationId;
_remoteEndPoint = endPoint;
_options = options;
}
public void SetExceptionHandler(Func<Exception, IKcpTransport<KcpRawChannel>, object?, bool> handler, object? state)
{
_exceptionHandler = handler;
_exceptionHandlerState = state;
}
protected override KcpRawChannel Activate()
{
return _conversationId.HasValue
? new KcpRawChannel(_remoteEndPoint, this, _conversationId.GetValueOrDefault(), _options)
: new KcpRawChannel(_remoteEndPoint, this, _options);
}
protected override bool HandleException(Exception ex)
{
if (_exceptionHandler is not null) return _exceptionHandler.Invoke(ex, this, _exceptionHandlerState);
return false;
}
}

View File

@@ -0,0 +1,199 @@
using KianaBH.Util;
using System.Buffers;
using System.Net;
using System.Net.Sockets;
namespace KianaBH.KcpSharp.Base;
/// <summary>
/// A Socket transport for upper-level connections.
/// </summary>
/// <typeparam name="T"></typeparam>
public abstract class KcpSocketTransport<T> : IKcpTransport, IDisposable where T : class, IKcpConversation
{
private readonly int _mtu;
private readonly UdpClient _udpListener;
private T? _connection;
private CancellationTokenSource? _cts;
private bool _disposed;
/// <summary>
/// Construct a socket transport with the specified socket and remote endpoint.
/// </summary>
/// <param name="socket">The socket instance.</param>
/// <param name="mtu">The maximum packet size that can be transmitted.</param>
protected KcpSocketTransport(UdpClient listener, int mtu)
{
_udpListener = listener ?? throw new ArgumentNullException(nameof(listener));
_mtu = mtu;
if (mtu < 50) throw new ArgumentOutOfRangeException(nameof(mtu));
}
/// <summary>
/// Get the upper-level connection instace. If Start is not called or the transport is closed,
/// <see cref="InvalidOperationException" /> will be thrown.
/// </summary>
/// <exception cref="InvalidOperationException">Start is not called or the transport is closed.</exception>
public T Connection => _connection ?? throw new InvalidOperationException();
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <inheritdoc />
public ValueTask SendPacketAsync(Memory<byte> packet, IPEndPoint endpoint,
CancellationToken cancellationToken = default)
{
if (_disposed) return default;
if (packet.Length > _mtu) return default;
return new ValueTask(_udpListener.SendAsync(packet.ToArray(), endpoint, cancellationToken).AsTask());
}
/// <summary>
/// Create the upper-level connection instance.
/// </summary>
/// <returns>The upper-level connection instance.</returns>
protected abstract T Activate();
/// <summary>
/// Allocate a block of memory used to receive from socket.
/// </summary>
/// <param name="size">The minimum size of the buffer.</param>
/// <returns>The allocated memory buffer.</returns>
protected virtual IMemoryOwner<byte> AllocateBuffer(int size)
{
#if NEED_POH_SHIM
return MemoryPool<byte>.Shared.Rent(size);
#else
return new ArrayMemoryOwner(GC.AllocateUninitializedArray<byte>(size, true));
#endif
}
/// <summary>
/// Handle exception thrown when receiving from remote endpoint.
/// </summary>
/// <param name="ex">The exception thrown.</param>
/// <returns>Whether error should be ignored.</returns>
protected virtual bool HandleException(Exception ex)
{
return false;
}
/// <summary>
/// Create the upper-level connection and start pumping packets from the socket to the upper-level connection.
/// </summary>
public void Start()
{
if (_disposed) throw new ObjectDisposedException(nameof(KcpSocketTransport));
if (_connection is not null) throw new InvalidOperationException();
_connection = Activate();
if (_connection is null) throw new InvalidOperationException();
_cts = new CancellationTokenSource();
RunReceiveLoop();
}
private async void RunReceiveLoop()
{
var cancellationToken = _cts?.Token ?? new CancellationToken(true);
IKcpConversation? connection = _connection;
if (connection is null || cancellationToken.IsCancellationRequested) return;
using var memoryOwner = AllocateBuffer(_mtu);
try
{
while (!cancellationToken.IsCancellationRequested)
{
var bytesReceived = 0;
var error = false;
UdpReceiveResult result = default;
try
{
result = await _udpListener.ReceiveAsync(cancellationToken);
bytesReceived = result.Buffer.Length;
}
catch
{
}
if (bytesReceived != 0 && bytesReceived <= _mtu)
{
if (bytesReceived == KcpConnection.HANDSHAKE_SIZE)
await KcpListener.HandleHandshake(result);
else if (!error)
await connection.InputPakcetAsync(result, cancellationToken).ConfigureAwait(false);
}
}
}
catch (OperationCanceledException)
{
// Do nothing
}
catch (Exception ex)
{
HandleExceptionWrapper(ex);
}
}
private bool HandleExceptionWrapper(Exception ex)
{
bool result;
try
{
new Logger("KcpServer").Error("KCP Error:", ex);
result = HandleException(ex);
}
catch
{
result = false;
}
_connection?.SetTransportClosed();
var cts = Interlocked.Exchange(ref _cts, null);
if (cts is not null)
{
cts.Cancel();
cts.Dispose();
}
return result;
}
/// <summary>
/// Dispose all the managed and the unmanaged resources used by this instance.
/// </summary>
/// <param name="disposing">If managed resources should be disposed.</param>
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
var cts = Interlocked.Exchange(ref _cts, null);
if (cts is not null)
{
cts.Cancel();
cts.Dispose();
}
_connection?.Dispose();
}
_connection = null;
_cts = null;
_disposed = true;
}
}
/// <summary>
/// Dispose the unmanaged resources used by this instance.
/// </summary>
~KcpSocketTransport()
{
Dispose(false);
}
}

176
KcpSharp/Base/KcpStream.cs Normal file
View File

@@ -0,0 +1,176 @@
namespace KianaBH.KcpSharp.Base;
/// <summary>
/// A stream wrapper of <see cref="KcpConversation" />.
/// </summary>
public sealed class KcpStream : Stream
{
private readonly bool _ownsConversation;
private KcpConversation? _conversation;
/// <summary>
/// Create a stream wrapper over an existing <see cref="KcpConversation" /> instance.
/// </summary>
/// <param name="conversation">The conversation instance. It must be in stream mode.</param>
/// <param name="ownsConversation">
/// Whether to dispose the <see cref="KcpConversation" /> instance when
/// <see cref="KcpStream" /> is disposed.
/// </param>
public KcpStream(KcpConversation conversation, bool ownsConversation)
{
if (conversation is null) throw new ArgumentNullException(nameof(conversation));
if (!conversation.StreamMode)
throw new ArgumentException("Non-stream mode conversation is not supported.", nameof(conversation));
_conversation = conversation;
_ownsConversation = ownsConversation;
}
/// <inheritdoc />
public override bool CanRead => true;
/// <inheritdoc />
public override bool CanSeek => false;
/// <inheritdoc />
public override bool CanWrite => true;
/// <summary>
/// The length of the stream. This always throws <see cref="NotSupportedException" />.
/// </summary>
public override long Length => throw new NotSupportedException();
/// <summary>
/// The position of the stream. This always throws <see cref="NotSupportedException" />.
/// </summary>
public override long Position
{
get => throw new NotSupportedException();
set => throw new NotSupportedException();
}
/// <summary>
/// Indicates data is available on the stream to be read. This property checks to see if at least one byte of data is
/// currently available
/// </summary>
public bool DataAvailable
{
get
{
if (_conversation is null) ThrowHelper.ThrowObjectDisposedForKcpStreamException();
return _conversation!.TryPeek(out var result) && result.BytesReceived != 0;
}
}
/// <inheritdoc />
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotSupportedException();
}
/// <inheritdoc />
public override void SetLength(long value)
{
throw new NotSupportedException();
}
/// <inheritdoc />
public override void Flush()
{
throw new NotSupportedException();
}
/// <inheritdoc />
public override Task FlushAsync(CancellationToken cancellationToken)
{
if (_conversation is null) return Task.FromException(ThrowHelper.NewObjectDisposedForKcpStreamException());
return _conversation!.FlushAsync(cancellationToken).AsTask();
}
/// <inheritdoc />
public override int Read(byte[] buffer, int offset, int count)
{
throw new NotSupportedException();
}
/// <inheritdoc />
public override void Write(byte[] buffer, int offset, int count)
{
throw new NotSupportedException();
}
/// <inheritdoc />
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
if (_conversation is null) return Task.FromException<int>(new ObjectDisposedException(nameof(KcpStream)));
return _conversation.ReadAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask();
}
/// <inheritdoc />
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
if (_conversation is null) return Task.FromException(new ObjectDisposedException(nameof(KcpStream)));
return _conversation.WriteAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask();
}
/// <inheritdoc />
public override int ReadByte()
{
throw new NotSupportedException();
}
/// <inheritdoc />
public override void WriteByte(byte value)
{
throw new NotSupportedException();
}
/// <inheritdoc />
protected override void Dispose(bool disposing)
{
if (disposing && _ownsConversation) _conversation?.Dispose();
_conversation = null;
base.Dispose(disposing);
}
#if !NO_FAST_SPAN
/// <inheritdoc />
public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
{
if (_conversation is null)
return new ValueTask<int>(Task.FromException<int>(new ObjectDisposedException(nameof(KcpStream))));
return _conversation.ReadAsync(buffer, cancellationToken);
}
/// <inheritdoc />
public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
{
if (_conversation is null)
return new ValueTask(Task.FromException(new ObjectDisposedException(nameof(KcpStream))));
return _conversation.WriteAsync(buffer, cancellationToken);
}
/// <inheritdoc />
public override ValueTask DisposeAsync()
{
if (_conversation is not null)
{
_conversation.Dispose();
_conversation = null;
}
return base.DisposeAsync();
}
/// <inheritdoc />
public override int Read(Span<byte> buffer)
{
throw new NotSupportedException();
}
/// <inheritdoc />
public override void Write(ReadOnlySpan<byte> buffer)
{
throw new NotSupportedException();
}
#endif
}

View File

@@ -0,0 +1,36 @@
#if NEED_SOCKET_SHIM
using System;
using System.Net.Sockets;
using System.Threading.Tasks;
using System.Threading.Tasks.Sources;
namespace KcpSharp
{
internal class AwaitableSocketAsyncEventArgs : SocketAsyncEventArgs, IValueTaskSource
{
private ManualResetValueTaskSourceCore<bool> _mrvtsc =
new ManualResetValueTaskSourceCore<bool> { RunContinuationsAsynchronously = true };
void IValueTaskSource.GetResult(short token) => _mrvtsc.GetResult(token);
ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) => _mrvtsc.GetStatus(token);
void IValueTaskSource.OnCompleted(Action<object?> continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags)
=> _mrvtsc.OnCompleted(continuation, state, token, flags);
protected override void OnCompleted(SocketAsyncEventArgs e)
{
_mrvtsc.SetResult(true);
}
public ValueTask WaitAsync()
{
return new ValueTask(this, _mrvtsc.Version);
}
public void Reset()
{
_mrvtsc.Reset();
}
}
}
#endif

View File

@@ -0,0 +1,12 @@
#if NEED_CANCELLATIONTOKEN_SHIM
namespace System.Threading
{
internal static class CancellationTokenShim
{
public static CancellationTokenRegistration UnsafeRegister(this CancellationToken cancellationToken, Action<object?> callback, object? state)
=> cancellationToken.Register(callback, state);
}
}
#endif

View File

@@ -0,0 +1,212 @@
#if NEED_LINKEDLIST_SHIM
using System;
using System.Diagnostics;
namespace KcpSharp.NetstandardShim
{
internal class LinkedList<T>
{
// This LinkedList is a doubly-Linked circular list.
internal LinkedListNode<T>? head;
internal int count;
internal int version;
public int Count
{
get { return count; }
}
public LinkedListNode<T>? First
{
get { return head; }
}
public LinkedListNode<T>? Last
{
get { return head == null ? null : head.prev; }
}
public void AddAfter(LinkedListNode<T> node, LinkedListNode<T> newNode)
{
ValidateNode(node);
ValidateNewNode(newNode);
InternalInsertNodeBefore(node.next!, newNode);
newNode.list = this;
}
public void AddBefore(LinkedListNode<T> node, LinkedListNode<T> newNode)
{
ValidateNode(node);
ValidateNewNode(newNode);
InternalInsertNodeBefore(node, newNode);
newNode.list = this;
if (node == head)
{
head = newNode;
}
}
public void AddFirst(LinkedListNode<T> node)
{
ValidateNewNode(node);
if (head == null)
{
InternalInsertNodeToEmptyList(node);
}
else
{
InternalInsertNodeBefore(head, node);
head = node;
}
node.list = this;
}
public void AddLast(LinkedListNode<T> node)
{
ValidateNewNode(node);
if (head == null)
{
InternalInsertNodeToEmptyList(node);
}
else
{
InternalInsertNodeBefore(head, node);
}
node.list = this;
}
public void Clear()
{
LinkedListNode<T>? current = head;
while (current != null)
{
LinkedListNode<T> temp = current;
current = current.Next; // use Next the instead of "next", otherwise it will loop forever
temp.Invalidate();
}
head = null;
count = 0;
version++;
}
public void Remove(LinkedListNode<T> node)
{
ValidateNode(node);
InternalRemoveNode(node);
}
public void RemoveFirst()
{
if (head == null) { throw new InvalidOperationException(); }
InternalRemoveNode(head);
}
private void InternalInsertNodeBefore(LinkedListNode<T> node, LinkedListNode<T> newNode)
{
newNode.next = node;
newNode.prev = node.prev;
node.prev!.next = newNode;
node.prev = newNode;
version++;
count++;
}
private void InternalInsertNodeToEmptyList(LinkedListNode<T> newNode)
{
Debug.Assert(head == null && count == 0, "LinkedList must be empty when this method is called!");
newNode.next = newNode;
newNode.prev = newNode;
head = newNode;
version++;
count++;
}
internal void InternalRemoveNode(LinkedListNode<T> node)
{
Debug.Assert(node.list == this, "Deleting the node from another list!");
Debug.Assert(head != null, "This method shouldn't be called on empty list!");
if (node.next == node)
{
Debug.Assert(count == 1 && head == node, "this should only be true for a list with only one node");
head = null;
}
else
{
node.next!.prev = node.prev;
node.prev!.next = node.next;
if (head == node)
{
head = node.next;
}
}
node.Invalidate();
count--;
version++;
}
internal static void ValidateNewNode(LinkedListNode<T> node)
{
if (node == null)
{
throw new ArgumentNullException(nameof(node));
}
if (node.list != null)
{
throw new InvalidOperationException();
}
}
internal void ValidateNode(LinkedListNode<T> node)
{
if (node == null)
{
throw new ArgumentNullException(nameof(node));
}
if (node.list != this)
{
throw new InvalidOperationException();
}
}
}
// Note following class is not serializable since we customized the serialization of LinkedList.
internal sealed class LinkedListNode<T>
{
internal LinkedList<T>? list;
internal LinkedListNode<T>? next;
internal LinkedListNode<T>? prev;
internal T item;
public LinkedListNode(T value)
{
item = value;
}
public LinkedListNode<T>? Next
{
get { return next == null || next == list!.head ? null : next; }
}
public LinkedListNode<T>? Previous
{
get { return prev == null || this == list!.head ? null : prev; }
}
/// <summary>Gets a reference to the value held by the node.</summary>
public ref T ValueRef => ref item;
internal void Invalidate()
{
list = null;
next = null;
prev = null;
}
}
}
#endif

View File

@@ -0,0 +1,10 @@
#if NEED_TCS_SHIM
namespace System.Threading.Tasks
{
internal class TaskCompletionSource : TaskCompletionSource<bool>
{
public void TrySetResult() => TrySetResult(true);
}
}
#endif

View File

@@ -0,0 +1,84 @@
namespace KianaBH.KcpSharp.Base;
internal static class ThrowHelper
{
public static void ThrowArgumentOutOfRangeException(string paramName)
{
throw new ArgumentOutOfRangeException(paramName);
}
public static void ThrowTransportClosedForStreanException()
{
throw new IOException("The underlying transport is closed.");
}
public static Exception NewMessageTooLargeForBufferArgument()
{
return new ArgumentException("Message is too large.", "buffer");
}
public static Exception NewBufferTooSmallForBufferArgument()
{
return new ArgumentException("Buffer is too small.", "buffer");
}
public static Exception ThrowBufferTooSmall()
{
throw new ArgumentException("Buffer is too small.", "buffer");
}
public static Exception ThrowAllowPartialSendArgumentException()
{
throw new ArgumentException("allowPartialSend should not be set to true in non-stream mode.",
"allowPartialSend");
}
public static Exception NewArgumentOutOfRangeException(string paramName)
{
return new ArgumentOutOfRangeException(paramName);
}
public static Exception NewConcurrentSendException()
{
return new InvalidOperationException("Concurrent send operations are not allowed.");
}
public static Exception NewConcurrentReceiveException()
{
return new InvalidOperationException("Concurrent receive operations are not allowed.");
}
public static Exception NewTransportClosedForStreamException()
{
throw new IOException("The underlying transport is closed.");
}
public static Exception NewOperationCanceledExceptionForCancelPendingSend(Exception? innerException,
CancellationToken cancellationToken)
{
return new OperationCanceledException("This operation is cancelled by a call to CancelPendingSend.",
innerException, cancellationToken);
}
public static Exception NewOperationCanceledExceptionForCancelPendingReceive(Exception? innerException,
CancellationToken cancellationToken)
{
return new OperationCanceledException("This operation is cancelled by a call to CancelPendingReceive.",
innerException, cancellationToken);
}
public static void ThrowConcurrentReceiveException()
{
throw new InvalidOperationException("Concurrent receive operations are not allowed.");
}
public static Exception NewObjectDisposedForKcpStreamException()
{
return new ObjectDisposedException(nameof(KcpStream));
}
public static void ThrowObjectDisposedForKcpStreamException()
{
throw new ObjectDisposedException(nameof(KcpStream));
}
}

67
KcpSharp/BasePacket.cs Normal file
View File

@@ -0,0 +1,67 @@
using KianaBH.Util.Extensions;
using Google.Protobuf;
namespace KianaBH.KcpSharp;
public class BasePacket(ushort cmdId)
{
private const uint HEADER_CONST = 0x01234567;
private const uint TAIL_CONST = 0x89ABCDEF;
private uint HeadMagic { get; set; }
private ushort PacketVersion { get; set; } = 1;
private ushort ClientVersion { get; set; }
private uint PacketId { get; set; }
public uint UserId { get; set; }
private uint UserIp { get; set; }
private uint Sign { get; set; }
private ushort SignType { get; set; }
public ushort CmdId { get; set; } = cmdId;
private ushort HeaderLength { get; set; }
private uint BodyLength { get; set; }
private byte[] Header { get; set; } = [];
public byte[] Body { get; set; } = [];
private uint TailMagic { get; set; }
public void SetData(byte[] data)
{
Body = data;
}
public void SetData(IMessage message)
{
Body = message.ToByteArray();
}
public void SetData(string base64)
{
SetData(Convert.FromBase64String(base64));
}
public byte[] BuildPacket()
{
using MemoryStream? ms = new();
using BinaryWriter? bw = new(ms);
bw.WriteUInt32BE(HEADER_CONST);
bw.WriteUInt16BE(PacketVersion);
bw.WriteUInt16BE(ClientVersion);
bw.WriteUInt32BE(PacketId);
bw.WriteUInt32BE(UserId);
bw.WriteUInt32BE(UserIp);
bw.WriteUInt32BE(Sign);
bw.WriteUInt16BE(SignType);
bw.WriteUInt16BE(CmdId);
bw.WriteUInt16BE((ushort)(Header.Length));
bw.WriteUInt32BE((uint)(Body.Length));
bw.Write(Header.ToArray());
bw.Write(Body.ToArray());
bw.WriteUInt32BE(TAIL_CONST);
var packet = ms.ToArray();
return packet;
}
}

165
KcpSharp/KcpConnection.cs Normal file
View File

@@ -0,0 +1,165 @@
using KianaBH.KcpSharp.Base;
using KianaBH.Proto;
using KianaBH.Util;
using KianaBH.Util.Security;
using Google.Protobuf;
using Google.Protobuf.Reflection;
using System.Collections.Concurrent;
using System.Net;
using System.Reflection;
namespace KianaBH.KcpSharp;
public class KcpConnection
{
public const int HANDSHAKE_SIZE = 20;
public static readonly ConcurrentBag<int> BannedPackets = [];
private static readonly Logger Logger = new("GameServer");
public static readonly ConcurrentDictionary<int, string> LogMap = [];
public static readonly ConcurrentBag<int> IgnoreLog =
[
//CmdIds.PlayerHeartBeatCsReq, CmdIds.PlayerHeartBeatScRsp,
//CmdIds.SceneEntityMoveCsReq, CmdIds.SceneEntityMoveScRsp,
//CmdIds.ClientDownloadDataScNotify
];
protected readonly CancellationTokenSource CancelToken;
protected readonly KcpConversation Conversation;
public readonly IPEndPoint RemoteEndPoint;
public string DebugFile = "";
public bool IsOnline = true;
public StreamWriter? Writer;
public KcpConnection(KcpConversation conversation, IPEndPoint remote)
{
Conversation = conversation;
RemoteEndPoint = remote;
CancelToken = new CancellationTokenSource();
Start();
}
public byte[]? XorKey { get; set; }
public ulong ClientSecretKeySeed { get; set; }
public long? ConversationId => Conversation.ConversationId;
public SessionStateEnum State { get; set; } = SessionStateEnum.INACTIVE;
public virtual void Start()
{
Logger.Info($"New connection from {RemoteEndPoint}.");
State = SessionStateEnum.WAITING_FOR_TOKEN;
}
public virtual void Stop(bool isServerStop = false)
{
Conversation.Dispose();
try
{
CancelToken.Cancel();
CancelToken.Dispose();
}
catch
{
}
IsOnline = false;
}
public void LogPacket(string sendOrRecv, ushort opcode, byte[] payload)
{
if (!ConfigManager.Config.ServerOption.EnableDebug) return;
try
{
//Logger.DebugWriteLine($"{sendOrRecv}: {Enum.GetName(typeof(OpCode), opcode)}({opcode})\r\n{Convert.ToHexString(payload)}");
if (IgnoreLog.Contains(opcode)) return;
if (!ConfigManager.Config.ServerOption.DebugDetailMessage) throw new Exception(); // go to catch block
var typ = AppDomain.CurrentDomain.GetAssemblies()
.SingleOrDefault(assembly => assembly.GetName().Name == "KianaProto")!.GetTypes()
.First(t => t.Name == $"{LogMap[opcode]}"); //get the type using the packet name
var descriptor =
typ.GetProperty("Descriptor", BindingFlags.Public | BindingFlags.Static)?.GetValue(
null, null) as MessageDescriptor; // get the static property Descriptor
var packet = descriptor?.Parser.ParseFrom(payload);
var formatter = JsonFormatter.Default;
var asJson = formatter.Format(packet);
var output = $"{sendOrRecv}: {LogMap[opcode]}({opcode})\r\n{asJson}";
if (ConfigManager.Config.ServerOption.DebugMessage)
Logger.Debug(output);
if (DebugFile == "" || !ConfigManager.Config.ServerOption.SavePersonalDebugFile) return;
var sw = GetWriter();
sw.WriteLine($"[{DateTime.Now:HH:mm:ss}] [GameServer] [DEBUG] " + output);
sw.Flush();
}
catch
{
var output = $"{sendOrRecv}: {LogMap.GetValueOrDefault(opcode, "UnknownPacket")}({opcode})";
if (ConfigManager.Config.ServerOption.DebugMessage)
Logger.Debug(output);
if (DebugFile != "" && ConfigManager.Config.ServerOption.SavePersonalDebugFile)
{
var sw = GetWriter();
sw.WriteLine($"[{DateTime.Now:HH:mm:ss}] [GameServer] [DEBUG] " + output);
sw.Flush();
}
}
}
private StreamWriter GetWriter()
{
// Create the file if it doesn't exist
var file = new FileInfo(DebugFile);
if (!file.Exists)
{
Directory.CreateDirectory(file.DirectoryName!);
File.Create(DebugFile).Dispose();
}
Writer ??= new StreamWriter(DebugFile, true);
return Writer;
}
public async Task SendPacket(byte[] packet)
{
try
{
_ = await Conversation.SendAsync(packet, CancelToken.Token);
}
catch
{
// ignore
}
}
public async Task SendPacket(BasePacket packet)
{
// Test
if (packet.CmdId <= 0)
{
Logger.Debug("Tried to send packet with missing cmd id!");
return;
}
// DO NOT REMOVE (unless we find a way to validate code before sending to client which I don't think we can)
if (BannedPackets.Contains(packet.CmdId)) return;
LogPacket("Send", packet.CmdId, packet.Body);
// Header
var packetBytes = packet.BuildPacket();
try
{
await SendPacket(packetBytes);
}
catch
{
// ignore
}
}
public async Task SendPacket(int cmdId)
{
await SendPacket(new BasePacket((ushort)cmdId));
}
}

148
KcpSharp/KcpListener.cs Normal file
View File

@@ -0,0 +1,148 @@
using KianaBH.Internationalization;
using KianaBH.KcpSharp.Base;
using KianaBH.Util;
using KianaBH.Util.Extensions;
using System.Net;
using System.Net.Sockets;
namespace KianaBH.KcpSharp;
public class KcpListener
{
private static UdpClient? UDPClient;
private static IPEndPoint? ListenAddress;
private static IKcpTransport<IKcpMultiplexConnection>? KCPTransport;
private static readonly Logger Logger = new("GameServer");
public static readonly SortedList<long, KcpConnection> Connections = [];
private static readonly KcpConversationOptions ConvOpt = new()
{
StreamMode = false,
Mtu = 1400,
ReceiveWindow = 256,
SendWindow = 256,
NoDelay = true,
UpdateInterval = 100,
KeepAliveOptions = new KcpKeepAliveOptions(1000, ConfigManager.Config.GameServer.KcpAliveMs)
};
public static Type BaseConnection { get; set; } = typeof(KcpConnection);
private static Socket? UDPListener => UDPClient?.Client;
private static IKcpMultiplexConnection? Multiplex => KCPTransport?.Connection;
private static int PORT => ConfigManager.Config.GameServer.Port;
public static KcpConnection? GetConnectionByEndPoint(IPEndPoint ep)
{
return Connections.Values.FirstOrDefault(c => c.RemoteEndPoint.Equals(ep));
}
public static void StartListener()
{
ListenAddress = new IPEndPoint(IPAddress.Parse(ConfigManager.Config.GameServer.BindAddress), (int)PORT);
UDPClient = new UdpClient(ListenAddress);
if (UDPListener == null) return;
KCPTransport = KcpSocketTransport.CreateMultiplexConnection(UDPClient, 1400);
KCPTransport.Start();
Logger.Info(I18NManager.Translate("Server.ServerInfo.ServerRunning", I18NManager.Translate("Word.Game"),
ConfigManager.Config.GameServer.GetDisplayAddress()));
}
private static void RegisterConnection(KcpConnection con)
{
if (!con.ConversationId.HasValue) return;
Connections[con.ConversationId.Value] = con;
}
public static void UnregisterConnection(KcpConnection con)
{
if (!con.ConversationId.HasValue) return;
var convId = con.ConversationId.Value;
if (Connections.Remove(convId))
{
Multiplex?.UnregisterConversation(convId);
Logger.Info($"Connection with {con.RemoteEndPoint} has been closed");
}
}
public static async Task HandleHandshake(UdpReceiveResult rcv)
{
try
{
var con = GetConnectionByEndPoint(rcv.RemoteEndPoint);
await using MemoryStream? ms = new(rcv.Buffer);
using BinaryReader? br = new(ms);
var code = br.ReadInt32BE();
br.ReadUInt32();
br.ReadUInt32();
var enet = br.ReadInt32BE();
br.ReadUInt32();
switch (code)
{
case 0x000000FF:
if (con != null)
{
Logger.Info($"Duplicate handshake from {con.RemoteEndPoint}");
return;
}
await AcceptConnection(rcv, enet);
break;
case 0x00000194:
if (con == null)
{
Logger.Info($"Inexistent connection asked for disconnect from {rcv.RemoteEndPoint}");
return;
}
await SendDisconnectPacket(con, 5);
break;
default:
Logger.Error($"Invalid handshake code received {code}");
return;
}
}
catch (Exception ex)
{
Logger.Error($"Failed to handle handshake: {ex}");
}
}
private static async Task AcceptConnection(UdpReceiveResult rcv, int enet)
{
var convId = Connections.GetNextAvailableIndex();
var convo = Multiplex?.CreateConversation(convId, rcv.RemoteEndPoint, ConvOpt);
if (convo == null) return;
var con = (KcpConnection)Activator.CreateInstance(BaseConnection, [convo, rcv.RemoteEndPoint])!;
RegisterConnection(con);
await SendHandshakeResponse(con, enet);
}
private static async Task SendHandshakeResponse(KcpConnection user, int enet)
{
if (user == null || UDPClient == null || !user.ConversationId.HasValue) return;
var convId = user.ConversationId.Value;
await using MemoryStream? ms = new();
await using BinaryWriter? bw = new(ms);
bw.WriteInt32BE(0x00000145);
bw.WriteConvID(convId);
bw.WriteInt32BE(enet);
bw.WriteInt32BE(0x14514545);
var data = ms.ToArray();
await UDPClient.SendAsync(data, data.Length, user.RemoteEndPoint);
}
public static async Task SendDisconnectPacket(KcpConnection user, int code)
{
if (user == null || UDPClient == null || !user.ConversationId.HasValue) return;
var convId = user.ConversationId.Value;
await using MemoryStream? ms = new();
await using BinaryWriter? bw = new(ms);
bw.WriteInt32BE(0x00000194);
bw.WriteConvID(convId);
bw.WriteInt32BE(code);
bw.WriteInt32BE(0x19419494);
var data = ms.ToArray();
await UDPClient.SendAsync(data, data.Length, user.RemoteEndPoint);
}
}

21
KcpSharp/KcpSharp.csproj Normal file
View File

@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<CETCompat>false</CETCompat>
<AssemblyName>KcpSharp</AssemblyName>
<RootNamespace>KianaBH.KcpSharp</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Google.Protobuf" Version="3.29.2" />
<PackageReference Include="System.IO.Pipelines" Version="9.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Common\Common.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,10 @@
namespace KianaBH.KcpSharp;
public enum SessionStateEnum
{
INACTIVE,
WAITING_FOR_TOKEN,
WAITING_FOR_LOGIN,
PICKING_CHARACTER,
ACTIVE
}