First push

This commit is contained in:
xeon 2024-02-08 00:41:39 +03:00
parent 58ba0a5b58
commit bb1d52cd4f
93 changed files with 22507 additions and 0 deletions

4
.editorconfig Normal file
View file

@ -0,0 +1,4 @@
[*.cs]
# IDE0290: Use primary constructor
csharp_style_prefer_primary_constructors = false

View file

@ -0,0 +1,20 @@
using System.Reflection;
using GameServer.Handlers;
using Microsoft.Extensions.DependencyInjection;
namespace GameServer.Extensions;
internal static class ServiceCollectionExtensions
{
public static IServiceCollection AddHandlers(this IServiceCollection services)
{
IEnumerable<Type> handlerTypes = Assembly.GetExecutingAssembly().GetTypes()
.Where(t => t.IsAssignableTo(typeof(MessageHandlerBase)) && !t.IsAbstract);
foreach (Type type in handlerTypes)
{
services.AddScoped(type);
}
return services;
}
}

View file

@ -0,0 +1,15 @@
namespace GameServer.Extensions;
internal static class SpanExtensions
{
public static void WriteInt24LittleEndian(this Span<byte> span, int value)
{
span[0] = (byte)value;
span[1] = (byte)(value >> 8);
span[2] = (byte)(value >> 16);
}
public static int ReadInt24LittleEndian(this ReadOnlySpan<byte> span)
{
return span[0] | span[1] << 8 | span[2] << 16;
}
}

View file

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\KcpSharp\KcpSharp.csproj" />
<ProjectReference Include="..\Protocol\Protocol.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,14 @@
using Protocol;
namespace GameServer.Handlers.Attributes;
[AttributeUsage(AttributeTargets.Method)]
internal class MessageHandlerAttribute : Attribute
{
public MessageId MessageId { get; }
public MessageHandlerAttribute(MessageId messageId)
{
MessageId = messageId;
}
}

View file

@ -0,0 +1,324 @@
using GameServer.Handlers.Attributes;
using GameServer.Network;
using Protocol;
namespace GameServer.Handlers;
internal class AuthMessageHandler : MessageHandlerBase
{
public AuthMessageHandler(KcpSession session) : base(session)
{
// AuthMessageHandler.
}
[MessageHandler(MessageId.LoginRequest)]
public async Task OnLoginRequest(ReadOnlyMemory<byte> data)
{
LoginRequest request = LoginRequest.Parser.ParseFrom(data.Span);
Console.WriteLine(request);
await Session.Rpc.ReturnAsync(MessageId.LoginResponse, new LoginResponse
{
Code = 0,
Platform = "PC",
Timestamp = DateTimeOffset.Now.ToUnixTimeSeconds()
});
}
[MessageHandler(MessageId.EnterGameRequest)]
public async Task OnEnterGameRequest(ReadOnlyMemory<byte> data)
{
EnterGameRequest request = EnterGameRequest.Parser.ParseFrom(data.Span);
Console.WriteLine(request);
await Session.PushMessage(MessageId.BasicInfoNotify, new BasicInfoNotify
{
RandomSeed = 1337,
Id = 1337,
Birthday = 0,
Attributes =
{
new PlayerAttr
{
Key = (int)PlayerAttrKey.Name,
ValueType = (int)PlayerAttrType.String,
StringValue = "ReversedRooms"
},
new PlayerAttr
{
Key = (int)PlayerAttrKey.Level,
ValueType = (int)PlayerAttrType.Int32,
Int32Value = 10
}
},
RoleShowList =
{
new RoleShowEntry
{
Level = 1,
RoleId = 1302
}
},
});
await Session.PushMessage(MessageId.JoinSceneNotify, new JoinSceneNotify
{
MaxEntityId = 2,
TransitionOption = new TransitionOptionPb
{
TransitionType = (int)TransitionType.Empty
},
SceneInfo = new SceneInformation
{
OwnerId = 1337,
Mode = (int)SceneMode.Single,
InstanceId = 8,
AoiData = new PlayerSceneAoiData
{
GenIds = {1},
Entities =
{
new EntityPb
{
EntityState = (int)EntityState.Born,
EntityType = (int)EEntityType.Player,
PlayerId = 1337,
LivingStatus = (int)LivingStatus.Alive,
ConfigId = 1302,
ConfigType = (int)EntityConfigType.Character,
Id = 1,
IsVisible = true,
Pos = new Vector
{
X = 4000,
Y = -2000,
Z = 260
},
Rot = new(),
InitLinearVelocity = new(),
PrefabIncId = 0,
InitPos = new Vector
{
X = 4000,
Y = -2000,
Z = 260
},
ComponentPbs =
{
new EntityComponentPb
{
VisionSkillComponent = new VisionSkillComponentPb
{
VisionSkillInfos =
{
new VisionSkillInformation
{
SkillId = 1302001,
Level = 1,
Quality = 1,
}
}
},
},
new EntityComponentPb
{
AttributeComponent = new AttributeComponentPb
{
GameAttributes =
{
new GameplayAttributeData
{
AttributeType = (int)EAttributeType.Life,
BaseValue = 1000,
CurrentValue = 1000
},
new GameplayAttributeData
{
AttributeType = (int)EAttributeType.LifeMax,
BaseValue = 1000,
CurrentValue = 1000
},
new GameplayAttributeData
{
AttributeType = (int)EAttributeType.EnergyMax,
BaseValue = 10,
CurrentValue = 10
},
new GameplayAttributeData
{
AttributeType = (int)EAttributeType.Energy,
BaseValue = 10,
CurrentValue = 10
},
new GameplayAttributeData
{
AttributeType = (int)EAttributeType.SpecialEnergy3,
BaseValue = 10,
CurrentValue = 10
},
new GameplayAttributeData
{
AttributeType = (int)EAttributeType.SpecialEnergy3Max,
BaseValue = 10,
CurrentValue = 10
},
new GameplayAttributeData
{
AttributeType = (int)EAttributeType.AutoAttackSpeed,
BaseValue = 10000,
CurrentValue = 10000
},
new GameplayAttributeData
{
AttributeType = (int)EAttributeType.CastAttackSpeed,
BaseValue = 10000,
CurrentValue = 10000
},
new GameplayAttributeData
{
AttributeType = (int)EAttributeType.Atk,
BaseValue = 1,
CurrentValue = 1
},
new GameplayAttributeData
{
AttributeType = (int)EAttributeType.Lv,
BaseValue = 1,
CurrentValue = 1
},
},
}
},
new EntityComponentPb
{
ConcomitantsComponentPb = new ConcomitantsComponentPb
{
CustomEntityIds = {1},
},
}
},
}
}
},
TimeInfo = new SceneTimeInfo
{
Hour = 23
},
PlayerInfos =
{
new ScenePlayerInformation
{
PlayerId = 1337,
Level = 1,
IsOffline = false,
Location = new()
{
X = 4000,
Y = -2000,
Z = 260
},
FightRoleInfos =
{
new FightRoleInformation
{
EntityId = 1,
CurHp = 1000,
MaxHp = 1000,
IsControl = true,
RoleId = 1302,
RoleLevel = 1,
}
},
PlayerName = "ReversedRooms"
}
},
CurContextId = 1337
}
});
await Session.Rpc.ReturnAsync(MessageId.EnterGameResponse, new EnterGameResponse());
}
[MessageHandler(MessageId.EntityOnLandedRequest)]
public async Task OnEntityOnLandedRequest(ReadOnlyMemory<byte> data)
{
await Session.Rpc.ReturnAsync(MessageId.EntityOnLandedResponse, new EntityOnLandedResponse
{
});
}
[MessageHandler(MessageId.UpdateSceneDateRequest)]
public async Task OnUpdateSceneDateRequest(ReadOnlyMemory<byte> data)
{
await Session.Rpc.ReturnAsync(MessageId.UpdateSceneDateResponse, new UpdateSceneDateResponse());
}
[MessageHandler(MessageId.PlayerMotionRequest)]
public async Task OnPlayerMotionRequest(ReadOnlyMemory<byte> data)
{
PlayerMotionRequest request = PlayerMotionRequest.Parser.ParseFrom(data.Span);
await Session.Rpc.ReturnAsync(MessageId.PlayerMotionResponse, new PlayerMotionResponse
{
ErrorId = 0
});
}
[MessageHandler(MessageId.EntityActiveRequest)]
public async Task OnEntityActiveRequest(ReadOnlyMemory<byte> data)
{
EntityActiveRequest request = EntityActiveRequest.Parser.ParseFrom(data.Span);
await Session.Rpc.ReturnAsync(MessageId.EntityActiveResponse, new EntityActiveResponse
{
ComponentPbs = { },
});
}
[MessageHandler(MessageId.GetFormationDataRequest)]
public async Task OnGetFormationDataRequest(ReadOnlyMemory<byte> _)
{
await Session.Rpc.ReturnAsync(MessageId.GetFormationDataResponse, new GetFormationDataResponse
{
Formations =
{
new FightFormation
{
CurRole = 1302,
FormationId = 1,
IsCurrent = true,
RoleIds = { 1302 },
}
},
});
}
[MessageHandler(MessageId.EntityLoadCompleteRequest)]
public async Task OnEntityLoadCompleteRequest(ReadOnlyMemory<byte> _)
{
await Session.Rpc.ReturnAsync(MessageId.EntityLoadCompleteResponse, new EntityLoadCompleteResponse());
}
[MessageHandler(MessageId.SceneLoadingFinishRequest)]
public async Task OnSceneLoadingFinishRequest(ReadOnlyMemory<byte> _)
{
await Session.Rpc.ReturnAsync(MessageId.SceneLoadingFinishResponse, new SceneLoadingFinishResponse
{
ErrorCode = 0
});
}
[MessageHandler(MessageId.HeartbeatRequest)]
public async Task OnHeartbeatRequest(ReadOnlyMemory<byte> _)
{
await Session.Rpc.ReturnAsync(MessageId.HeartbeatResponse, new HeartbeatResponse());
}
[MessageHandler(MessageId.GuideInfoRequest)]
public async Task OnGuideInfoRequest(ReadOnlyMemory<byte> _)
{
await Session.Rpc.ReturnAsync(MessageId.GuideInfoResponse, new GuideInfoResponse()
{
GuideGroupFinishList = { 60001, 60002, 60003, 60004, 60005, 60006, 60007, 60008, 60009, 60010, 60011, 60012, 60013, 60014, 60015, 60016, 60017, 60018, 60019, 60020, 60021, 60101, 60102, 60103, 62002, 62004, 62005, 62006, 62007, 62009, 62010, 62011, 62012, 62013, 62014, 62015, 62016, 62017, 62022, 62027, 62028, 62029, 62030, 62031, 62032, 62033, 62034, 62036, 65001, 67001, 67002, 67003, 67004, 67005, 67006, 67007, 67008, 67009, 67010, 67011, 67012, 67013, 67014, 67015, 67016, 67017, 67018, 67019, 67022, 62001, 62008, 62018, 62019, 62020, 62021, 62023, 62024, 62025, 62026, 62035, 65002, 65003, 65004, 65005 }
});
}
}

