mirror of
https://github.com/EggLinks/DanhengServer-OpenSource.git
synced 2026-01-02 20:26:03 +08:00
Implement packet handler sender
This commit is contained in:
@@ -37,7 +37,7 @@ namespace EggLink.DanhengServer.Configuration
|
||||
public string BindAddress { get; set; } = "127.0.0.1";
|
||||
public int Port { get; set; } = 23301;
|
||||
public string PublicAddress { get; set; } = "127.0.0.1";
|
||||
public int PublicPort { get; set; } = 23301;
|
||||
public uint PublicPort { get; set; } = 23301;
|
||||
public string GameServerId { get; set; } = "dan_heng";
|
||||
public string GameServerName { get; set; } = "DanhengServer";
|
||||
public string GameServerDescription { get; set; } = "A re-implementation of StarRail server";
|
||||
|
||||
@@ -8,7 +8,7 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace EggLink.DanhengServer.Util
|
||||
{
|
||||
internal class Crypto
|
||||
public class Crypto
|
||||
{
|
||||
private static Random secureRandom = new();
|
||||
public static Logger logger = new("Crypto");
|
||||
|
||||
70
Common/Util/Extensions.cs
Normal file
70
Common/Util/Extensions.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
using System.Buffers.Binary;
|
||||
|
||||
namespace EggLink.DanhengServer.Util;
|
||||
|
||||
public static class Extensions
|
||||
{
|
||||
public static string JoinFormat<T>(this IEnumerable<T> list, string separator,
|
||||
string formatString)
|
||||
{
|
||||
formatString = string.IsNullOrWhiteSpace(formatString) ? "{0}" : formatString;
|
||||
return string.Join(separator,
|
||||
list.Select(item => string.Format(formatString, item)));
|
||||
}
|
||||
public static void WriteConvID(this BinaryWriter bw, long convId)
|
||||
{
|
||||
//bw.Write(convId);
|
||||
bw.Write((int)(convId >> 32));
|
||||
bw.Write((int)(convId & 0xFFFFFFFF));
|
||||
}
|
||||
|
||||
public static long GetNextAvailableIndex<T>(this SortedList<long, T> sortedList)
|
||||
{
|
||||
long key = 1;
|
||||
long count = sortedList.Count;
|
||||
long counter = 0;
|
||||
do
|
||||
{
|
||||
if (count == 0) break;
|
||||
long nextKeyInList = sortedList.Keys.ElementAt((Index)counter++);
|
||||
if (key != nextKeyInList) break;
|
||||
key = nextKeyInList + 1;
|
||||
} while (count != 1 && counter != count && key == sortedList.Keys.ElementAt((Index)counter));
|
||||
return key;
|
||||
}
|
||||
|
||||
public static long AddNext<T>(this SortedList<long, T> sortedList, T item)
|
||||
{
|
||||
long key = sortedList.GetNextAvailableIndex();
|
||||
sortedList.Add(key, item);
|
||||
return key;
|
||||
}
|
||||
public static int ReadInt32BE(this BinaryReader br) => BinaryPrimitives.ReadInt32BigEndian(br.ReadBytes(sizeof(int)));
|
||||
public static uint ReadUInt32BE(this BinaryReader br) => BinaryPrimitives.ReadUInt32BigEndian(br.ReadBytes(sizeof(uint)));
|
||||
public static ushort ReadUInt16BE(this BinaryReader br) => BinaryPrimitives.ReadUInt16BigEndian(br.ReadBytes(sizeof(ushort)));
|
||||
public static void WriteUInt16BE(this BinaryWriter bw, ushort value)
|
||||
{
|
||||
Span<byte> data = stackalloc byte[sizeof(ushort)];
|
||||
BinaryPrimitives.WriteUInt16BigEndian(data, value);
|
||||
bw.Write(data);
|
||||
}
|
||||
public static void WriteInt32BE(this BinaryWriter bw, int value)
|
||||
{
|
||||
Span<byte> data = stackalloc byte[sizeof(int)];
|
||||
BinaryPrimitives.WriteInt32BigEndian(data, value);
|
||||
bw.Write(data);
|
||||
}
|
||||
public static void WriteUInt32BE(this BinaryWriter bw, uint value)
|
||||
{
|
||||
Span<byte> data = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32BigEndian(data, value);
|
||||
bw.Write(data);
|
||||
}
|
||||
|
||||
public static void WriteUInt64BE(this BinaryWriter bw, ulong value)
|
||||
{
|
||||
Span<byte> data = stackalloc byte[sizeof(ulong)];
|
||||
BinaryPrimitives.WriteUInt64BigEndian(data, value);
|
||||
bw.Write(data);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
@@ -105,6 +106,11 @@ namespace EggLink.DanhengServer.Util
|
||||
sw.WriteLine(message);
|
||||
}
|
||||
}
|
||||
|
||||
public static Logger GetByClassName()
|
||||
{
|
||||
return new Logger(new StackTrace().GetFrame(1).GetMethod().ReflectedType.Name);
|
||||
}
|
||||
}
|
||||
|
||||
public enum LoggerLevel
|
||||
|
||||
11
GameServer/Enums/SessionState.cs
Normal file
11
GameServer/Enums/SessionState.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace EggLink.DanhengServer.Enums
|
||||
{
|
||||
public enum SessionState
|
||||
{
|
||||
INACTIVE,
|
||||
WAITING_FOR_TOKEN,
|
||||
WAITING_FOR_LOGIN,
|
||||
PICKING_CHARACTER,
|
||||
ACTIVE
|
||||
}
|
||||
}
|
||||
16
GameServer/Game/Player/Player.cs
Normal file
16
GameServer/Game/Player/Player.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace EggLink.DanhengServer.Game.Player
|
||||
{
|
||||
public class Player
|
||||
{
|
||||
public async Task OnLogoutAsync()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,29 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Server\" />
|
||||
<Folder Include="Game\" />
|
||||
<Folder Include="Server\Packet\Recv\Rogue\" />
|
||||
<Folder Include="Server\Packet\Recv\Scene\" />
|
||||
<Folder Include="Server\Packet\Recv\Achievement\" />
|
||||
<Folder Include="Server\Packet\Recv\ChessRogue\" />
|
||||
<Folder Include="Server\Packet\Recv\Tutorial\" />
|
||||
<Folder Include="Server\Packet\Recv\Mission\" />
|
||||
<Folder Include="Server\Packet\Recv\Quest\" />
|
||||
<Folder Include="Server\Packet\Recv\Mail\" />
|
||||
<Folder Include="Server\Packet\Recv\Friend\" />
|
||||
<Folder Include="Server\Packet\Recv\Battle\" />
|
||||
<Folder Include="Server\Packet\Recv\Others\" />
|
||||
<Folder Include="Server\Packet\Send\Achievement\" />
|
||||
<Folder Include="Server\Packet\Send\Battle\" />
|
||||
<Folder Include="Server\Packet\Send\ChessRogue\" />
|
||||
<Folder Include="Server\Packet\Send\Friend\" />
|
||||
<Folder Include="Server\Packet\Send\Mail\" />
|
||||
<Folder Include="Server\Packet\Send\Mission\" />
|
||||
<Folder Include="Server\Packet\Send\Others\" />
|
||||
<Folder Include="Server\Packet\Send\Player\" />
|
||||
<Folder Include="Server\Packet\Send\Quest\" />
|
||||
<Folder Include="Server\Packet\Send\Rogue\" />
|
||||
<Folder Include="Server\Packet\Send\Scene\" />
|
||||
<Folder Include="Server\Packet\Send\Tutorial\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
22
GameServer/KcpSharp/ArrayMemoryOwner.cs
Normal file
22
GameServer/KcpSharp/ArrayMemoryOwner.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
#if !NEED_POH_SHIM
|
||||
|
||||
using System.Buffers;
|
||||
|
||||
namespace EggLink.DanhengServer.KcpSharp
|
||||
{
|
||||
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
|
||||
117
GameServer/KcpSharp/AsyncAutoResetEvent.cs
Normal file
117
GameServer/KcpSharp/AsyncAutoResetEvent.cs
Normal file
@@ -0,0 +1,117 @@
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks.Sources;
|
||||
|
||||
namespace EggLink.DanhengServer.KcpSharp
|
||||
{
|
||||
internal class AsyncAutoResetEvent<T> : IValueTaskSource<T>
|
||||
{
|
||||
private ManualResetValueTaskSourceCore<T> _rvtsc;
|
||||
private SpinLock _lock;
|
||||
private bool _isSet;
|
||||
private bool _activeWait;
|
||||
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();
|
||||
|
||||
bool lockTaken = false;
|
||||
try
|
||||
{
|
||||
_lock.Enter(ref lockTaken);
|
||||
|
||||
_activeWait = false;
|
||||
_signaled = false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (lockTaken)
|
||||
{
|
||||
_lock.Exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ValueTaskSourceStatus IValueTaskSource<T>.GetStatus(short token) => _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()
|
||||
{
|
||||
bool 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;
|
||||
T 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)
|
||||
{
|
||||
bool 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
13
GameServer/KcpSharp/DefaultArrayPoolBufferAllocator.cs
Normal file
13
GameServer/KcpSharp/DefaultArrayPoolBufferAllocator.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace EggLink.DanhengServer.KcpSharp
|
||||
{
|
||||
internal sealed class DefaultArrayPoolBufferAllocator : IKcpBufferPool
|
||||
{
|
||||
public static DefaultArrayPoolBufferAllocator Default { get; } = new();
|
||||
|
||||
public KcpRentedBuffer Rent(KcpBufferPoolRentOptions options)
|
||||
{
|
||||
return KcpRentedBuffer.FromSharedArrayPool(options.Size);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
15
GameServer/KcpSharp/IKcpBufferPool.cs
Normal file
15
GameServer/KcpSharp/IKcpBufferPool.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace EggLink.DanhengServer.KcpSharp
|
||||
{
|
||||
/// <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);
|
||||
}
|
||||
}
|
||||
23
GameServer/KcpSharp/IKcpConversation.cs
Normal file
23
GameServer/KcpSharp/IKcpConversation.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace EggLink.DanhengServer.KcpSharp
|
||||
{
|
||||
/// <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,8 @@
|
||||
namespace EggLink.DanhengServer.KcpSharp
|
||||
{
|
||||
internal interface IKcpConversationUpdateNotificationSource
|
||||
{
|
||||
ReadOnlyMemory<byte> Packet { get; }
|
||||
void Release();
|
||||
}
|
||||
}
|
||||
16
GameServer/KcpSharp/IKcpExceptionProducer.cs
Normal file
16
GameServer/KcpSharp/IKcpExceptionProducer.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace EggLink.DanhengServer.KcpSharp
|
||||
{
|
||||
/// <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);
|
||||
}
|
||||
}
|
||||
58
GameServer/KcpSharp/IKcpMultiplexConnection.cs
Normal file
58
GameServer/KcpSharp/IKcpMultiplexConnection.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using KcpSharp;
|
||||
using System.Net;
|
||||
|
||||
namespace EggLink.DanhengServer.KcpSharp
|
||||
{
|
||||
/// <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);
|
||||
}
|
||||
}
|
||||
54
GameServer/KcpSharp/IKcpMultiplexConnectionOfT.cs
Normal file
54
GameServer/KcpSharp/IKcpMultiplexConnectionOfT.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using KcpSharp;
|
||||
using System.Net;
|
||||
|
||||
namespace EggLink.DanhengServer.KcpSharp
|
||||
{
|
||||
/// <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);
|
||||
}
|
||||
}
|
||||
19
GameServer/KcpSharp/IKcpTransport.cs
Normal file
19
GameServer/KcpSharp/IKcpTransport.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System.Net;
|
||||
|
||||
namespace EggLink.DanhengServer.KcpSharp
|
||||
{
|
||||
/// <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
GameServer/KcpSharp/IKcpTransportOfT.cs
Normal file
22
GameServer/KcpSharp/IKcpTransportOfT.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
namespace EggLink.DanhengServer.KcpSharp
|
||||
{
|
||||
/// <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();
|
||||
}
|
||||
}
|
||||
104
GameServer/KcpSharp/KcpAcknowledgeList.cs
Normal file
104
GameServer/KcpSharp/KcpAcknowledgeList.cs
Normal file
@@ -0,0 +1,104 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace EggLink.DanhengServer.KcpSharp
|
||||
{
|
||||
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)
|
||||
{
|
||||
bool 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()
|
||||
{
|
||||
bool lockTaken = false;
|
||||
try
|
||||
{
|
||||
_lock.Enter(ref lockTaken);
|
||||
|
||||
_count = 0;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (lockTaken)
|
||||
{
|
||||
_lock.Exit();
|
||||
}
|
||||
}
|
||||
_sendQueue.NotifyAckListChanged(false);
|
||||
}
|
||||
|
||||
public void Add(uint serialNumber, uint timestamp)
|
||||
{
|
||||
bool 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()
|
||||
{
|
||||
int capacity = _count + 1;
|
||||
capacity = Math.Max(capacity + capacity / 2, 16);
|
||||
(uint SerialNumber, uint Timestamp)[]? newArray = new (uint SerialNumber, uint Timestamp)[capacity];
|
||||
_array.AsSpan(0, _count).CopyTo(newArray);
|
||||
_array = newArray;
|
||||
}
|
||||
}
|
||||
}
|
||||
59
GameServer/KcpSharp/KcpBuffer.cs
Normal file
59
GameServer/KcpSharp/KcpBuffer.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace EggLink.DanhengServer.KcpSharp
|
||||
{
|
||||
internal readonly struct KcpBuffer
|
||||
{
|
||||
private readonly object? _owner;
|
||||
private readonly Memory<byte> _memory;
|
||||
private readonly int _length;
|
||||
|
||||
public ReadOnlyMemory<byte> DataRegion => _memory.Slice(0, _length);
|
||||
|
||||
public int Length => _length;
|
||||
|
||||
private KcpBuffer(object? owner, Memory<byte> memory, int length)
|
||||
{
|
||||
_owner = owner;
|
||||
_memory = memory;
|
||||
_length = length;
|
||||
}
|
||||
|
||||
public static KcpBuffer CreateFromSpan(KcpRentedBuffer buffer, ReadOnlySpan<byte> dataSource)
|
||||
{
|
||||
Memory<byte> 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
41
GameServer/KcpSharp/KcpBufferPoolRentOptions.cs
Normal file
41
GameServer/KcpSharp/KcpBufferPoolRentOptions.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
namespace EggLink.DanhengServer.KcpSharp
|
||||
{
|
||||
/// <summary>
|
||||
/// The options to use when renting buffers from the pool.
|
||||
/// </summary>
|
||||
public readonly struct KcpBufferPoolRentOptions : IEquatable<KcpBufferPoolRentOptions>
|
||||
{
|
||||
private readonly int _size;
|
||||
private readonly bool _isOutbound;
|
||||
|
||||
/// <summary>
|
||||
/// The minimum size of the buffer.
|
||||
/// </summary>
|
||||
public int Size => _size;
|
||||
|
||||
/// <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 => _isOutbound;
|
||||
|
||||
/// <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) => _size == other._size && _isOutbound == other.IsOutbound;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Equals(object? obj) => obj is KcpBufferPoolRentOptions other && Equals(other);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int GetHashCode() => HashCode.Combine(_size, _isOutbound);
|
||||
}
|
||||
}
|
||||
10
GameServer/KcpSharp/KcpCommand.cs
Normal file
10
GameServer/KcpSharp/KcpCommand.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace EggLink.DanhengServer.KcpSharp
|
||||
{
|
||||
internal enum KcpCommand : byte
|
||||
{
|
||||
Push = 81,
|
||||
Ack = 82,
|
||||
WindowProbe = 83,
|
||||
WindowSize = 84
|
||||
}
|
||||
}
|
||||
275
GameServer/KcpSharp/KcpConversation.FlushAsyncMethodBuilder.cs
Normal file
275
GameServer/KcpSharp/KcpConversation.FlushAsyncMethodBuilder.cs
Normal file
@@ -0,0 +1,275 @@
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Threading.Tasks.Sources;
|
||||
|
||||
namespace KcpSharp
|
||||
{
|
||||
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()
|
||||
{
|
||||
KcpConversation? 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;
|
||||
}
|
||||
StateMachineBox 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
|
||||
{
|
||||
StateMachineBox 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)
|
||||
{
|
||||
ExceptionDispatchInfo? 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)
|
||||
{
|
||||
ExceptionDispatchInfo? 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
|
||||
{
|
||||
StateMachineBox<TStateMachine>? stateMachineBox = boxFieldRef as StateMachineBox<TStateMachine>;
|
||||
if (stateMachineBox != null)
|
||||
{
|
||||
return stateMachineBox;
|
||||
}
|
||||
StateMachineBox<IAsyncStateMachine>? stateMachineBox2 = boxFieldRef as StateMachineBox<IAsyncStateMachine>;
|
||||
if (stateMachineBox2 != null)
|
||||
{
|
||||
if (stateMachineBox2.StateMachine == null)
|
||||
{
|
||||
Debugger.NotifyOfCrossThreadDependency();
|
||||
stateMachineBox2.StateMachine = stateMachine;
|
||||
}
|
||||
return stateMachineBox2;
|
||||
}
|
||||
Debugger.NotifyOfCrossThreadDependency();
|
||||
StateMachineBox<TStateMachine> stateMachineBox3 = (StateMachineBox<TStateMachine>)(boxFieldRef = StateMachineBox<TStateMachine>.GetOrCreateBox(conversation));
|
||||
stateMachineBox3.StateMachine = stateMachine;
|
||||
return stateMachineBox3;
|
||||
}
|
||||
|
||||
private abstract class StateMachineBox : IValueTaskSource
|
||||
{
|
||||
protected ManualResetValueTaskSourceCore<bool> _mrvtsc;
|
||||
protected Action? _moveNextAction;
|
||||
|
||||
public virtual Action MoveNextAction => _moveNextAction!;
|
||||
|
||||
public short Version => _mrvtsc.Version;
|
||||
|
||||
public void SetResult()
|
||||
{
|
||||
_mrvtsc.SetResult(true);
|
||||
}
|
||||
|
||||
public void SetException(Exception error)
|
||||
{
|
||||
_mrvtsc.SetException(error);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class SyncSuccessSentinelStateMachineBox : StateMachineBox
|
||||
{
|
||||
public SyncSuccessSentinelStateMachineBox()
|
||||
{
|
||||
SetResult();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private sealed class StateMachineBox<TStateMachine> : StateMachineBox, IValueTaskSource where TStateMachine : IAsyncStateMachine
|
||||
{
|
||||
[MaybeNull]
|
||||
public TStateMachine StateMachine;
|
||||
private KcpConversation? _conversation;
|
||||
|
||||
public override Action MoveNextAction => _moveNextAction ??= MoveNext;
|
||||
|
||||
internal StateMachineBox(KcpConversation? conversation)
|
||||
{
|
||||
_conversation = conversation;
|
||||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
|
||||
void IValueTaskSource.GetResult(short token)
|
||||
{
|
||||
try
|
||||
{
|
||||
_mrvtsc.GetResult(token);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ReturnOrDropBox();
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
1399
GameServer/KcpSharp/KcpConversation.cs
Normal file
1399
GameServer/KcpSharp/KcpConversation.cs
Normal file
File diff suppressed because it is too large
Load Diff
99
GameServer/KcpSharp/KcpConversationOptions.cs
Normal file
99
GameServer/KcpSharp/KcpConversationOptions.cs
Normal file
@@ -0,0 +1,99 @@
|
||||
using KcpSharp;
|
||||
|
||||
namespace EggLink.DanhengServer.KcpSharp
|
||||
{
|
||||
/// <summary>
|
||||
/// Options used to control the behaviors of <see cref="KcpConversation"/>.
|
||||
/// </summary>
|
||||
public class KcpConversationOptions
|
||||
{
|
||||
/// <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; }
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
61
GameServer/KcpSharp/KcpConversationReceiveResult.cs
Normal file
61
GameServer/KcpSharp/KcpConversationReceiveResult.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace EggLink.DanhengServer.KcpSharp
|
||||
{
|
||||
/// <summary>
|
||||
/// The result of a receive or peek operation.
|
||||
/// </summary>
|
||||
public readonly struct KcpConversationReceiveResult : IEquatable<KcpConversationReceiveResult>
|
||||
{
|
||||
private readonly int _bytesReceived;
|
||||
private readonly bool _connectionAlive;
|
||||
|
||||
/// <summary>
|
||||
/// The number of bytes received.
|
||||
/// </summary>
|
||||
public int BytesReceived => _bytesReceived;
|
||||
|
||||
/// <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) => 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) => !left.Equals(right);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Equals(KcpConversationReceiveResult other) => BytesReceived == other.BytesReceived && TransportClosed == other.TransportClosed;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Equals(object? obj) => obj is KcpConversationReceiveResult other && Equals(other);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int GetHashCode() => HashCode.Combine(BytesReceived, TransportClosed);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() => _connectionAlive ? _bytesReceived.ToString(CultureInfo.InvariantCulture) : "Transport is closed.";
|
||||
}
|
||||
}
|
||||
490
GameServer/KcpSharp/KcpConversationUpdateActivation.cs
Normal file
490
GameServer/KcpSharp/KcpConversationUpdateActivation.cs
Normal file
@@ -0,0 +1,490 @@
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks.Sources;
|
||||
|
||||
namespace EggLink.DanhengServer.KcpSharp
|
||||
{
|
||||
internal sealed class KcpConversationUpdateActivation : IValueTaskSource<KcpConversationUpdateNotification>, IDisposable
|
||||
{
|
||||
private readonly Timer _timer;
|
||||
private ManualResetValueTaskSourceCore<KcpConversationUpdateNotification> _mrvtsc;
|
||||
|
||||
private bool _disposed;
|
||||
private bool _notificationPending;
|
||||
private bool _signaled;
|
||||
private bool _activeWait;
|
||||
private CancellationToken _cancellationToken;
|
||||
private CancellationTokenRegistration _cancellationRegistration;
|
||||
|
||||
private readonly WaitList _waitList;
|
||||
|
||||
ValueTaskSourceStatus IValueTaskSource<KcpConversationUpdateNotification>.GetStatus(short token) => _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 KcpConversationUpdateActivation(int interval)
|
||||
{
|
||||
_timer = new Timer(state =>
|
||||
{
|
||||
WeakReference<KcpConversationUpdateActivation>? reference = (WeakReference<KcpConversationUpdateActivation>?)state!;
|
||||
if (reference.TryGetTarget(out KcpConversationUpdateActivation? target))
|
||||
{
|
||||
target.Notify();
|
||||
}
|
||||
}, new WeakReference<KcpConversationUpdateActivation>(this), interval, interval);
|
||||
_mrvtsc = new ManualResetValueTaskSourceCore<KcpConversationUpdateNotification> { RunContinuationsAsynchronously = true };
|
||||
_waitList = new WaitList(this);
|
||||
}
|
||||
|
||||
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 KcpConversationUpdateNotification notification))
|
||||
{
|
||||
_signaled = true;
|
||||
_cancellationToken = default;
|
||||
bool 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 KcpConversationUpdateNotification notification))
|
||||
{
|
||||
bool 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)
|
||||
{
|
||||
CancellationToken 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);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_disposed = true;
|
||||
if (_activeWait && !_signaled)
|
||||
{
|
||||
_signaled = true;
|
||||
_cancellationToken = default;
|
||||
_mrvtsc.SetResult(default);
|
||||
}
|
||||
}
|
||||
_timer.Dispose();
|
||||
_waitList.Dispose();
|
||||
}
|
||||
|
||||
private class WaitList : IValueTaskSource, IKcpConversationUpdateNotificationSource, IDisposable
|
||||
{
|
||||
private readonly KcpConversationUpdateActivation _parent;
|
||||
private LinkedList<WaitItem>? _list;
|
||||
private ManualResetValueTaskSourceCore<bool> _mrvtsc;
|
||||
|
||||
private bool _available; // activeWait
|
||||
private bool _occupied;
|
||||
private bool _signaled;
|
||||
private bool _disposed;
|
||||
|
||||
private ReadOnlyMemory<byte> _packet;
|
||||
private CancellationToken _cancellationToken;
|
||||
private CancellationTokenRegistration _cancellationRegistration;
|
||||
|
||||
public ReadOnlyMemory<byte> Packet
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
if (_available && _occupied && !_signaled)
|
||||
{
|
||||
return _packet;
|
||||
}
|
||||
}
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
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 WaitList(KcpConversationUpdateActivation parent)
|
||||
{
|
||||
_parent = parent;
|
||||
_mrvtsc = new ManualResetValueTaskSourceCore<bool> { RunContinuationsAsynchronously = true };
|
||||
}
|
||||
|
||||
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;
|
||||
CancellationToken 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;
|
||||
}
|
||||
LinkedListNode<WaitItem>? node = _list.First;
|
||||
if (node is not null)
|
||||
{
|
||||
_list.Remove(node);
|
||||
notification = new KcpConversationUpdateNotification(node.Value, true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
notification = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Release()
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
if (_available && _occupied && !_signaled)
|
||||
{
|
||||
_signaled = true;
|
||||
_packet = default;
|
||||
_cancellationToken = default;
|
||||
_mrvtsc.SetResult(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal bool TryRemove(WaitItem item)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
LinkedList<WaitItem>? list = _list;
|
||||
if (list is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
LinkedListNode<WaitItem> node = item.Node;
|
||||
if (node.Previous is null && node.Next is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
list.Remove(node);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
lock (this)
|
||||
{
|
||||
_disposed = true;
|
||||
if (_available && !_occupied && !_signaled)
|
||||
{
|
||||
_signaled = true;
|
||||
_packet = default;
|
||||
_cancellationToken = default;
|
||||
_mrvtsc.SetResult(false);
|
||||
}
|
||||
|
||||
LinkedList<WaitItem>? list = _list;
|
||||
if (list is not null)
|
||||
{
|
||||
_list = null;
|
||||
|
||||
LinkedListNode<WaitItem>? node = list.First;
|
||||
LinkedListNode<WaitItem>? next = node?.Next;
|
||||
while (node is not null)
|
||||
{
|
||||
node.Value.Release();
|
||||
|
||||
list.Remove(node);
|
||||
node = next;
|
||||
next = node?.Next;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class WaitItem : TaskCompletionSource, IKcpConversationUpdateNotificationSource
|
||||
{
|
||||
private readonly WaitList _parent;
|
||||
private ReadOnlyMemory<byte> _packet;
|
||||
private CancellationToken _cancellationToken;
|
||||
private CancellationTokenRegistration _cancellationRegistration;
|
||||
private bool _released;
|
||||
|
||||
public LinkedListNode<WaitItem> Node { get; }
|
||||
|
||||
public ReadOnlyMemory<byte> Packet
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
if (!_released)
|
||||
{
|
||||
return _packet;
|
||||
}
|
||||
}
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
public WaitItem(WaitList parent, ReadOnlyMemory<byte> packet, CancellationToken cancellationToken)
|
||||
{
|
||||
_parent = parent;
|
||||
_packet = packet;
|
||||
_cancellationToken = cancellationToken;
|
||||
|
||||
Node = new LinkedListNode<WaitItem>(this);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
public void Release()
|
||||
{
|
||||
CancellationTokenRegistration cancellationRegistration;
|
||||
lock (this)
|
||||
{
|
||||
_released = true;
|
||||
cancellationRegistration = _cancellationRegistration;
|
||||
_packet = default;
|
||||
_cancellationToken = default;
|
||||
_cancellationRegistration = default;
|
||||
}
|
||||
TrySetResult();
|
||||
cancellationRegistration.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
30
GameServer/KcpSharp/KcpConversationUpdateNotification.cs
Normal file
30
GameServer/KcpSharp/KcpConversationUpdateNotification.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
namespace EggLink.DanhengServer.KcpSharp
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
134
GameServer/KcpSharp/KcpExceptionProducerExtensions.cs
Normal file
134
GameServer/KcpSharp/KcpExceptionProducerExtensions.cs
Normal file
@@ -0,0 +1,134 @@
|
||||
namespace EggLink.DanhengServer.KcpSharp
|
||||
{
|
||||
/// <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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
14
GameServer/KcpSharp/KcpGlobalVars.cs
Normal file
14
GameServer/KcpSharp/KcpGlobalVars.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace EggLink.DanhengServer.KcpSharp
|
||||
{
|
||||
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
|
||||
}
|
||||
}
|
||||
30
GameServer/KcpSharp/KcpKeepAliveOptions.cs
Normal file
30
GameServer/KcpSharp/KcpKeepAliveOptions.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
namespace EggLink.DanhengServer.KcpSharp
|
||||
{
|
||||
/// <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; }
|
||||
}
|
||||
}
|
||||
335
GameServer/KcpSharp/KcpMultiplexConnection.cs
Normal file
335
GameServer/KcpSharp/KcpMultiplexConnection.cs
Normal file
@@ -0,0 +1,335 @@
|
||||
using KcpSharp;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace EggLink.DanhengServer.KcpSharp
|
||||
{
|
||||
/// <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 IKcpTransport _transport;
|
||||
|
||||
private readonly ConcurrentDictionary<long, (IKcpConversation Conversation, T? State)> _conversations = new();
|
||||
private bool _transportClosed;
|
||||
private bool _disposed;
|
||||
|
||||
private readonly Action<T?>? _disposeAction;
|
||||
|
||||
/// <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;
|
||||
}
|
||||
|
||||
private void CheckDispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
ThrowObjectDisposedException();
|
||||
}
|
||||
}
|
||||
|
||||
private static void ThrowObjectDisposedException()
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(KcpMultiplexConnection<T>));
|
||||
}
|
||||
|
||||
/// <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;
|
||||
}
|
||||
long id = BinaryPrimitives.ReadInt64BigEndian(span);
|
||||
if (_conversations.TryGetValue(id, out (IKcpConversation Conversation, T? State) value))
|
||||
{
|
||||
return value.Conversation.InputPakcetAsync(packet, cancellationToken);
|
||||
}
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <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();
|
||||
(IKcpConversation addedConversation, T? _) = _conversations.GetOrAdd(id, (conversation, state));
|
||||
if (!ReferenceEquals(addedConversation, conversation))
|
||||
{
|
||||
throw new InvalidOperationException("Duplicated conversation.");
|
||||
}
|
||||
if (_disposed)
|
||||
{
|
||||
if (_conversations.TryRemove(id, out (IKcpConversation Conversation, T? State) 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 (IKcpConversation Conversation, T? State) 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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetTransportClosed()
|
||||
{
|
||||
_transportClosed = true;
|
||||
foreach ((IKcpConversation conversation, T? _) in _conversations.Values)
|
||||
{
|
||||
conversation.SetTransportClosed();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_transportClosed = true;
|
||||
_disposed = true;
|
||||
while (!_conversations.IsEmpty)
|
||||
{
|
||||
foreach (long id in _conversations.Keys)
|
||||
{
|
||||
if (_conversations.TryRemove(id, out (IKcpConversation Conversation, T? State) value))
|
||||
{
|
||||
value.Conversation.Dispose();
|
||||
if (_disposeAction is not null)
|
||||
{
|
||||
_disposeAction.Invoke(value.State);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
75
GameServer/KcpSharp/KcpPacketHeader.cs
Normal file
75
GameServer/KcpSharp/KcpPacketHeader.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace EggLink.DanhengServer.KcpSharp
|
||||
{
|
||||
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) => Command == other.Command && Fragment == other.Fragment && WindowSize == other.WindowSize && Timestamp == other.Timestamp && SerialNumber == other.SerialNumber && Unacknowledged == other.Unacknowledged;
|
||||
public override bool Equals(object? obj) => obj is KcpPacketHeader other && Equals(other);
|
||||
public override int GetHashCode() => 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
10
GameServer/KcpSharp/KcpProbeType.cs
Normal file
10
GameServer/KcpSharp/KcpProbeType.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace EggLink.DanhengServer.KcpSharp
|
||||
{
|
||||
[Flags]
|
||||
internal enum KcpProbeType
|
||||
{
|
||||
None = 0,
|
||||
AskSend = 1,
|
||||
AskTell = 2,
|
||||
}
|
||||
}
|
||||
372
GameServer/KcpSharp/KcpRawChannel.cs
Normal file
372
GameServer/KcpSharp/KcpRawChannel.cs
Normal file
@@ -0,0 +1,372 @@
|
||||
using KcpSharp;
|
||||
using System.Buffers.Binary;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace EggLink.DanhengServer.KcpSharp
|
||||
{
|
||||
/// <summary>
|
||||
/// An unreliable channel with a conversation ID.
|
||||
/// </summary>
|
||||
public sealed class KcpRawChannel : IKcpConversation, IKcpExceptionProducer<KcpRawChannel>
|
||||
{
|
||||
private readonly IKcpBufferPool _bufferPool;
|
||||
private readonly IKcpTransport _transport;
|
||||
private readonly IPEndPoint _remoteEndPoint;
|
||||
private readonly ulong? _id;
|
||||
private readonly int _mtu;
|
||||
private readonly int _preBufferSize;
|
||||
private readonly int _postBufferSize;
|
||||
|
||||
private CancellationTokenSource? _sendLoopCts;
|
||||
private readonly KcpRawReceiveQueue _receiveQueue;
|
||||
private readonly KcpRawSendOperation _sendOperation;
|
||||
private readonly AsyncAutoResetEvent<int> _sendNotification;
|
||||
|
||||
private Func<Exception, KcpRawChannel, object?, bool>? _exceptionHandler;
|
||||
private object? _exceptionHandlerState;
|
||||
|
||||
/// <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));
|
||||
}
|
||||
|
||||
int 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>
|
||||
/// 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>
|
||||
/// 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;
|
||||
|
||||
/// <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)
|
||||
=> _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()
|
||||
=> _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)
|
||||
=> _sendOperation.CancelPendingOperation(innerException, cancellationToken);
|
||||
|
||||
|
||||
private async void RunSendLoop()
|
||||
{
|
||||
CancellationToken cancellationToken = _sendLoopCts?.Token ?? new CancellationToken(true);
|
||||
KcpRawSendOperation sendOperation = _sendOperation;
|
||||
AsyncAutoResetEvent<int> ev = _sendNotification;
|
||||
int mss = _mtu - _preBufferSize - _postBufferSize;
|
||||
if (_id.HasValue)
|
||||
{
|
||||
mss -= 8;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
int payloadSize = await ev.WaitAsync().ConfigureAwait(false);
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (payloadSize < 0 || payloadSize > mss)
|
||||
{
|
||||
_ = sendOperation.TryConsume(default, out _);
|
||||
continue;
|
||||
}
|
||||
|
||||
int overhead = _preBufferSize + _postBufferSize;
|
||||
if (_id.HasValue)
|
||||
{
|
||||
overhead += 8;
|
||||
}
|
||||
{
|
||||
using KcpRentedBuffer owner = _bufferPool.Rent(new KcpBufferPoolRentOptions(payloadSize + overhead, true));
|
||||
Memory<byte> 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 int 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)
|
||||
{
|
||||
Func<Exception, KcpRawChannel, object?, bool>? handler = _exceptionHandler;
|
||||
object? state = _exceptionHandlerState;
|
||||
bool result = false;
|
||||
if (handler is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
result = handler.Invoke(ex, this, state);
|
||||
}
|
||||
catch
|
||||
{
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!result)
|
||||
{
|
||||
SetTransportClosed();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask InputPakcetAsync(UdpReceiveResult packet, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ReadOnlySpan<byte> span = packet.Buffer.AsSpan();
|
||||
int 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;
|
||||
}
|
||||
|
||||
/// <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)
|
||||
=> _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)
|
||||
=> _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)
|
||||
=> _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)
|
||||
=> _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()
|
||||
=> _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)
|
||||
=> _receiveQueue.CancelPendingOperation(innerException, cancellationToken);
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetTransportClosed()
|
||||
{
|
||||
CancellationTokenSource? 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
33
GameServer/KcpSharp/KcpRawChannelOptions.cs
Normal file
33
GameServer/KcpSharp/KcpRawChannelOptions.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
namespace EggLink.DanhengServer.KcpSharp
|
||||
{
|
||||
/// <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; }
|
||||
}
|
||||
}
|
||||
358
GameServer/KcpSharp/KcpRawReceiveQueue.cs
Normal file
358
GameServer/KcpSharp/KcpRawReceiveQueue.cs
Normal file
@@ -0,0 +1,358 @@
|
||||
using System.Threading.Tasks.Sources;
|
||||
using System.Diagnostics;
|
||||
|
||||
|
||||
|
||||
|
||||
#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<EggLink.DanhengServer.KcpSharp.KcpBuffer>;
|
||||
using LinkedListNodeOfQueueItem = System.Collections.Generic.LinkedListNode<EggLink.DanhengServer.KcpSharp.KcpBuffer>;
|
||||
#endif
|
||||
|
||||
namespace EggLink.DanhengServer.KcpSharp
|
||||
{
|
||||
internal sealed class KcpRawReceiveQueue : IValueTaskSource<KcpConversationReceiveResult>, IDisposable
|
||||
{
|
||||
private ManualResetValueTaskSourceCore<KcpConversationReceiveResult> _mrvtsc;
|
||||
|
||||
private readonly IKcpBufferPool _bufferPool;
|
||||
private readonly int _capacity;
|
||||
private readonly LinkedListOfQueueItem _queue;
|
||||
private readonly LinkedListOfQueueItem _recycled;
|
||||
|
||||
private bool _transportClosed;
|
||||
private bool _disposed;
|
||||
|
||||
private bool _activeWait;
|
||||
private bool _signaled;
|
||||
private bool _bufferProvided;
|
||||
private Memory<byte> _buffer;
|
||||
private CancellationToken _cancellationToken;
|
||||
private CancellationTokenRegistration _cancellationRegistration;
|
||||
|
||||
public KcpRawReceiveQueue(IKcpBufferPool bufferPool, int capacity)
|
||||
{
|
||||
_bufferPool = bufferPool;
|
||||
_capacity = capacity;
|
||||
_queue = new LinkedListOfQueueItem();
|
||||
_recycled = new LinkedListOfQueueItem();
|
||||
}
|
||||
|
||||
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) => _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();
|
||||
}
|
||||
LinkedListNodeOfQueueItem? 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));
|
||||
}
|
||||
|
||||
LinkedListNodeOfQueueItem? 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();
|
||||
}
|
||||
LinkedListNodeOfQueueItem? first = _queue.First;
|
||||
if (first is null)
|
||||
{
|
||||
result = new KcpConversationReceiveResult(0);
|
||||
return false;
|
||||
}
|
||||
|
||||
ref KcpBuffer 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));
|
||||
}
|
||||
|
||||
LinkedListNodeOfQueueItem? first = _queue.First;
|
||||
if (first is not null)
|
||||
{
|
||||
ref KcpBuffer source = ref first.ValueRef;
|
||||
int 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)
|
||||
{
|
||||
CancellationToken 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;
|
||||
}
|
||||
|
||||
int queueSize = _queue.Count;
|
||||
if (queueSize > 0 || !_activeWait)
|
||||
{
|
||||
if (queueSize >= _capacity)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
KcpRentedBuffer owner = _bufferPool.Rent(new KcpBufferPoolRentOptions(buffer.Length, false));
|
||||
_queue.AddLast(AllocateNode(KcpBuffer.CreateFromSpan(owner, buffer)));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_bufferProvided)
|
||||
{
|
||||
KcpRentedBuffer 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)
|
||||
{
|
||||
KcpRentedBuffer 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)
|
||||
{
|
||||
LinkedListNodeOfQueueItem? 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;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock (_queue)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (_activeWait && !_signaled)
|
||||
{
|
||||
ClearPreviousOperation();
|
||||
_mrvtsc.SetResult(default);
|
||||
}
|
||||
LinkedListNodeOfQueueItem? node = _queue.First;
|
||||
while (node is not null)
|
||||
{
|
||||
node.ValueRef.Release();
|
||||
node = node.Next;
|
||||
}
|
||||
_queue.Clear();
|
||||
_recycled.Clear();
|
||||
_disposed = true;
|
||||
_transportClosed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
182
GameServer/KcpSharp/KcpRawSendOperation.cs
Normal file
182
GameServer/KcpSharp/KcpRawSendOperation.cs
Normal file
@@ -0,0 +1,182 @@
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks.Sources;
|
||||
|
||||
namespace EggLink.DanhengServer.KcpSharp
|
||||
{
|
||||
internal sealed class KcpRawSendOperation : IValueTaskSource<bool>, IDisposable
|
||||
{
|
||||
private readonly AsyncAutoResetEvent<int> _notification;
|
||||
private ManualResetValueTaskSourceCore<bool> _mrvtsc;
|
||||
|
||||
private bool _transportClosed;
|
||||
private bool _disposed;
|
||||
|
||||
private bool _activeWait;
|
||||
private bool _signaled;
|
||||
private ReadOnlyMemory<byte> _buffer;
|
||||
private CancellationToken _cancellationToken;
|
||||
private CancellationTokenRegistration _cancellationRegistration;
|
||||
|
||||
public KcpRawSendOperation(AsyncAutoResetEvent<int> notification)
|
||||
{
|
||||
_notification = notification;
|
||||
|
||||
_mrvtsc = new ManualResetValueTaskSourceCore<bool>()
|
||||
{
|
||||
RunContinuationsAsynchronously = 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) => _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)
|
||||
{
|
||||
CancellationToken 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;
|
||||
}
|
||||
ReadOnlyMemory<byte> 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;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (_activeWait && !_signaled)
|
||||
{
|
||||
ClearPreviousOperation();
|
||||
_mrvtsc.SetResult(false);
|
||||
}
|
||||
_disposed = true;
|
||||
_transportClosed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
698
GameServer/KcpSharp/KcpReceiveQueue.cs
Normal file
698
GameServer/KcpSharp/KcpReceiveQueue.cs
Normal file
@@ -0,0 +1,698 @@
|
||||
using System.Threading.Tasks.Sources;
|
||||
using System.Diagnostics;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#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<(EggLink.DanhengServer.KcpSharp.KcpBuffer Data, byte Fragment)>;
|
||||
using LinkedListNodeOfQueueItem = System.Collections.Generic.LinkedListNode<(EggLink.DanhengServer.KcpSharp.KcpBuffer Data, byte Fragment)>;
|
||||
#endif
|
||||
|
||||
namespace EggLink.DanhengServer.KcpSharp
|
||||
{
|
||||
internal sealed class KcpReceiveQueue : IValueTaskSource<KcpConversationReceiveResult>, IValueTaskSource<int>, IValueTaskSource<bool>, IDisposable
|
||||
{
|
||||
private ManualResetValueTaskSourceCore<KcpConversationReceiveResult> _mrvtsc;
|
||||
|
||||
private readonly LinkedListOfQueueItem _queue;
|
||||
private readonly bool _stream;
|
||||
private readonly int _queueSize;
|
||||
private readonly KcpSendReceiveQueueItemCache _cache;
|
||||
private int _completedPacketsCount;
|
||||
|
||||
private bool _transportClosed;
|
||||
private bool _disposed;
|
||||
|
||||
private bool _activeWait;
|
||||
private bool _signaled;
|
||||
private byte _operationMode; // 0-receive 1-wait for message 2-wait for available data
|
||||
private Memory<byte> _buffer;
|
||||
private int _minimumBytes;
|
||||
private int _minimumSegments;
|
||||
private CancellationToken _cancellationToken;
|
||||
private CancellationTokenRegistration _cancellationRegistration;
|
||||
|
||||
public KcpReceiveQueue(bool stream, int queueSize, KcpSendReceiveQueueItemCache cache)
|
||||
{
|
||||
_mrvtsc = new ManualResetValueTaskSourceCore<KcpConversationReceiveResult>()
|
||||
{
|
||||
RunContinuationsAsynchronously = true
|
||||
};
|
||||
_queue = new LinkedListOfQueueItem();
|
||||
_stream = stream;
|
||||
_queueSize = queueSize;
|
||||
_cache = cache;
|
||||
}
|
||||
|
||||
public ValueTaskSourceStatus GetStatus(short token) => _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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
LinkedListNodeOfQueueItem? node = _queue.First;
|
||||
if (node is null)
|
||||
{
|
||||
result = new KcpConversationReceiveResult(0);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (CalculatePacketSize(node, out int 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 KcpConversationReceiveResult result, out bool bufferTooSmall);
|
||||
ClearPreviousOperation(false);
|
||||
if (bufferTooSmall)
|
||||
{
|
||||
Debug.Assert(false, "This should never be reached.");
|
||||
return new ValueTask<KcpConversationReceiveResult>(Task.FromException<KcpConversationReceiveResult>(ThrowHelper.NewBufferTooSmallForBufferArgument()));
|
||||
}
|
||||
else
|
||||
{
|
||||
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 bool 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 KcpConversationReceiveResult result, out bool bufferTooSmall);
|
||||
ClearPreviousOperation(false);
|
||||
if (bufferTooSmall)
|
||||
{
|
||||
return new ValueTask<KcpConversationReceiveResult>(Task.FromException<KcpConversationReceiveResult>(ThrowHelper.NewBufferTooSmallForBufferArgument()));
|
||||
}
|
||||
else
|
||||
{
|
||||
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 KcpConversationReceiveResult result, out bool bufferTooSmall);
|
||||
ClearPreviousOperation(false);
|
||||
if (bufferTooSmall)
|
||||
{
|
||||
return new ValueTask<int>(Task.FromException<int>(ThrowHelper.NewBufferTooSmallForBufferArgument()));
|
||||
}
|
||||
else
|
||||
{
|
||||
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)
|
||||
{
|
||||
CancellationToken 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
|
||||
{
|
||||
LinkedListNodeOfQueueItem? 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 KcpConversationReceiveResult result, out bool 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)
|
||||
{
|
||||
LinkedListNodeOfQueueItem? node = _queue.First;
|
||||
if (node is null)
|
||||
{
|
||||
result = default;
|
||||
bufferTooSmall = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// peek
|
||||
if (_operationMode == 1)
|
||||
{
|
||||
if (CalculatePacketSize(node, out int bytesRecevied))
|
||||
{
|
||||
result = new KcpConversationReceiveResult(bytesRecevied);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = default;
|
||||
}
|
||||
bufferTooSmall = false;
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Assert(_operationMode == 0);
|
||||
|
||||
// ensure buffer is big enough
|
||||
int 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;
|
||||
}
|
||||
}
|
||||
|
||||
bool anyDataReceived = false;
|
||||
bytesInPacket = 0;
|
||||
node = _queue.First;
|
||||
LinkedListNodeOfQueueItem? next;
|
||||
while (node is not null)
|
||||
{
|
||||
next = node.Next;
|
||||
|
||||
byte fragment = node.ValueRef.Fragment;
|
||||
ref KcpBuffer data = ref node.ValueRef.Data;
|
||||
|
||||
int 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)
|
||||
{
|
||||
int bytesRecevied = first.ValueRef.Data.Length;
|
||||
if (first.ValueRef.Fragment == 0)
|
||||
{
|
||||
packetSize = bytesRecevied;
|
||||
return true;
|
||||
}
|
||||
|
||||
LinkedListNodeOfQueueItem? 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)
|
||||
{
|
||||
LinkedListNodeOfQueueItem? node = queue.First;
|
||||
while (node is not null)
|
||||
{
|
||||
ref KcpBuffer 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);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock (_queue)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (_activeWait && !_signaled)
|
||||
{
|
||||
ClearPreviousOperation(true);
|
||||
_mrvtsc.SetResult(default);
|
||||
}
|
||||
LinkedListNodeOfQueueItem? node = _queue.First;
|
||||
while (node is not null)
|
||||
{
|
||||
node.ValueRef.Data.Release();
|
||||
node = node.Next;
|
||||
}
|
||||
_queue.Clear();
|
||||
_disposed = true;
|
||||
_transportClosed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
37
GameServer/KcpSharp/KcpReceiveWindowNotificationOptions.cs
Normal file
37
GameServer/KcpSharp/KcpReceiveWindowNotificationOptions.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
namespace EggLink.DanhengServer.KcpSharp
|
||||
{
|
||||
/// <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; }
|
||||
}
|
||||
}
|
||||
222
GameServer/KcpSharp/KcpRentedBuffer.cs
Normal file
222
GameServer/KcpSharp/KcpRentedBuffer.cs
Normal file
@@ -0,0 +1,222 @@
|
||||
using System.Buffers;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace EggLink.DanhengServer.KcpSharp
|
||||
{
|
||||
/// <summary>
|
||||
/// The buffer rented and owned by KcpSharp.
|
||||
/// </summary>
|
||||
public readonly struct KcpRentedBuffer : IEquatable<KcpRentedBuffer>, IDisposable
|
||||
{
|
||||
private readonly object? _owner;
|
||||
private readonly Memory<byte> _memory;
|
||||
|
||||
internal object? Owner => _owner;
|
||||
|
||||
/// <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));
|
||||
}
|
||||
byte[] 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)
|
||||
{
|
||||
Memory<byte> 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)
|
||||
{
|
||||
Memory<byte> 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) => ReferenceEquals(_owner, other._owner) && _memory.Equals(other._memory);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Equals(object? obj) => obj is KcpRentedBuffer other && Equals(other);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int GetHashCode() => _owner is null ? _memory.GetHashCode() : HashCode.Combine(RuntimeHelpers.GetHashCode(_owner), _memory);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() => $"KcpSharp.KcpRentedBuffer[{_memory.Length}]";
|
||||
}
|
||||
}
|
||||
718
GameServer/KcpSharp/KcpSendQueue.cs
Normal file
718
GameServer/KcpSharp/KcpSendQueue.cs
Normal file
@@ -0,0 +1,718 @@
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks.Sources;
|
||||
|
||||
|
||||
|
||||
|
||||
#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<(EggLink.DanhengServer.KcpSharp.KcpBuffer Data, byte Fragment)>;
|
||||
using LinkedListNodeOfQueueItem = System.Collections.Generic.LinkedListNode<(EggLink.DanhengServer.KcpSharp.KcpBuffer Data, byte Fragment)>;
|
||||
#endif
|
||||
|
||||
namespace EggLink.DanhengServer.KcpSharp
|
||||
{
|
||||
internal sealed class KcpSendQueue : IValueTaskSource<bool>, IValueTaskSource, IDisposable
|
||||
{
|
||||
private readonly IKcpBufferPool _bufferPool;
|
||||
private readonly KcpConversationUpdateActivation _updateActivation;
|
||||
private readonly bool _stream;
|
||||
private readonly int _capacity;
|
||||
private readonly int _mss;
|
||||
private readonly KcpSendReceiveQueueItemCache _cache;
|
||||
private ManualResetValueTaskSourceCore<bool> _mrvtsc;
|
||||
|
||||
private readonly LinkedListOfQueueItem _queue;
|
||||
private long _unflushedBytes;
|
||||
|
||||
private bool _transportClosed;
|
||||
private bool _disposed;
|
||||
|
||||
private bool _activeWait;
|
||||
private bool _signled;
|
||||
private bool _forStream;
|
||||
private byte _operationMode; // 0-send 1-flush 2-wait for space
|
||||
private ReadOnlyMemory<byte> _buffer;
|
||||
private int _waitForByteCount;
|
||||
private int _waitForSegmentCount;
|
||||
private CancellationToken _cancellationToken;
|
||||
private CancellationTokenRegistration _cancellationRegistration;
|
||||
|
||||
private bool _ackListNotEmpty;
|
||||
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 ValueTaskSourceStatus GetStatus(short token) => _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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void IValueTaskSource.GetResult(short token)
|
||||
{
|
||||
try
|
||||
{
|
||||
_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)
|
||||
{
|
||||
int mss = _mss;
|
||||
int availableFragments = _capacity - _queue.Count;
|
||||
if (availableFragments < 0)
|
||||
{
|
||||
byteCount = 0;
|
||||
segmentCount = 0;
|
||||
return;
|
||||
}
|
||||
int availableBytes = availableFragments * mss;
|
||||
if (_stream)
|
||||
{
|
||||
LinkedListNodeOfQueueItem? 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 int currentByteCount, out int 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;
|
||||
}
|
||||
|
||||
int mss = _mss;
|
||||
// Make sure there is enough space.
|
||||
if (!allowPartialSend)
|
||||
{
|
||||
int spaceAvailable = mss * (_capacity - _queue.Count);
|
||||
if (spaceAvailable < 0)
|
||||
{
|
||||
bytesWritten = 0;
|
||||
return false;
|
||||
}
|
||||
if (_stream)
|
||||
{
|
||||
LinkedListNodeOfQueueItem? 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)
|
||||
{
|
||||
LinkedListNodeOfQueueItem? node = _queue.Last;
|
||||
if (node is not null)
|
||||
{
|
||||
ref KcpBuffer data = ref node.ValueRef.Data;
|
||||
int 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;
|
||||
}
|
||||
}
|
||||
|
||||
bool anySegmentAdded = false;
|
||||
int count = buffer.Length <= mss ? 1 : (buffer.Length + mss - 1) / mss;
|
||||
Debug.Assert(count >= 1);
|
||||
while (count > 0 && _queue.Count < _capacity)
|
||||
{
|
||||
int fragment = --count;
|
||||
|
||||
int size = buffer.Length > mss ? mss : buffer.Length;
|
||||
|
||||
KcpRentedBuffer owner = _bufferPool.Rent(new KcpBufferPoolRentOptions(mss, false));
|
||||
KcpBuffer 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));
|
||||
}
|
||||
|
||||
int mss = _mss;
|
||||
if (_stream)
|
||||
{
|
||||
LinkedListNodeOfQueueItem? node = _queue.Last;
|
||||
if (node is not null)
|
||||
{
|
||||
ref KcpBuffer data = ref node.ValueRef.Data;
|
||||
int 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);
|
||||
}
|
||||
}
|
||||
|
||||
int 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)
|
||||
{
|
||||
int fragment = --count;
|
||||
|
||||
int size = buffer.Length > mss ? mss : buffer.Length;
|
||||
KcpRentedBuffer owner = _bufferPool.Rent(new KcpBufferPoolRentOptions(mss, false));
|
||||
KcpBuffer 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));
|
||||
}
|
||||
|
||||
int mss = _mss;
|
||||
if (_stream)
|
||||
{
|
||||
LinkedListNodeOfQueueItem? node = _queue.Last;
|
||||
if (node is not null)
|
||||
{
|
||||
ref KcpBuffer data = ref node.ValueRef.Data;
|
||||
int 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;
|
||||
}
|
||||
}
|
||||
|
||||
int 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)
|
||||
{
|
||||
int size = buffer.Length > mss ? mss : buffer.Length;
|
||||
KcpRentedBuffer owner = _bufferPool.Rent(new KcpBufferPoolRentOptions(mss, false));
|
||||
KcpBuffer 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)
|
||||
{
|
||||
CancellationToken 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)
|
||||
{
|
||||
LinkedListNodeOfQueueItem? node = _queue.First;
|
||||
if (node is null)
|
||||
{
|
||||
data = default;
|
||||
fragment = default;
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
(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)
|
||||
{
|
||||
ReadOnlyMemory<byte> buffer = _buffer;
|
||||
int mss = _mss;
|
||||
int count = buffer.Length <= mss ? 1 : (buffer.Length + mss - 1) / mss;
|
||||
|
||||
int size = buffer.Length > mss ? mss : buffer.Length;
|
||||
KcpRentedBuffer owner = _bufferPool.Rent(new KcpBufferPoolRentOptions(mss, false));
|
||||
KcpBuffer 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 int byteCount, out int 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)
|
||||
{
|
||||
long 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);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock (_queue)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (_activeWait && !_signled)
|
||||
{
|
||||
if (_forStream)
|
||||
{
|
||||
ClearPreviousOperation();
|
||||
_mrvtsc.SetException(ThrowHelper.NewTransportClosedForStreamException());
|
||||
}
|
||||
else
|
||||
{
|
||||
ClearPreviousOperation();
|
||||
_mrvtsc.SetResult(false);
|
||||
}
|
||||
}
|
||||
LinkedListNodeOfQueueItem? node = _queue.First;
|
||||
while (node is not null)
|
||||
{
|
||||
node.ValueRef.Data.Release();
|
||||
node = node.Next;
|
||||
}
|
||||
_queue.Clear();
|
||||
_disposed = true;
|
||||
_transportClosed = true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
9
GameServer/KcpSharp/KcpSendReceiveBufferItem.cs
Normal file
9
GameServer/KcpSharp/KcpSendReceiveBufferItem.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace EggLink.DanhengServer.KcpSharp
|
||||
{
|
||||
internal struct KcpSendReceiveBufferItem
|
||||
{
|
||||
public KcpBuffer Data;
|
||||
public KcpPacketHeader Segment;
|
||||
public KcpSendSegmentStats Stats;
|
||||
}
|
||||
}
|
||||
73
GameServer/KcpSharp/KcpSendReceiveBufferItemCache.cs
Normal file
73
GameServer/KcpSharp/KcpSendReceiveBufferItemCache.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
|
||||
#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<EggLink.DanhengServer.KcpSharp.KcpSendReceiveBufferItem>;
|
||||
using LinkedListOfBufferItem = System.Collections.Generic.LinkedList<EggLink.DanhengServer.KcpSharp.KcpSendReceiveBufferItem>;
|
||||
#endif
|
||||
|
||||
namespace EggLink.DanhengServer.KcpSharp
|
||||
{
|
||||
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)
|
||||
{
|
||||
bool lockAcquired = false;
|
||||
try
|
||||
{
|
||||
_lock.Enter(ref lockAcquired);
|
||||
|
||||
LinkedListNodeOfBufferItem? 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)
|
||||
{
|
||||
bool lockAcquired = false;
|
||||
try
|
||||
{
|
||||
_lock.Enter(ref lockAcquired);
|
||||
|
||||
node.ValueRef = default;
|
||||
_items.AddLast(node);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (lockAcquired)
|
||||
{
|
||||
_lock.Exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
84
GameServer/KcpSharp/KcpSendReceiveQueueItemCache.cs
Normal file
84
GameServer/KcpSharp/KcpSendReceiveQueueItemCache.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
|
||||
#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<(EggLink.DanhengServer.KcpSharp.KcpBuffer Data, byte Fragment)>;
|
||||
using LinkedListOfQueueItem = System.Collections.Generic.LinkedList<(EggLink.DanhengServer.KcpSharp.KcpBuffer Data, byte Fragment)>;
|
||||
#endif
|
||||
|
||||
namespace EggLink.DanhengServer.KcpSharp
|
||||
{
|
||||
internal sealed class KcpSendReceiveQueueItemCache
|
||||
{
|
||||
private LinkedListOfQueueItem _list = new();
|
||||
private SpinLock _lock;
|
||||
|
||||
public LinkedListNodeOfQueueItem Rent(in KcpBuffer buffer, byte fragment)
|
||||
{
|
||||
bool lockTaken = false;
|
||||
try
|
||||
{
|
||||
_lock.Enter(ref lockTaken);
|
||||
|
||||
LinkedListNodeOfQueueItem? 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;
|
||||
|
||||
bool lockTaken = false;
|
||||
try
|
||||
{
|
||||
_lock.Enter(ref lockTaken);
|
||||
|
||||
_list.AddLast(node);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (lockTaken)
|
||||
{
|
||||
_lock.Exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
bool lockTaken = false;
|
||||
try
|
||||
{
|
||||
_lock.Enter(ref lockTaken);
|
||||
|
||||
_list.Clear();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (lockTaken)
|
||||
{
|
||||
_lock.Exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
20
GameServer/KcpSharp/KcpSendSegmentStats.cs
Normal file
20
GameServer/KcpSharp/KcpSendSegmentStats.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
namespace EggLink.DanhengServer.KcpSharp
|
||||
{
|
||||
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; }
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
159
GameServer/KcpSharp/KcpSocketTransport.cs
Normal file
159
GameServer/KcpSharp/KcpSocketTransport.cs
Normal file
@@ -0,0 +1,159 @@
|
||||
using KcpSharp;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace EggLink.DanhengServer.KcpSharp
|
||||
{
|
||||
|
||||
/// <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);
|
||||
}
|
||||
}
|
||||
}
|
||||
46
GameServer/KcpSharp/KcpSocketTransportForConversation.cs
Normal file
46
GameServer/KcpSharp/KcpSocketTransportForConversation.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using KcpSharp;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace EggLink.DanhengServer.KcpSharp
|
||||
{
|
||||
/// <summary>
|
||||
/// Socket transport for KCP conversation.
|
||||
/// </summary>
|
||||
internal sealed class KcpSocketTransportForConversation : KcpSocketTransport<KcpConversation>, IKcpTransport<KcpConversation>
|
||||
{
|
||||
private readonly long? _conversationId;
|
||||
private readonly IPEndPoint _remoteEndPoint;
|
||||
private readonly KcpConversationOptions? _options;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
protected override KcpConversation Activate() => _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;
|
||||
}
|
||||
|
||||
public void SetExceptionHandler(Func<Exception, IKcpTransport<KcpConversation>, object?, bool> handler, object? state)
|
||||
{
|
||||
_exceptionHandler = handler;
|
||||
_exceptionHandlerState = state;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace EggLink.DanhengServer.KcpSharp
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
protected override KcpMultiplexConnection<T> Activate() => new(this, _disposeAction);
|
||||
|
||||
IKcpMultiplexConnection<T> IKcpTransport<IKcpMultiplexConnection<T>>.Connection => Connection;
|
||||
|
||||
protected override bool HandleException(Exception ex)
|
||||
{
|
||||
if (_exceptionHandler is not null)
|
||||
{
|
||||
return _exceptionHandler.Invoke(ex, this, _exceptionHandlerState);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void SetExceptionHandler(Func<Exception, IKcpTransport<IKcpMultiplexConnection<T>>, object?, bool> handler, object? state)
|
||||
{
|
||||
_exceptionHandler = handler;
|
||||
_exceptionHandlerState = state;
|
||||
}
|
||||
}
|
||||
}
|
||||
41
GameServer/KcpSharp/KcpSocketTransportForRawChannel.cs
Normal file
41
GameServer/KcpSharp/KcpSocketTransportForRawChannel.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace EggLink.DanhengServer.KcpSharp
|
||||
{
|
||||
internal sealed class KcpSocketTransportForRawChannel : KcpSocketTransport<KcpRawChannel>, IKcpTransport<KcpRawChannel>
|
||||
{
|
||||
private readonly long? _conversationId;
|
||||
private readonly IPEndPoint _remoteEndPoint;
|
||||
private readonly KcpRawChannelOptions? _options;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
protected override KcpRawChannel Activate() => _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;
|
||||
}
|
||||
|
||||
public void SetExceptionHandler(Func<Exception, IKcpTransport<KcpRawChannel>, object?, bool> handler, object? state)
|
||||
{
|
||||
_exceptionHandler = handler;
|
||||
_exceptionHandlerState = state;
|
||||
}
|
||||
}
|
||||
}
|
||||
222
GameServer/KcpSharp/KcpSocketTransportOfT.cs
Normal file
222
GameServer/KcpSharp/KcpSocketTransportOfT.cs
Normal file
@@ -0,0 +1,222 @@
|
||||
using EggLink.DanhengServer.Server;
|
||||
using EggLink.DanhengServer.Util;
|
||||
using System.Buffers;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace EggLink.DanhengServer.KcpSharp
|
||||
{
|
||||
/// <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 UdpClient _udpListener;
|
||||
private readonly int _mtu;
|
||||
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();
|
||||
|
||||
/// <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, pinned: 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) => 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();
|
||||
}
|
||||
|
||||
/// <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());
|
||||
}
|
||||
|
||||
private async void RunReceiveLoop()
|
||||
{
|
||||
CancellationToken cancellationToken = _cts?.Token ?? new CancellationToken(true);
|
||||
IKcpConversation? connection = _connection;
|
||||
if (connection is null || cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using IMemoryOwner<byte> memoryOwner = AllocateBuffer(_mtu);
|
||||
try
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
int bytesReceived = 0;
|
||||
bool error = false;
|
||||
UdpReceiveResult result = default;
|
||||
try
|
||||
{
|
||||
result = await _udpListener.ReceiveAsync(cancellationToken);
|
||||
bytesReceived = result.Buffer.Length;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
/*if (!HandleExceptionWrapper(ex))
|
||||
{
|
||||
break;
|
||||
}
|
||||
error = true;
|
||||
*/
|
||||
}
|
||||
|
||||
if (bytesReceived != 0 && bytesReceived <= _mtu)
|
||||
{
|
||||
if (bytesReceived == Listener.HANDSHAKE_SIZE)
|
||||
await Listener.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();
|
||||
CancellationTokenSource? 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)
|
||||
{
|
||||
CancellationTokenSource? 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(disposing: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
168
GameServer/KcpSharp/KcpStream.cs
Normal file
168
GameServer/KcpSharp/KcpStream.cs
Normal file
@@ -0,0 +1,168 @@
|
||||
using KcpSharp;
|
||||
|
||||
namespace EggLink.DanhengServer.KcpSharp
|
||||
{
|
||||
/// <summary>
|
||||
/// A stream wrapper of <see cref="KcpConversation"/>.
|
||||
/// </summary>
|
||||
public sealed class KcpStream : Stream
|
||||
{
|
||||
private KcpConversation? _conversation;
|
||||
private readonly bool _ownsConversation;
|
||||
|
||||
/// <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(); }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void SetLength(long value) => 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 KcpConversationReceiveResult result) && result.BytesReceived != 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <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
|
||||
13
GameServer/KcpSharp/NetstandardShim/CancellationTokenShim.cs
Normal file
13
GameServer/KcpSharp/NetstandardShim/CancellationTokenShim.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
#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
|
||||
213
GameServer/KcpSharp/NetstandardShim/LinkedListNetstandard.cs
Normal file
213
GameServer/KcpSharp/NetstandardShim/LinkedListNetstandard.cs
Normal file
@@ -0,0 +1,213 @@
|
||||
#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
|
||||
11
GameServer/KcpSharp/NetstandardShim/TaskCompletionSource.cs
Normal file
11
GameServer/KcpSharp/NetstandardShim/TaskCompletionSource.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
#if NEED_TCS_SHIM
|
||||
|
||||
namespace System.Threading.Tasks
|
||||
{
|
||||
internal class TaskCompletionSource : TaskCompletionSource<bool>
|
||||
{
|
||||
public void TrySetResult() => TrySetResult(true);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
66
GameServer/KcpSharp/ThrowHelper.cs
Normal file
66
GameServer/KcpSharp/ThrowHelper.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
namespace EggLink.DanhengServer.KcpSharp
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,8 @@ using EggLink.DanhengServer.Configuration;
|
||||
using EggLink.DanhengServer.WebServer;
|
||||
using EggLink.DanhengServer.Database;
|
||||
using System.Runtime.InteropServices;
|
||||
using EggLink.DanhengServer.Server;
|
||||
using EggLink.DanhengServer.Server.Packet;
|
||||
|
||||
namespace EggLink.DanhengServer.Program
|
||||
{
|
||||
@@ -11,6 +13,9 @@ namespace EggLink.DanhengServer.Program
|
||||
{
|
||||
private static Logger logger = new("Program");
|
||||
public static DatabaseHelper DatabaseHelper = new();
|
||||
public static Listener Listener = new();
|
||||
public static HandlerManager HandlerManager = new();
|
||||
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
var time = DateTime.Now;
|
||||
@@ -61,12 +66,14 @@ namespace EggLink.DanhengServer.Program
|
||||
Console.ReadLine();
|
||||
return;
|
||||
}
|
||||
|
||||
WebProgram.Main([$"--urls=http://{GetConfig().HttpServer.PublicAddress}:{GetConfig().HttpServer.PublicPort}/"]);
|
||||
logger.Info($"DispatchServer is running on http://{GetConfig().HttpServer.PublicAddress}:{GetConfig().HttpServer.PublicPort}/");
|
||||
|
||||
|
||||
Listener.StartListener();
|
||||
|
||||
var elapsed = DateTime.Now - time;
|
||||
logger.Info($"Done in {elapsed.TotalSeconds.ToString()[..4]}s! Type '/help' to get help of commands.");
|
||||
|
||||
while (true)
|
||||
{
|
||||
Console.ReadLine();
|
||||
|
||||
235
GameServer/Server/Connection.cs
Normal file
235
GameServer/Server/Connection.cs
Normal file
@@ -0,0 +1,235 @@
|
||||
using System.Buffers;
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
using EggLink.DanhengServer.Enums;
|
||||
using EggLink.DanhengServer.Game.Player;
|
||||
using EggLink.DanhengServer.KcpSharp;
|
||||
using EggLink.DanhengServer.Program;
|
||||
using EggLink.DanhengServer.Server.Packet;
|
||||
using EggLink.DanhengServer.Util;
|
||||
using Google.Protobuf;
|
||||
using Google.Protobuf.Reflection;
|
||||
using KcpSharp;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace EggLink.DanhengServer.Server;
|
||||
public partial class Connection
|
||||
{
|
||||
public long? ConversationID => Conversation.ConversationId;
|
||||
private readonly KcpConversation Conversation;
|
||||
private readonly CancellationTokenSource CancelToken;
|
||||
public readonly IPEndPoint RemoteEndPoint;
|
||||
public SessionState State { get; set; } = SessionState.INACTIVE;
|
||||
private bool UseSecretKey { get; set; } = false;
|
||||
private byte[] SecretKey = new byte[0x1000];
|
||||
public Player? Player { get; set; }
|
||||
public uint ClientTime { get; private set; }
|
||||
public long LastPingTime { get; private set; }
|
||||
private uint LastClientSeq = 10;
|
||||
public static readonly List<int> BANNED_PACKETS = [];
|
||||
private static Logger Logger = new("GameServer");
|
||||
#if DEBUG
|
||||
private static uint LogIndex = 0;
|
||||
#endif
|
||||
public Connection(KcpConversation conversation, IPEndPoint remote)
|
||||
{
|
||||
Conversation = conversation;
|
||||
RemoteEndPoint = remote;
|
||||
CancelToken = new CancellationTokenSource();
|
||||
Start();
|
||||
}
|
||||
|
||||
private async void Start()
|
||||
{
|
||||
Logger.Info($"New connection to {RemoteEndPoint} created with conversation id {Conversation.ConversationId}");
|
||||
State = SessionState.WAITING_FOR_TOKEN;
|
||||
await ReceiveLoop();
|
||||
}
|
||||
public async void Stop()
|
||||
{
|
||||
if (Player != null)
|
||||
{
|
||||
await Player.OnLogoutAsync();
|
||||
}
|
||||
Listener.UnregisterConnection(this);
|
||||
Conversation.Dispose();
|
||||
try
|
||||
{
|
||||
CancelToken.Cancel();
|
||||
CancelToken.Dispose();
|
||||
}
|
||||
catch { }
|
||||
|
||||
}
|
||||
|
||||
private void UpdateLastPingTime(uint clientTime)
|
||||
{
|
||||
ClientTime = clientTime;
|
||||
LastPingTime = DateTimeOffset.Now.ToUnixTimeMilliseconds();
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
public static void LogPacket(string sendOrRecv, ushort opcode, byte[] payload)
|
||||
{
|
||||
//Logger.DebugWriteLine($"{sendOrRecv}: {Enum.GetName(typeof(OpCode), opcode)}({opcode})\r\n{Convert.ToHexString(payload)}");
|
||||
Type? typ = AppDomain.CurrentDomain.GetAssemblies().
|
||||
SingleOrDefault(assembly => assembly.GetName().Name == "Shared").GetTypes().First(t => t.Name == $"{Enum.GetName(typeof(CmdId), opcode)}"); //get the type using the packet name
|
||||
MessageDescriptor? descriptor = (MessageDescriptor)typ.GetProperty("Descriptor", BindingFlags.Public | BindingFlags.Static).GetValue(null, null); // get the static property Descriptor
|
||||
IMessage? packet = descriptor.Parser.ParseFrom(payload);
|
||||
JsonFormatter? formatter = JsonFormatter.Default;
|
||||
string? asJson = formatter.Format(packet);
|
||||
Logger.Debug($"{sendOrRecv}: {Enum.GetName(typeof(CmdId), opcode)}({opcode})\r\n{asJson}");
|
||||
}
|
||||
|
||||
#endif
|
||||
private async Task ReceiveLoop()
|
||||
{
|
||||
while (!CancelToken.IsCancellationRequested)
|
||||
{
|
||||
// WaitToReceiveAsync call completes when there is at least one message is received or the transport is closed.
|
||||
KcpConversationReceiveResult result = await Conversation.WaitToReceiveAsync(CancelToken.Token);
|
||||
if (result.TransportClosed)
|
||||
{
|
||||
Logger.Debug("Connection was closed");
|
||||
break;
|
||||
}
|
||||
if (result.BytesReceived > Listener.MAX_MSG_SIZE)
|
||||
{
|
||||
// The message is too large.
|
||||
Logger.Error("Packet too large");
|
||||
Conversation.SetTransportClosed();
|
||||
break;
|
||||
}
|
||||
|
||||
byte[] buffer = ArrayPool<byte>.Shared.Rent(result.BytesReceived);
|
||||
try
|
||||
{
|
||||
// TryReceive should not return false here, unless the transport is closed.
|
||||
// So we don't need to check for result.TransportClosed.
|
||||
if (!Conversation.TryReceive(buffer, out result))
|
||||
{
|
||||
Logger.Error("Failed to receive packet");
|
||||
break;
|
||||
}
|
||||
await ProcessMessageAsync(buffer.AsMemory(0, result.BytesReceived));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error("Packet parse error", ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
}
|
||||
Stop();
|
||||
}
|
||||
|
||||
// DO THE PROCESSING OF THE GAME PACKET
|
||||
private async Task ProcessMessageAsync(Memory<byte> data)
|
||||
{
|
||||
byte[] gamePacket = data.ToArray();
|
||||
|
||||
// Decrypt and turn back into a packet
|
||||
//Crypto.Xor(gamePacket, UseSecretKey ? SecretKey : Crypto.DISPATCH_KEY);
|
||||
await using MemoryStream? ms = new(gamePacket);
|
||||
using BinaryReader? br = new(ms);
|
||||
|
||||
// Handle
|
||||
try
|
||||
{
|
||||
while (br.BaseStream.Position < br.BaseStream.Length)
|
||||
{
|
||||
// Length
|
||||
if (br.BaseStream.Length - br.BaseStream.Position < 12)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Packet sanity check
|
||||
uint Magic1 = br.ReadUInt32BE();
|
||||
if (Magic1 != 0x9D74C714)
|
||||
{
|
||||
Logger.Error($"Bad Data Package Received: got 0x{Magic1:X}, expect 0x9D74C714");
|
||||
return; // Bad packet
|
||||
}
|
||||
// Data
|
||||
ushort opcode = br.ReadUInt16BE();
|
||||
ushort headerLength = br.ReadUInt16BE();
|
||||
uint payloadLength = br.ReadUInt32BE();
|
||||
byte[] header = br.ReadBytes(headerLength);
|
||||
byte[] payload = br.ReadBytes((int)payloadLength);
|
||||
|
||||
await HandlePacketAsync(opcode, header, payload);
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(e.Message, e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await ms.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> HandlePacketAsync(ushort opcode, byte[] header, byte[] payload)
|
||||
{
|
||||
// Find the Handler for this opcode
|
||||
Handler? handler = EntryPoint.HandlerManager.GetHandler(opcode);
|
||||
if (handler != null)
|
||||
{
|
||||
// Handle
|
||||
// Make sure session is ready for packets
|
||||
SessionState state = State;
|
||||
switch ((int)opcode)
|
||||
{
|
||||
case CmdId.PlayerGetTokenCsReq:
|
||||
{
|
||||
if (state != SessionState.WAITING_FOR_TOKEN)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
goto default;
|
||||
}
|
||||
case CmdId.PlayerLoginCsReq:
|
||||
{
|
||||
if (state != SessionState.WAITING_FOR_LOGIN)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
goto default;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
handler.OnHandle(header, payload);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public async Task SendPacketAsync(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 (BANNED_PACKETS.Contains(packet.CmdId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Header
|
||||
byte[] packetBytes = packet.BuildPacket();
|
||||
|
||||
await Conversation.SendAsync(packetBytes, CancelToken.Token);
|
||||
}
|
||||
}
|
||||
6
GameServer/Server/GameSession.cs
Normal file
6
GameServer/Server/GameSession.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace EggLink.DanhengServer.Server
|
||||
{
|
||||
public class GameSession()
|
||||
{
|
||||
}
|
||||
}
|
||||
138
GameServer/Server/Listener.cs
Normal file
138
GameServer/Server/Listener.cs
Normal file
@@ -0,0 +1,138 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using EggLink.DanhengServer.KcpSharp;
|
||||
using EggLink.DanhengServer.Util;
|
||||
using KcpSharp;
|
||||
|
||||
namespace EggLink.DanhengServer.Server
|
||||
{
|
||||
public class Listener
|
||||
{
|
||||
public const int MAX_MSG_SIZE = 16384;
|
||||
public const int HANDSHAKE_SIZE = 20;
|
||||
private static Socket? UDPListener => UDPClient?.Client;
|
||||
private static UdpClient? UDPClient;
|
||||
private static IPEndPoint? ListenAddress;
|
||||
private static IKcpTransport<IKcpMultiplexConnection>? KCPTransport;
|
||||
private static readonly CancellationTokenSource CancelToken = new();
|
||||
private static readonly Logger Logger = new("GameServer");
|
||||
private static IKcpMultiplexConnection? Multiplex => KCPTransport?.Connection;
|
||||
public static readonly SortedList<long, Connection> Connections = [];
|
||||
public static Connection? GetConnectionByEndPoint(IPEndPoint ep) => Connections.Values.FirstOrDefault(c => c.RemoteEndPoint.Equals(ep));
|
||||
|
||||
private static readonly KcpConversationOptions ConvOpt = new()
|
||||
{
|
||||
StreamMode = false,
|
||||
Mtu = 1400,
|
||||
ReceiveWindow = 256,
|
||||
SendWindow = 256,
|
||||
NoDelay = true,
|
||||
UpdateInterval = 100,
|
||||
KeepAliveOptions = new KcpKeepAliveOptions(1000, 30000)
|
||||
};
|
||||
private static uint PORT => ConfigManager.Config.GameServer.PublicPort;
|
||||
public static void StartListener()
|
||||
{
|
||||
ListenAddress = new IPEndPoint(IPAddress.Parse(ConfigManager.Config.GameServer.PublicAddress), (int)PORT);
|
||||
UDPClient = new UdpClient(ListenAddress);
|
||||
if (UDPListener == null) return;
|
||||
KCPTransport = KcpSocketTransport.CreateMultiplexConnection(UDPClient, 1400);
|
||||
KCPTransport.Start();
|
||||
Logger.Info($"Game Server started. Listening on port: {PORT}");
|
||||
}
|
||||
|
||||
private static void RegisterConnection(Connection con)
|
||||
{
|
||||
if (!con.ConversationID.HasValue) return;
|
||||
Connections[con.ConversationID.Value] = con;
|
||||
}
|
||||
public static void UnregisterConnection(Connection con)
|
||||
{
|
||||
if (!con.ConversationID.HasValue) return;
|
||||
long 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
|
||||
{
|
||||
Connection? con = GetConnectionByEndPoint(rcv.RemoteEndPoint);
|
||||
await using MemoryStream? ms = new(rcv.Buffer);
|
||||
using BinaryReader? br = new(ms);
|
||||
int code = br.ReadInt32BE();
|
||||
br.ReadUInt32();
|
||||
br.ReadUInt32();
|
||||
int 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)
|
||||
{
|
||||
long convId = Connections.GetNextAvailableIndex();
|
||||
KcpConversation? convo = Multiplex?.CreateConversation(convId, rcv.RemoteEndPoint, ConvOpt);
|
||||
if (convo == null) return;
|
||||
Connection? con = new(convo, rcv.RemoteEndPoint);
|
||||
RegisterConnection(con);
|
||||
await SendHandshakeResponse(con, enet);
|
||||
}
|
||||
|
||||
private static async Task SendHandshakeResponse(Connection user, int enet)
|
||||
{
|
||||
if (user == null || UDPClient == null || !user.ConversationID.HasValue) return;
|
||||
long convId = user.ConversationID.Value;
|
||||
await using MemoryStream? ms = new();
|
||||
using BinaryWriter? bw = new(ms);
|
||||
bw.WriteInt32BE(0x00000145);
|
||||
bw.WriteConvID(convId);
|
||||
bw.WriteInt32BE(enet);
|
||||
bw.WriteInt32BE(0x14514545);
|
||||
byte[]? data = ms.ToArray();
|
||||
await UDPClient.SendAsync(data, data.Length, user.RemoteEndPoint);
|
||||
}
|
||||
public static async Task SendDisconnectPacket(Connection user, int code)
|
||||
{
|
||||
if (user == null || UDPClient == null || !user.ConversationID.HasValue) return;
|
||||
long convId = user.ConversationID.Value;
|
||||
await using MemoryStream? ms = new();
|
||||
using BinaryWriter? bw = new(ms);
|
||||
bw.WriteInt32BE(0x00000194);
|
||||
bw.WriteConvID(convId);
|
||||
bw.WriteInt32BE(code);
|
||||
bw.WriteInt32BE(0x19419494);
|
||||
byte[]? data = ms.ToArray();
|
||||
await UDPClient.SendAsync(data, data.Length, user.RemoteEndPoint);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
18
GameServer/Server/Packet/BasePacket.cs
Normal file
18
GameServer/Server/Packet/BasePacket.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace EggLink.DanhengServer.Server.Packet
|
||||
{
|
||||
public class BasePacket
|
||||
{
|
||||
public int CmdId;
|
||||
|
||||
public byte[] BuildPacket()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
1164
GameServer/Server/Packet/CmdId.cs
Normal file
1164
GameServer/Server/Packet/CmdId.cs
Normal file
File diff suppressed because it is too large
Load Diff
13
GameServer/Server/Packet/Handler.cs
Normal file
13
GameServer/Server/Packet/Handler.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace EggLink.DanhengServer.Server.Packet
|
||||
{
|
||||
public abstract class Handler
|
||||
{
|
||||
public abstract void OnHandle(byte[] header, byte[] data);
|
||||
}
|
||||
}
|
||||
40
GameServer/Server/Packet/HandlerManager.cs
Normal file
40
GameServer/Server/Packet/HandlerManager.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using EggLink.DanhengServer.Data;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace EggLink.DanhengServer.Server.Packet
|
||||
{
|
||||
public class HandlerManager
|
||||
{
|
||||
public Dictionary<int, Handler> handlers = [];
|
||||
|
||||
public HandlerManager()
|
||||
{
|
||||
var classes = Assembly.GetExecutingAssembly().GetTypes(); // Get all classes in the assembly
|
||||
foreach (var cls in classes)
|
||||
{
|
||||
var attribute = (Opcode)Attribute.GetCustomAttribute(cls, typeof(Opcode));
|
||||
|
||||
if (attribute != null)
|
||||
{
|
||||
handlers.Add(attribute.CmdId, (Handler)Activator.CreateInstance(cls));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Handler? GetHandler(int cmdId)
|
||||
{
|
||||
try
|
||||
{
|
||||
return handlers[cmdId];
|
||||
} catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
14
GameServer/Server/Packet/Opcode.cs
Normal file
14
GameServer/Server/Packet/Opcode.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace EggLink.DanhengServer.Server.Packet
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class Opcode(int cmdId) : Attribute
|
||||
{
|
||||
public int CmdId = cmdId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using EggLink.DanhengServer.Proto;
|
||||
using EggLink.DanhengServer.Util;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace EggLink.DanhengServer.Server.Packet.Recv.Player
|
||||
{
|
||||
[Opcode(CmdId.PlayerGetTokenCsReq)]
|
||||
public class HandlerPlayerGetTokenCsReq : Handler
|
||||
{
|
||||
public override void OnHandle(byte[] header, byte[] data)
|
||||
{
|
||||
var req = PlayerGetTokenCsReq.Parser.ParseFrom(data);
|
||||
Logger.GetByClassName().Debug("OnHandle" + req.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,13 +17,13 @@ namespace EggLink.DanhengServer.Server.Http.Handler
|
||||
var gateServer = new Gateserver() {
|
||||
RegionName = config.GameServer.GameServerId,
|
||||
Ip = config.GameServer.PublicAddress,
|
||||
Port = (uint)config.GameServer.PublicPort,
|
||||
Port = config.GameServer.PublicPort,
|
||||
Msg = "Access verification failed. Please check if you have logged in to the correct account and server.",
|
||||
Unk1 = true,
|
||||
Unk2 = true,
|
||||
Unk3 = true,
|
||||
Unk4 = true,
|
||||
Unk5 = true
|
||||
Unk5 = true,
|
||||
};
|
||||
|
||||
if (urlData.AssetBundleUrl != null)
|
||||
@@ -39,13 +39,13 @@ namespace EggLink.DanhengServer.Server.Http.Handler
|
||||
if (urlData.LuaUrl != null)
|
||||
{
|
||||
gateServer.LuaUrl = urlData.LuaUrl;
|
||||
gateServer.MdkResVersion = urlData.LuaUrl.Split('/')[urlData.LuaUrl.Split('/').Length - 1].Split('_')[1];
|
||||
gateServer.MdkResVersion = urlData.LuaUrl.Split('/')[^1].Split('_')[1];
|
||||
}
|
||||
|
||||
if (urlData.IfixUrl != null)
|
||||
{
|
||||
gateServer.IfixUrl = urlData.IfixUrl;
|
||||
gateServer.IfixVersion = urlData.IfixUrl.Split('/')[urlData.IfixUrl.Split('/').Length - 1].Split('_')[1];
|
||||
gateServer.IfixVersion = urlData.IfixUrl.Split('/')[^1].Split('_')[1];
|
||||
}
|
||||
Logger.Info("Client request: query_gateway");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user