mirror of
https://github.com/MikuLeaks/KianaBH3.git
synced 2025-12-12 21:04:41 +01:00
Init enter game
This commit is contained in:
23
KcpSharp/Base/ArrayMemoryOwner.cs
Normal file
23
KcpSharp/Base/ArrayMemoryOwner.cs
Normal 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
|
||||
112
KcpSharp/Base/AsyncAutoResetEvent.cs
Normal file
112
KcpSharp/Base/AsyncAutoResetEvent.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
11
KcpSharp/Base/DefaultArrayPoolBufferAllocator.cs
Normal file
11
KcpSharp/Base/DefaultArrayPoolBufferAllocator.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
14
KcpSharp/Base/IKcpBufferPool.cs
Normal file
14
KcpSharp/Base/IKcpBufferPool.cs
Normal 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);
|
||||
}
|
||||
25
KcpSharp/Base/IKcpConversation.cs
Normal file
25
KcpSharp/Base/IKcpConversation.cs
Normal 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();
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace KianaBH.KcpSharp.Base;
|
||||
|
||||
internal interface IKcpConversationUpdateNotificationSource
|
||||
{
|
||||
ReadOnlyMemory<byte> Packet { get; }
|
||||
void Release();
|
||||
}
|
||||
16
KcpSharp/Base/IKcpExceptionProducer.cs
Normal file
16
KcpSharp/Base/IKcpExceptionProducer.cs
Normal 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);
|
||||
}
|
||||
56
KcpSharp/Base/IKcpMultiplexConnection.cs
Normal file
56
KcpSharp/Base/IKcpMultiplexConnection.cs
Normal 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);
|
||||
}
|
||||
56
KcpSharp/Base/IKcpMultiplexConnectionOfT.cs
Normal file
56
KcpSharp/Base/IKcpMultiplexConnectionOfT.cs
Normal 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);
|
||||
}
|
||||
18
KcpSharp/Base/IKcpTransport.cs
Normal file
18
KcpSharp/Base/IKcpTransport.cs
Normal 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);
|
||||
}
|
||||
22
KcpSharp/Base/IKcpTransportOfT.cs
Normal file
22
KcpSharp/Base/IKcpTransportOfT.cs
Normal 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();
|
||||
}
|
||||
93
KcpSharp/Base/KcpAcknowledgeList.cs
Normal file
93
KcpSharp/Base/KcpAcknowledgeList.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
51
KcpSharp/Base/KcpBuffer.cs
Normal file
51
KcpSharp/Base/KcpBuffer.cs
Normal 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.");
|
||||
}
|
||||
}
|
||||
50
KcpSharp/Base/KcpBufferPoolRentOptions.cs
Normal file
50
KcpSharp/Base/KcpBufferPoolRentOptions.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
9
KcpSharp/Base/KcpCommand.cs
Normal file
9
KcpSharp/Base/KcpCommand.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace KianaBH.KcpSharp.Base;
|
||||
|
||||
internal enum KcpCommand : byte
|
||||
{
|
||||
Push = 81,
|
||||
Ack = 82,
|
||||
WindowProbe = 83,
|
||||
WindowSize = 84
|
||||
}
|
||||
272
KcpSharp/Base/KcpConversation.FlushAsyncMethodBuilder.cs
Normal file
272
KcpSharp/Base/KcpConversation.FlushAsyncMethodBuilder.cs
Normal 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
|
||||
}
|
||||
1467
KcpSharp/Base/KcpConversation.cs
Normal file
1467
KcpSharp/Base/KcpConversation.cs
Normal file
File diff suppressed because it is too large
Load Diff
98
KcpSharp/Base/KcpConversationOptions.cs
Normal file
98
KcpSharp/Base/KcpConversationOptions.cs
Normal 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; }
|
||||
}
|
||||
77
KcpSharp/Base/KcpConversationReceiveResult.cs
Normal file
77
KcpSharp/Base/KcpConversationReceiveResult.cs
Normal 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.";
|
||||
}
|
||||
}
|
||||
474
KcpSharp/Base/KcpConversationUpdateActivation.cs
Normal file
474
KcpSharp/Base/KcpConversationUpdateActivation.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
27
KcpSharp/Base/KcpConversationUpdateNotification.cs
Normal file
27
KcpSharp/Base/KcpConversationUpdateNotification.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
106
KcpSharp/Base/KcpExceptionProducerExtensions.cs
Normal file
106
KcpSharp/Base/KcpExceptionProducerExtensions.cs
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
13
KcpSharp/Base/KcpGlobalVars.cs
Normal file
13
KcpSharp/Base/KcpGlobalVars.cs
Normal 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
|
||||
}
|
||||
26
KcpSharp/Base/KcpKeepAliveOptions.cs
Normal file
26
KcpSharp/Base/KcpKeepAliveOptions.cs
Normal 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; }
|
||||
}
|
||||
281
KcpSharp/Base/KcpMultiplexConnection.cs
Normal file
281
KcpSharp/Base/KcpMultiplexConnection.cs
Normal 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>));
|
||||
}
|
||||
}
|
||||
89
KcpSharp/Base/KcpPacketHeader.cs
Normal file
89
KcpSharp/Base/KcpPacketHeader.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
9
KcpSharp/Base/KcpProbeType.cs
Normal file
9
KcpSharp/Base/KcpProbeType.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace KianaBH.KcpSharp.Base;
|
||||
|
||||
[Flags]
|
||||
internal enum KcpProbeType
|
||||
{
|
||||
None = 0,
|
||||
AskSend = 1,
|
||||
AskTell = 2
|
||||
}
|
||||
400
KcpSharp/Base/KcpRawChannel.cs
Normal file
400
KcpSharp/Base/KcpRawChannel.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
34
KcpSharp/Base/KcpRawChannelOptions.cs
Normal file
34
KcpSharp/Base/KcpRawChannelOptions.cs
Normal 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; }
|
||||
}
|
||||
343
KcpSharp/Base/KcpRawReceiveQueue.cs
Normal file
343
KcpSharp/Base/KcpRawReceiveQueue.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
184
KcpSharp/Base/KcpRawSendOperation.cs
Normal file
184
KcpSharp/Base/KcpRawSendOperation.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
638
KcpSharp/Base/KcpReceiveQueue.cs
Normal file
638
KcpSharp/Base/KcpReceiveQueue.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
30
KcpSharp/Base/KcpReceiveWindowNotificationOptions.cs
Normal file
30
KcpSharp/Base/KcpReceiveWindowNotificationOptions.cs
Normal 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; }
|
||||
}
|
||||
195
KcpSharp/Base/KcpRentedBuffer.cs
Normal file
195
KcpSharp/Base/KcpRentedBuffer.cs
Normal 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}]";
|
||||
}
|
||||
}
|
||||
653
KcpSharp/Base/KcpSendQueue.cs
Normal file
653
KcpSharp/Base/KcpSendQueue.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
8
KcpSharp/Base/KcpSendReceiveBufferItem.cs
Normal file
8
KcpSharp/Base/KcpSendReceiveBufferItem.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace KianaBH.KcpSharp.Base;
|
||||
|
||||
internal struct KcpSendReceiveBufferItem
|
||||
{
|
||||
public KcpBuffer Data;
|
||||
public KcpPacketHeader Segment;
|
||||
public KcpSendSegmentStats Stats;
|
||||
}
|
||||
68
KcpSharp/Base/KcpSendReceiveBufferItemCache.cs
Normal file
68
KcpSharp/Base/KcpSendReceiveBufferItemCache.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
76
KcpSharp/Base/KcpSendReceiveQueueItemCache.cs
Normal file
76
KcpSharp/Base/KcpSendReceiveQueueItemCache.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
17
KcpSharp/Base/KcpSendSegmentStats.cs
Normal file
17
KcpSharp/Base/KcpSendSegmentStats.cs
Normal 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; }
|
||||
}
|
||||
123
KcpSharp/Base/KcpSocketTransport.cs
Normal file
123
KcpSharp/Base/KcpSocketTransport.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
48
KcpSharp/Base/KcpSocketTransportForConversation.cs
Normal file
48
KcpSharp/Base/KcpSocketTransportForConversation.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
42
KcpSharp/Base/KcpSocketTransportForMultiplexConnection.cs
Normal file
42
KcpSharp/Base/KcpSocketTransportForMultiplexConnection.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
43
KcpSharp/Base/KcpSocketTransportForRawChannel.cs
Normal file
43
KcpSharp/Base/KcpSocketTransportForRawChannel.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
199
KcpSharp/Base/KcpSocketTransportOfT.cs
Normal file
199
KcpSharp/Base/KcpSocketTransportOfT.cs
Normal 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
176
KcpSharp/Base/KcpStream.cs
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
12
KcpSharp/Base/NetstandardShim/CancellationTokenShim.cs
Normal file
12
KcpSharp/Base/NetstandardShim/CancellationTokenShim.cs
Normal 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
|
||||
212
KcpSharp/Base/NetstandardShim/LinkedListNetstandard.cs
Normal file
212
KcpSharp/Base/NetstandardShim/LinkedListNetstandard.cs
Normal 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
|
||||
10
KcpSharp/Base/NetstandardShim/TaskCompletionSource.cs
Normal file
10
KcpSharp/Base/NetstandardShim/TaskCompletionSource.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
#if NEED_TCS_SHIM
|
||||
namespace System.Threading.Tasks
|
||||
{
|
||||
internal class TaskCompletionSource : TaskCompletionSource<bool>
|
||||
{
|
||||
public void TrySetResult() => TrySetResult(true);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
84
KcpSharp/Base/ThrowHelper.cs
Normal file
84
KcpSharp/Base/ThrowHelper.cs
Normal 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
67
KcpSharp/BasePacket.cs
Normal 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
165
KcpSharp/KcpConnection.cs
Normal 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
148
KcpSharp/KcpListener.cs
Normal 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
21
KcpSharp/KcpSharp.csproj
Normal 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>
|
||||
10
KcpSharp/SessionStateEnum.cs
Normal file
10
KcpSharp/SessionStateEnum.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace KianaBH.KcpSharp;
|
||||
|
||||
public enum SessionStateEnum
|
||||
{
|
||||
INACTIVE,
|
||||
WAITING_FOR_TOKEN,
|
||||
WAITING_FOR_LOGIN,
|
||||
PICKING_CHARACTER,
|
||||
ACTIVE
|
||||
}
|
||||
Reference in New Issue
Block a user