View file

@ -0,0 +1,56 @@
using System.Collections.Immutable;
using System.Linq.Expressions;
using System.Reflection;
using GameServer.Handlers.Attributes;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Protocol;
namespace GameServer.Handlers.Factory;
internal class MessageHandlerFactory
{
private readonly ImmutableDictionary<MessageId, MessageHandler> s_messageHandlers;
public MessageHandlerFactory(ILogger<MessageHandlerFactory> logger)
{
IEnumerable<Type> handlerTypes = Assembly.GetExecutingAssembly().GetTypes()
.Where(t => t.IsAssignableTo(typeof(MessageHandlerBase)) && !t.IsAbstract);
s_messageHandlers = GenerateHandlerMethods(handlerTypes);
logger.LogInformation("Registered {count} message handlers", s_messageHandlers.Count);
}
public MessageHandler? GetHandler(MessageId messageId)
{
s_messageHandlers.TryGetValue(messageId, out MessageHandler? handler);
return handler;
}
private static ImmutableDictionary<MessageId, MessageHandler> GenerateHandlerMethods(IEnumerable<Type> handlerTypes)
{
var builder = ImmutableDictionary.CreateBuilder<MessageId, MessageHandler>();
MethodInfo getServiceMethod = typeof(ServiceProviderServiceExtensions).GetMethod("GetRequiredService", [typeof(IServiceProvider)])!;
foreach (Type type in handlerTypes)
{
IEnumerable<MethodInfo> methods = type.GetMethods()
.Where(method => method.GetCustomAttribute<MessageHandlerAttribute>() != null);
foreach (MethodInfo method in methods)
{
MessageHandlerAttribute attribute = method.GetCustomAttribute<MessageHandlerAttribute>()!;
ParameterExpression serviceProviderParam = Expression.Parameter(typeof(IServiceProvider));
ParameterExpression dataParam = Expression.Parameter(typeof(ReadOnlyMemory<byte>));
MethodCallExpression getServiceCall = Expression.Call(getServiceMethod.MakeGenericMethod(type), serviceProviderParam);
MethodCallExpression handlerCall = Expression.Call(getServiceCall, method, dataParam);
Expression<MessageHandler> lambda = Expression.Lambda<MessageHandler>(handlerCall, serviceProviderParam, dataParam);
builder.Add(attribute.MessageId, lambda.Compile());
}
}
return builder.ToImmutable();
}
}

View file

@ -0,0 +1,12 @@
using GameServer.Network;
namespace GameServer.Handlers;
internal abstract class MessageHandlerBase
{
protected KcpSession Session { get; }
public MessageHandlerBase(KcpSession session)
{
Session = session;
}
}

View file

@ -0,0 +1,55 @@
using GameServer.Handlers.Factory;
using GameServer.Network;
using GameServer.Network.Messages;
using GameServer.Network.Packets;
using Protocol;
namespace GameServer.Handlers;
internal delegate Task MessageHandler(IServiceProvider serviceProvider, ReadOnlyMemory<byte> data);
internal class MessageManager
{
private readonly MessageHandlerFactory _handlerFactory;
private readonly IServiceProvider _serviceProvider;
public MessageManager(IServiceProvider serviceProvider, MessageHandlerFactory handlerFactory)
{
_handlerFactory = handlerFactory;
_serviceProvider = serviceProvider;
}
public async Task<bool> ProcessMessage(MessageId messageId, ReadOnlyMemory<byte> data)
{
MessageHandler? handler = _handlerFactory.GetHandler(messageId);
if (handler != null)
{
await handler(_serviceProvider, data);
return true;
}
return false;
}
public static void EncodeMessage(Memory<byte> buffer, BaseMessage message)
{
buffer.Span[0] = (byte)message.Type;
message.Encode(buffer);
}
public static BaseMessage DecodeMessage(ReadOnlyMemory<byte> buffer)
{
MessageType type = (MessageType)buffer.Span[0];
BaseMessage message = type switch
{
MessageType.Request => new RequestMessage(),
MessageType.Response => new ResponseMessage(),
MessageType.Push => new PushMessage(),
_ => throw new NotSupportedException("Message type not implemented: " + type)
};
message.Decode(buffer);
return message;
}
}

View file

@ -0,0 +1,69 @@
using System.Buffers;
using System.Buffers.Binary;
using System.Net;
using System.Net.Sockets;
using KcpSharp;
using Microsoft.Extensions.Logging;
namespace GameServer.Network;
internal class KcpGateway
{
private const int KcpSynPacketSize = 1;
private const int KcpAckPacketSize = 5;
private const byte NetFlagSyn = 0xED;
private const byte NetFlagAck = 0xEE;
private readonly ILogger _logger;
private readonly SessionManager _sessionManager;
private IKcpTransport<IKcpMultiplexConnection>? _kcpTransport;
private int _convCounter;
public KcpGateway(ILogger<KcpGateway> logger, SessionManager sessionManager)
{
_logger = logger;
_sessionManager = sessionManager;
}
public void Start()
{
IPEndPoint endPoint = new(IPAddress.Any, 1337);
_kcpTransport = KcpSocketTransport.CreateMultiplexConnection(new(endPoint), 1400);
_kcpTransport.SetHandshakeHandler(KcpSynPacketSize, HandleKcpSynPacket);
_kcpTransport.Start();
_logger.LogInformation("Listening for incoming connections at {endPoint}", endPoint);
}
private async ValueTask HandleKcpSynPacket(UdpReceiveResult recvResult)
{
if (recvResult.Buffer[0] != NetFlagSyn) return;
_logger.LogInformation("Received SYN from {remoteEndPoint}", recvResult.RemoteEndPoint);
int conv = Interlocked.Increment(ref _convCounter);
_ = _sessionManager.RunSessionAsync(_kcpTransport!.Connection.CreateConversation(conv, recvResult.RemoteEndPoint));
await SendAckAsync(conv, recvResult.RemoteEndPoint);
}
private async ValueTask SendAckAsync(int conv, IPEndPoint remoteEndPoint)
{
_logger.LogInformation("Sending ACK to {remoteEndPoint}, convID: {conv}", remoteEndPoint, conv);
byte[] buffer = ArrayPool<byte>.Shared.Rent(KcpAckPacketSize);
try
{
buffer[0] = NetFlagAck;
BinaryPrimitives.WriteInt32LittleEndian(buffer.AsSpan(1), conv);
await _kcpTransport!.SendPacketAsync(buffer.AsMemory(0, KcpAckPacketSize), remoteEndPoint, CancellationToken.None);
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
}

View file

@ -0,0 +1,104 @@
using System.Buffers;
using GameServer.Extensions;
using GameServer.Handlers;
using GameServer.Network.Messages;
using GameServer.Network.Packets;
using GameServer.Network.Rpc;
using Google.Protobuf;
using KcpSharp;
using Microsoft.Extensions.Logging;
using Protocol;
namespace GameServer.Network;
internal class KcpSession
{
private readonly ILogger _logger;
private readonly MessageManager _messageManager;
private readonly byte[] _recvBuffer;
private KcpConversation? _conv;
private uint _upStreamSeqNo;
private uint _downStreamSeqNo;
public RpcManager Rpc { get; }
public KcpSession(ILogger<KcpSession> logger, MessageManager messageManager, RpcManager rpcManager)
{
_logger = logger;
_messageManager = messageManager;
Rpc = rpcManager;
_recvBuffer = GC.AllocateUninitializedArray<byte>(8192);
}
public async Task RunAsync()
{
while (_conv != null)
{
KcpConversationReceiveResult result = await _conv.ReceiveAsync(_recvBuffer.AsMemory(), CancellationToken.None);
if (result.TransportClosed) return;
ReadOnlyMemory<byte> buffer = _recvBuffer.AsMemory(0, result.BytesReceived);
await HandleMessageAsync(MessageManager.DecodeMessage(buffer.Slice(BaseMessage.LengthFieldSize, buffer.Span.ReadInt24LittleEndian())));
}
}
private async Task HandleMessageAsync(BaseMessage message)
{
if (_downStreamSeqNo >= message.SeqNo) return;
_downStreamSeqNo = message.SeqNo;
switch (message)
{
case RequestMessage request:
await Rpc.HandleRpcRequest(request);
break;
case PushMessage push:
if (!await _messageManager.ProcessMessage(push.MessageId, push.Payload))
_logger.LogWarning("Push message ({id}) was not handled", push.MessageId);
break;
}
}
public Task PushMessage<TProtoBuf>(MessageId id, TProtoBuf data) where TProtoBuf : IMessage<TProtoBuf>
{
return Send(new PushMessage
{
MessageId = id,
Payload = data.ToByteArray()
});
}
public Task Send(BaseMessage message)
{
message.SeqNo = NextUpStreamSeqNo();
return SendAsyncImpl(message);
}
public void SetConv(KcpConversation conv)
{
if (_conv != null) throw new InvalidOperationException("Conv was already set");
_conv = conv;
}
private uint NextUpStreamSeqNo()
{
return Interlocked.Increment(ref _upStreamSeqNo);
}
private async Task SendAsyncImpl(BaseMessage message)
{
int networkSize = message.NetworkSize;
using IMemoryOwner<byte> memoryOwner = MemoryPool<byte>.Shared.Rent(networkSize);
Memory<byte> memory = memoryOwner.Memory;
memory.Span.WriteInt24LittleEndian(networkSize - BaseMessage.LengthFieldSize);
MessageManager.EncodeMessage(memory[BaseMessage.LengthFieldSize..], message);
if (_conv == null) throw new InvalidOperationException("Trying to send message when conv is null");
await _conv.SendAsync(memoryOwner.Memory[..networkSize]);
}
}

View file

@ -0,0 +1,8 @@
namespace GameServer.Network;
internal enum MessageType : byte
{
Request = 1,
Response,
Exception,
Push
}

View file

@ -0,0 +1,27 @@
using System.Buffers.Binary;
namespace GameServer.Network.Packets;
internal abstract class BaseMessage
{
public const int LengthFieldSize = 3;
public abstract MessageType Type { get; }
public abstract int HeaderSize { get; }
public uint SeqNo { get; set; }
public ReadOnlyMemory<byte> Payload { get; set; }
public virtual void Encode(Memory<byte> buffer)
{
BinaryPrimitives.WriteUInt32LittleEndian(buffer.Span[1..], SeqNo);
Payload.CopyTo(buffer[HeaderSize..]);
}
public virtual void Decode(ReadOnlyMemory<byte> buffer)
{
SeqNo = BinaryPrimitives.ReadUInt32LittleEndian(buffer.Span[1..]);
Payload = buffer[HeaderSize..];
}
public int NetworkSize => HeaderSize + Payload.Length + LengthFieldSize;
}

View file

@ -0,0 +1,29 @@
using System.Buffers.Binary;
using GameServer.Network.Packets;
using Protocol;
namespace GameServer.Network.Messages;
internal class PushMessage : BaseMessage
{
public override MessageType Type => MessageType.Push;
public override int HeaderSize => 11;
public MessageId MessageId { get; set; }
public override void Encode(Memory<byte> buffer)
{
base.Encode(buffer);
Span<byte> span = buffer.Span;
BinaryPrimitives.WriteUInt16LittleEndian(span[5..], (ushort)MessageId);
BinaryPrimitives.WriteUInt32LittleEndian(span[7..], 0);
}
public override void Decode(ReadOnlyMemory<byte> buffer)
{
base.Decode(buffer);
ReadOnlySpan<byte> span = buffer.Span;
MessageId = (MessageId)BinaryPrimitives.ReadUInt16LittleEndian(span[5..]);
_ = BinaryPrimitives.ReadUInt32LittleEndian(span[7..]);
}
}

View file

@ -0,0 +1,33 @@
using System.Buffers.Binary;
using GameServer.Network.Packets;
using Protocol;
namespace GameServer.Network.Messages;
internal class RequestMessage : BaseMessage
{
public override MessageType Type => MessageType.Request;
public override int HeaderSize => 13;
public ushort RpcID { get; set; }
public MessageId MessageId { get; set; }
public override void Encode(Memory<byte> buffer)
{
base.Encode(buffer);
Span<byte> span = buffer.Span;
BinaryPrimitives.WriteUInt16LittleEndian(span[5..], RpcID);
BinaryPrimitives.WriteUInt16LittleEndian(span[7..], (ushort)MessageId);
BinaryPrimitives.WriteUInt32LittleEndian(span[9..], 0);
}
public override void Decode(ReadOnlyMemory<byte> buffer)
{
base.Decode(buffer);
ReadOnlySpan<byte> span = buffer.Span;
RpcID = BinaryPrimitives.ReadUInt16LittleEndian(span[5..]);
MessageId = (MessageId)BinaryPrimitives.ReadUInt16LittleEndian(span[7..]);
_ = BinaryPrimitives.ReadUInt32LittleEndian(span[9..]);
}
}

View file

@ -0,0 +1,33 @@
using System.Buffers.Binary;
using GameServer.Network.Packets;
using Protocol;
namespace GameServer.Network.Messages;
internal class ResponseMessage : BaseMessage
{
public override MessageType Type => MessageType.Response;
public override int HeaderSize => 13;
public ushort RpcID { get; set; }
public MessageId MessageId { get; set; }
public override void Encode(Memory<byte> buffer)
{
base.Encode(buffer);
Span<byte> span = buffer.Span;
BinaryPrimitives.WriteUInt16LittleEndian(span[5..], RpcID);
BinaryPrimitives.WriteUInt16LittleEndian(span[7..], (ushort)MessageId);
BinaryPrimitives.WriteUInt32LittleEndian(span[9..], 0);
}
public override void Decode(ReadOnlyMemory<byte> buffer)
{
base.Decode(buffer);
ReadOnlySpan<byte> span = buffer.Span;
RpcID = BinaryPrimitives.ReadUInt16LittleEndian(span[5..]);
MessageId = (MessageId)BinaryPrimitives.ReadUInt16LittleEndian(span[7..]);
_ = BinaryPrimitives.ReadUInt32LittleEndian(span[9..]);
}
}

View file

@ -0,0 +1,7 @@
using GameServer.Network.Messages;
namespace GameServer.Network.Rpc;
internal interface IRpcEndPoint
{
Task SendRpcResult(ResponseMessage message);
}

View file

@ -0,0 +1,50 @@
using GameServer.Handlers;
using GameServer.Network.Messages;
using Google.Protobuf;
using Microsoft.Extensions.Logging;
using Protocol;
namespace GameServer.Network.Rpc;
internal class RpcManager
{
private readonly IRpcEndPoint _endPoint;
private readonly ILogger _logger;
private readonly MessageManager _messageManager;
private ushort _curId;
public RpcManager(MessageManager messageManager, IRpcEndPoint endPoint, ILogger<RpcManager> logger)
{
_endPoint = endPoint;
_logger = logger;
_messageManager = messageManager;
}
public async Task HandleRpcRequest(RequestMessage request)
{
_logger.LogInformation("Processing Rpc, message: {msg_id}, id: {rpc_id}", request.MessageId, request.RpcID);
_curId = request.RpcID;
_ = await _messageManager.ProcessMessage(request.MessageId, request.Payload);
if (_curId != 0)
{
_logger.LogWarning("Rpc was not handled properly (message: {msg_id}, id: {rpc_id})", request.MessageId, request.RpcID);
_curId = 0;
}
}
public async Task ReturnAsync<TProtoBuf>(MessageId messageId, TProtoBuf data) where TProtoBuf : IMessage<TProtoBuf>
{
if (_curId == 0) throw new InvalidOperationException("RpcManager::ReturnAsync called - no rpc being processed!");
await _endPoint.SendRpcResult(new ResponseMessage
{
MessageId = messageId,
RpcID = _curId,
Payload = data.ToByteArray()
});
_logger.LogInformation("Rpc with id {rpc_id} was handled, return message: {msg_id}", _curId, messageId);
_curId = 0;
}
}

View file

@ -0,0 +1,18 @@
using GameServer.Network.Messages;
using Microsoft.Extensions.DependencyInjection;
namespace GameServer.Network.Rpc;
internal class RpcSessionEndPoint : IRpcEndPoint
{
private readonly IServiceProvider _serviceProvider;
private KcpSession? _session;
private KcpSession Session => _session ??= _serviceProvider.GetRequiredService<KcpSession>();
public RpcSessionEndPoint(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public Task SendRpcResult(ResponseMessage message) => Session.Send(message);
}

View file

@ -0,0 +1,32 @@
using KcpSharp;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace GameServer.Network;
internal class SessionManager
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger _logger;
public SessionManager(IServiceScopeFactory scopeFactory, ILogger<SessionManager> logger)
{
_scopeFactory = scopeFactory;
_logger = logger;
}
public async Task RunSessionAsync(KcpConversation kcpConv)
{
using IServiceScope scope = _scopeFactory.CreateScope();
KcpSession session = scope.ServiceProvider.GetRequiredService<KcpSession>();
try
{
session.SetConv(kcpConv);
await session.RunAsync();
}
catch (Exception exception)
{
_logger.LogError("Exception occurred in session processing: {trace}", exception);
}
}
}

28
GameServer/Program.cs Normal file
View file

@ -0,0 +1,28 @@
using GameServer.Extensions;
using GameServer.Handlers;
using GameServer.Handlers.Factory;
using GameServer.Network;
using GameServer.Network.Rpc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace GameServer;
internal static class Program
{
private static async Task Main(string[] args)
{
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Logging.AddConsole();
builder.Services.AddHandlers()
.AddSingleton<KcpGateway>().AddScoped<KcpSession>()
.AddScoped<MessageManager>().AddSingleton<MessageHandlerFactory>()
.AddScoped<RpcManager>().AddScoped<IRpcEndPoint, RpcSessionEndPoint>()
.AddSingleton<SessionManager>()
.AddHostedService<WWGameServer>();
await builder.Build().RunAsync();
}
}

View file

@ -0,0 +1,27 @@
using GameServer.Handlers.Factory;
using GameServer.Network;
using Microsoft.Extensions.Hosting;
namespace GameServer;
internal class WWGameServer : IHostedService
{
private readonly KcpGateway _gateway;
public WWGameServer(KcpGateway gateway, MessageHandlerFactory messageHandlerFactory)
{
_ = messageHandlerFactory;
_gateway = gateway;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_gateway.Start();
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}

View file

@ -0,0 +1,23 @@
#if !NEED_POH_SHIM
using System;
using System.Buffers;
namespace 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

1
KcpSharp/AssemblyInfo.cs Normal file
View file

@ -0,0 +1 @@
[assembly: System.CLSCompliant(true)]

View file

@ -0,0 +1,120 @@
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Sources;
namespace 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();
}
}
}
}
}

View file

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

View file

@ -0,0 +1,15 @@
namespace 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);
}
}

View file

@ -0,0 +1,25 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace 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(ReadOnlyMemory<byte> packet, CancellationToken cancellationToken);
/// <summary>
/// Mark the underlying transport as closed. Abort all active send or receive operations.
/// </summary>
void SetTransportClosed();
}
}

View file

@ -0,0 +1,10 @@
using System;
namespace KcpSharp
{
internal interface IKcpConversationUpdateNotificationSource
{
ReadOnlyMemory<byte> Packet { get; }
void Release();
}
}

View file

@ -0,0 +1,18 @@
using System;
namespace 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);
}
}

View file

@ -0,0 +1,56 @@
using System;
using System.Net;
namespace 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(int id);
/// <summary>
/// Create a raw channel with the specified conversation ID.
/// </summary>
/// <param name="id">The conversation ID.</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(int 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="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(int 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, int 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(int id);
}
}

View file

@ -0,0 +1,52 @@
using System;
using System.Net;
namespace 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="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(int 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="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(int 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, int 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(int id, out T? state);
}
}

21
KcpSharp/IKcpTransport.cs Normal file
View file

@ -0,0 +1,21 @@
using System.Net;
using System.Net.Sockets;
namespace 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="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 endpoint, CancellationToken cancellationToken);
void SetHandshakeHandler(int size, Func<UdpReceiveResult, ValueTask> handshakeHandler);
}
}

View file

@ -0,0 +1,24 @@
using System;
namespace 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();
}
}

View file

@ -0,0 +1,106 @@
using System;
using System.Runtime.CompilerServices;
using System.Threading;
namespace 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);
var newArray = new (uint SerialNumber, uint Timestamp)[capacity];
_array.AsSpan(0, _count).CopyTo(newArray);
_array = newArray;
}
}
}

60
KcpSharp/KcpBuffer.cs Normal file
View file

@ -0,0 +1,60 @@
using System;
using System.Diagnostics;
namespace 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.");
}
}
}

View file

@ -0,0 +1,43 @@
using System;
namespace 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
KcpSharp/KcpCommand.cs Normal file
View file

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

View file

@ -0,0 +1,278 @@
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.ExceptionServices;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Sources;
namespace KcpSharp
{
partial class KcpConversation
{
#if NET6_0_OR_GREATER
[ThreadStatic]
private static KcpConversation? s_currentObject;
private object? _flushStateMachine;
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)
{
var edi = ExceptionDispatchInfo.Capture(exception);
ThreadPool.QueueUserWorkItem(static state => ((ExceptionDispatchInfo)state!).Throw(), edi);
}
}
private static void AwaitUnsafeOnCompleted<TAwaiter>(ref TAwaiter awaiter, StateMachineBox box) where TAwaiter : ICriticalNotifyCompletion
{
try
{
awaiter.UnsafeOnCompleted(box.MoveNextAction);
}
catch (Exception exception)
{
var edi = ExceptionDispatchInfo.Capture(exception);
ThreadPool.QueueUserWorkItem(static state => ((ExceptionDispatchInfo)state!).Throw(), edi);
}
}
private static StateMachineBox CreateWeaklyTypedStateMachineBox()
{
return new StateMachineBox<IAsyncStateMachine>(null);
}
private static StateMachineBox GetStateMachineBox<TStateMachine>(ref TStateMachine stateMachine, ref StateMachineBox? boxFieldRef, KcpConversation conversation) where TStateMachine : IAsyncStateMachine
{
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;
}
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();
}
}
sealed class SyncSuccessSentinelStateMachineBox : StateMachineBox
{
public SyncSuccessSentinelStateMachineBox()
{
SetResult();
}
}
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
}
}

1382
KcpSharp/KcpConversation.cs Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,97 @@
namespace 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;
}
}

View file

@ -0,0 +1,62 @@
using System;
using System.Globalization;
namespace 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.";
}
}

View file

@ -0,0 +1,494 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Sources;
namespace 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 =>
{
var 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();
}
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;
}
}
}
}
}
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();
}
}
}
}

View file

@ -0,0 +1,32 @@
using System;
namespace 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();
}
}
}
}

View file

@ -0,0 +1,136 @@
using System;
namespace 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
);
}
}
}

View file

@ -0,0 +1,32 @@
using System;
namespace 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; }
}
}

View file

@ -0,0 +1,338 @@
using System;
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
namespace 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<int, (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(ReadOnlyMemory<byte> packet, CancellationToken cancellationToken = default)
{
ReadOnlySpan<byte> span = packet.Span;
if (span.Length < 4)
{
return default;
}
if (_transportClosed || _disposed)
{
return default;
}
int id = (int)BinaryPrimitives.ReadUInt32LittleEndian(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(int 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="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(int id, IPEndPoint remoteEndPoint, KcpRawChannelOptions? options = null)
{
KcpRawChannel? channel = new KcpRawChannel(remoteEndPoint, this, id, options);
try
{
RegisterConversation(channel, id, default);
if (_transportClosed)
{
channel.SetTransportClosed();
}
return Interlocked.Exchange<KcpRawChannel?>(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="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(int id, IPEndPoint remoteEndPoint, T state, KcpRawChannelOptions? options = null)
{
var channel = new KcpRawChannel(remoteEndPoint, this, id, options);
try
{
RegisterConversation(channel, id, state);
if (_transportClosed)
{
channel.SetTransportClosed();
}
return Interlocked.Exchange<KcpRawChannel?>(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="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(int id, IPEndPoint remoteEndPoint, KcpConversationOptions? options = null)
{
var conversation = new KcpConversation(remoteEndPoint, this, id, options);
try
{
RegisterConversation(conversation, id, default);
if (_transportClosed)
{
conversation.SetTransportClosed();
}
return Interlocked.Exchange<KcpConversation?>(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="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(int id, IPEndPoint remoteEndPoint, T state, KcpConversationOptions? options = null)
{
var conversation = new KcpConversation(remoteEndPoint, this, id, options);
try
{
RegisterConversation(conversation, id, state);
if (_transportClosed)
{
conversation.SetTransportClosed();
}
return Interlocked.Exchange<KcpConversation?>(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, int 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, int 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(int 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(int 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 endpoint, CancellationToken cancellationToken = default)
{
if (_transportClosed || _disposed)
{
return default;
}
return _transport.SendPacketAsync(packet, endpoint, 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 (int 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);
}
}
}
}
}
public void SetHandshakeHandler(int size, Func<UdpReceiveResult, ValueTask> handshakeHandler)
{
throw new NotImplementedException("SetHandshakeHandler not designed for this type of transport.");
}
}
}

View file

@ -0,0 +1,76 @@
using System;
using System.Buffers.Binary;
using System.Diagnostics;
namespace 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(uint? conversationId, int payloadLength, Span<byte> destination, out int bytesWritten)
{
Debug.Assert(destination.Length >= 20);
if (conversationId.HasValue)
{
BinaryPrimitives.WriteUInt32LittleEndian(destination, conversationId.GetValueOrDefault());
destination = destination.Slice(4);
bytesWritten = 24;
}
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.WriteInt32LittleEndian(destination.Slice(16), payloadLength);
}
}
}

12
KcpSharp/KcpProbeType.cs Normal file
View file

@ -0,0 +1,12 @@
using System;
namespace KcpSharp
{
[Flags]
internal enum KcpProbeType
{
None = 0,
AskSend = 1,
AskTell = 2,
}
}

371
KcpSharp/KcpRawChannel.cs Normal file
View file

@ -0,0 +1,371 @@
using System;
using System.Buffers.Binary;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
namespace KcpSharp
{
/// <summary>
/// An unreliable channel with a conversation ID.
/// </summary>
public sealed class KcpRawChannel : IKcpConversation, IKcpExceptionProducer<KcpRawChannel>
{
private readonly IPEndPoint _remoteEndPoint;
private readonly IKcpBufferPool _bufferPool;
private readonly IKcpTransport _transport;
private readonly uint? _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="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="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, int conversationId, KcpRawChannelOptions? options)
: this(remoteEndPoint, transport, (uint)conversationId, options)
{ }
private KcpRawChannel(IPEndPoint remoteEndPoint, IKcpTransport transport, uint? conversationId, KcpRawChannelOptions? options)
{
_remoteEndPoint = remoteEndPoint;
_bufferPool = options?.BufferPool ?? DefaultArrayPoolBufferAllocator.Default;
_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 int? ConversationId => (int?)_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 -= 4;
}
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 += 4;
}
{
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.WriteUInt32LittleEndian(memory.Span, _id.GetValueOrDefault());
memory = memory.Slice(4);
}
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(ReadOnlyMemory<byte> packet, CancellationToken cancellationToken = default)
{
ReadOnlySpan<byte> span = packet.Span;
int overhead = _id.HasValue ? 4 : 0;
if (span.Length < overhead || span.Length > _mtu)
{
return default;
}
if (_id.HasValue)
{
if (BinaryPrimitives.ReadUInt32LittleEndian(span) != _id.GetValueOrDefault())
{
return default;
}
span = span.Slice(4);
}
_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();
}
}
}

View file

@ -0,0 +1,33 @@
namespace 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; }
}
}

View file

@ -0,0 +1,358 @@
using System;
using System.Threading;
using System.Threading.Tasks;
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<KcpSharp.KcpBuffer>;
using LinkedListNodeOfQueueItem = System.Collections.Generic.LinkedListNode<KcpSharp.KcpBuffer>;
#endif
namespace 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;
}
}
}
}

View file

@ -0,0 +1,185 @@
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Sources;
namespace 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;
}
}
}
}

696
KcpSharp/KcpReceiveQueue.cs Normal file
View file

@ -0,0 +1,696 @@
using System;
using System.Threading;
using System.Threading.Tasks;
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<(KcpSharp.KcpBuffer Data, byte Fragment)>;
using LinkedListNodeOfQueueItem = System.Collections.Generic.LinkedListNode<(KcpSharp.KcpBuffer Data, byte Fragment)>;
#endif
namespace 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;
}
}
}
}

View file

@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace 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; }
}
}

223
KcpSharp/KcpRentedBuffer.cs Normal file
View file

@ -0,0 +1,223 @@
using System;
using System.Buffers;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace 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
KcpSharp/KcpSendQueue.cs Normal file
View file

@ -0,0 +1,718 @@
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
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<(KcpSharp.KcpBuffer Data, byte Fragment)>;
using LinkedListNodeOfQueueItem = System.Collections.Generic.LinkedListNode<(KcpSharp.KcpBuffer Data, byte Fragment)>;
#endif
namespace 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;
}
}
}
}

View file

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

View file

@ -0,0 +1,74 @@
using System.Threading;
#if NEED_LINKEDLIST_SHIM
using LinkedListOfBufferItem = KcpSharp.NetstandardShim.LinkedList<KcpSharp.KcpSendReceiveBufferItem>;
using LinkedListNodeOfBufferItem = KcpSharp.NetstandardShim.LinkedListNode<KcpSharp.KcpSendReceiveBufferItem>;
#else
using LinkedListOfBufferItem = System.Collections.Generic.LinkedList<KcpSharp.KcpSendReceiveBufferItem>;
using LinkedListNodeOfBufferItem = System.Collections.Generic.LinkedListNode<KcpSharp.KcpSendReceiveBufferItem>;
#endif
namespace 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();
}
}
}
}
}

View file

@ -0,0 +1,85 @@
using System.Threading;
#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<(KcpSharp.KcpBuffer Data, byte Fragment)>;
using LinkedListNodeOfQueueItem = System.Collections.Generic.LinkedListNode<(KcpSharp.KcpBuffer Data, byte Fragment)>;
#endif
namespace 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();
}
}
}
}
}

View file

@ -0,0 +1,20 @@
namespace 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; }
}
}

9
KcpSharp/KcpSharp.csproj Normal file
View file

@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View file

@ -0,0 +1,151 @@
using System;
using System.Net;
using System.Net.Sockets;
namespace 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="socket">The socket 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 socket, IPEndPoint endPoint, int conversationId, KcpConversationOptions? options)
{
if (socket is null)
{
throw new ArgumentNullException(nameof(socket));
}
if (endPoint is null)
{
throw new ArgumentNullException(nameof(endPoint));
}
return new KcpSocketTransportForConversation(socket, endPoint, conversationId, options);
}
/// <summary>
/// Create a socket transport for KCP covnersation with no conversation ID.
/// </summary>
/// <param name="socket">The socket 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 socket, IPEndPoint endPoint, KcpConversationOptions? options)
{
if (socket is null)
{
throw new ArgumentNullException(nameof(socket));
}
if (endPoint is null)
{
throw new ArgumentNullException(nameof(endPoint));
}
return new KcpSocketTransportForConversation(socket, endPoint, null, options);
}
/// <summary>
/// Create a socket transport for raw channel.
/// </summary>
/// <param name="socket">The socket 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 socket, IPEndPoint endPoint, int conversationId, KcpRawChannelOptions? options)
{
if (socket is null)
{
throw new ArgumentNullException(nameof(socket));
}
if (endPoint is null)
{
throw new ArgumentNullException(nameof(endPoint));
}
return new KcpSocketTransportForRawChannel(socket, endPoint, conversationId, options);
}
/// <summary>
/// Create a socket transport for raw channel with no conversation ID.
/// </summary>
/// <param name="socket">The socket 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 socket, IPEndPoint endPoint, KcpRawChannelOptions? options)
{
if (socket is null)
{
throw new ArgumentNullException(nameof(socket));
}
if (endPoint is null)
{
throw new ArgumentNullException(nameof(endPoint));
}
return new KcpSocketTransportForRawChannel(socket, endPoint, null, options);
}
/// <summary>
/// Create a socket transport for multiplex connection.
/// </summary>
/// <param name="socket">The socket 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 socket, int mtu)
{
if (socket is null)
{
throw new ArgumentNullException(nameof(socket));
}
return new KcpSocketTransportForMultiplexConnection<object>(socket, mtu);
}
/// <summary>
/// Create a socket transport for multiplex connection.
/// </summary>
/// <typeparam name="T">The type of the user state.</typeparam>
/// <param name="socket">The socket instance.</param>
/// <param name="endPoint">The remote endpoint.</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 socket, int mtu)
{
if (socket is null)
{
throw new ArgumentNullException(nameof(socket));
}
return new KcpSocketTransportForMultiplexConnection<T>(socket, mtu);
}
/// <summary>
/// Create a socket transport for multiplex connection.
/// </summary>
/// <typeparam name="T">The type of the user state.</typeparam>
/// <param name="socket">The socket instance.</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 socket, int mtu, Action<T?>? disposeAction)
{
if (socket is null)
{
throw new ArgumentNullException(nameof(socket));
}
return new KcpSocketTransportForMultiplexConnection<T>(socket, mtu, disposeAction);
}
}
}

View file

@ -0,0 +1,46 @@
using System;
using System.Net;
using System.Net.Sockets;
namespace KcpSharp
{
/// <summary>
/// Socket transport for KCP conversation.
/// </summary>
internal sealed class KcpSocketTransportForConversation : KcpSocketTransport<KcpConversation>, IKcpTransport<KcpConversation>
{
private readonly int? _conversationId;
private readonly IPEndPoint _remoteEndPoint;
private readonly KcpConversationOptions? _options;
private Func<Exception, IKcpTransport<KcpConversation>, object?, bool>? _exceptionHandler;
private object? _exceptionHandlerState;
internal KcpSocketTransportForConversation(UdpClient socket, IPEndPoint endPoint, int? conversationId, KcpConversationOptions? options)
: base(socket, 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;
}
}
}

View file

@ -0,0 +1,42 @@
using System;
using System.Net;
using System.Net.Sockets;
namespace 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 socket, int mtu)
: base(socket, mtu)
{ }
internal KcpSocketTransportForMultiplexConnection(UdpClient socket, int mtu, Action<T?>? disposeAction)
: base(socket, mtu)
{
_disposeAction = disposeAction;
}
protected override KcpMultiplexConnection<T> Activate() => new KcpMultiplexConnection<T>(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;
}
}
}

View file

@ -0,0 +1,42 @@
using System;
using System.Net;
using System.Net.Sockets;
namespace KcpSharp
{
internal sealed class KcpSocketTransportForRawChannel : KcpSocketTransport<KcpRawChannel>, IKcpTransport<KcpRawChannel>
{
private readonly int? _conversationId;
private readonly IPEndPoint _remoteEndPoint;
private readonly KcpRawChannelOptions? _options;
private Func<Exception, IKcpTransport<KcpRawChannel>, object?, bool>? _exceptionHandler;
private object? _exceptionHandlerState;
internal KcpSocketTransportForRawChannel(UdpClient socket, IPEndPoint endPoint, int? conversationId, KcpRawChannelOptions? options)
: base(socket, options?.Mtu ?? KcpConversationOptions.MtuDefaultValue)
{
_remoteEndPoint = endPoint;
_conversationId = conversationId;
_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;
}
}
}

View file

@ -0,0 +1,325 @@
using System;
using System.Buffers;
using System.Net;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
namespace 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 _udp;
private readonly int _mtu;
private T? _connection;
private CancellationTokenSource? _cts;
private bool _disposed;
private int _handshakeSize;
private Func<UdpReceiveResult, ValueTask>? _handshakeHandler;
/// <summary>
/// Construct a socket transport with the specified socket and remote endpoint.
/// </summary>
/// <param name="udp">The socket instance.</param>
/// <param name="endPoint">The remote endpoint.</param>
/// <param name="mtu">The maximum packet size that can be transmitted.</param>
protected KcpSocketTransport(UdpClient udp, int mtu)
{
_udp = udp ?? throw new ArgumentNullException(nameof(udp));
_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();
}
public void SetHandshakeHandler(int size, Func<UdpReceiveResult, ValueTask> handshakeHandler)
{
_handshakeSize = size;
_handshakeHandler = handshakeHandler;
}
#if NEED_SOCKET_SHIM
/// <inheritdoc />
public async ValueTask SendPacketAsync(Memory<byte> packet, CancellationToken cancellationToken = default)
{
if (_disposed)
{
return;
}
cancellationToken.ThrowIfCancellationRequested();
if (packet.Length > _mtu)
{
return;
}
byte[]? rentedArray = null;
if (!MemoryMarshal.TryGetArray(packet, out ArraySegment<byte> segment))
{
rentedArray = ArrayPool<byte>.Shared.Rent(packet.Length);
segment = new ArraySegment<byte>(rentedArray, 0, packet.Length);
packet.CopyTo(segment.AsMemory());
}
try
{
using var saea = new AwaitableSocketAsyncEventArgs();
saea.SetBuffer(segment.Array, segment.Offset, segment.Count);
saea.SocketFlags = SocketFlags.None;
saea.RemoteEndPoint = _endPoint;
if (_socket.SendToAsync(saea))
{
await saea.WaitAsync().ConfigureAwait(false);
saea.Reset();
}
if (saea.SocketError != SocketError.Success)
{
throw new SocketException((int)saea.SocketError);
}
}
finally
{
if (rentedArray is not null)
{
ArrayPool<byte>.Shared.Return(rentedArray);
}
}
}
private static async ValueTask<int> ReceiveFromAsync(Socket socket, Memory<byte> buffer, EndPoint endPoint, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
byte[]? rentedArray = null;
if (!MemoryMarshal.TryGetArray(buffer, out ArraySegment<byte> segment))
{
rentedArray = ArrayPool<byte>.Shared.Rent(buffer.Length);
segment = new ArraySegment<byte>(rentedArray, 0, buffer.Length);
}
try
{
using var saea = new AwaitableSocketAsyncEventArgs();
saea.SetBuffer(segment.Array, segment.Offset, segment.Count);
saea.SocketFlags = SocketFlags.None;
saea.RemoteEndPoint = endPoint;
if (socket.SendToAsync(saea))
{
await saea.WaitAsync().ConfigureAwait(false);
saea.Reset();
}
if (saea.SocketError != SocketError.Success)
{
throw new SocketException((int)saea.SocketError);
}
if (rentedArray is not null)
{
segment.AsMemory().CopyTo(buffer);
}
return saea.BytesTransferred;
}
finally
{
if (rentedArray is not null)
{
ArrayPool<byte>.Shared.Return(rentedArray);
}
}
}
#else
/// <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(_udp.SendAsync(packet.ToArray(), endpoint, cancellationToken).AsTask());
}
#endif
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
{
Memory<byte> memory = memoryOwner.Memory;
while (!cancellationToken.IsCancellationRequested)
{
int bytesReceived;
UdpReceiveResult result = default;
try
{
result = await _udp.ReceiveAsync(cancellationToken);
bytesReceived = result.Buffer.Length;
}
catch (Exception)
{
bytesReceived = 0;
}
if (bytesReceived != 0 && bytesReceived <= _mtu)
{
if (bytesReceived == _handshakeSize && _handshakeHandler != null)
await _handshakeHandler(result);
else
await connection.InputPakcetAsync(result.Buffer, cancellationToken).ConfigureAwait(false);
}
}
}
catch (OperationCanceledException)
{
// Do nothing
}
catch (Exception ex)
{
HandleExceptionWrapper(ex);
}
}
private bool HandleExceptionWrapper(Exception ex)
{
bool result;
try
{
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);
}
}
}

171
KcpSharp/KcpStream.cs Normal file
View file

@ -0,0 +1,171 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace 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
}
}

View file

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

View file

@ -0,0 +1,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

View 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

View file

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

70
KcpSharp/ThrowHelper.cs Normal file
View file

@ -0,0 +1,70 @@
using System;
using System.IO;
using System.Threading;
namespace 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));
}
}
}

1379
Protocol/MessageId.cs Normal file

File diff suppressed because it is too large Load diff

25
Protocol/Protocol.csproj Normal file
View file

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<None Remove="message.proto" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Google.Protobuf" Version="3.25.2" />
<PackageReference Include="Grpc.Tools" Version="2.60.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Protobuf Include="message.proto" />
</ItemGroup>
</Project>

12290
Protocol/message.proto Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,55 @@
using Microsoft.AspNetCore.Http.HttpResults;
using SDKServer.Models.BaseConfig;
namespace SDKServer.Handlers;
internal static class ConfigHandler
{
public static JsonHttpResult<BaseConfigModel> GetBaseConfig()
{
return TypedResults.Json(new BaseConfigModel
{
CdnUrl = [
new CdnUrlEntry
{
Url = "http://127.0.0.1:5500/dev/client/",
Weight = "100"
},
new CdnUrlEntry
{
Url = "http://127.0.0.1:5500/dev/client/",
Weight = "100"
}
],
SecondaryUrl = [],
GmOpen = false,
PayUrl = "http://114.132.150.182:12281/ReceiptNotify/PayNotify",
TDCfg = new TDConfig
{
Url = "https://ali-sh-datareceiver.kurogame.xyz",
AppID = "364c899beea94b92b87ae30869b492d6"
},
LogReport = new LogReportConfig
{
Ak = "AKIDseIrMkz66ymcSBrjpocFt9IO0lT1SiIk",
Sk = "MXeeVBfs0ywnleS83xiGczCPVROCnFds",
Name = "aki-upload-log-1319073642",
Region = "ap-shanghai"
},
NoticUrl = "https://prod-alicdn-gmserver-static.kurogame.com",
LoginServers = [
new LoginServerEntry
{
Id = "1074",
Name = "ReversedRooms",
Ip = "127.0.0.1"
}
],
PrivateServers = new PrivateServersConfig
{
Enable = false,
ServerUrl = ""
}
});
}
}

View file

@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Http.HttpResults;
namespace SDKServer.Handlers;
internal static class HotPatchHandler
{
public static string OnConfigRequest() => "N2wiD0yJQntN+Kv98DJOLWvjpgvdTa3/KRA9Rf288hPWxbPgAOzkf/mSfCXKMOFldB3zoIftzB7NIGkF+slYu1MoBcFyxkhx+lRW9oDsuqKVS32cgEyuopKuN71QdjPII82UGXGd4M5mBilVEkTaRKBZVypka5CB79x5Rs2iiPTxOk1zdOUi5F6uUD0cjQod/KM+7TtaiUsSNS+FVBldLPWJOdcQWomUw+q5cCLBJqhxYUJg/hQAt8rRRHyDj2H+0RAdMt1WS5qbYb+QkVCvZlT4UWUyaes3p3xsyyr0HpiqzHXf5iLjaxSv5iKNrLUpLMexn2c3bf+TG7Kca/QYwbJYz4TeYk8zuV7A1ccC4fgSSxwC50xChiOuPwyi7NWkP7Z2FEyqtBWpgoWo6+uFn+f1PaalaZt9e2JZXvmZjcY5pQwGfbXSHAGu3yopAScBvsX1nQzrRgi8LFN/2XTMaoYjfZp+FiNiVMFCK7IlcfjeL1Shd5FrwcKnEGPeu6dVBvrZnKWaK81lzZnZc0Yce95rZMa7o8ccQBTWXE/S/ZXV10ctCO6KYa1/+l+zhvAfPt3WcU7OvcOh24kg/WSYOZFg9nFaLpMzSCTqZPhi7OE1LU5o0uLfk8kFJtJFj7CVG0YiD/dZhWhA0HW6WZmbE/DESkUMagfVPanZCZpweBBnmmRnJZr+LMm5btmgmZZ8/pwGk/JbvpwRaSvP1Cw/hIVq6m/EJ+j+oHr/EscxoVa2f9pGB20TOS7ggTPiwkvfWxX04CXCQ6VgQ8dFNZB4tO6LiNsCfSARDz0VNjqdpdRpON0W2KjfhKlX4IlhsQj9Cnet4Dk/knWe/vO2e2/JGw4JGXzJmguhT9EQE81IaGoKkcOBjED5odgONH37TyNK86piMBHYHRy/ZhpKOHbMhNh5Vv2mv94kzLMD6elXnRiL888tcc3hgi2fmZR6DVKvbiN2SIXRABQy6YNtKQVjMBzJEGART29AyD4r89L85T4jbheTiyXiDIX4H0eGGfqLsCJnSocV7GLm2ZJrKY/ebsg50fadj3AlxnU2Hqkxyn3Xdmf5ilM9lQ8xurrtsbGSSn6cQ/56X8Vm5gJa/4bFyMvuSl4ygKuAq6Ae8S7faeRUPkmObPOBdU841OmMWrjf8IeXzt+jkK00VglUC8y+ZhvX33ZYNXGjAACwZF11GCS3plkhI7zwtOlLNavtO94INQQGulQ/fG+mu4BAh19xtMiZpUXIumleO0ehq5LHiE/UYMZizvONyvPbCnqmkvEI8f9kgkODPkPBfQjqQ7qeGpU7BdbKEv83BTr5SyG6m80Fq0BDVdX5BwkGdk28YeJdbjUhUH0/fmSr7ODw6YYpE3x8TaDpOq+taNZu40KZ6USeNqEofSZPHC4e1+SwSpjldOoQTdH7BBaQ6hN0plJkm+4HL7KbWAQOX82W7H3Ks/GoMeHmokTo9T2JUTi+dzNpuaOjxlxyiuoDkiZ+0cOcLIbKk5Df0cphrkq2GrvCJ5g4FGvLvcPrs8tdA15D89i8Ygrh6ERy6beiwTCEtK5sqWM11URUrCCTRi3JRprzq+R+ghOPVGK1svfpUbzAyWZK6ugt3OYdg0Ze3WKFnvLqRAhWu6ouy+iHrT8D1qVna+6IZ3OCZbDpTlszQTA+5YNY9iO2O9seOJvbUOep5T04RETWFEr0vunp3M0FAH9OtaUN9ePmshqk3h1wC0oftx+8d5QvyJ/EkrXi4LA1huKDGtbf2P8RK2oH7wXKZaMZKYl2IGtc1lzIebtzkkqY/6ygl4K8mhFUf1g4vXSvA17mjdcemrptVCxB5EAnjiyBbjhVtoSE4RAmN7ysA95MeF104cf4AFUS2V3c4h7RTRb0HRIOfWhPc/UP6UZnXYx+ALCKyfSDXZ7v9Ctxfqnd/WwxKg233tcPBOgOVP72wSYiXt3HtHve5/NqBrz3PW7KJLoA5wTG43iURlkhvgIXJV6PK6BbCXgusT0ZHOtKUYIbhomZ1Wa+2VmCcljQdfN/qH7C1nS6Icr+OGHqoA4XBoj3OMZ1wQrP+9BwWhp9dxsLlT8lNQny8f9IIzvxZNyDNK7WOfRAHuO1x0Wf8aqA11SM8BL99m90PuLzMXK7zZgqKxJVF1Ry640/JMr8vaK01aKGwMEQXaUt/so94Z/FXm8uwZJ9Rw7ZIviyzdPmq7JXNa8W2kkeEEdekBKdCOD+Q63F0pzFoqvkrqram17GJHbGN/0DXoYrQGkJjuo7ZiwdJGagtuFah7Fut+bWvfmy5CnK6/6VKmLaWxPqjXZrCPru05KoKaMEPtJNElGIGkYmohX4fbwwwe0LvwZBrST+45+KzcApMOIqj2iSo+m5iwEMwpMIdJoor6Tsx7gdhS3ekeK9Ad7hZBFLNSYxL+sirfBfMHVYIDCLRrDwkssT0Qzd8wUTZG88TkyKxLWKMNAqwkab5lvEc5Vk3gjFLyQXbxKk8zhx9pBa9fWIcVJeQpVhJXayqWYBTsLCXZ42aWRehX9M6tuR9j8cyXHNQI6NXDB5WA05P5qKV8VauoVoLIWZlmVoH/bfiiPxTHTFdj08jpwPWtDKPYG6XRt3/DgF+e9TcIxsDT/Tci885fA6SP4e46yML5F4e4bUMusTkjBkMBuqrSzAZyIuj1Xady3xFW35EanmzAH0jOoz2Qycgv5BpFmjmHytw1nOxAC/gOja8UwzIxgFmB55QGQKikI8u9hNCAJEMkz1yozOucWR7rUbtj9YvXXTAfn/p3Ea517/FnxjVMvpUwJssldhApzEdN+eyNbs806+ucaJvevmM5IP7/xDFgAf/wx1zorLQHT6N5rDqqvRnDr7inwcLyQSjcS+DU47UEnyvTzoZ6jBwrJWJ22a8O4jwnVmqMqmsfjrcprJ/HjTwIbnKzN1fIIIZQuC0V2E5IyxcPdYMJPBBGQM6jd583pYU5Zg6bjdNy5K7w==";
public static string OnKeyListRequest() => "dJf580+dhY42au4yMyAS8bT83C9lOj2Igaw6GHLdgMgu45ugRZGzR2HUowgKaW/qQMY2v1+DubYu4hOeINFYWRbKpZUoKO+FHvPeeBAt00gqLBeHEnChvUSOPpAeJJp8ryfahbqy+LQ5d+2W/UOP83XZEVUIYPWleXbWUVMXLCCznOg76nWywXdHe9/Yukff8Di99tDt3NiSTdQPEtmk/c4vgXay+QTcFW8mwHgEhVh6vb9jQOMwvG4cmmjnigedTqW7Q4jm6GbgSWdb/tesmg70Jrjj4aHn6PI9jMSBSwk3tClBx8/Qwz1Ji+gyt/7XwvFmf+MZ8MyAH1DuIKrsomr7IfsMboKlr3T0EEIwVsb6idrbYaphGhefSemA/v2D";
public static FileContentHttpResult OnPakDataRequest()
=> TypedResults.File(Convert.FromHexString("446144334C5075782F544D35586C5A623669315A6A66774A32536265514843762B43567567764F41596B737A596874397164455861454B51626376474F664E544B536335754135444867385A674263356C4245654D6C374C43392F64326565473755796E754E59655753446C48624F544430785338514D39577165306F45706D5541353879377856304B38445970706D47683338386F51744C7A307472314C7743667A416D2F69536D70633D0A"));
}

View file

@ -0,0 +1,20 @@
using Microsoft.AspNetCore.Http.HttpResults;
using SDKServer.Models;
namespace SDKServer.Handlers;
internal static class LoginHandler
{
public static JsonHttpResult<LoginInfoModel> Login(string token, uint userData)
{
return TypedResults.Json(new LoginInfoModel
{
Code = 0,
Token = token,
UserData = userData,
Host = "127.0.0.1",
Port = 1337,
HasRpc = true
});
}
}

View file

@ -0,0 +1,23 @@
namespace SDKServer.Middleware;
public class NotFoundMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger _logger;
public NotFoundMiddleware(RequestDelegate next, ILogger<NotFoundMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task Invoke(HttpContext ctx)
{
await _next(ctx);
if (ctx.Response.StatusCode is 404)
{
_logger.LogWarning("Unhandled: {query}", ctx.Request.Path);
}
}
}

View file

@ -0,0 +1,33 @@
using System.Text.Json.Serialization;
namespace SDKServer.Models.BaseConfig;
public record BaseConfigModel
{
[JsonPropertyName("CdnUrl")]
public required CdnUrlEntry[] CdnUrl { get; set; }
[JsonPropertyName("SecondaryUrl")]
public required CdnUrlEntry[] SecondaryUrl { get; set; }
[JsonPropertyName("GmOpen")]
public bool GmOpen { get; set; }
[JsonPropertyName("PayUrl")]
public string PayUrl { get; set; } = string.Empty;
[JsonPropertyName("TDCfg")]
public TDConfig? TDCfg { get; set; }
[JsonPropertyName("LogReport")]
public LogReportConfig? LogReport { get; set; }
[JsonPropertyName("NoticUrl")]
public string NoticUrl { get; set; } = string.Empty;
[JsonPropertyName("LoginServers")]
public required LoginServerEntry[] LoginServers { get; set; }
[JsonPropertyName("PrivateServers")]
public required PrivateServersConfig PrivateServers { get; set; }
}

View file

@ -0,0 +1,12 @@
using System.Text.Json.Serialization;
namespace SDKServer.Models.BaseConfig;
public record CdnUrlEntry
{
[JsonPropertyName("url")]
public required string Url { get; set; }
[JsonPropertyName("weight")]
public string Weight { get; set; } = "100";
}

View file

@ -0,0 +1,18 @@
using System.Text.Json.Serialization;
namespace SDKServer.Models.BaseConfig;
public record LogReportConfig
{
[JsonPropertyName("ak")]
public required string Ak { get; set; }
[JsonPropertyName("sk")]
public required string Sk { get; set; }
[JsonPropertyName("name")]
public required string Name { get; set; }
[JsonPropertyName("region")]
public required string Region { get; set; }
}

View file

@ -0,0 +1,15 @@
using System.Text.Json.Serialization;
namespace SDKServer.Models.BaseConfig;
public record LoginServerEntry
{
[JsonPropertyName("id")]
public required string Id { get; set; }
[JsonPropertyName("name")]
public required string Name { get; set; }
[JsonPropertyName("ip")]
public required string Ip { get; set; }
}

View file

@ -0,0 +1,12 @@
using System.Text.Json.Serialization;
namespace SDKServer.Models.BaseConfig;
public record PrivateServersConfig
{
[JsonPropertyName("enable")]
public bool Enable { get; set; }
[JsonPropertyName("serverUrl")]
public string ServerUrl { get; set; } = string.Empty;
}

View file

@ -0,0 +1,12 @@
using System.Text.Json.Serialization;
namespace SDKServer.Models.BaseConfig;
public record TDConfig
{
[JsonPropertyName("URL")]
public required string Url { get; set; }
[JsonPropertyName("AppID")]
public required string AppID { get; set; }
}

View file

@ -0,0 +1,28 @@
using System.Text.Json.Serialization;
namespace SDKServer.Models;
public record LoginInfoModel
{
[JsonPropertyName("token")]
public required string Token { get; set; }
[JsonPropertyName("host")]
public required string Host { get; set; }
[JsonPropertyName("port")]
public required int Port { get; set; }
[JsonPropertyName("code")]
public required int Code { get; set; }
[JsonPropertyName("userData")]
public required uint UserData { get; set; }
[JsonPropertyName("hasRpc")]
public bool HasRpc { get; set; }
[JsonPropertyName("errMessage")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? ErrMessage { get; set; }
}

26
SDKServer/Program.cs Normal file
View file

@ -0,0 +1,26 @@
using SDKServer.Handlers;
using SDKServer.Middleware;
namespace SDKServer;
internal static class Program
{
private static async Task Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseUrls("http://*:5500");
builder.Logging.AddSimpleConsole();
var app = builder.Build();
app.UseMiddleware<NotFoundMiddleware>();
app.MapGet("/api/login", LoginHandler.Login);
app.MapGet("/config/index.json", ConfigHandler.GetBaseConfig);
app.MapGet("/dev/client/7cyFLmtLJlUauZ1hM8DsL5Sj7cXxSNQD/Windows/KeyList_0.8.0.json", HotPatchHandler.OnKeyListRequest);
app.MapGet("/dev/client/7cyFLmtLJlUauZ1hM8DsL5Sj7cXxSNQD/Windows/config.json", HotPatchHandler.OnConfigRequest);
app.MapGet("/dev/client/7cyFLmtLJlUauZ1hM8DsL5Sj7cXxSNQD/Windows/client_key/0.8.0/xFrH845q3t8Pgy5eB2/PakData", HotPatchHandler.OnPakDataRequest);
await app.RunAsync();
}
}

View file

@ -0,0 +1,7 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true
}
}

View file

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<None Include="..\.editorconfig" Link=".editorconfig" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View file

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information"
}
},
"AllowedHosts": "*"
}

48
WutheringWaves.sln Normal file
View file

@ -0,0 +1,48 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SDKServer", "SDKServer\SDKServer.csproj", "{7A336DE4-8714-4EF6-A5C2-74C00103547F}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{2EA6F280-5F4B-4753-90EC-CEB29A974838}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GameServer", "GameServer\GameServer.csproj", "{78D639E8-D607-41F1-B0B8-AB1611ADE08F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KcpSharp", "KcpSharp\KcpSharp.csproj", "{C2BDCF0A-C256-4E97-9D9A-45FF5C8614CD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Protocol", "Protocol\Protocol.csproj", "{9900A88C-7818-4335-84F7-1538ECC8B338}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{7A336DE4-8714-4EF6-A5C2-74C00103547F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7A336DE4-8714-4EF6-A5C2-74C00103547F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7A336DE4-8714-4EF6-A5C2-74C00103547F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7A336DE4-8714-4EF6-A5C2-74C00103547F}.Release|Any CPU.Build.0 = Release|Any CPU
{78D639E8-D607-41F1-B0B8-AB1611ADE08F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{78D639E8-D607-41F1-B0B8-AB1611ADE08F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{78D639E8-D607-41F1-B0B8-AB1611ADE08F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{78D639E8-D607-41F1-B0B8-AB1611ADE08F}.Release|Any CPU.Build.0 = Release|Any CPU
{C2BDCF0A-C256-4E97-9D9A-45FF5C8614CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C2BDCF0A-C256-4E97-9D9A-45FF5C8614CD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C2BDCF0A-C256-4E97-9D9A-45FF5C8614CD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C2BDCF0A-C256-4E97-9D9A-45FF5C8614CD}.Release|Any CPU.Build.0 = Release|Any CPU
{9900A88C-7818-4335-84F7-1538ECC8B338}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9900A88C-7818-4335-84F7-1538ECC8B338}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9900A88C-7818-4335-84F7-1538ECC8B338}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9900A88C-7818-4335-84F7-1538ECC8B338}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {56A60267-0142-4A77-A4BE-E35061C12027}
EndGlobalSection
EndGlobal