├── src ├── KcpSharp │ ├── AssemblyInfo.cs │ ├── KcpCommand.cs │ ├── KcpProbeType.cs │ ├── KcpSendReceiveBufferItem.cs │ ├── IKcpConversationUpdateNotificationSource.cs │ ├── NetstandardShim │ │ ├── TaskCompletionSource.cs │ │ ├── CancellationTokenShim.cs │ │ └── AwaitableSocketAsyncEventArgs.cs │ ├── DefaultArrayPoolBufferAllocator.cs │ ├── IKcpBufferPool.cs │ ├── ArrayMemoryOwner.cs │ ├── KcpSendSegmentStats.cs │ ├── IKcpTransport.cs │ ├── IKcpExceptionProducer.cs │ ├── IKcpConversation.cs │ ├── KcpConversationUpdateNotification.cs │ ├── IKcpTransportOfT.cs │ ├── KcpKeepAliveOptions.cs │ ├── KcpRawChannelOptions.cs │ ├── KcpSocketTransportForRawChannel.cs │ ├── KcpReceiveWindowNotificationOptions.cs │ ├── KcpBufferPoolRentOptions.cs │ ├── KcpSocketTransportForConversation.cs │ ├── KcpSocketTransportForMultiplexConnection.cs │ ├── KcpSharp.csproj │ ├── KcpBuffer.cs │ ├── KcpSendReceiveBufferItemCache.cs │ ├── KcpSendReceiveQueueItemCache.cs │ ├── KcpConversationReceiveResult.cs │ ├── IKcpMultiplexConnectionOfT.cs │ ├── IKcpMultiplexConnection.cs │ ├── ThrowHelper.cs │ ├── KcpAcknowledgeList.cs │ ├── KcpPacketHeader.cs │ ├── AsyncAutoResetEvent.cs │ ├── KcpConversationOptions.cs │ └── KcpExceptionProducerExtensions.cs └── Directory.Build.props ├── samples ├── KcpTunnel │ ├── KcpTunnelClientOptions.cs │ ├── KcpTunnelServiceOptions.cs │ ├── KcpTunnel.csproj │ ├── UdpSocketDispatcherOptions.cs │ ├── SocketHelper.cs │ ├── KcpTunnelServiceFactory.cs │ ├── ConnectionIdPool.cs │ ├── Program.cs │ ├── KcpTunnelServer.cs │ ├── KcpTunnelService.cs │ ├── TcpSourceConnection.cs │ ├── TcpForwardConnection.cs │ └── KcpTunnelClient.cs ├── Samples.slnf ├── KcpChatWasm │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── KcpChatWasm.csproj │ ├── wwwroot │ │ └── index.html │ ├── PerfectKcpConversationPipe.cs │ └── App.razor └── KcpEcho │ ├── KcpEcho.csproj │ ├── SocketHelper.cs │ ├── KcpEchoServer.cs │ ├── Program.cs │ ├── KcpEchoService.cs │ └── KcpEchoClient.cs ├── version.json ├── MainProjects.slnf ├── tests ├── KcpSharp.Benchmarks │ ├── Program.cs │ ├── KcpSharp.Benchmarks.csproj │ ├── PreallocatedBufferPool.cs │ ├── LargeStreamTransferBenchmark.cs │ ├── LargeStreamTransferNoDelayBenchmark.cs │ ├── PerfectKcpConversationPipe.cs │ └── PreallocatedQueue.cs ├── KcpSharp.ThroughputBanchmarks │ ├── KcpSharp.ThroughputBanchmarks.csproj │ ├── UdpServerDispatcherOptions.cs │ ├── SocketHelper.cs │ ├── MemoryPool │ │ ├── MemoryPoolBlock.cs │ │ └── PinnedBlockMemoryPool.cs │ ├── Program.cs │ ├── PacketsThroughputBenchmark │ │ ├── PacketsThroughputBenchmarkServiceFactory.cs │ │ ├── PacketsThroughputBenchmarkServer.cs │ │ ├── PacketsThroughputBenchmarkService.cs │ │ ├── PacketsThroughputBenchmarkProgram.cs │ │ └── PacketsThroughputBenchmarkClient.cs │ └── StreamThroughputBenchmark │ │ ├── StreamThroughputBenchmarkServiceFactory.cs │ │ ├── StreamThroughputBenchmarkServer.cs │ │ ├── StreamThroughputBenchmarkService.cs │ │ ├── StreamThroughputBenchmarkProgram.cs │ │ └── StreamThroughputBenchmarkClient.cs └── KcpSharp.Tests │ ├── Utils │ ├── TestHelper.cs │ ├── KcpConversationFactory.cs │ ├── TrackedBufferAllocator.cs │ ├── PerfectKcpConversationPipe.cs │ ├── KcpRawDuplexChannel.cs │ └── BadKcpConversationPipe.cs │ ├── KcpSharp.Tests.csproj │ ├── ConversationDisposedTests.cs │ ├── ConcurrentSendTests.cs │ ├── KeepAliveTests.cs │ ├── RawChannelTransferTests.cs │ ├── RawChannelTransferTestsNoConversationId.cs │ └── SendAndFlushTests.cs ├── Directory.Build.props └── LICENSE /src/KcpSharp/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | [assembly: System.CLSCompliant(true)] 2 | -------------------------------------------------------------------------------- /samples/KcpTunnel/KcpTunnelClientOptions.cs: -------------------------------------------------------------------------------- 1 | namespace KcpTunnel 2 | { 3 | internal sealed record KcpTunnelClientOptions 4 | { 5 | public int Mtu { get; init; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/KcpSharp/KcpCommand.cs: -------------------------------------------------------------------------------- 1 | namespace KcpSharp 2 | { 3 | internal enum KcpCommand : byte 4 | { 5 | Push = 81, 6 | Ack = 82, 7 | WindowProbe = 83, 8 | WindowSize = 84 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/KcpSharp/KcpProbeType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace KcpSharp 4 | { 5 | [Flags] 6 | internal enum KcpProbeType 7 | { 8 | None = 0, 9 | AskSend = 1, 10 | AskTell = 2, 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/KcpSharp/KcpSendReceiveBufferItem.cs: -------------------------------------------------------------------------------- 1 | namespace KcpSharp 2 | { 3 | internal struct KcpSendReceiveBufferItem 4 | { 5 | public KcpBuffer Data; 6 | public KcpPacketHeader Segment; 7 | public KcpSendSegmentStats Stats; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/KcpSharp/IKcpConversationUpdateNotificationSource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace KcpSharp 4 | { 5 | internal interface IKcpConversationUpdateNotificationSource 6 | { 7 | ReadOnlyMemory Packet { get; } 8 | void Release(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /samples/KcpTunnel/KcpTunnelServiceOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | 3 | namespace KcpTunnel 4 | { 5 | internal sealed record KcpTunnelServiceOptions 6 | { 7 | public int Mtu { get; init; } 8 | public EndPoint? ForwardEndPoint { get; init; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/KcpSharp/NetstandardShim/TaskCompletionSource.cs: -------------------------------------------------------------------------------- 1 | #if NEED_TCS_SHIM 2 | 3 | namespace System.Threading.Tasks 4 | { 5 | internal class TaskCompletionSource : TaskCompletionSource 6 | { 7 | public void TrySetResult() => TrySetResult(true); 8 | } 9 | } 10 | 11 | #endif 12 | -------------------------------------------------------------------------------- /samples/Samples.slnf: -------------------------------------------------------------------------------- 1 | { 2 | "solution": { 3 | "path": "..\\KcpSharp.sln", 4 | "projects": [ 5 | "samples\\KcpChatWasm\\KcpChatWasm.csproj", 6 | "samples\\KcpEcho\\KcpEcho.csproj", 7 | "samples\\KcpTunnel\\KcpTunnel.csproj", 8 | "src\\KcpSharp\\KcpSharp.csproj" 9 | ] 10 | } 11 | } -------------------------------------------------------------------------------- /version.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", 3 | "version": "0.9-dev", 4 | "publicReleaseRefSpec": [ 5 | "^refs/heads/release/\\d+(?:\\.\\d+)?$" 6 | ], 7 | "cloudBuild": { 8 | "buildNumber": { 9 | "enabled": true 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /MainProjects.slnf: -------------------------------------------------------------------------------- 1 | { 2 | "solution": { 3 | "path": "KcpSharp.sln", 4 | "projects": [ 5 | "src\\KcpSharp\\KcpSharp.csproj", 6 | "tests\\KcpSharp.Benchmarks\\KcpSharp.Benchmarks.csproj", 7 | "tests\\KcpSharp.Tests\\KcpSharp.Tests.csproj", 8 | "tests\\KcpSharp.ThroughputBanchmarks\\KcpSharp.ThroughputBanchmarks.csproj" 9 | ] 10 | } 11 | } -------------------------------------------------------------------------------- /tests/KcpSharp.Benchmarks/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using BenchmarkDotNet.Running; 3 | 4 | namespace KcpSharp.Benchmarks 5 | { 6 | internal static class Program 7 | { 8 | private static void Main(string[] args) 9 | { 10 | new BenchmarkSwitcher(typeof(Program).GetTypeInfo().Assembly).Run(args); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/KcpSharp/NetstandardShim/CancellationTokenShim.cs: -------------------------------------------------------------------------------- 1 | #if NEED_CANCELLATIONTOKEN_SHIM 2 | 3 | namespace System.Threading 4 | { 5 | internal static class CancellationTokenShim 6 | { 7 | public static CancellationTokenRegistration UnsafeRegister(this CancellationToken cancellationToken, Action callback, object? state) 8 | => cancellationToken.Register(callback, state); 9 | } 10 | } 11 | 12 | 13 | #endif 14 | -------------------------------------------------------------------------------- /src/KcpSharp/DefaultArrayPoolBufferAllocator.cs: -------------------------------------------------------------------------------- 1 | namespace KcpSharp 2 | { 3 | internal sealed class DefaultArrayPoolBufferAllocator : IKcpBufferPool 4 | { 5 | public static DefaultArrayPoolBufferAllocator Default { get; } = new DefaultArrayPoolBufferAllocator(); 6 | 7 | public KcpRentedBuffer Rent(KcpBufferPoolRentOptions options) 8 | { 9 | return KcpRentedBuffer.FromSharedArrayPool(options.Size); 10 | } 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /samples/KcpChatWasm/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Components.WebAssembly.Hosting; 3 | 4 | namespace KcpChatWasm 5 | { 6 | public class Program 7 | { 8 | public static async Task Main(string[] args) 9 | { 10 | var builder = WebAssemblyHostBuilder.CreateDefault(args); 11 | builder.RootComponents.Add("#app"); 12 | 13 | await builder.Build().RunAsync(); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/KcpSharp.Benchmarks/KcpSharp.Benchmarks.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /samples/KcpEcho/KcpEcho.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/KcpSharp/IKcpBufferPool.cs: -------------------------------------------------------------------------------- 1 | namespace KcpSharp 2 | { 3 | /// 4 | /// The buffer pool to rent buffers from. 5 | /// 6 | public interface IKcpBufferPool 7 | { 8 | /// 9 | /// Rent a buffer using the specified options. 10 | /// 11 | /// The options used to rent this buffer. 12 | /// 13 | KcpRentedBuffer Rent(KcpBufferPoolRentOptions options); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /samples/KcpTunnel/KcpTunnel.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /samples/KcpTunnel/UdpSocketDispatcherOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | 4 | namespace KcpTunnel 5 | { 6 | internal abstract class UdpSocketDispatcherOptions where T : class, IUdpService 7 | { 8 | public virtual TimeSpan KeepAliveInterval => TimeSpan.FromMinutes(1); 9 | public virtual TimeSpan ScanInterval => TimeSpan.FromMinutes(2); 10 | 11 | public abstract T Activate(IUdpServiceDispatcher dispatcher, EndPoint endpoint); 12 | public abstract void Close(T service); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/KcpSharp.ThroughputBanchmarks/KcpSharp.ThroughputBanchmarks.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/KcpSharp/ArrayMemoryOwner.cs: -------------------------------------------------------------------------------- 1 | #if !NEED_POH_SHIM 2 | 3 | using System; 4 | using System.Buffers; 5 | 6 | namespace KcpSharp 7 | { 8 | internal sealed class ArrayMemoryOwner : IMemoryOwner 9 | { 10 | private readonly byte[] _buffer; 11 | 12 | public ArrayMemoryOwner(byte[] buffer) 13 | { 14 | _buffer = buffer ?? throw new ArgumentNullException(nameof(buffer)); 15 | } 16 | 17 | public Memory Memory => _buffer; 18 | 19 | public void Dispose() { } 20 | } 21 | } 22 | 23 | #endif 24 | -------------------------------------------------------------------------------- /samples/KcpChatWasm/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "KcpChatWasm": { 4 | "commandName": "Project", 5 | "dotnetRunMessages": "true", 6 | "launchBrowser": true, 7 | "hotReloadProfile": "blazorwasm", 8 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 9 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 10 | "environmentVariables": { 11 | "ASPNETCORE_ENVIRONMENT": "Development" 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/KcpSharp.ThroughputBanchmarks/UdpServerDispatcherOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | 4 | namespace KcpSharp.ThroughputBanchmarks 5 | { 6 | internal abstract class UdpSocketDispatcherOptions where T : class, IUdpService 7 | { 8 | public virtual TimeSpan KeepAliveInterval => TimeSpan.FromMinutes(1); 9 | public virtual TimeSpan ScanInterval => TimeSpan.FromMinutes(2); 10 | 11 | public abstract T Activate(IUdpServiceDispatcher dispatcher, EndPoint endpoint); 12 | public abstract void Close(T service); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /samples/KcpEcho/SocketHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Sockets; 3 | 4 | namespace KcpEcho 5 | { 6 | internal static class SocketHelper 7 | { 8 | public static void PatchSocket(Socket socket) 9 | { 10 | if (OperatingSystem.IsWindows()) 11 | { 12 | uint IOC_IN = 0x80000000; 13 | uint IOC_VENDOR = 0x18000000; 14 | uint SIO_UDP_CONNRESET = IOC_IN | IOC_VENDOR | 12; 15 | socket.IOControl((int)SIO_UDP_CONNRESET, new byte[] { Convert.ToByte(false) }, null); 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/KcpSharp/KcpSendSegmentStats.cs: -------------------------------------------------------------------------------- 1 | namespace KcpSharp 2 | { 3 | internal readonly struct KcpSendSegmentStats 4 | { 5 | public KcpSendSegmentStats(uint resendTimestamp, uint rto, uint fastAck, uint transmitCount) 6 | { 7 | ResendTimestamp = resendTimestamp; 8 | Rto = rto; 9 | FastAck = fastAck; 10 | TransmitCount = transmitCount; 11 | } 12 | 13 | public uint ResendTimestamp { get; } 14 | public uint Rto { get; } 15 | public uint FastAck { get; } 16 | public uint TransmitCount { get; } 17 | 18 | 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /samples/KcpTunnel/SocketHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Sockets; 3 | 4 | namespace KcpTunnel 5 | { 6 | internal static class SocketHelper 7 | { 8 | public static void PatchSocket(Socket socket) 9 | { 10 | if (OperatingSystem.IsWindows()) 11 | { 12 | uint IOC_IN = 0x80000000; 13 | uint IOC_VENDOR = 0x18000000; 14 | uint SIO_UDP_CONNRESET = IOC_IN | IOC_VENDOR | 12; 15 | socket.IOControl((int)SIO_UDP_CONNRESET, new byte[] { Convert.ToByte(false) }, null); 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /samples/KcpChatWasm/KcpChatWasm.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /samples/KcpChatWasm/wwwroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | KcpChatWasm 8 | 9 | 17 | 18 | 19 | 20 |
Loading...
21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /tests/KcpSharp.ThroughputBanchmarks/SocketHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Sockets; 3 | 4 | namespace KcpSharp.ThroughputBanchmarks 5 | { 6 | internal static class SocketHelper 7 | { 8 | public static void PatchSocket(Socket socket) 9 | { 10 | if (OperatingSystem.IsWindows()) 11 | { 12 | uint IOC_IN = 0x80000000; 13 | uint IOC_VENDOR = 0x18000000; 14 | uint SIO_UDP_CONNRESET = IOC_IN | IOC_VENDOR | 12; 15 | socket.IOControl((int)SIO_UDP_CONNRESET, new byte[] { Convert.ToByte(false) }, null); 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/KcpSharp/IKcpTransport.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace KcpSharp 6 | { 7 | /// 8 | /// A transport to send and receive packets. 9 | /// 10 | public interface IKcpTransport 11 | { 12 | /// 13 | /// Send a packet into the transport. 14 | /// 15 | /// The content of the packet. 16 | /// A token to cancel this operation. 17 | /// A that completes when the packet is sent. 18 | ValueTask SendPacketAsync(Memory packet, CancellationToken cancellationToken); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/KcpSharp.Tests/Utils/TestHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace KcpSharp.Tests 6 | { 7 | internal static class TestHelper 8 | { 9 | public static async Task RunWithTimeout(TimeSpan timeout, Func action) 10 | { 11 | using var cts = new CancellationTokenSource(timeout); 12 | try 13 | { 14 | await action(cts.Token); 15 | } 16 | catch (OperationCanceledException e) 17 | { 18 | if (cts.Token == e.CancellationToken) 19 | { 20 | throw new TimeoutException("Test execution timed out.", e); 21 | } 22 | throw; 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /samples/KcpTunnel/KcpTunnelServiceFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | 3 | namespace KcpTunnel 4 | { 5 | internal class KcpTunnelServiceFactory : UdpSocketDispatcherOptions 6 | { 7 | private readonly KcpTunnelServiceOptions _options; 8 | 9 | public KcpTunnelServiceFactory(KcpTunnelServiceOptions options) 10 | { 11 | _options = options; 12 | } 13 | public override KcpTunnelService Activate(IUdpServiceDispatcher dispatcher, EndPoint endpoint) 14 | { 15 | var service = new KcpTunnelService(dispatcher, endpoint, _options); 16 | service.Start(); 17 | return service; 18 | } 19 | 20 | public override void Close(KcpTunnelService service) 21 | { 22 | service.Stop(); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/KcpSharp/IKcpExceptionProducer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace KcpSharp 4 | { 5 | /// 6 | /// An instance that can produce exceptions in background jobs. 7 | /// 8 | /// The type of the instance. 9 | public interface IKcpExceptionProducer 10 | { 11 | /// 12 | /// 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. 13 | /// 14 | /// The exception handler. 15 | /// The state object to pass into the exception handler. 16 | void SetExceptionHandler(Func handler, object? state); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/KcpSharp.ThroughputBanchmarks/MemoryPool/MemoryPoolBlock.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | 4 | namespace KcpSharp.ThroughputBanchmarks 5 | { 6 | /// 7 | /// Wraps an array in a reusable block of managed memory 8 | /// 9 | internal sealed class MemoryPoolBlock : IMemoryOwner 10 | { 11 | internal MemoryPoolBlock(PinnedBlockMemoryPool pool, int length) 12 | { 13 | Pool = pool; 14 | 15 | Memory = GC.AllocateUninitializedArray(length, pinned: true); 16 | } 17 | 18 | /// 19 | /// Back-reference to the memory pool which this block was allocated from. It may only be returned to this pool. 20 | /// 21 | public PinnedBlockMemoryPool Pool { get; } 22 | 23 | public Memory Memory { get; } 24 | 25 | public void Dispose() 26 | { 27 | Pool.Return(this); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/KcpSharp.ThroughputBanchmarks/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.CommandLine.Builder; 3 | using System.CommandLine.Parsing; 4 | using System.Threading.Tasks; 5 | using KcpSharp.ThroughputBanchmarks.PacketsThroughputBenchmark; 6 | using KcpSharp.ThroughputBanchmarks.StreamThroughputBenchmark; 7 | 8 | namespace KcpSharp.ThroughputBanchmarks 9 | { 10 | internal static class Program 11 | { 12 | static async Task Main(string[] args) 13 | { 14 | Console.WriteLine($"Process ID: {Environment.ProcessId}"); 15 | 16 | var builder = new CommandLineBuilder(); 17 | builder.Command.Description = "KcpSharp.ThroughputBanchmarks"; 18 | 19 | builder.Command.AddCommand(PacketsThroughputBenchmarkProgram.BuildCommand()); 20 | builder.Command.AddCommand(StreamThroughputBenchmarkProgram.BuildCommand()); 21 | 22 | builder.UseDefaults(); 23 | 24 | Parser parser = builder.Build(); 25 | return await parser.InvokeAsync(args).ConfigureAwait(false); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | KcpSharp 7 | yigolden 8 | Copyright (c) 2021 yigolden 9 | kcp;arq;udp;networking 10 | MIT 11 | https://github.com/yigolden/KcpSharp 12 | git 13 | https://github.com/yigolden/KcpSharp.git 14 | 15 | $(MSBuildThisFileDirectory)build\key.snk 16 | true 17 | 18 | 19 | 20 | 21 | $(NerdbankGitVersioningPackageVersion) 22 | all 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/KcpSharp/IKcpConversation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace KcpSharp 6 | { 7 | /// 8 | /// A conversation or a channel over the transport. 9 | /// 10 | public interface IKcpConversation : IDisposable 11 | { 12 | /// 13 | /// Put message into the receive queue of the channel. 14 | /// 15 | /// The packet content with the optional conversation ID. This buffer should not contain space for pre-buffer and post-buffer. 16 | /// The token to cancel this operation. 17 | /// A that completes when the packet is put into the receive queue. 18 | ValueTask InputPakcetAsync(ReadOnlyMemory packet, CancellationToken cancellationToken); 19 | 20 | /// 21 | /// Mark the underlying transport as closed. Abort all active send or receive operations. 22 | /// 23 | void SetTransportClosed(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 yigolden 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/KcpSharp.Tests/KcpSharp.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | all 17 | 18 | 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | all 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/KcpSharp/KcpConversationUpdateNotification.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace KcpSharp 4 | { 5 | internal readonly struct KcpConversationUpdateNotification : IDisposable 6 | { 7 | private readonly IKcpConversationUpdateNotificationSource? _source; 8 | private readonly bool _skipTimerNotification; 9 | 10 | public ReadOnlyMemory Packet => _source?.Packet ?? default; 11 | public bool TimerNotification => !_skipTimerNotification; 12 | 13 | public KcpConversationUpdateNotification(IKcpConversationUpdateNotificationSource? source, bool skipTimerNotification) 14 | { 15 | _source = source; 16 | _skipTimerNotification = skipTimerNotification; 17 | } 18 | 19 | public KcpConversationUpdateNotification WithTimerNotification(bool timerNotification) 20 | { 21 | return new KcpConversationUpdateNotification(_source, !_skipTimerNotification | timerNotification); 22 | } 23 | 24 | public void Dispose() 25 | { 26 | if (_source is not null) 27 | { 28 | _source.Release(); 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/KcpSharp/IKcpTransportOfT.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace KcpSharp 4 | { 5 | /// 6 | /// A transport instance for upper-level connections. 7 | /// 8 | /// The type of the upper-level connection. 9 | public interface IKcpTransport : IKcpTransport, IKcpExceptionProducer>, IDisposable 10 | { 11 | /// 12 | /// Get the upper-level connection instace. If Start is not called or the transport is closed, will be thrown. 13 | /// 14 | /// Start is not called or the transport is closed. 15 | T Connection { get; } 16 | 17 | /// 18 | /// Create the upper-level connection and start pumping packets from the socket to the upper-level connection. 19 | /// 20 | /// The current instance is disposed. 21 | /// has been called before. 22 | void Start(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/KcpSharp.ThroughputBanchmarks/PacketsThroughputBenchmark/PacketsThroughputBenchmarkServiceFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace KcpSharp.ThroughputBanchmarks.PacketsThroughputBenchmark 9 | { 10 | internal sealed class PacketsThroughputBenchmarkServiceFactory : UdpSocketDispatcherOptions 11 | { 12 | private readonly KcpConversationOptions _options; 13 | 14 | public PacketsThroughputBenchmarkServiceFactory(KcpConversationOptions options) 15 | { 16 | _options = options; 17 | } 18 | 19 | public override TimeSpan KeepAliveInterval => TimeSpan.FromMinutes(2); 20 | public override TimeSpan ScanInterval => TimeSpan.FromMinutes(2); 21 | 22 | public override PacketsThroughputBenchmarkService Activate(IUdpServiceDispatcher dispatcher, EndPoint endpoint) 23 | => new PacketsThroughputBenchmarkService(dispatcher, endpoint, _options); 24 | public override void Close(PacketsThroughputBenchmarkService service) 25 | => service.Dispose(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/KcpSharp.ThroughputBanchmarks/StreamThroughputBenchmark/StreamThroughputBenchmarkServiceFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace KcpSharp.ThroughputBanchmarks.StreamThroughputBenchmark 9 | { 10 | internal sealed class StreamThroughputBenchmarkServiceFactory : UdpSocketDispatcherOptions 11 | { 12 | private readonly KcpConversationOptions _options; 13 | 14 | public StreamThroughputBenchmarkServiceFactory(KcpConversationOptions options) 15 | { 16 | _options = options; 17 | } 18 | 19 | public override TimeSpan KeepAliveInterval => TimeSpan.FromMinutes(2); 20 | public override TimeSpan ScanInterval => TimeSpan.FromMinutes(2); 21 | 22 | public override StreamThroughputBenchmarkService Activate(IUdpServiceDispatcher dispatcher, EndPoint endpoint) 23 | => new StreamThroughputBenchmarkService(dispatcher, endpoint, _options); 24 | 25 | public override void Close(StreamThroughputBenchmarkService service) 26 | => service.Dispose(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/KcpSharp/KcpKeepAliveOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace KcpSharp 4 | { 5 | /// 6 | /// Options for customized keep-alive functionality. 7 | /// 8 | public sealed class KcpKeepAliveOptions 9 | { 10 | /// 11 | /// Create an instance of option object for customized keep-alive functionality. 12 | /// 13 | /// The minimum interval in milliseconds between sending keep-alive messages. 14 | /// When no packets are received during this period (in milliseconds), the transport is considered to be closed. 15 | public KcpKeepAliveOptions(int sendInterval, int gracePeriod) 16 | { 17 | if (sendInterval <= 0) 18 | { 19 | throw new ArgumentOutOfRangeException(nameof(sendInterval)); 20 | } 21 | if (gracePeriod <= 0) 22 | { 23 | throw new ArgumentOutOfRangeException(nameof(gracePeriod)); 24 | } 25 | SendInterval = sendInterval; 26 | GracePeriod = gracePeriod; 27 | } 28 | 29 | internal int SendInterval { get; } 30 | internal int GracePeriod { get; } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /samples/KcpTunnel/ConnectionIdPool.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Threading; 4 | 5 | namespace KcpTunnel 6 | { 7 | internal class ConnectionIdPool 8 | { 9 | private const ushort _maxValue = ushort.MaxValue - 1; 10 | 11 | private ConcurrentQueue _queue = new(); 12 | private ushort _nextValue = 0; 13 | private int _activeIds; 14 | 15 | public ushort Rent() 16 | { 17 | if (Interlocked.Increment(ref _activeIds) > 64 && _queue.TryDequeue(out ushort id)) 18 | { 19 | return id; 20 | } 21 | lock (_queue) 22 | { 23 | id = (ushort)(_nextValue + 1); 24 | if (id > _maxValue) 25 | { 26 | ThrowInvalidOperationException(); 27 | } 28 | _nextValue = id; 29 | return id; 30 | } 31 | } 32 | 33 | public void Return(ushort id) 34 | { 35 | Interlocked.Decrement(ref _activeIds); 36 | _queue.Enqueue(id); 37 | } 38 | 39 | private static void ThrowInvalidOperationException() 40 | { 41 | throw new InvalidOperationException(); 42 | } 43 | 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/KcpSharp/NetstandardShim/AwaitableSocketAsyncEventArgs.cs: -------------------------------------------------------------------------------- 1 | #if NEED_SOCKET_SHIM 2 | 3 | using System; 4 | using System.Net.Sockets; 5 | using System.Threading.Tasks; 6 | using System.Threading.Tasks.Sources; 7 | 8 | namespace KcpSharp 9 | { 10 | internal class AwaitableSocketAsyncEventArgs : SocketAsyncEventArgs, IValueTaskSource 11 | { 12 | private ManualResetValueTaskSourceCore _mrvtsc = new ManualResetValueTaskSourceCore { RunContinuationsAsynchronously = true }; 13 | 14 | void IValueTaskSource.GetResult(short token) => _mrvtsc.GetResult(token); 15 | ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) => _mrvtsc.GetStatus(token); 16 | void IValueTaskSource.OnCompleted(Action continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) 17 | => _mrvtsc.OnCompleted(continuation, state, token, flags); 18 | 19 | protected override void OnCompleted(SocketAsyncEventArgs e) 20 | { 21 | _mrvtsc.SetResult(true); 22 | } 23 | 24 | public ValueTask WaitAsync() 25 | { 26 | return new ValueTask(this, _mrvtsc.Version); 27 | } 28 | 29 | public void Reset() 30 | { 31 | _mrvtsc.Reset(); 32 | } 33 | } 34 | } 35 | 36 | #endif 37 | -------------------------------------------------------------------------------- /src/KcpSharp/KcpRawChannelOptions.cs: -------------------------------------------------------------------------------- 1 | namespace KcpSharp 2 | { 3 | /// 4 | /// Options used to control the behaviors of . 5 | /// 6 | public sealed class KcpRawChannelOptions 7 | { 8 | /// 9 | /// The buffer pool to rent buffer from. 10 | /// 11 | public IKcpBufferPool? BufferPool { get; set; } 12 | 13 | /// 14 | /// The maximum packet size that can be transmitted over the underlying transport. 15 | /// 16 | public int Mtu { get; set; } = 1400; 17 | 18 | /// 19 | /// The number of packets in the receive queue. 20 | /// 21 | public int ReceiveQueueSize { get; set; } = 32; 22 | 23 | /// 24 | /// The number of bytes to reserve at the start of buffer passed into the underlying transport. The transport should fill this reserved space. 25 | /// 26 | public int PreBufferSize { get; set; } 27 | 28 | /// 29 | /// The number of bytes to reserve at the end of buffer passed into the underlying transport. The transport should fill this reserved space. 30 | /// 31 | public int PostBufferSize { get; set; } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/KcpSharp.Tests/ConversationDisposedTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Moq; 5 | using Xunit; 6 | 7 | namespace KcpSharp.Tests 8 | { 9 | public class ConversationDisposedTests 10 | { 11 | [Fact] 12 | public async Task TestDispose() 13 | { 14 | var blackholeConnection = new Mock(); 15 | blackholeConnection.Setup(conn => conn.SendPacketAsync(It.IsAny>(), It.IsAny())) 16 | .Returns(ValueTask.CompletedTask); 17 | 18 | using var conversation = new KcpConversation(blackholeConnection.Object, 42, null); 19 | conversation.Dispose(); 20 | 21 | KcpConversationReceiveResult result; 22 | Assert.False(conversation.TryPeek(out result)); 23 | Assert.True(result.TransportClosed); 24 | Assert.False(conversation.TryReceive(default, out result)); 25 | Assert.True(result.TransportClosed); 26 | Assert.False(await conversation.SendAsync(new byte[100], CancellationToken.None)); 27 | Assert.False(await conversation.FlushAsync(CancellationToken.None)); 28 | result = await conversation.WaitToReceiveAsync(CancellationToken.None); 29 | Assert.True(result.TransportClosed); 30 | await conversation.ReceiveAsync(new byte[100], CancellationToken.None); 31 | Assert.True(result.TransportClosed); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/KcpSharp.Tests/ConcurrentSendTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Moq; 5 | using Xunit; 6 | 7 | namespace KcpSharp.Tests 8 | { 9 | public class ConcurrentSendTests 10 | { 11 | [Fact] 12 | public async Task TestConcurrentSend() 13 | { 14 | var blackholeConnection = new Mock(); 15 | blackholeConnection.Setup(conn => conn.SendPacketAsync(It.IsAny>(), It.IsAny())) 16 | .Returns(ValueTask.CompletedTask); 17 | 18 | using var conversation = new KcpConversation(blackholeConnection.Object, null); 19 | 20 | await TestHelper.RunWithTimeout(TimeSpan.FromSeconds(10), async cancellationToken => 21 | { 22 | const int Count = 50; 23 | int waitingTask = 0; 24 | Task[] tasks = new Task[Count]; 25 | for (int i = 0; i < Count; i++) 26 | { 27 | tasks[i] = Task.Run(async () => 28 | { 29 | ValueTask valueTask = conversation.InputPakcetAsync(new byte[500], cancellationToken); 30 | if (!valueTask.IsCompleted) 31 | { 32 | Interlocked.Increment(ref waitingTask); 33 | } 34 | await valueTask; 35 | }); 36 | } 37 | await Task.WhenAll(tasks); 38 | 39 | Assert.True(waitingTask > 1); 40 | }); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/KcpSharp/KcpSocketTransportForRawChannel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Net.Sockets; 4 | 5 | namespace KcpSharp 6 | { 7 | internal sealed class KcpSocketTransportForRawChannel : KcpSocketTransport, IKcpTransport 8 | { 9 | private readonly int? _conversationId; 10 | private readonly KcpRawChannelOptions? _options; 11 | 12 | private Func, object?, bool>? _exceptionHandler; 13 | private object? _exceptionHandlerState; 14 | 15 | 16 | internal KcpSocketTransportForRawChannel(Socket socket, EndPoint endPoint, int? conversationId, KcpRawChannelOptions? options) 17 | : base(socket, endPoint, options?.Mtu ?? KcpConversationOptions.MtuDefaultValue) 18 | { 19 | _conversationId = conversationId; 20 | _options = options; 21 | } 22 | 23 | protected override KcpRawChannel Activate() => _conversationId.HasValue ? new KcpRawChannel(this, _conversationId.GetValueOrDefault(), _options) : new KcpRawChannel(this, _options); 24 | 25 | protected override bool HandleException(Exception ex) 26 | { 27 | if (_exceptionHandler is not null) 28 | { 29 | return _exceptionHandler.Invoke(ex, this, _exceptionHandlerState); 30 | } 31 | return false; 32 | } 33 | 34 | public void SetExceptionHandler(Func, object?, bool> handler, object? state) 35 | { 36 | _exceptionHandler = handler; 37 | _exceptionHandlerState = state; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/KcpSharp.Tests/KeepAliveTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Xunit; 4 | 5 | namespace KcpSharp.Tests 6 | { 7 | public class KeepAliveTests 8 | { 9 | 10 | [Fact] 11 | public async Task TestAliveAndThenDead() 12 | { 13 | using KcpConversationPipe pipe = KcpConversationFactory.CreatePerfectPipe(0x12345678, new KcpConversationOptions { KeepAliveOptions = new KcpKeepAliveOptions(500, 3000) }); 14 | await Task.Delay(TimeSpan.FromSeconds(10)); 15 | Assert.False(pipe.Alice.TransportClosed); 16 | Assert.False(pipe.Bob.TransportClosed); 17 | pipe.Alice.SetTransportClosed(); 18 | Assert.True(pipe.Alice.TransportClosed); 19 | Assert.False(pipe.Bob.TransportClosed); 20 | await Task.Delay(TimeSpan.FromSeconds(5)); 21 | Assert.True(pipe.Alice.TransportClosed); 22 | Assert.True(pipe.Bob.TransportClosed); 23 | } 24 | 25 | [Fact] 26 | public async Task TestDisabled() 27 | { 28 | using KcpConversationPipe pipe = KcpConversationFactory.CreatePerfectPipe(0x12345678); 29 | await Task.Delay(TimeSpan.FromSeconds(5)); 30 | Assert.False(pipe.Alice.TransportClosed); 31 | Assert.False(pipe.Bob.TransportClosed); 32 | pipe.Alice.SetTransportClosed(); 33 | Assert.True(pipe.Alice.TransportClosed); 34 | Assert.False(pipe.Bob.TransportClosed); 35 | await Task.Delay(TimeSpan.FromSeconds(5)); 36 | Assert.True(pipe.Alice.TransportClosed); 37 | Assert.False(pipe.Bob.TransportClosed); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/KcpSharp/KcpReceiveWindowNotificationOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace KcpSharp 6 | { 7 | /// 8 | /// Options for sending receive window size notification. 9 | /// 10 | public sealed class KcpReceiveWindowNotificationOptions 11 | { 12 | /// 13 | /// Create an instance of option object for receive window size notification functionality. 14 | /// 15 | /// The initial interval in milliseconds of sending window size notification. 16 | /// The maximum interval in milliseconds of sending window size notification. 17 | public KcpReceiveWindowNotificationOptions(int initialInterval, int maximumInterval) 18 | { 19 | if (initialInterval <= 0) 20 | { 21 | throw new ArgumentOutOfRangeException(nameof(initialInterval)); 22 | } 23 | if (maximumInterval < initialInterval) 24 | { 25 | throw new ArgumentOutOfRangeException(nameof(maximumInterval)); 26 | } 27 | InitialInterval = initialInterval; 28 | MaximumInterval = maximumInterval; 29 | } 30 | 31 | /// 32 | /// The initial interval in milliseconds of sending window size notification. 33 | /// 34 | public int InitialInterval { get; } 35 | 36 | /// 37 | /// The maximum interval in milliseconds of sending window size notification. 38 | /// 39 | public int MaximumInterval { get; } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/KcpSharp/KcpBufferPoolRentOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace KcpSharp 4 | { 5 | /// 6 | /// The options to use when renting buffers from the pool. 7 | /// 8 | public readonly struct KcpBufferPoolRentOptions : IEquatable 9 | { 10 | private readonly int _size; 11 | private readonly bool _isOutbound; 12 | 13 | /// 14 | /// The minimum size of the buffer. 15 | /// 16 | public int Size => _size; 17 | 18 | /// 19 | /// True if the buffer may be passed to the outside of KcpSharp. False if the buffer is only used internally in KcpSharp. 20 | /// 21 | public bool IsOutbound => _isOutbound; 22 | 23 | /// 24 | /// Create a with the specified parameters. 25 | /// 26 | /// The minimum size of the buffer. 27 | /// True if the buffer may be passed to the outside of KcpSharp. False if the buffer is only used internally in KcpSharp. 28 | public KcpBufferPoolRentOptions(int size, bool isOutbound) 29 | { 30 | _size = size; 31 | _isOutbound = isOutbound; 32 | } 33 | 34 | /// 35 | public bool Equals(KcpBufferPoolRentOptions other) => _size == other._size && _isOutbound == other.IsOutbound; 36 | 37 | /// 38 | public override bool Equals(object? obj) => obj is KcpBufferPoolRentOptions other && Equals(other); 39 | 40 | /// 41 | public override int GetHashCode() => HashCode.Combine(_size, _isOutbound); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/KcpSharp/KcpSocketTransportForConversation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Net.Sockets; 4 | 5 | namespace KcpSharp 6 | { 7 | /// 8 | /// Socket transport for KCP conversation. 9 | /// 10 | internal sealed class KcpSocketTransportForConversation : KcpSocketTransport, IKcpTransport 11 | { 12 | private readonly int? _conversationId; 13 | private readonly KcpConversationOptions? _options; 14 | 15 | private Func, object?, bool>? _exceptionHandler; 16 | private object? _exceptionHandlerState; 17 | 18 | 19 | internal KcpSocketTransportForConversation(Socket socket, EndPoint endPoint, int? conversationId, KcpConversationOptions? options) 20 | : base(socket, endPoint, options?.Mtu ?? KcpConversationOptions.MtuDefaultValue) 21 | { 22 | _conversationId = conversationId; 23 | _options = options; 24 | } 25 | 26 | protected override KcpConversation Activate() => _conversationId.HasValue ? new KcpConversation(this, _conversationId.GetValueOrDefault(), _options) : new KcpConversation(this, _options); 27 | 28 | protected override bool HandleException(Exception ex) 29 | { 30 | if (_exceptionHandler is not null) 31 | { 32 | return _exceptionHandler.Invoke(ex, this, _exceptionHandlerState); 33 | } 34 | return false; 35 | } 36 | 37 | public void SetExceptionHandler(Func, object?, bool> handler, object? state) 38 | { 39 | _exceptionHandler = handler; 40 | _exceptionHandlerState = state; 41 | } 42 | 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/KcpSharp/KcpSocketTransportForMultiplexConnection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Net.Sockets; 4 | 5 | namespace KcpSharp 6 | { 7 | internal sealed class KcpSocketTransportForMultiplexConnection : KcpSocketTransport>, IKcpTransport> 8 | { 9 | private readonly Action? _disposeAction; 10 | private Func>, object?, bool>? _exceptionHandler; 11 | private object? _exceptionHandlerState; 12 | 13 | internal KcpSocketTransportForMultiplexConnection(Socket socket, EndPoint endPoint, int mtu) 14 | : base(socket, endPoint, mtu) 15 | { } 16 | 17 | internal KcpSocketTransportForMultiplexConnection(Socket socket, EndPoint endPoint, int mtu, Action? disposeAction) 18 | : base(socket, endPoint, mtu) 19 | { 20 | _disposeAction = disposeAction; 21 | } 22 | 23 | protected override KcpMultiplexConnection Activate() => new KcpMultiplexConnection(this, _disposeAction); 24 | 25 | IKcpMultiplexConnection IKcpTransport>.Connection => Connection; 26 | 27 | protected override bool HandleException(Exception ex) 28 | { 29 | if (_exceptionHandler is not null) 30 | { 31 | return _exceptionHandler.Invoke(ex, this, _exceptionHandlerState); 32 | } 33 | return false; 34 | } 35 | 36 | public void SetExceptionHandler(Func>, object?, bool> handler, object? state) 37 | { 38 | _exceptionHandler = handler; 39 | _exceptionHandlerState = state; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/KcpSharp.Tests/Utils/KcpConversationFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace KcpSharp.Tests 4 | { 5 | internal static class KcpConversationFactory 6 | { 7 | public static KcpConversationPipe CreatePerfectPipe(uint conversationId) 8 | { 9 | return new PerfectKcpConversationPipe(conversationId, null, null); 10 | } 11 | 12 | public static KcpConversationPipe CreatePerfectPipe(uint conversationId, KcpConversationOptions? options) 13 | { 14 | return new PerfectKcpConversationPipe(conversationId, options, options); 15 | } 16 | 17 | public static KcpConversationPipe CreatePerfectPipe() 18 | { 19 | return new PerfectKcpConversationPipe(null, null, null); 20 | } 21 | 22 | public static KcpConversationPipe CreatePerfectPipe(KcpConversationOptions? options) 23 | { 24 | return new PerfectKcpConversationPipe(null, options, options); 25 | } 26 | 27 | public static KcpConversationPipe CreateBadPipe(uint conversationId, BadOneWayConnectionOptions connectionOptions) 28 | { 29 | return new BadKcpConversationPipe(conversationId, connectionOptions, null, null); 30 | } 31 | 32 | public static KcpConversationPipe CreateBadPipe(uint conversationId, BadOneWayConnectionOptions connectionOptions, KcpConversationOptions? options) 33 | { 34 | return new BadKcpConversationPipe(conversationId, connectionOptions, options, options); 35 | } 36 | } 37 | 38 | internal abstract class KcpConversationPipe : IDisposable 39 | { 40 | public abstract KcpConversation Alice { get; } 41 | public abstract KcpConversation Bob { get; } 42 | 43 | public abstract void Dispose(); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /tests/KcpSharp.Benchmarks/PreallocatedBufferPool.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.Collections.Concurrent; 4 | 5 | namespace KcpSharp.Benchmarks 6 | { 7 | internal class PreallocatedBufferPool : IKcpBufferPool 8 | { 9 | private readonly ConcurrentQueue _queue = new(); 10 | private readonly int _mtu; 11 | 12 | public PreallocatedBufferPool(int mtu) 13 | { 14 | _mtu = mtu; 15 | } 16 | 17 | public void Fill(int count) 18 | { 19 | for (int i = 0; i < count; i++) 20 | { 21 | _queue.Enqueue(new ArrayBlock(_queue, GC.AllocateUninitializedArray(_mtu))); 22 | } 23 | } 24 | 25 | public KcpRentedBuffer Rent(KcpBufferPoolRentOptions options) 26 | { 27 | if (options.Size > _mtu) 28 | { 29 | throw new InvalidOperationException(); 30 | } 31 | if (_queue.TryDequeue(out ArrayBlock? block)) 32 | { 33 | return KcpRentedBuffer.FromMemoryOwner(block); 34 | } 35 | 36 | return KcpRentedBuffer.FromMemoryOwner(new ArrayBlock(_queue, GC.AllocateUninitializedArray(_mtu))); 37 | } 38 | 39 | sealed class ArrayBlock : IMemoryOwner 40 | { 41 | private readonly ConcurrentQueue _queue; 42 | private readonly byte[] _buffer; 43 | 44 | public ArrayBlock(ConcurrentQueue queue, byte[] buffer) 45 | { 46 | _queue = queue; 47 | _buffer = buffer; 48 | } 49 | 50 | public Memory Memory => _buffer; 51 | 52 | public void Dispose() => _queue.Enqueue(this); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/KcpSharp.Tests/Utils/TrackedBufferAllocator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.Threading; 4 | 5 | namespace KcpSharp.Tests 6 | { 7 | internal sealed class TrackedBufferAllocator : IKcpBufferPool 8 | { 9 | private int _inuseBufferCount; 10 | 11 | public KcpRentedBuffer Rent(KcpBufferPoolRentOptions options) 12 | { 13 | Interlocked.Increment(ref _inuseBufferCount); 14 | return KcpRentedBuffer.FromMemoryOwner(new TrackedBufferOwner(this, options.Size)); 15 | } 16 | 17 | internal void Return() 18 | { 19 | Interlocked.Decrement(ref _inuseBufferCount); 20 | } 21 | 22 | public int InuseBufferCount => _inuseBufferCount; 23 | } 24 | 25 | internal sealed class TrackedBufferOwner : IMemoryOwner 26 | { 27 | private readonly TrackedBufferAllocator _allocator; 28 | private bool _isFreed; 29 | private byte[] _buffer; 30 | 31 | public TrackedBufferOwner(TrackedBufferAllocator allocator, int size) 32 | { 33 | _allocator = allocator; 34 | _buffer = new byte[size]; 35 | } 36 | 37 | public Memory Memory 38 | { 39 | get 40 | { 41 | if (_isFreed) 42 | { 43 | throw new InvalidOperationException("Trying to access freed memory."); 44 | } 45 | return _buffer; 46 | } 47 | } 48 | 49 | public void Dispose() 50 | { 51 | if (_isFreed) 52 | { 53 | throw new InvalidOperationException("Trying to free the same buffer twice."); 54 | } 55 | _isFreed = true; 56 | _allocator.Return(); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /samples/KcpEcho/KcpEchoServer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Net.Sockets; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using KcpSharp; 7 | 8 | namespace KcpEcho 9 | { 10 | internal static class KcpEchoServer 11 | { 12 | public static async Task RunAsync(string listen, int mtu, uint conversationId, CancellationToken cancellationToken) 13 | { 14 | if (!IPEndPoint.TryParse(listen, out IPEndPoint? ipEndPoint)) 15 | { 16 | throw new ArgumentException("endpoint is not a valid IPEndPoint.", nameof(listen)); 17 | } 18 | if (mtu < 50 || mtu > ushort.MaxValue) 19 | { 20 | throw new ArgumentOutOfRangeException(nameof(mtu), "mtu is not valid."); 21 | } 22 | 23 | var options = new KcpConversationOptions 24 | { 25 | Mtu = mtu 26 | }; 27 | 28 | var socket = new Socket(ipEndPoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp); 29 | SocketHelper.PatchSocket(socket); 30 | if (ipEndPoint.Equals(IPAddress.IPv6Any)) 31 | { 32 | socket.DualMode = true; 33 | } 34 | socket.Bind(ipEndPoint); 35 | 36 | var dispatcher = new UdpSocketServiceDispatcher( 37 | socket, TimeSpan.FromMinutes(2), TimeSpan.FromMinutes(5), 38 | (sender, ep, state) => new KcpEchoService(sender, ep, ((Tuple?)state!).Item1, ((Tuple?)state!).Item2), 39 | (service, state) => service.Dispose(), 40 | Tuple.Create(options, conversationId)); 41 | await dispatcher.RunAsync(ipEndPoint, GC.AllocateUninitializedArray(mtu), cancellationToken); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10.0 7 | enable 8 | true 9 | AllEnabledByDefault 10 | latest 11 | 12 | $(NoWarn);CA1303;CA2000 13 | 14 | 15 | 16 | 17 | true 18 | true 19 | snupkg 20 | 21 | 22 | 23 | true 24 | true 25 | true 26 | false 27 | 28 | 29 | true 30 | true 31 | 32 | 33 | true 34 | $(PreviouslyPublishedPackageVersion) 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/KcpSharp/KcpSharp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0;net6.0 5 | true 6 | AllEnabledByDefault 7 | latest 8 | true 9 | 10 | C# asynchronous KCP protocol implementation. (ported from https://github.com/skywind3000/kcp) 11 | 12 | $(NoWarn);CA1031;CA1508;CA1815;CA2002;CA2213;CA2231 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | $(DefineConstants);NEED_MATH_SHIM;NEED_TCS_SHIM;NEED_LINKEDLIST_SHIM;NEED_CANCELLATIONTOKEN_SHIM;NEED_POH_SHIM;NEED_SOCKET_SHIM;NO_FAST_SPAN 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/KcpSharp/KcpBuffer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | 4 | namespace KcpSharp 5 | { 6 | internal readonly struct KcpBuffer 7 | { 8 | private readonly object? _owner; 9 | private readonly Memory _memory; 10 | private readonly int _length; 11 | 12 | public ReadOnlyMemory DataRegion => _memory.Slice(0, _length); 13 | 14 | public int Length => _length; 15 | 16 | private KcpBuffer(object? owner, Memory memory, int length) 17 | { 18 | _owner = owner; 19 | _memory = memory; 20 | _length = length; 21 | } 22 | 23 | public static KcpBuffer CreateFromSpan(KcpRentedBuffer buffer, ReadOnlySpan dataSource) 24 | { 25 | Memory memory = buffer.Memory; 26 | if (dataSource.Length > memory.Length) 27 | { 28 | ThrowRentedBufferTooSmall(); 29 | } 30 | dataSource.CopyTo(memory.Span); 31 | return new KcpBuffer(buffer.Owner, memory, dataSource.Length); 32 | } 33 | 34 | public KcpBuffer AppendData(ReadOnlySpan data) 35 | { 36 | if ((_length + data.Length) > _memory.Length) 37 | { 38 | ThrowRentedBufferTooSmall(); 39 | } 40 | data.CopyTo(_memory.Span.Slice(_length)); 41 | return new KcpBuffer(_owner, _memory, _length + data.Length); 42 | } 43 | 44 | public KcpBuffer Consume(int length) 45 | { 46 | Debug.Assert((uint)length <= (uint)_length); 47 | return new KcpBuffer(_owner, _memory.Slice(length), _length - length); 48 | } 49 | 50 | public void Release() 51 | { 52 | new KcpRentedBuffer(_owner, _memory).Dispose(); 53 | } 54 | 55 | private static void ThrowRentedBufferTooSmall() 56 | { 57 | throw new InvalidOperationException("The rented buffer is not large enough to hold the data."); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/KcpSharp.ThroughputBanchmarks/PacketsThroughputBenchmark/PacketsThroughputBenchmarkServer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Net.Sockets; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace KcpSharp.ThroughputBanchmarks.PacketsThroughputBenchmark 8 | { 9 | internal class PacketsThroughputBenchmarkServer 10 | { 11 | public async Task RunAsync(string listen, int mtu, int windowSize, int updateInterval, bool noDelay, CancellationToken cancellationToken) 12 | { 13 | if (!IPEndPoint.TryParse(listen, out IPEndPoint? ipEndPoint)) 14 | { 15 | throw new ArgumentException("endpoint is not a valid IPEndPoint.", nameof(listen)); 16 | } 17 | if (mtu < 50 || mtu > ushort.MaxValue) 18 | { 19 | throw new ArgumentOutOfRangeException(nameof(mtu), "mtu is not valid."); 20 | } 21 | if (windowSize <= 0 || windowSize >= ushort.MaxValue) 22 | { 23 | throw new ArgumentOutOfRangeException(nameof(windowSize), "windowSize is not valid."); 24 | } 25 | if (updateInterval <= 0 || updateInterval > 1000) 26 | { 27 | throw new ArgumentOutOfRangeException(nameof(updateInterval), "updateInterval is not valid."); 28 | } 29 | 30 | var allocator = new PinnedBlockMemoryPool(mtu); 31 | var options = new KcpConversationOptions 32 | { 33 | BufferPool = allocator, 34 | Mtu = mtu, 35 | SendWindow = windowSize, 36 | RemoteReceiveWindow = windowSize, 37 | UpdateInterval = updateInterval, 38 | NoDelay = noDelay 39 | }; 40 | 41 | var socket = new Socket(ipEndPoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp); 42 | SocketHelper.PatchSocket(socket); 43 | if (ipEndPoint.Equals(IPAddress.IPv6Any)) 44 | { 45 | socket.DualMode = true; 46 | } 47 | socket.Bind(ipEndPoint); 48 | 49 | var dispatcher = UdpSocketServiceDispatcher.Create(socket, new PacketsThroughputBenchmarkServiceFactory(options)); 50 | await dispatcher.RunAsync(ipEndPoint, GC.AllocateUninitializedArray(mtu), cancellationToken); 51 | } 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /tests/KcpSharp.ThroughputBanchmarks/StreamThroughputBenchmark/StreamThroughputBenchmarkServer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Net.Sockets; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace KcpSharp.ThroughputBanchmarks.StreamThroughputBenchmark 8 | { 9 | internal class StreamThroughputBenchmarkServer 10 | { 11 | public async Task RunAsync(string listen, int mtu, int windowSize, int updateInterval, bool noDelay, CancellationToken cancellationToken) 12 | { 13 | if (!IPEndPoint.TryParse(listen, out IPEndPoint? ipEndPoint)) 14 | { 15 | throw new ArgumentException("endpoint is not a valid IPEndPoint.", nameof(listen)); 16 | } 17 | if (mtu < 50 || mtu > ushort.MaxValue) 18 | { 19 | throw new ArgumentOutOfRangeException(nameof(mtu), "mtu is not valid."); 20 | } 21 | if (windowSize <= 0 || windowSize >= ushort.MaxValue) 22 | { 23 | throw new ArgumentOutOfRangeException(nameof(windowSize), "windowSize is not valid."); 24 | } 25 | if (updateInterval <= 0 || updateInterval > 1000) 26 | { 27 | throw new ArgumentOutOfRangeException(nameof(updateInterval), "updateInterval is not valid."); 28 | } 29 | 30 | var allocator = new PinnedBlockMemoryPool(mtu); 31 | var options = new KcpConversationOptions 32 | { 33 | BufferPool = allocator, 34 | Mtu = mtu, 35 | SendWindow = windowSize, 36 | RemoteReceiveWindow = windowSize, 37 | UpdateInterval = updateInterval, 38 | NoDelay = noDelay, 39 | StreamMode = true 40 | }; 41 | 42 | var socket = new Socket(ipEndPoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp); 43 | SocketHelper.PatchSocket(socket); 44 | if (ipEndPoint.Equals(IPAddress.IPv6Any)) 45 | { 46 | socket.DualMode = true; 47 | } 48 | socket.Bind(ipEndPoint); 49 | 50 | var dispatcher = UdpSocketServiceDispatcher.Create(socket, new StreamThroughputBenchmarkServiceFactory(options)); 51 | await dispatcher.RunAsync(ipEndPoint, GC.AllocateUninitializedArray(mtu), cancellationToken); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/KcpSharp/KcpSendReceiveBufferItemCache.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | 3 | #if NEED_LINKEDLIST_SHIM 4 | using LinkedListOfBufferItem = KcpSharp.NetstandardShim.LinkedList; 5 | using LinkedListNodeOfBufferItem = KcpSharp.NetstandardShim.LinkedListNode; 6 | #else 7 | using LinkedListOfBufferItem = System.Collections.Generic.LinkedList; 8 | using LinkedListNodeOfBufferItem = System.Collections.Generic.LinkedListNode; 9 | #endif 10 | 11 | namespace KcpSharp 12 | { 13 | internal struct KcpSendReceiveBufferItemCache 14 | { 15 | private LinkedListOfBufferItem _items; 16 | private SpinLock _lock; 17 | 18 | public static KcpSendReceiveBufferItemCache Create() 19 | { 20 | return new KcpSendReceiveBufferItemCache 21 | { 22 | _items = new LinkedListOfBufferItem(), 23 | _lock = new SpinLock() 24 | }; 25 | } 26 | 27 | public LinkedListNodeOfBufferItem Allocate(in KcpSendReceiveBufferItem item) 28 | { 29 | bool lockAcquired = false; 30 | try 31 | { 32 | _lock.Enter(ref lockAcquired); 33 | 34 | LinkedListNodeOfBufferItem? node = _items.First; 35 | if (node is null) 36 | { 37 | node = new LinkedListNodeOfBufferItem(item); 38 | } 39 | else 40 | { 41 | _items.Remove(node); 42 | node.ValueRef = item; 43 | } 44 | return node; 45 | } 46 | finally 47 | { 48 | if (lockAcquired) 49 | { 50 | _lock.Exit(); 51 | } 52 | } 53 | } 54 | 55 | public void Return(LinkedListNodeOfBufferItem node) 56 | { 57 | bool lockAcquired = false; 58 | try 59 | { 60 | _lock.Enter(ref lockAcquired); 61 | 62 | node.ValueRef = default; 63 | _items.AddLast(node); 64 | } 65 | finally 66 | { 67 | if (lockAcquired) 68 | { 69 | _lock.Exit(); 70 | } 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /samples/KcpEcho/Program.cs: -------------------------------------------------------------------------------- 1 | using System.CommandLine; 2 | using System.CommandLine.Builder; 3 | using System.CommandLine.Invocation; 4 | using System.CommandLine.Parsing; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace KcpEcho 9 | { 10 | internal static class Program 11 | { 12 | static async Task Main(string[] args) 13 | { 14 | var builder = new CommandLineBuilder(); 15 | builder.Command.Description = "KcpEcho"; 16 | 17 | builder.Command.AddCommand(BuildServerCommand()); 18 | builder.Command.AddCommand(BuildClientCommand()); 19 | 20 | builder.UseDefaults(); 21 | 22 | Parser parser = builder.Build(); 23 | return await parser.InvokeAsync(args).ConfigureAwait(false); 24 | } 25 | 26 | static Command BuildServerCommand() 27 | { 28 | var command = new Command("server", "Run server side."); 29 | var listenOptions = new Option("--listen", "Endpoint where the server listens.") 30 | { 31 | Arity = ArgumentArity.ExactlyOne 32 | }; 33 | var mtuOption = new Option("--mtu", () => 1400, "MTU."); 34 | var conversationOption = new Option("--conversation-id", () => 0, "Conversation ID."); 35 | 36 | command.AddOption(listenOptions); 37 | command.AddOption(mtuOption); 38 | command.AddOption(conversationOption); 39 | command.SetHandler(KcpEchoServer.RunAsync, listenOptions, mtuOption, conversationOption); 40 | return command; 41 | } 42 | 43 | static Command BuildClientCommand() 44 | { 45 | var command = new Command("client", "Run client side."); 46 | var endpointOption = new Option("--endpoint", "Endpoint which the client connects to.") 47 | { 48 | Arity = ArgumentArity.ExactlyOne 49 | }; 50 | var mtuOption = new Option("--mtu", () => 1400, "MTU."); 51 | var conversationOption = new Option("--conversation-id", () => 0, "Conversation ID."); 52 | 53 | command.AddOption(endpointOption); 54 | command.AddOption(mtuOption); 55 | command.AddOption(conversationOption); 56 | command.SetHandler(KcpEchoClient.RunAsync, endpointOption, mtuOption, conversationOption); 57 | return command; 58 | 59 | } 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/KcpSharp/KcpSendReceiveQueueItemCache.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | 3 | #if NEED_LINKEDLIST_SHIM 4 | using LinkedListOfQueueItem = KcpSharp.NetstandardShim.LinkedList<(KcpSharp.KcpBuffer Data, byte Fragment)>; 5 | using LinkedListNodeOfQueueItem = KcpSharp.NetstandardShim.LinkedListNode<(KcpSharp.KcpBuffer Data, byte Fragment)>; 6 | #else 7 | using LinkedListOfQueueItem = System.Collections.Generic.LinkedList<(KcpSharp.KcpBuffer Data, byte Fragment)>; 8 | using LinkedListNodeOfQueueItem = System.Collections.Generic.LinkedListNode<(KcpSharp.KcpBuffer Data, byte Fragment)>; 9 | #endif 10 | 11 | namespace KcpSharp 12 | { 13 | internal sealed class KcpSendReceiveQueueItemCache 14 | { 15 | private LinkedListOfQueueItem _list = new(); 16 | private SpinLock _lock; 17 | 18 | public LinkedListNodeOfQueueItem Rent(in KcpBuffer buffer, byte fragment) 19 | { 20 | bool lockTaken = false; 21 | try 22 | { 23 | _lock.Enter(ref lockTaken); 24 | 25 | LinkedListNodeOfQueueItem? node = _list.First; 26 | if (node is null) 27 | { 28 | node = new LinkedListNodeOfQueueItem((buffer, fragment)); 29 | } 30 | else 31 | { 32 | node.ValueRef = (buffer, fragment); 33 | _list.RemoveFirst(); 34 | } 35 | 36 | return node; 37 | } 38 | finally 39 | { 40 | if (lockTaken) 41 | { 42 | _lock.Exit(); 43 | } 44 | } 45 | } 46 | 47 | public void Return(LinkedListNodeOfQueueItem node) 48 | { 49 | node.ValueRef = default; 50 | 51 | bool lockTaken = false; 52 | try 53 | { 54 | _lock.Enter(ref lockTaken); 55 | 56 | _list.AddLast(node); 57 | } 58 | finally 59 | { 60 | if (lockTaken) 61 | { 62 | _lock.Exit(); 63 | } 64 | } 65 | } 66 | 67 | public void Clear() 68 | { 69 | bool lockTaken = false; 70 | try 71 | { 72 | _lock.Enter(ref lockTaken); 73 | 74 | _list.Clear(); 75 | } 76 | finally 77 | { 78 | if (lockTaken) 79 | { 80 | _lock.Exit(); 81 | } 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /samples/KcpTunnel/Program.cs: -------------------------------------------------------------------------------- 1 | using System.CommandLine; 2 | using System.CommandLine.Builder; 3 | using System.CommandLine.Parsing; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace KcpTunnel 8 | { 9 | internal static class Program 10 | { 11 | static async Task Main(string[] args) 12 | { 13 | var builder = new CommandLineBuilder(); 14 | builder.Command.Description = "KcpTunnel"; 15 | 16 | builder.Command.AddCommand(BuildServerCommand()); 17 | builder.Command.AddCommand(BuildClientCommand()); 18 | 19 | builder.UseDefaults(); 20 | 21 | Parser parser = builder.Build(); 22 | return await parser.InvokeAsync(args).ConfigureAwait(false); 23 | } 24 | 25 | static Command BuildServerCommand() 26 | { 27 | var command = new Command("server", "Run server side."); 28 | var listenOption = new Option("--listen", "Endpoint where the server listens.") 29 | { 30 | Arity = ArgumentArity.ExactlyOne 31 | }; 32 | var tcpForwardOption = new Option("--tcp-forward", "The TCP endpoint to forward to.") 33 | { 34 | Arity = ArgumentArity.ExactlyOne 35 | }; 36 | var mtuOption = new Option("--mtu", () => 1400, "MTU."); 37 | 38 | command.AddOption(listenOption); 39 | command.AddOption(tcpForwardOption); 40 | command.AddOption(mtuOption); 41 | command.SetHandler(KcpTunnelServerProgram.RunAsync, listenOption, tcpForwardOption, mtuOption); 42 | 43 | return command; 44 | } 45 | 46 | static Command BuildClientCommand() 47 | { 48 | var command = new Command("client", "Run client side."); 49 | var endpointOption = new Option("--endpoint", "Endpoint which the client connects to.") 50 | { 51 | Arity = ArgumentArity.ExactlyOne 52 | }; 53 | var tcpListenOption = new Option("--tcp-listen", "The TCP endpoint to listen on.") 54 | { 55 | Arity = ArgumentArity.ExactlyOne 56 | }; 57 | var mtuOption = new Option("--mtu", () => 1400, "MTU."); 58 | 59 | command.AddOption(endpointOption); 60 | command.AddOption(tcpListenOption); 61 | command.AddOption(mtuOption); 62 | command.SetHandler(KcpTunnelClientProgram.RunAsync, endpointOption, tcpListenOption, mtuOption); 63 | 64 | return command; 65 | } 66 | 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/KcpSharp/KcpConversationReceiveResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | 4 | namespace KcpSharp 5 | { 6 | /// 7 | /// The result of a receive or peek operation. 8 | /// 9 | public readonly struct KcpConversationReceiveResult : IEquatable 10 | { 11 | private readonly int _bytesReceived; 12 | private readonly bool _connectionAlive; 13 | 14 | /// 15 | /// The number of bytes received. 16 | /// 17 | public int BytesReceived => _bytesReceived; 18 | 19 | /// 20 | /// Whether the underlying transport is marked as closed. 21 | /// 22 | public bool TransportClosed => !_connectionAlive; 23 | 24 | /// 25 | /// Construct a with the specified number of bytes received. 26 | /// 27 | /// The number of bytes received. 28 | public KcpConversationReceiveResult(int bytesReceived) 29 | { 30 | _bytesReceived = bytesReceived; 31 | _connectionAlive = true; 32 | } 33 | 34 | /// 35 | /// Checks whether the two instance is equal. 36 | /// 37 | /// The one instance. 38 | /// The other instance. 39 | /// Whether the two instance is equal 40 | public static bool operator ==(KcpConversationReceiveResult left, KcpConversationReceiveResult right) => left.Equals(right); 41 | 42 | /// 43 | /// Checks whether the two instance is not equal. 44 | /// 45 | /// The one instance. 46 | /// The other instance. 47 | /// Whether the two instance is not equal 48 | public static bool operator !=(KcpConversationReceiveResult left, KcpConversationReceiveResult right) => !left.Equals(right); 49 | 50 | /// 51 | public bool Equals(KcpConversationReceiveResult other) => BytesReceived == other.BytesReceived && TransportClosed == other.TransportClosed; 52 | 53 | /// 54 | public override bool Equals(object? obj) => obj is KcpConversationReceiveResult other && Equals(other); 55 | 56 | /// 57 | public override int GetHashCode() => HashCode.Combine(BytesReceived, TransportClosed); 58 | 59 | /// 60 | public override string ToString() => _connectionAlive ? _bytesReceived.ToString(CultureInfo.InvariantCulture) : "Transport is closed."; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /samples/KcpTunnel/KcpTunnelServer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Net.Sockets; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace KcpTunnel 8 | { 9 | internal static class KcpTunnelServerProgram 10 | { 11 | public static async Task RunAsync(string listen, string tcpForward, int mtu, CancellationToken cancellationToken) 12 | { 13 | if (string.IsNullOrEmpty(listen) || !IPEndPoint.TryParse(listen, out IPEndPoint? listenEndPoint)) 14 | { 15 | throw new ArgumentException("listen is not a valid IPEndPoint.", nameof(listen)); 16 | } 17 | if (string.IsNullOrEmpty(listen) || !IPEndPoint.TryParse(tcpForward, out IPEndPoint? tcpForwardEndPoint)) 18 | { 19 | throw new ArgumentException("tcpForward is not a valid IPEndPoint.", nameof(tcpForward)); 20 | } 21 | if (mtu < 64 || mtu > ushort.MaxValue) 22 | { 23 | throw new ArgumentOutOfRangeException(nameof(mtu)); 24 | } 25 | 26 | var server = new KcpTunnelServer(listenEndPoint, tcpForwardEndPoint, mtu); 27 | try 28 | { 29 | await server.RunAsync(cancellationToken); 30 | } 31 | catch (OperationCanceledException) 32 | { 33 | // Ignore 34 | } 35 | } 36 | } 37 | 38 | internal class KcpTunnelServer 39 | { 40 | private readonly IPEndPoint _listenEndPoint; 41 | private readonly IPEndPoint _forwardEndPoint; 42 | private readonly int _mtu; 43 | 44 | public KcpTunnelServer(IPEndPoint listenEndPoint, IPEndPoint forwardEndPoint, int mtu) 45 | { 46 | _listenEndPoint = listenEndPoint; 47 | _forwardEndPoint = forwardEndPoint; 48 | _mtu = mtu; 49 | } 50 | 51 | public async Task RunAsync(CancellationToken cancellationToken) 52 | { 53 | using var socket = new Socket(_listenEndPoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp); 54 | SocketHelper.PatchSocket(socket); 55 | if (_listenEndPoint.Equals(IPAddress.IPv6Any)) 56 | { 57 | socket.DualMode = true; 58 | } 59 | socket.Bind(_listenEndPoint); 60 | 61 | var factory = new KcpTunnelServiceFactory(new KcpTunnelServiceOptions 62 | { 63 | ForwardEndPoint = _forwardEndPoint, 64 | Mtu = _mtu 65 | }); 66 | using var dispatcher = UdpSocketServiceDispatcher.Create(socket, factory); 67 | await dispatcher.RunAsync(_listenEndPoint, GC.AllocateUninitializedArray(_mtu), cancellationToken); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/KcpSharp.ThroughputBanchmarks/StreamThroughputBenchmark/StreamThroughputBenchmarkService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.Net; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace KcpSharp.ThroughputBanchmarks.StreamThroughputBenchmark 8 | { 9 | internal sealed class StreamThroughputBenchmarkService : IUdpService, IKcpTransport, IDisposable 10 | { 11 | private readonly IUdpServiceDispatcher _sender; 12 | private readonly EndPoint _endPoint; 13 | private readonly KcpConversation _conversation; 14 | private readonly int _mtu; 15 | private CancellationTokenSource? _cts; 16 | 17 | public StreamThroughputBenchmarkService(IUdpServiceDispatcher sender, EndPoint endPoint, KcpConversationOptions options) 18 | { 19 | _sender = sender; 20 | _endPoint = endPoint; 21 | _conversation = new KcpConversation(this, 0, options); 22 | _mtu = options.Mtu; 23 | _cts = new CancellationTokenSource(); 24 | _ = Task.Run(() => ReceiveLoop(_cts)); 25 | Console.WriteLine($"{DateTime.Now:O}: Connected from {endPoint}"); 26 | } 27 | 28 | ValueTask IKcpTransport.SendPacketAsync(Memory packet, CancellationToken cancellationToken) 29 | => _sender.SendPacketAsync(_endPoint, packet, cancellationToken); 30 | 31 | ValueTask IUdpService.InputPacketAsync(ReadOnlyMemory packet, CancellationToken cancellationToken) 32 | => _conversation.InputPakcetAsync(packet, cancellationToken); 33 | 34 | void IUdpService.SetTransportClosed() 35 | => _conversation.SetTransportClosed(); 36 | 37 | private async Task ReceiveLoop(CancellationTokenSource cts) 38 | { 39 | CancellationToken cancellationToken = cts.Token; 40 | byte[] buffer = ArrayPool.Shared.Rent(_mtu); 41 | try 42 | { 43 | while (!cancellationToken.IsCancellationRequested) 44 | { 45 | KcpConversationReceiveResult result = await _conversation.ReceiveAsync(buffer, cancellationToken); 46 | if (result.TransportClosed) 47 | { 48 | break; 49 | } 50 | } 51 | } 52 | catch (OperationCanceledException) 53 | { 54 | // Do nothing 55 | } 56 | finally 57 | { 58 | ArrayPool.Shared.Return(buffer); 59 | cts.Dispose(); 60 | } 61 | } 62 | 63 | public void Dispose() 64 | { 65 | Console.WriteLine($"{DateTime.Now:O}: Connection from {_endPoint} eliminated."); 66 | Interlocked.Exchange(ref _cts, null)?.Dispose(); 67 | _conversation.Dispose(); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/KcpSharp.ThroughputBanchmarks/PacketsThroughputBenchmark/PacketsThroughputBenchmarkService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.Net; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace KcpSharp.ThroughputBanchmarks.PacketsThroughputBenchmark 8 | { 9 | internal sealed class PacketsThroughputBenchmarkService : IUdpService, IKcpTransport, IDisposable 10 | { 11 | private readonly IUdpServiceDispatcher _sender; 12 | private readonly EndPoint _endPoint; 13 | private readonly KcpConversation _conversation; 14 | private readonly int _mtu; 15 | private CancellationTokenSource? _cts; 16 | 17 | public PacketsThroughputBenchmarkService(IUdpServiceDispatcher sender, EndPoint endPoint, KcpConversationOptions options) 18 | { 19 | _sender = sender; 20 | _endPoint = endPoint; 21 | _conversation = new KcpConversation(this, 0, options); 22 | _mtu = options.Mtu; 23 | _cts = new CancellationTokenSource(); 24 | _ = Task.Run(() => ReceiveLoop(_cts)); 25 | Console.WriteLine($"{DateTime.Now:O}: Connected from {endPoint}"); 26 | } 27 | 28 | ValueTask IKcpTransport.SendPacketAsync(Memory packet, CancellationToken cancellationToken) 29 | => _sender.SendPacketAsync(_endPoint, packet, cancellationToken); 30 | 31 | ValueTask IUdpService.InputPacketAsync(ReadOnlyMemory packet, CancellationToken cancellationToken) 32 | => _conversation.InputPakcetAsync(packet, cancellationToken); 33 | 34 | void IUdpService.SetTransportClosed() 35 | => _conversation.SetTransportClosed(); 36 | 37 | private async Task ReceiveLoop(CancellationTokenSource cts) 38 | { 39 | CancellationToken cancellationToken = cts.Token; 40 | byte[] buffer = ArrayPool.Shared.Rent(_mtu); 41 | try 42 | { 43 | while (!cancellationToken.IsCancellationRequested) 44 | { 45 | KcpConversationReceiveResult result = await _conversation.ReceiveAsync(buffer, cancellationToken); 46 | if (result.TransportClosed) 47 | { 48 | break; 49 | } 50 | } 51 | } 52 | catch (OperationCanceledException) 53 | { 54 | // Do nothing 55 | } 56 | finally 57 | { 58 | ArrayPool.Shared.Return(buffer); 59 | cts.Dispose(); 60 | } 61 | } 62 | 63 | public void Dispose() 64 | { 65 | Console.WriteLine($"{DateTime.Now:O}: Connection from {_endPoint} eliminated."); 66 | Interlocked.Exchange(ref _cts, null)?.Dispose(); 67 | _conversation.Dispose(); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/KcpSharp/IKcpMultiplexConnectionOfT.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace KcpSharp 4 | { 5 | /// 6 | /// Multiplex many channels or conversations over the same transport. 7 | /// 8 | public interface IKcpMultiplexConnection : IKcpMultiplexConnection 9 | { 10 | /// 11 | /// Create a raw channel with the specified conversation ID. 12 | /// 13 | /// The conversation ID. 14 | /// The user state of this channel. 15 | /// The options of the . 16 | /// The raw channel created. 17 | /// The current instance is disposed. 18 | /// Another channel or conversation with the same ID was already registered. 19 | KcpRawChannel CreateRawChannel(int id, T state, KcpRawChannelOptions? options = null); 20 | 21 | /// 22 | /// Create a conversation with the specified conversation ID. 23 | /// 24 | /// The conversation ID. 25 | /// The user state of this conversation. 26 | /// The options of the . 27 | /// The KCP conversation created. 28 | /// The current instance is disposed. 29 | /// Another channel or conversation with the same ID was already registered. 30 | KcpConversation CreateConversation(int id, T state, KcpConversationOptions? options = null); 31 | 32 | /// 33 | /// Register a conversation or channel with the specified conversation ID and user state. 34 | /// 35 | /// The conversation or channel to register. 36 | /// The conversation ID. 37 | /// The user state 38 | /// is not provided. 39 | /// The current instance is disposed. 40 | /// Another channel or conversation with the same ID was already registered. 41 | void RegisterConversation(IKcpConversation conversation, int id, T? state); 42 | 43 | /// 44 | /// Unregister a conversation or channel with the specified conversation ID. 45 | /// 46 | /// The conversation ID. 47 | /// The user state. 48 | /// The conversation unregistered with the user state. Returns default when the conversation with the specified ID is not found. 49 | IKcpConversation? UnregisterConversation(int id, out T? state); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/KcpSharp/IKcpMultiplexConnection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace KcpSharp 4 | { 5 | /// 6 | /// Multiplex many channels or conversations over the same transport. 7 | /// 8 | public interface IKcpMultiplexConnection : IDisposable 9 | { 10 | /// 11 | /// Determine whether the multiplex connection contains a conversation with the specified id. 12 | /// 13 | /// The conversation ID. 14 | /// True if the multiplex connection contains the specified conversation. Otherwise false. 15 | bool Contains(int id); 16 | 17 | /// 18 | /// Create a raw channel with the specified conversation ID. 19 | /// 20 | /// The conversation ID. 21 | /// The options of the . 22 | /// The raw channel created. 23 | /// The current instance is disposed. 24 | /// Another channel or conversation with the same ID was already registered. 25 | KcpRawChannel CreateRawChannel(int id, KcpRawChannelOptions? options = null); 26 | 27 | /// 28 | /// Create a conversation with the specified conversation ID. 29 | /// 30 | /// The conversation ID. 31 | /// The options of the . 32 | /// The KCP conversation created. 33 | /// The current instance is disposed. 34 | /// Another channel or conversation with the same ID was already registered. 35 | KcpConversation CreateConversation(int id, KcpConversationOptions? options = null); 36 | 37 | /// 38 | /// Register a conversation or channel with the specified conversation ID and user state. 39 | /// 40 | /// The conversation or channel to register. 41 | /// The conversation ID. 42 | /// is not provided. 43 | /// The current instance is disposed. 44 | /// Another channel or conversation with the same ID was already registered. 45 | void RegisterConversation(IKcpConversation conversation, int id); 46 | 47 | 48 | /// 49 | /// Unregister a conversation or channel with the specified conversation ID. 50 | /// 51 | /// The conversation ID. 52 | /// The conversation unregistered. Returns null when the conversation with the specified ID is not found. 53 | IKcpConversation? UnregisterConversation(int id); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/KcpSharp/ThrowHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading; 4 | 5 | namespace KcpSharp 6 | { 7 | internal static class ThrowHelper 8 | { 9 | public static void ThrowArgumentOutOfRangeException(string paramName) 10 | { 11 | throw new ArgumentOutOfRangeException(paramName); 12 | } 13 | public static void ThrowTransportClosedForStreanException() 14 | { 15 | throw new IOException("The underlying transport is closed."); 16 | } 17 | public static Exception NewMessageTooLargeForBufferArgument() 18 | { 19 | return new ArgumentException("Message is too large.", "buffer"); 20 | } 21 | public static Exception NewBufferTooSmallForBufferArgument() 22 | { 23 | return new ArgumentException("Buffer is too small.", "buffer"); 24 | } 25 | public static Exception ThrowBufferTooSmall() 26 | { 27 | throw new ArgumentException("Buffer is too small.", "buffer"); 28 | } 29 | public static Exception ThrowAllowPartialSendArgumentException() 30 | { 31 | throw new ArgumentException("allowPartialSend should not be set to true in non-stream mode.", "allowPartialSend"); 32 | } 33 | public static Exception NewArgumentOutOfRangeException(string paramName) 34 | { 35 | return new ArgumentOutOfRangeException(paramName); 36 | } 37 | public static Exception NewConcurrentSendException() 38 | { 39 | return new InvalidOperationException("Concurrent send operations are not allowed."); 40 | } 41 | public static Exception NewConcurrentReceiveException() 42 | { 43 | return new InvalidOperationException("Concurrent receive operations are not allowed."); 44 | } 45 | public static Exception NewTransportClosedForStreamException() 46 | { 47 | throw new IOException("The underlying transport is closed."); 48 | } 49 | public static Exception NewOperationCanceledExceptionForCancelPendingSend(Exception? innerException, CancellationToken cancellationToken) 50 | { 51 | return new OperationCanceledException("This operation is cancelled by a call to CancelPendingSend.", innerException, cancellationToken); 52 | } 53 | public static Exception NewOperationCanceledExceptionForCancelPendingReceive(Exception? innerException, CancellationToken cancellationToken) 54 | { 55 | return new OperationCanceledException("This operation is cancelled by a call to CancelPendingReceive.", innerException, cancellationToken); 56 | } 57 | public static void ThrowConcurrentReceiveException() 58 | { 59 | throw new InvalidOperationException("Concurrent receive operations are not allowed."); 60 | } 61 | public static Exception NewObjectDisposedForKcpStreamException() 62 | { 63 | return new ObjectDisposedException(nameof(KcpStream)); 64 | } 65 | public static void ThrowObjectDisposedForKcpStreamException() 66 | { 67 | throw new ObjectDisposedException(nameof(KcpStream)); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/KcpSharp/KcpAcknowledgeList.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | using System.Threading; 4 | 5 | namespace KcpSharp 6 | { 7 | internal sealed class KcpAcknowledgeList 8 | { 9 | private readonly KcpSendQueue _sendQueue; 10 | private (uint SerialNumber, uint Timestamp)[] _array; 11 | private int _count; 12 | private SpinLock _lock; 13 | 14 | public KcpAcknowledgeList(KcpSendQueue sendQueue, int windowSize) 15 | { 16 | _array = new (uint SerialNumber, uint Timestamp)[windowSize]; 17 | _count = 0; 18 | _lock = new SpinLock(); 19 | _sendQueue = sendQueue; 20 | } 21 | 22 | public bool TryGetAt(int index, out uint serialNumber, out uint timestamp) 23 | { 24 | bool lockTaken = false; 25 | try 26 | { 27 | _lock.Enter(ref lockTaken); 28 | 29 | if ((uint)index >= (uint)_count) 30 | { 31 | serialNumber = default; 32 | timestamp = default; 33 | return false; 34 | } 35 | 36 | (serialNumber, timestamp) = _array[index]; 37 | return true; 38 | } 39 | finally 40 | { 41 | if (lockTaken) 42 | { 43 | _lock.Exit(); 44 | } 45 | } 46 | } 47 | 48 | public void Clear() 49 | { 50 | bool lockTaken = false; 51 | try 52 | { 53 | _lock.Enter(ref lockTaken); 54 | 55 | _count = 0; 56 | } 57 | finally 58 | { 59 | if (lockTaken) 60 | { 61 | _lock.Exit(); 62 | } 63 | } 64 | _sendQueue.NotifyAckListChanged(false); 65 | } 66 | 67 | public void Add(uint serialNumber, uint timestamp) 68 | { 69 | bool lockTaken = false; 70 | try 71 | { 72 | _lock.Enter(ref lockTaken); 73 | 74 | EnsureCapacity(); 75 | _array[_count++] = (serialNumber, timestamp); 76 | } 77 | finally 78 | { 79 | if (lockTaken) 80 | { 81 | _lock.Exit(); 82 | } 83 | } 84 | _sendQueue.NotifyAckListChanged(true); 85 | } 86 | 87 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 88 | private void EnsureCapacity() 89 | { 90 | if (_count == _array.Length) 91 | { 92 | Expand(); 93 | } 94 | } 95 | 96 | [MethodImpl(MethodImplOptions.NoInlining)] 97 | private void Expand() 98 | { 99 | int capacity = _count + 1; 100 | capacity = Math.Max(capacity + capacity / 2, 16); 101 | var newArray = new (uint SerialNumber, uint Timestamp)[capacity]; 102 | _array.AsSpan(0, _count).CopyTo(newArray); 103 | _array = newArray; 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/KcpSharp/KcpPacketHeader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers.Binary; 3 | using System.Diagnostics; 4 | 5 | namespace KcpSharp 6 | { 7 | internal readonly struct KcpPacketHeader : IEquatable 8 | { 9 | public KcpPacketHeader(KcpCommand command, byte fragment, ushort windowSize, uint timestamp, uint serialNumber, uint unacknowledged) 10 | { 11 | Command = command; 12 | Fragment = fragment; 13 | WindowSize = windowSize; 14 | Timestamp = timestamp; 15 | SerialNumber = serialNumber; 16 | Unacknowledged = unacknowledged; 17 | } 18 | 19 | internal KcpPacketHeader(byte fragment) 20 | { 21 | Command = 0; 22 | Fragment = fragment; 23 | WindowSize = 0; 24 | Timestamp = 0; 25 | SerialNumber = 0; 26 | Unacknowledged = 0; 27 | } 28 | 29 | public KcpCommand Command { get; } 30 | public byte Fragment { get; } 31 | public ushort WindowSize { get; } 32 | public uint Timestamp { get; } 33 | public uint SerialNumber { get; } 34 | public uint Unacknowledged { get; } 35 | 36 | public bool Equals(KcpPacketHeader other) => Command == other.Command && Fragment == other.Fragment && WindowSize == other.WindowSize && Timestamp == other.Timestamp && SerialNumber == other.SerialNumber && Unacknowledged == other.Unacknowledged; 37 | public override bool Equals(object? obj) => obj is KcpPacketHeader other && Equals(other); 38 | public override int GetHashCode() => HashCode.Combine(Command, Fragment, WindowSize, Timestamp, SerialNumber, Unacknowledged); 39 | 40 | public static KcpPacketHeader Parse(ReadOnlySpan buffer) 41 | { 42 | Debug.Assert(buffer.Length >= 16); 43 | return new KcpPacketHeader( 44 | (KcpCommand)buffer[0], 45 | buffer[1], 46 | BinaryPrimitives.ReadUInt16LittleEndian(buffer.Slice(2)), 47 | BinaryPrimitives.ReadUInt32LittleEndian(buffer.Slice(4)), 48 | BinaryPrimitives.ReadUInt32LittleEndian(buffer.Slice(8)), 49 | BinaryPrimitives.ReadUInt32LittleEndian(buffer.Slice(12)) 50 | ); 51 | } 52 | 53 | internal void EncodeHeader(uint? conversationId, int payloadLength, Span destination, out int bytesWritten) 54 | { 55 | Debug.Assert(destination.Length >= 20); 56 | if (conversationId.HasValue) 57 | { 58 | BinaryPrimitives.WriteUInt32LittleEndian(destination, conversationId.GetValueOrDefault()); 59 | destination = destination.Slice(4); 60 | bytesWritten = 24; 61 | } 62 | else 63 | { 64 | bytesWritten = 20; 65 | } 66 | Debug.Assert(destination.Length >= 20); 67 | destination[1] = Fragment; 68 | destination[0] = (byte)Command; 69 | BinaryPrimitives.WriteUInt16LittleEndian(destination.Slice(2), WindowSize); 70 | BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(4), Timestamp); 71 | BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(8), SerialNumber); 72 | BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(12), Unacknowledged); 73 | BinaryPrimitives.WriteInt32LittleEndian(destination.Slice(16), payloadLength); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tests/KcpSharp.Benchmarks/LargeStreamTransferBenchmark.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using BenchmarkDotNet.Attributes; 5 | 6 | namespace KcpSharp.Benchmarks 7 | { 8 | [MemoryDiagnoser] 9 | public class LargeStreamTransferBenchmark 10 | { 11 | private const int MTU = 1400; 12 | private const int fileSize = 16 * 1024 * 1024; // 16MB 13 | 14 | [Params(256)] 15 | public int WindowSize { get; set; } 16 | 17 | [Params(512)] 18 | public int QueueSize { get; set; } 19 | 20 | [Params(30, 50, 100)] 21 | public int UpdateInterval { get; set; } 22 | 23 | [Params(512)] 24 | public int PipeCapacity { get; set; } 25 | 26 | private readonly PreallocatedBufferPool _bufferPool = new(MTU); 27 | private PerfectKcpConversationPipe? _pipe; 28 | private byte[] _sendBuffer = new byte[16384]; 29 | private byte[] _receiveBuffer = new byte[16384]; 30 | 31 | [GlobalSetup] 32 | public void Setup() 33 | { 34 | _bufferPool.Fill(WindowSize * 2 + QueueSize * 2 + PipeCapacity * 2); 35 | _pipe = new PerfectKcpConversationPipe(_bufferPool, MTU, PipeCapacity, new KcpConversationOptions 36 | { 37 | BufferPool = _bufferPool, 38 | Mtu = MTU, 39 | UpdateInterval = UpdateInterval, 40 | StreamMode = true, 41 | SendQueueSize = QueueSize, 42 | ReceiveWindow = QueueSize, 43 | SendWindow = WindowSize, 44 | RemoteReceiveWindow = WindowSize, 45 | DisableCongestionControl = true 46 | }); 47 | } 48 | 49 | [Benchmark] 50 | public Task SendLargeFileAsync() 51 | { 52 | Task sendTask = SendAll(_pipe!.Alice, fileSize, _sendBuffer, CancellationToken.None); 53 | Task receiveTask = ReceiveAll(_pipe!.Bob, fileSize, _receiveBuffer, CancellationToken.None); 54 | return Task.WhenAll(sendTask, receiveTask); 55 | 56 | static async Task SendAll(KcpConversation conversation, int length, byte[] buffer, CancellationToken cancellationToken) 57 | { 58 | int totalSentBytes = 0; 59 | while (totalSentBytes < length) 60 | { 61 | int sendSize = Math.Min(buffer.Length, length - totalSentBytes); 62 | if (!await conversation.SendAsync(buffer.AsMemory(0, sendSize), cancellationToken).ConfigureAwait(false)) 63 | { 64 | return; 65 | } 66 | totalSentBytes += sendSize; 67 | } 68 | await conversation.FlushAsync(cancellationToken).ConfigureAwait(false); 69 | } 70 | 71 | static async Task ReceiveAll(KcpConversation conversation, int length, byte[] buffer, CancellationToken cancellationToken) 72 | { 73 | int totalReceivedBytes = 0; 74 | while (totalReceivedBytes < length) 75 | { 76 | KcpConversationReceiveResult result = await conversation.ReceiveAsync(buffer, cancellationToken).ConfigureAwait(false); 77 | if (result.TransportClosed) 78 | { 79 | break; 80 | } 81 | totalReceivedBytes += result.BytesReceived; 82 | } 83 | } 84 | } 85 | 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /samples/KcpTunnel/KcpTunnelService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers.Binary; 3 | using System.Net; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using KcpSharp; 7 | 8 | namespace KcpTunnel 9 | { 10 | internal sealed class KcpTunnelService : IUdpService, IKcpTransport 11 | { 12 | private readonly IUdpServiceDispatcher _sender; 13 | private readonly EndPoint _endPoint; 14 | private readonly KcpTunnelServiceOptions _options; 15 | private readonly KcpMultiplexConnection _connection; 16 | private CancellationTokenSource? _cts; 17 | 18 | public EndPoint RemoteEndPoint => _endPoint; 19 | 20 | public KcpTunnelService(IUdpServiceDispatcher sender, EndPoint endPoint, KcpTunnelServiceOptions options) 21 | { 22 | _sender = sender; 23 | _endPoint = endPoint; 24 | _options = options; 25 | _connection = new KcpMultiplexConnection(this, state => state?.Dispose()); 26 | } 27 | ValueTask IKcpTransport.SendPacketAsync(Memory packet, CancellationToken cancellationToken) => _sender.SendPacketAsync(_endPoint, packet, cancellationToken); 28 | void IUdpService.SetTransportClosed() => _connection.SetTransportClosed(); 29 | ValueTask IUdpService.InputPacketAsync(ReadOnlyMemory packet, CancellationToken cancellationToken) 30 | { 31 | if (BinaryPrimitives.TryReadInt32LittleEndian(packet.Span, out int id)) 32 | { 33 | if (id != 0 && (uint)id <= ushort.MaxValue && !_connection.Contains(id)) 34 | { 35 | ProcessNewConnection(id); 36 | } 37 | } 38 | return _connection.InputPakcetAsync(packet, cancellationToken); 39 | } 40 | 41 | public void Start() 42 | { 43 | _cts = new CancellationTokenSource(); 44 | _ = Task.Run(() => SendHeartbeatLoop(_cts)); 45 | Console.WriteLine($"Multiplex connection created: {_endPoint}"); 46 | } 47 | 48 | public void Stop() 49 | { 50 | try 51 | { 52 | Interlocked.Exchange(ref _cts, null)?.Cancel(); 53 | } 54 | catch (ObjectDisposedException) 55 | { 56 | // Ignore 57 | } 58 | _connection.Dispose(); 59 | Console.WriteLine($"Multiplex connection eliminated: {_endPoint}"); 60 | } 61 | 62 | private void ProcessNewConnection(int id) 63 | { 64 | TcpForwardConnection.Start(_connection, id, _options); 65 | } 66 | 67 | private async Task SendHeartbeatLoop(CancellationTokenSource cts) 68 | { 69 | CancellationToken cancellationToken = cts.Token; 70 | try 71 | { 72 | KcpRawChannel channel = _connection.CreateRawChannel(0, new KcpRawChannelOptions { ReceiveQueueSize = 1 }); 73 | while (!cancellationToken.IsCancellationRequested) 74 | { 75 | if (!await channel.SendAsync(default, cancellationToken)) 76 | { 77 | break; 78 | } 79 | Console.WriteLine("Heartbeat sent. " + _endPoint); 80 | await Task.Delay(TimeSpan.FromSeconds(30), cancellationToken); 81 | } 82 | } 83 | finally 84 | { 85 | cts.Dispose(); 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /tests/KcpSharp.Benchmarks/LargeStreamTransferNoDelayBenchmark.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using BenchmarkDotNet.Attributes; 5 | 6 | namespace KcpSharp.Benchmarks 7 | { 8 | [MemoryDiagnoser] 9 | public class LargeStreamTransferNoDelayBenchmark 10 | { 11 | private const int MTU = 1400; 12 | private const int fileSize = 128 * 1024 * 1024; // 128MB 13 | 14 | [Params(64)] 15 | public int WindowSize { get; set; } 16 | 17 | [Params(256)] 18 | public int QueueSize { get; set; } 19 | 20 | [Params(30, 50, 100)] 21 | public int UpdateInterval { get; set; } 22 | 23 | [Params(512)] 24 | public int PipeCapacity { get; set; } 25 | 26 | private readonly PreallocatedBufferPool _bufferPool = new(MTU); 27 | private PerfectKcpConversationPipe? _pipe; 28 | private byte[] _sendBuffer = new byte[16384]; 29 | private byte[] _receiveBuffer = new byte[16384]; 30 | 31 | [GlobalSetup] 32 | public void Setup() 33 | { 34 | _bufferPool.Fill(WindowSize * 2 + QueueSize * 2 + PipeCapacity * 2); 35 | _pipe = new PerfectKcpConversationPipe(_bufferPool, MTU, PipeCapacity, new KcpConversationOptions 36 | { 37 | BufferPool = _bufferPool, 38 | Mtu = MTU, 39 | UpdateInterval = UpdateInterval, 40 | StreamMode = true, 41 | SendQueueSize = QueueSize, 42 | ReceiveWindow = QueueSize, 43 | SendWindow = WindowSize, 44 | RemoteReceiveWindow = WindowSize, 45 | DisableCongestionControl = true, 46 | NoDelay = true 47 | }); 48 | } 49 | 50 | [Benchmark] 51 | public Task SendLargeFileAsync() 52 | { 53 | Task sendTask = SendAll(_pipe!.Alice, fileSize, _sendBuffer, CancellationToken.None); 54 | Task receiveTask = ReceiveAll(_pipe!.Bob, fileSize, _receiveBuffer, CancellationToken.None); 55 | return Task.WhenAll(sendTask, receiveTask); 56 | 57 | static async Task SendAll(KcpConversation conversation, int length, byte[] buffer, CancellationToken cancellationToken) 58 | { 59 | int totalSentBytes = 0; 60 | while (totalSentBytes < length) 61 | { 62 | int sendSize = Math.Min(buffer.Length, length - totalSentBytes); 63 | if (!await conversation.SendAsync(buffer.AsMemory(0, sendSize), cancellationToken).ConfigureAwait(false)) 64 | { 65 | return; 66 | } 67 | totalSentBytes += sendSize; 68 | } 69 | await conversation.FlushAsync(cancellationToken).ConfigureAwait(false); 70 | } 71 | 72 | static async Task ReceiveAll(KcpConversation conversation, int length, byte[] buffer, CancellationToken cancellationToken) 73 | { 74 | int totalReceivedBytes = 0; 75 | while (totalReceivedBytes < length) 76 | { 77 | KcpConversationReceiveResult result = await conversation.ReceiveAsync(buffer, cancellationToken).ConfigureAwait(false); 78 | if (result.TransportClosed) 79 | { 80 | break; 81 | } 82 | totalReceivedBytes += result.BytesReceived; 83 | } 84 | } 85 | } 86 | 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /samples/KcpEcho/KcpEchoService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.Net; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using KcpSharp; 7 | 8 | namespace KcpEcho 9 | { 10 | internal class KcpEchoService : IUdpService, IKcpTransport, IDisposable 11 | { 12 | private readonly IUdpServiceDispatcher _sender; 13 | private readonly EndPoint _endPoint; 14 | private readonly KcpConversation _conversation; 15 | private readonly int _mtu; 16 | private CancellationTokenSource? _cts; 17 | 18 | public KcpEchoService(IUdpServiceDispatcher sender, EndPoint endPoint, KcpConversationOptions options, uint conversationId) 19 | { 20 | _sender = sender; 21 | _endPoint = endPoint; 22 | _conversation = new KcpConversation(this, (int)conversationId, options); 23 | _mtu = options.Mtu; 24 | _cts = new CancellationTokenSource(); 25 | _ = Task.Run(() => ReceiveLoop(_cts)); 26 | Console.WriteLine($"{DateTime.Now:O}: Connected from {endPoint}"); 27 | } 28 | 29 | ValueTask IKcpTransport.SendPacketAsync(Memory packet, CancellationToken cancellationToken) 30 | => _sender.SendPacketAsync(_endPoint, packet, cancellationToken); 31 | 32 | ValueTask IUdpService.InputPacketAsync(ReadOnlyMemory packet, CancellationToken cancellationToken) 33 | => _conversation.InputPakcetAsync(packet, cancellationToken); 34 | 35 | void IUdpService.SetTransportClosed() 36 | => _conversation.SetTransportClosed(); 37 | 38 | 39 | private async Task ReceiveLoop(CancellationTokenSource cts) 40 | { 41 | CancellationToken cancellationToken = cts.Token; 42 | try 43 | { 44 | while (!cancellationToken.IsCancellationRequested) 45 | { 46 | KcpConversationReceiveResult result = await _conversation.WaitToReceiveAsync(cancellationToken); 47 | if (result.TransportClosed) 48 | { 49 | break; 50 | } 51 | 52 | byte[] buffer = ArrayPool.Shared.Rent(result.BytesReceived); 53 | try 54 | { 55 | if (!_conversation.TryReceive(buffer, out result)) 56 | { 57 | // We don't need to check for result.TransportClosed because there is no way TryReceive can return true when transport is closed. 58 | return; 59 | } 60 | 61 | Console.WriteLine($"Message received from {_endPoint}. Length = {result.BytesReceived} bytes."); 62 | 63 | if (!await _conversation.SendAsync(buffer.AsMemory(0, result.BytesReceived), cancellationToken)) 64 | { 65 | break; 66 | } 67 | } 68 | finally 69 | { 70 | ArrayPool.Shared.Return(buffer); 71 | } 72 | } 73 | } 74 | finally 75 | { 76 | cts.Dispose(); 77 | } 78 | } 79 | 80 | public void Dispose() 81 | { 82 | Console.WriteLine($"{DateTime.Now:O}: Connection from {_endPoint} eliminated."); 83 | Interlocked.Exchange(ref _cts, null)?.Dispose(); 84 | _conversation.Dispose(); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /samples/KcpChatWasm/PerfectKcpConversationPipe.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | 3 | using System; 4 | using System.Threading; 5 | using System.Threading.Channels; 6 | using System.Threading.Tasks; 7 | using KcpSharp; 8 | 9 | namespace KcpChatWasm 10 | { 11 | internal sealed class PerfectKcpConversationPipe 12 | { 13 | private readonly PerfectOneWayConnection _alice; 14 | private readonly PerfectOneWayConnection _bob; 15 | private readonly Channel _aliceToBobChannel; 16 | private readonly Channel _bobToAliceChannel; 17 | private readonly CancellationTokenSource _cts; 18 | 19 | public KcpConversation Alice => _alice.Conversation; 20 | public KcpConversation Bob => _bob.Conversation; 21 | 22 | public PerfectKcpConversationPipe(uint conversationId, KcpConversationOptions? aliceOptions, KcpConversationOptions? bobOptions) 23 | { 24 | _aliceToBobChannel = Channel.CreateUnbounded(); 25 | _bobToAliceChannel = Channel.CreateUnbounded(); 26 | _alice = new PerfectOneWayConnection(conversationId, _aliceToBobChannel.Writer, aliceOptions); 27 | _bob = new PerfectOneWayConnection(conversationId, _bobToAliceChannel.Writer, bobOptions); 28 | _cts = new CancellationTokenSource(); 29 | _ = Task.Run(() => PipeFromAliceToBob(_cts.Token)); 30 | _ = Task.Run(() => PipeFromBobToAlice(_cts.Token)); 31 | } 32 | 33 | private async Task PipeFromAliceToBob(CancellationToken cancellationToken) 34 | { 35 | while (!cancellationToken.IsCancellationRequested) 36 | { 37 | byte[] packet = await _aliceToBobChannel.Reader.ReadAsync(cancellationToken).ConfigureAwait(false); 38 | await _bob.PutPacketAsync(packet, cancellationToken).ConfigureAwait(false); 39 | } 40 | } 41 | 42 | private async Task PipeFromBobToAlice(CancellationToken cancellationToken) 43 | { 44 | while (!cancellationToken.IsCancellationRequested) 45 | { 46 | byte[] packet = await _bobToAliceChannel.Reader.ReadAsync(cancellationToken).ConfigureAwait(false); 47 | await _alice.PutPacketAsync(packet, cancellationToken).ConfigureAwait(false); 48 | } 49 | } 50 | 51 | public void Dispose() 52 | { 53 | _cts.Cancel(); 54 | _alice.CloseConnection(); 55 | _bob.CloseConnection(); 56 | } 57 | 58 | } 59 | 60 | internal class PerfectOneWayConnection : IKcpTransport 61 | { 62 | private KcpConversation _conversation; 63 | private readonly ChannelWriter _output; 64 | 65 | public PerfectOneWayConnection(uint conversationId, ChannelWriter output, KcpConversationOptions? options = null) 66 | { 67 | _conversation = new KcpConversation(this, (int)conversationId, options); 68 | _output = output; 69 | } 70 | 71 | public KcpConversation Conversation => _conversation; 72 | 73 | public void CloseConnection() 74 | { 75 | _conversation.SetTransportClosed(); 76 | _conversation.Dispose(); 77 | } 78 | 79 | async ValueTask IKcpTransport.SendPacketAsync(Memory packet, CancellationToken cancellationToken) 80 | { 81 | await _output.WriteAsync(packet.ToArray(), cancellationToken).ConfigureAwait(false); 82 | } 83 | 84 | public async Task PutPacketAsync(byte[] packet, CancellationToken cancellationToken) 85 | { 86 | await _conversation.InputPakcetAsync(packet, cancellationToken).ConfigureAwait(false); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/KcpSharp/AsyncAutoResetEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using System.Threading.Tasks.Sources; 6 | 7 | namespace KcpSharp 8 | { 9 | internal class AsyncAutoResetEvent : IValueTaskSource 10 | { 11 | private ManualResetValueTaskSourceCore _rvtsc; 12 | private SpinLock _lock; 13 | private bool _isSet; 14 | private bool _activeWait; 15 | private bool _signaled; 16 | 17 | private T? _value; 18 | 19 | public AsyncAutoResetEvent() 20 | { 21 | _rvtsc = new ManualResetValueTaskSourceCore() 22 | { 23 | RunContinuationsAsynchronously = true 24 | }; 25 | _lock = new SpinLock(); 26 | } 27 | 28 | T IValueTaskSource.GetResult(short token) 29 | { 30 | try 31 | { 32 | return _rvtsc.GetResult(token); 33 | } 34 | finally 35 | { 36 | _rvtsc.Reset(); 37 | 38 | bool lockTaken = false; 39 | try 40 | { 41 | _lock.Enter(ref lockTaken); 42 | 43 | _activeWait = false; 44 | _signaled = false; 45 | } 46 | finally 47 | { 48 | if (lockTaken) 49 | { 50 | _lock.Exit(); 51 | } 52 | } 53 | } 54 | } 55 | 56 | ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) => _rvtsc.GetStatus(token); 57 | void IValueTaskSource.OnCompleted(Action continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) 58 | => _rvtsc.OnCompleted(continuation, state, token, flags); 59 | 60 | public ValueTask WaitAsync() 61 | { 62 | bool lockTaken = false; 63 | try 64 | { 65 | _lock.Enter(ref lockTaken); 66 | 67 | if (_activeWait) 68 | { 69 | return new ValueTask(Task.FromException(new InvalidOperationException("Another thread is already waiting."))); 70 | } 71 | if (_isSet) 72 | { 73 | _isSet = false; 74 | T value = _value!; 75 | _value = default; 76 | return new ValueTask(value); 77 | } 78 | 79 | _activeWait = true; 80 | Debug.Assert(!_signaled); 81 | 82 | return new ValueTask(this, _rvtsc.Version); 83 | } 84 | finally 85 | { 86 | if (lockTaken) 87 | { 88 | _lock.Exit(); 89 | } 90 | } 91 | } 92 | 93 | public void Set(T value) 94 | { 95 | bool lockTaken = false; 96 | try 97 | { 98 | _lock.Enter(ref lockTaken); 99 | 100 | if (_activeWait && !_signaled) 101 | { 102 | _signaled = true; 103 | _rvtsc.SetResult(value); 104 | return; 105 | } 106 | 107 | _isSet = true; 108 | _value = value; 109 | } 110 | finally 111 | { 112 | if (lockTaken) 113 | { 114 | _lock.Exit(); 115 | } 116 | } 117 | } 118 | } 119 | 120 | } 121 | -------------------------------------------------------------------------------- /src/KcpSharp/KcpConversationOptions.cs: -------------------------------------------------------------------------------- 1 | namespace KcpSharp 2 | { 3 | /// 4 | /// Options used to control the behaviors of . 5 | /// 6 | public class KcpConversationOptions 7 | { 8 | /// 9 | /// The buffer pool to rent buffer from. 10 | /// 11 | public IKcpBufferPool? BufferPool { get; set; } 12 | 13 | /// 14 | /// The maximum packet size that can be transmitted over the underlying transport. 15 | /// 16 | public int Mtu { get; set; } = 1400; 17 | 18 | /// 19 | /// The number of packets in the send window. 20 | /// 21 | public int SendWindow { get; set; } = 32; 22 | 23 | /// 24 | /// The number of packets in the receive window. 25 | /// 26 | public int ReceiveWindow { get; set; } = 128; 27 | 28 | /// 29 | /// The nuber of packets in the receive window of the remote host. 30 | /// 31 | public int RemoteReceiveWindow { get; set; } = 128; 32 | 33 | /// 34 | /// The interval in milliseconds to update the internal state of . 35 | /// 36 | public int UpdateInterval { get; set; } = 100; 37 | 38 | /// 39 | /// Wether no-delay mode is enabled. 40 | /// 41 | public bool NoDelay { get; set; } 42 | 43 | /// 44 | /// The number of ACK packet skipped before a resend is triggered. 45 | /// 46 | public int FastResend { get; set; } 47 | 48 | /// 49 | /// Whether congestion control is disabled. 50 | /// 51 | public bool DisableCongestionControl { get; set; } 52 | 53 | /// 54 | /// Whether stream mode is enabled. 55 | /// 56 | public bool StreamMode { get; set; } 57 | 58 | /// 59 | /// The number of packets in the send queue. 60 | /// 61 | public int SendQueueSize { get; set; } 62 | 63 | /// 64 | /// The number of packets in the receive queue. 65 | /// 66 | public int ReceiveQueueSize { get; set; } 67 | 68 | /// 69 | /// The number of bytes to reserve at the start of buffer passed into the underlying transport. The transport should fill this reserved space. 70 | /// 71 | public int PreBufferSize { get; set; } 72 | 73 | /// 74 | /// The number of bytes to reserve at the end of buffer passed into the underlying transport. The transport should fill this reserved space. 75 | /// 76 | public int PostBufferSize { get; set; } 77 | 78 | /// 79 | /// Options for customized keep-alive functionality. 80 | /// 81 | public KcpKeepAliveOptions? KeepAliveOptions { get; set; } 82 | 83 | /// 84 | /// Options for receive window size notification functionality. 85 | /// 86 | public KcpReceiveWindowNotificationOptions? ReceiveWindowNotificationOptions { get; set; } 87 | 88 | internal const int MtuDefaultValue = 1400; 89 | internal const uint SendWindowDefaultValue = 32; 90 | internal const uint ReceiveWindowDefaultValue = 128; 91 | internal const uint RemoteReceiveWindowDefaultValue = 128; 92 | internal const uint UpdateIntervalDefaultValue = 100; 93 | 94 | internal const int SendQueueSizeDefaultValue = 32; 95 | internal const int ReceiveQueueSizeDefaultValue = 32; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /tests/KcpSharp.Tests/Utils/PerfectKcpConversationPipe.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Channels; 4 | using System.Threading.Tasks; 5 | 6 | namespace KcpSharp.Tests 7 | { 8 | internal sealed class PerfectKcpConversationPipe : KcpConversationPipe 9 | { 10 | private readonly PerfectOneWayConnection _alice; 11 | private readonly PerfectOneWayConnection _bob; 12 | private readonly Channel _aliceToBobChannel; 13 | private readonly Channel _bobToAliceChannel; 14 | private readonly CancellationTokenSource _cts; 15 | 16 | public override KcpConversation Alice => _alice.Conversation; 17 | public override KcpConversation Bob => _bob.Conversation; 18 | 19 | public PerfectKcpConversationPipe(uint? conversationId, KcpConversationOptions? aliceOptions, KcpConversationOptions? bobOptions) 20 | { 21 | _aliceToBobChannel = Channel.CreateUnbounded(); 22 | _bobToAliceChannel = Channel.CreateUnbounded(); 23 | _alice = new PerfectOneWayConnection(conversationId, _aliceToBobChannel.Writer, aliceOptions); 24 | _bob = new PerfectOneWayConnection(conversationId, _bobToAliceChannel.Writer, bobOptions); 25 | _cts = new CancellationTokenSource(); 26 | _ = Task.Run(() => PipeFromAliceToBob(_cts.Token)); 27 | _ = Task.Run(() => PipeFromBobToAlice(_cts.Token)); 28 | } 29 | 30 | private async Task PipeFromAliceToBob(CancellationToken cancellationToken) 31 | { 32 | while (!cancellationToken.IsCancellationRequested) 33 | { 34 | byte[] packet = await _aliceToBobChannel.Reader.ReadAsync(cancellationToken).ConfigureAwait(false); 35 | await _bob.PutPacketAsync(packet, cancellationToken).ConfigureAwait(false); 36 | } 37 | } 38 | 39 | private async Task PipeFromBobToAlice(CancellationToken cancellationToken) 40 | { 41 | while (!cancellationToken.IsCancellationRequested) 42 | { 43 | byte[] packet = await _bobToAliceChannel.Reader.ReadAsync(cancellationToken).ConfigureAwait(false); 44 | await _alice.PutPacketAsync(packet, cancellationToken).ConfigureAwait(false); 45 | } 46 | } 47 | 48 | public override void Dispose() 49 | { 50 | _cts.Cancel(); 51 | _alice.CloseConnection(); 52 | _bob.CloseConnection(); 53 | } 54 | 55 | } 56 | 57 | internal class PerfectOneWayConnection : IKcpTransport 58 | { 59 | private KcpConversation _conversation; 60 | private readonly ChannelWriter _output; 61 | 62 | public PerfectOneWayConnection(uint? conversationId, ChannelWriter output, KcpConversationOptions? options = null) 63 | { 64 | _conversation = conversationId.HasValue ? new KcpConversation(this, (int)conversationId.GetValueOrDefault(), options) : new KcpConversation(this, options); 65 | _output = output; 66 | } 67 | 68 | public KcpConversation Conversation => _conversation; 69 | 70 | public void CloseConnection() 71 | { 72 | _conversation.SetTransportClosed(); 73 | _conversation.Dispose(); 74 | } 75 | 76 | async ValueTask IKcpTransport.SendPacketAsync(Memory packet, CancellationToken cancellationToken) 77 | { 78 | await _output.WriteAsync(packet.ToArray(), cancellationToken).ConfigureAwait(false); 79 | } 80 | 81 | public async Task PutPacketAsync(byte[] packet, CancellationToken cancellationToken) 82 | { 83 | await _conversation.InputPakcetAsync(packet, cancellationToken).ConfigureAwait(false); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /samples/KcpTunnel/TcpSourceConnection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Sockets; 3 | using System.Runtime.InteropServices; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using KcpSharp; 7 | 8 | namespace KcpTunnel 9 | { 10 | internal sealed class TcpSourceConnection : IDisposable 11 | { 12 | private readonly IKcpMultiplexConnection _connection; 13 | private readonly ConnectionIdPool _idPool; 14 | private readonly Socket _socket; 15 | private readonly KcpConversation _conversation; 16 | 17 | public static void Start(IKcpMultiplexConnection connection, Socket socket, ConnectionIdPool idPool, KcpTunnelClientOptions options) 18 | { 19 | var conn = new TcpSourceConnection(connection, socket, idPool, options); 20 | _ = Task.Run(() => conn.RunAsync()); 21 | } 22 | 23 | public TcpSourceConnection(IKcpMultiplexConnection connection, Socket socket, ConnectionIdPool idPool, KcpTunnelClientOptions options) 24 | { 25 | _connection = connection; 26 | _idPool = idPool; 27 | _socket = socket; 28 | _conversation = connection.CreateConversation(idPool.Rent(), this, new KcpConversationOptions { Mtu = options.Mtu, StreamMode = true }); 29 | } 30 | 31 | public async Task RunAsync() 32 | { 33 | Console.WriteLine("Processing conversation: " + _conversation.ConversationId); 34 | try 35 | { 36 | // send connection request 37 | // and wait for result 38 | { 39 | byte b = 0; 40 | using var timeoutToken = new CancellationTokenSource(TimeSpan.FromSeconds(60)); 41 | _conversation.TrySend(MemoryMarshal.CreateSpan(ref b, 1)); 42 | Console.WriteLine("Waiting for server to create tunnel. " + _conversation.ConversationId); 43 | KcpConversationReceiveResult result = await _conversation.WaitToReceiveAsync(timeoutToken.Token); 44 | if (result.TransportClosed) 45 | { 46 | return; 47 | } 48 | if (!_conversation.TryReceive(MemoryMarshal.CreateSpan(ref b, 1), out result)) 49 | { 50 | // We don't need to check for result.TransportClosed because there is no way TryReceive can return true when transport is closed. 51 | return; 52 | } 53 | if (b != 0) 54 | { 55 | Console.WriteLine("Failed to create tunnel. " + _conversation.ConversationId); 56 | return; 57 | } 58 | } 59 | 60 | // connection is established 61 | { 62 | Console.WriteLine("Tunnel is established. Exchanging data. " + _conversation.ConversationId); 63 | var dataExchange = new TcpKcpDataExchange(_socket, _conversation); 64 | await dataExchange.RunAsync(); 65 | } 66 | } 67 | catch (Exception ex) 68 | { 69 | Console.WriteLine("Unhandled exception in TcpSourceConnection."); 70 | Console.WriteLine(ex); 71 | } 72 | finally 73 | { 74 | _connection.UnregisterConversation(_conversation.ConversationId.GetValueOrDefault())?.Dispose(); 75 | _conversation.Dispose(); 76 | _idPool.Return((ushort)_conversation.ConversationId.GetValueOrDefault()); 77 | Console.WriteLine("Conversation closed: " + _conversation.ConversationId); 78 | } 79 | } 80 | 81 | public void Dispose() 82 | { 83 | _socket.Dispose(); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /samples/KcpTunnel/TcpForwardConnection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Sockets; 3 | using System.Runtime.CompilerServices; 4 | using System.Runtime.InteropServices; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using KcpSharp; 8 | 9 | namespace KcpTunnel 10 | { 11 | internal sealed class TcpForwardConnection : IDisposable 12 | { 13 | private readonly IKcpMultiplexConnection _connection; 14 | private readonly KcpTunnelServiceOptions _options; 15 | private readonly KcpConversation _conversation; 16 | private Socket? _socket; 17 | 18 | public static void Start(IKcpMultiplexConnection connection, int id, KcpTunnelServiceOptions options) 19 | { 20 | var conn = new TcpForwardConnection(connection, id, options); 21 | _ = Task.Run(() => conn.RunAsync()); 22 | } 23 | 24 | public TcpForwardConnection(IKcpMultiplexConnection connection, int id, KcpTunnelServiceOptions options) 25 | { 26 | _connection = connection; 27 | _options = options; 28 | _options = options; 29 | _conversation = connection.CreateConversation(id, this, new KcpConversationOptions { Mtu = options.Mtu, StreamMode = true }); 30 | } 31 | 32 | public async Task RunAsync() 33 | { 34 | Console.WriteLine("Processing conversation: " + _conversation.ConversationId); 35 | try 36 | { 37 | // connect to remote host 38 | { 39 | using var timeoutToken = new CancellationTokenSource(TimeSpan.FromSeconds(20)); 40 | KcpConversationReceiveResult result = await _conversation.WaitToReceiveAsync(timeoutToken.Token); 41 | Unsafe.SkipInit(out byte b); 42 | if (result.TransportClosed) 43 | { 44 | return; 45 | } 46 | if (!_conversation.TryReceive(MemoryMarshal.CreateSpan(ref b, 1), out result)) 47 | { 48 | // We don't need to check for result.TransportClosed because there is no way TryReceive can return true when transport is closed. 49 | return; 50 | } 51 | _socket = new Socket(SocketType.Stream, ProtocolType.Tcp); 52 | try 53 | { 54 | Console.WriteLine("Connecting to forward endpoint." + _conversation.ConversationId); 55 | await _socket.ConnectAsync(_options.ForwardEndPoint!, timeoutToken.Token); 56 | b = 0; 57 | _conversation.TrySend(MemoryMarshal.CreateSpan(ref b, 1)); 58 | } 59 | catch 60 | { 61 | Console.WriteLine("Connection failed. " + _conversation.ConversationId); 62 | using var replyTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(10)); 63 | b = 1; 64 | _conversation.TrySend(MemoryMarshal.CreateSpan(ref b, 1)); 65 | await _conversation.FlushAsync(replyTimeout.Token); 66 | return; 67 | } 68 | } 69 | 70 | // connection is established 71 | { 72 | Console.WriteLine("Tunnel is established. Exchanging data. " + _conversation.ConversationId); 73 | var dataExchange = new TcpKcpDataExchange(_socket, _conversation); 74 | await dataExchange.RunAsync(); 75 | } 76 | } 77 | finally 78 | { 79 | _connection.UnregisterConversation(_conversation.ConversationId.GetValueOrDefault())?.Dispose(); 80 | _conversation.Dispose(); 81 | Console.WriteLine("Conversation closed: " + _conversation.ConversationId); 82 | } 83 | 84 | } 85 | 86 | public void Dispose() 87 | { 88 | _socket?.Dispose(); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /samples/KcpEcho/KcpEchoClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.Net; 4 | using System.Net.Sockets; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using KcpSharp; 9 | 10 | namespace KcpEcho 11 | { 12 | internal static class KcpEchoClient 13 | { 14 | public static async Task RunAsync(string endpoint, int mtu, uint conversationId, CancellationToken cancellationToken) 15 | { 16 | if (!IPEndPoint.TryParse(endpoint, out IPEndPoint? ipEndPoint)) 17 | { 18 | throw new ArgumentException("endpoint is not a valid IPEndPoint.", nameof(endpoint)); 19 | } 20 | if (mtu < 50 || mtu > ushort.MaxValue) 21 | { 22 | throw new ArgumentOutOfRangeException(nameof(mtu), "mtu is not valid."); 23 | } 24 | 25 | var options = new KcpConversationOptions() 26 | { 27 | Mtu = mtu 28 | }; 29 | 30 | var socket = new Socket(ipEndPoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp); 31 | SocketHelper.PatchSocket(socket); 32 | await socket.ConnectAsync(ipEndPoint, cancellationToken); 33 | 34 | using IKcpTransport transport = KcpSocketTransport.CreateConversation(socket, ipEndPoint, (int)conversationId, options); 35 | transport.Start(); 36 | KcpConversation conversation = transport.Connection; 37 | 38 | _ = Task.Run(() => ReceiveAndDisplay(conversation, cancellationToken)); 39 | 40 | int mss = mtu - 24; 41 | while (!cancellationToken.IsCancellationRequested) 42 | { 43 | Console.WriteLine("Input message: (Press Enter to send)"); 44 | string message = Console.ReadLine() ?? string.Empty; 45 | int length = Encoding.UTF8.GetByteCount(message); 46 | if (length > 256 * mss) 47 | { 48 | Console.WriteLine("Error: input is too long."); 49 | continue; 50 | } 51 | 52 | byte[] buffer = ArrayPool.Shared.Rent(length); 53 | try 54 | { 55 | length = Encoding.UTF8.GetBytes(message, buffer); 56 | if (!await conversation.SendAsync(buffer.AsMemory(0, length), cancellationToken)) 57 | { 58 | break; 59 | } 60 | } 61 | finally 62 | { 63 | ArrayPool.Shared.Return(buffer); 64 | } 65 | } 66 | } 67 | 68 | private static async Task ReceiveAndDisplay(KcpConversation conversation, CancellationToken cancellationToken) 69 | { 70 | while (!cancellationToken.IsCancellationRequested) 71 | { 72 | KcpConversationReceiveResult result = await conversation.WaitToReceiveAsync(cancellationToken); 73 | if (result.TransportClosed) 74 | { 75 | break; 76 | } 77 | 78 | byte[] buffer = ArrayPool.Shared.Rent(result.BytesReceived); 79 | try 80 | { 81 | if (!conversation.TryReceive(buffer, out result)) 82 | { 83 | // We don't need to check for result.TransportClosed because there is no way TryReceive can return true when transport is closed. 84 | return; 85 | } 86 | 87 | try 88 | { 89 | string message = Encoding.UTF8.GetString(buffer.AsSpan(0, result.BytesReceived)); 90 | Console.WriteLine("Received: " + message); 91 | } 92 | catch (Exception) 93 | { 94 | Console.WriteLine("Error: Failed to decode message."); 95 | } 96 | } 97 | finally 98 | { 99 | ArrayPool.Shared.Return(buffer); 100 | } 101 | } 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /tests/KcpSharp.ThroughputBanchmarks/PacketsThroughputBenchmark/PacketsThroughputBenchmarkProgram.cs: -------------------------------------------------------------------------------- 1 | using System.CommandLine; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace KcpSharp.ThroughputBanchmarks.PacketsThroughputBenchmark 6 | { 7 | internal static class PacketsThroughputBenchmarkProgram 8 | { 9 | public static Command BuildCommand() 10 | { 11 | var rootCommand = new Command("packets-throughput", "Run packets throughput benchmark."); 12 | rootCommand.AddCommand(BuildServerCommand()); 13 | rootCommand.AddCommand(BuildClientCommand()); 14 | return rootCommand; 15 | } 16 | 17 | private static Command BuildServerCommand() 18 | { 19 | var command = new Command("server", "Run server side."); 20 | var listenOption = new Option("--listen", "Endpoint where the server listens.") 21 | { 22 | Arity = ArgumentArity.ExactlyOne 23 | }; 24 | var mtuOption = new Option("--mtu", () => 1400, "MTU."); 25 | var windowSizeOption = new Option("--window-size", () => 128, "Window size."); 26 | var updateIntervalOption = new Option("--update-interval", () => 50, "Update interval."); 27 | var noDelayOption = new Option("--no-delay", () => false, "No delay mode."); 28 | 29 | command.AddOption(listenOption); 30 | command.AddOption(mtuOption); 31 | command.AddOption(windowSizeOption); 32 | command.AddOption(updateIntervalOption); 33 | command.AddOption(noDelayOption); 34 | command.SetHandler(RunServerAsync, listenOption, mtuOption, windowSizeOption, updateIntervalOption, noDelayOption); 35 | 36 | return command; 37 | } 38 | 39 | public static Task RunServerAsync(string listen, int mtu, int windowSize, int updateInterval, bool noDelay, CancellationToken cancellationToken) 40 | { 41 | var server = new PacketsThroughputBenchmarkServer(); 42 | return server.RunAsync(listen, mtu, windowSize, updateInterval, noDelay, cancellationToken); 43 | } 44 | 45 | private static Command BuildClientCommand() 46 | { 47 | var command = new Command("client", "Run client side."); 48 | var endpointOption = new Option("--endpoint", "Endpoint which the client connects to.") 49 | { 50 | Arity = ArgumentArity.ExactlyOne 51 | }; 52 | var mtuOption = new Option("--mtu", () => 1400, "MTU."); 53 | var concurrencyOption = new Option("--concurrency", () => 1, "Concurrency."); 54 | var packetSizeOption = new Option("--packet-size", () => 1376, "Packet size."); 55 | var windowSizeOption = new Option("--window-size", () => 128, "Window size."); 56 | var queueSizeOption = new Option("--queue-size", () => 256, "Queue size."); 57 | var updateIntervalOption = new Option("--update-interval", () => 50, "Update interval."); 58 | var noDelayOption = new Option("--no-delay", () => false, "No delay mode."); 59 | 60 | command.AddOption(endpointOption); 61 | command.AddOption(mtuOption); 62 | command.AddOption(concurrencyOption); 63 | command.AddOption(packetSizeOption); 64 | command.AddOption(windowSizeOption); 65 | command.AddOption(queueSizeOption); 66 | command.AddOption(updateIntervalOption); 67 | command.AddOption(noDelayOption); 68 | command.SetHandler(RunClientAsync, endpointOption, mtuOption, concurrencyOption, packetSizeOption, windowSizeOption, queueSizeOption, updateIntervalOption, noDelayOption); 69 | 70 | return command; 71 | } 72 | 73 | public static Task RunClientAsync(string endpoint, int mtu, int concurrency, int packetSize, int windowSize, int queueSize, int updateInterval, bool noDelay, CancellationToken cancellationToken) 74 | { 75 | var client = new PacketsThroughputBenchmarkClient(); 76 | return client.RunAsync(endpoint, mtu, concurrency, packetSize, windowSize, queueSize, updateInterval, noDelay, cancellationToken); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tests/KcpSharp.ThroughputBanchmarks/StreamThroughputBenchmark/StreamThroughputBenchmarkProgram.cs: -------------------------------------------------------------------------------- 1 | using System.CommandLine; 2 | using System.CommandLine.Invocation; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace KcpSharp.ThroughputBanchmarks.StreamThroughputBenchmark 7 | { 8 | internal static class StreamThroughputBenchmarkProgram 9 | { 10 | public static Command BuildCommand() 11 | { 12 | var rootCommand = new Command("stream-throughput", "Run stream throughput benchmark."); 13 | rootCommand.AddCommand(BuildServerCommand()); 14 | rootCommand.AddCommand(BuildClientCommand()); 15 | return rootCommand; 16 | } 17 | 18 | private static Command BuildServerCommand() 19 | { 20 | var command = new Command("server", "Run server side."); 21 | var listenOption = new Option("--listen", "Endpoint where the server listens.") 22 | { 23 | Arity = ArgumentArity.ExactlyOne 24 | }; 25 | var mtuOption = new Option("--mtu", () => 1400, "MTU."); 26 | var windowSizeOption = new Option("--window-size", () => 128, "Window size."); 27 | var updateIntervalOption = new Option("--update-interval", () => 50, "Update interval."); 28 | var noDelayOption = new Option("--no-delay", () => false, "No delay mode."); 29 | 30 | command.AddOption(listenOption); 31 | command.AddOption(mtuOption); 32 | command.AddOption(windowSizeOption); 33 | command.AddOption(updateIntervalOption); 34 | command.AddOption(noDelayOption); 35 | command.SetHandler(RunServerAsync, listenOption, mtuOption, windowSizeOption, updateIntervalOption, noDelayOption); 36 | 37 | return command; 38 | } 39 | 40 | public static Task RunServerAsync(string listen, int mtu, int windowSize, int updateInterval, bool noDelay, CancellationToken cancellationToken) 41 | { 42 | var server = new StreamThroughputBenchmarkServer(); 43 | return server.RunAsync(listen, mtu, windowSize, updateInterval, noDelay, cancellationToken); 44 | } 45 | 46 | private static Command BuildClientCommand() 47 | { 48 | var command = new Command("client", "Run client side."); 49 | var endpointOption = new Option("--endpoint", "Endpoint which the client connects to.") 50 | { 51 | Arity = ArgumentArity.ExactlyOne 52 | }; 53 | var mtuOption = new Option("--mtu", () => 1400, "MTU."); 54 | var concurrencyOption = new Option("--concurrency", () => 1, "Concurrency."); 55 | var bufferSizeOption = new Option("--buffer-size", () => 16384, "Buffer size."); 56 | var windowSizeOption = new Option("--window-size", () => 128, "Window size."); 57 | var queueSizeOption = new Option("--queue-size", () => 256, "Queue size."); 58 | var updateIntervalOption = new Option("--update-interval", () => 50, "Update interval."); 59 | var noDelayOption = new Option("--no-delay", () => false, "No delay mode."); 60 | 61 | command.AddOption(endpointOption); 62 | command.AddOption(mtuOption); 63 | command.AddOption(concurrencyOption); 64 | command.AddOption(bufferSizeOption); 65 | command.AddOption(windowSizeOption); 66 | command.AddOption(queueSizeOption); 67 | command.AddOption(updateIntervalOption); 68 | command.AddOption(noDelayOption); 69 | 70 | command.SetHandler(RunClientAsync, endpointOption, mtuOption, concurrencyOption, bufferSizeOption, windowSizeOption, queueSizeOption, updateIntervalOption, noDelayOption); 71 | 72 | return command; 73 | } 74 | 75 | public static Task RunClientAsync(string endpoint, int mtu, int concurrency, int bufferSize, int windowSize, int queueSize, int updateInterval, bool noDelay, CancellationToken cancellationToken) 76 | { 77 | var client = new StreamThroughputBenchmarkClient(); 78 | return client.RunAsync(endpoint, mtu, concurrency, bufferSize, windowSize, queueSize, updateInterval, noDelay, cancellationToken); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tests/KcpSharp.Tests/RawChannelTransferTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Xunit; 7 | 8 | namespace KcpSharp.Tests 9 | { 10 | public class RawChannelTransferTests 11 | { 12 | [InlineData(64, 1000, 1200, false)] 13 | [InlineData(64, 1000, 1200, true)] 14 | [Theory] 15 | public Task TestMultiplePacketSendReceive(int packetCount, int minPacketSize, int maxPacketSize, bool waitToReceive) 16 | { 17 | List packets = new( 18 | Enumerable.Range(1, packetCount) 19 | .Select(i => new byte[Random.Shared.Next(minPacketSize, maxPacketSize + 1)]) 20 | ); 21 | foreach (byte[] packet in packets) 22 | { 23 | Random.Shared.NextBytes(packet); 24 | } 25 | 26 | return TestHelper.RunWithTimeout(TimeSpan.FromSeconds(40), async cancellationToken => 27 | { 28 | using KcpRawDuplexChannel pipe = KcpRawDuplexChannelFactory.CreateDuplexChannel(0x12345678, new KcpRawChannelOptions { ReceiveQueueSize = packetCount }); 29 | 30 | Assert.False(pipe.Bob.TryPeek(out KcpConversationReceiveResult result)); 31 | Assert.False(result.TransportClosed, "Transport should not be closed."); 32 | Assert.Equal(0, result.BytesReceived); 33 | 34 | Task sendTask = SendMultplePacketsAsync(pipe.Alice, packets, cancellationToken); 35 | Task receiveTask = ReceiveMultiplePacketsAsync(pipe.Bob, packets, waitToReceive, cancellationToken); 36 | await Task.WhenAll(sendTask, receiveTask); 37 | 38 | await AssertReceiveNoDataAsync(pipe.Bob, waitToReceive, new byte[1400]); 39 | }); 40 | 41 | 42 | static async Task SendMultplePacketsAsync(KcpRawChannel conversation, IEnumerable packets, CancellationToken cancellationToken) 43 | { 44 | foreach (byte[] packet in packets) 45 | { 46 | Assert.True(await conversation.SendAsync(packet, cancellationToken)); 47 | await Task.Delay(50, cancellationToken); 48 | } 49 | } 50 | 51 | static async Task ReceiveMultiplePacketsAsync(KcpRawChannel conversation, IEnumerable packets, bool waitToReceive, CancellationToken cancellationToken) 52 | { 53 | byte[] buffer = new byte[1400]; 54 | foreach (byte[] packet in packets) 55 | { 56 | Assert.True(packet.Length <= buffer.Length); 57 | KcpConversationReceiveResult result; 58 | if (waitToReceive) 59 | { 60 | result = await conversation.WaitToReceiveAsync(cancellationToken); 61 | Assert.False(result.TransportClosed, "Transport should not be closed."); 62 | Assert.Equal(packet.Length, result.BytesReceived); 63 | 64 | Assert.True(conversation.TryPeek(out result)); 65 | Assert.False(result.TransportClosed, "Transport should not be closed."); 66 | Assert.Equal(packet.Length, result.BytesReceived); 67 | } 68 | 69 | result = await conversation.ReceiveAsync(buffer, cancellationToken); 70 | Assert.False(result.TransportClosed, "Transport should not be closed."); 71 | Assert.Equal(packet.Length, result.BytesReceived); 72 | Assert.True(buffer.AsSpan(0, result.BytesReceived).SequenceEqual(packet)); 73 | } 74 | } 75 | } 76 | 77 | private static async Task AssertReceiveNoDataAsync(KcpRawChannel channel, bool waitToReceive, byte[]? buffer = null) 78 | { 79 | buffer = buffer ?? new byte[1]; 80 | 81 | if (waitToReceive) 82 | { 83 | using var cts = new CancellationTokenSource(1000); 84 | await Assert.ThrowsAsync(async () => await channel.WaitToReceiveAsync(cts.Token)); 85 | } 86 | 87 | { 88 | using var cts = new CancellationTokenSource(1000); 89 | await Assert.ThrowsAsync(async () => await channel.ReceiveAsync(buffer, cts.Token)); 90 | } 91 | } 92 | 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tests/KcpSharp.Tests/RawChannelTransferTestsNoConversationId.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Xunit; 7 | 8 | namespace KcpSharp.Tests 9 | { 10 | public class RawChannelTransferTestsNoConversationId 11 | { 12 | [InlineData(64, 1000, 1200, false)] 13 | [InlineData(64, 1000, 1200, true)] 14 | [Theory] 15 | public Task TestMultiplePacketSendReceive(int packetCount, int minPacketSize, int maxPacketSize, bool waitToReceive) 16 | { 17 | List packets = new( 18 | Enumerable.Range(1, packetCount) 19 | .Select(i => new byte[Random.Shared.Next(minPacketSize, maxPacketSize + 1)]) 20 | ); 21 | foreach (byte[] packet in packets) 22 | { 23 | Random.Shared.NextBytes(packet); 24 | } 25 | 26 | return TestHelper.RunWithTimeout(TimeSpan.FromSeconds(40), async cancellationToken => 27 | { 28 | using KcpRawDuplexChannel pipe = KcpRawDuplexChannelFactory.CreateDuplexChannel(new KcpRawChannelOptions { ReceiveQueueSize = packetCount }); 29 | 30 | Assert.False(pipe.Bob.TryPeek(out KcpConversationReceiveResult result)); 31 | Assert.False(result.TransportClosed, "Transport should not be closed."); 32 | Assert.Equal(0, result.BytesReceived); 33 | 34 | Task sendTask = SendMultplePacketsAsync(pipe.Alice, packets, cancellationToken); 35 | Task receiveTask = ReceiveMultiplePacketsAsync(pipe.Bob, packets, waitToReceive, cancellationToken); 36 | await Task.WhenAll(sendTask, receiveTask); 37 | 38 | await AssertReceiveNoDataAsync(pipe.Bob, waitToReceive, new byte[1400]); 39 | }); 40 | 41 | 42 | static async Task SendMultplePacketsAsync(KcpRawChannel conversation, IEnumerable packets, CancellationToken cancellationToken) 43 | { 44 | foreach (byte[] packet in packets) 45 | { 46 | Assert.True(await conversation.SendAsync(packet, cancellationToken)); 47 | await Task.Delay(50, cancellationToken); 48 | } 49 | } 50 | 51 | static async Task ReceiveMultiplePacketsAsync(KcpRawChannel conversation, IEnumerable packets, bool waitToReceive, CancellationToken cancellationToken) 52 | { 53 | byte[] buffer = new byte[1400]; 54 | foreach (byte[] packet in packets) 55 | { 56 | Assert.True(packet.Length <= buffer.Length); 57 | KcpConversationReceiveResult result; 58 | if (waitToReceive) 59 | { 60 | result = await conversation.WaitToReceiveAsync(cancellationToken); 61 | Assert.False(result.TransportClosed, "Transport should not be closed."); 62 | Assert.Equal(packet.Length, result.BytesReceived); 63 | 64 | Assert.True(conversation.TryPeek(out result)); 65 | Assert.False(result.TransportClosed, "Transport should not be closed."); 66 | Assert.Equal(packet.Length, result.BytesReceived); 67 | } 68 | 69 | result = await conversation.ReceiveAsync(buffer, cancellationToken); 70 | Assert.False(result.TransportClosed, "Transport should not be closed."); 71 | Assert.Equal(packet.Length, result.BytesReceived); 72 | Assert.True(buffer.AsSpan(0, result.BytesReceived).SequenceEqual(packet)); 73 | } 74 | } 75 | } 76 | 77 | private static async Task AssertReceiveNoDataAsync(KcpRawChannel channel, bool waitToReceive, byte[]? buffer = null) 78 | { 79 | buffer = buffer ?? new byte[1]; 80 | 81 | if (waitToReceive) 82 | { 83 | using var cts = new CancellationTokenSource(1000); 84 | await Assert.ThrowsAsync(async () => await channel.WaitToReceiveAsync(cts.Token)); 85 | } 86 | 87 | { 88 | using var cts = new CancellationTokenSource(1000); 89 | await Assert.ThrowsAsync(async () => await channel.ReceiveAsync(buffer, cts.Token)); 90 | } 91 | } 92 | 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tests/KcpSharp.ThroughputBanchmarks/MemoryPool/PinnedBlockMemoryPool.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.Collections.Concurrent; 4 | 5 | namespace KcpSharp.ThroughputBanchmarks 6 | { 7 | /// 8 | /// Used to allocate and distribute re-usable blocks of memory. 9 | /// 10 | internal sealed class PinnedBlockMemoryPool : MemoryPool, IKcpBufferPool 11 | { 12 | /// 13 | /// The size of a block. 14 | /// 15 | private readonly int _blockSize; 16 | 17 | /// 18 | /// Max allocation block size for pooled blocks, 19 | /// larger values can be leased but they will be disposed after use rather than returned to the pool. 20 | /// 21 | public override int MaxBufferSize => _blockSize; 22 | 23 | /// 24 | /// Thread-safe collection of blocks which are currently in the pool. A slab will pre-allocate all of the block tracking objects 25 | /// and add them to this collection. When memory is requested it is taken from here first, and when it is returned it is re-added. 26 | /// 27 | private readonly ConcurrentQueue _blocks = new ConcurrentQueue(); 28 | 29 | /// 30 | /// This is part of implementing the IDisposable pattern. 31 | /// 32 | private bool _isDisposed; // To detect redundant calls 33 | 34 | private readonly object _disposeSync = new object(); 35 | 36 | /// 37 | /// This default value passed in to Rent to use the default value for the pool. 38 | /// 39 | private const int AnySize = -1; 40 | 41 | public PinnedBlockMemoryPool(int blockSize) 42 | { 43 | if (blockSize < 0) 44 | { 45 | throw new ArgumentOutOfRangeException(nameof(blockSize)); 46 | } 47 | _blockSize = blockSize; 48 | } 49 | 50 | public override IMemoryOwner Rent(int size = AnySize) 51 | { 52 | if (size > _blockSize) 53 | { 54 | throw new ArgumentOutOfRangeException(nameof(size)); 55 | } 56 | 57 | if (_isDisposed) 58 | { 59 | throw new ObjectDisposedException(nameof(PinnedBlockMemoryPool)); 60 | } 61 | 62 | if (_blocks.TryDequeue(out MemoryPoolBlock? block)) 63 | { 64 | // block successfully taken from the stack - return it 65 | return block; 66 | } 67 | return new MemoryPoolBlock(this, _blockSize); 68 | } 69 | 70 | /// 71 | /// Called to return a block to the pool. Once Return has been called the memory no longer belongs to the caller, and 72 | /// Very Bad Things will happen if the memory is read of modified subsequently. If a caller fails to call Return and the 73 | /// block tracking object is garbage collected, the block tracking object's finalizer will automatically re-create and return 74 | /// a new tracking object into the pool. This will only happen if there is a bug in the server, however it is necessary to avoid 75 | /// leaving "dead zones" in the slab due to lost block tracking objects. 76 | /// 77 | /// The block to return. It must have been acquired by calling Lease on the same memory pool instance. 78 | internal void Return(MemoryPoolBlock block) 79 | { 80 | if (!_isDisposed) 81 | { 82 | _blocks.Enqueue(block); 83 | } 84 | } 85 | 86 | protected override void Dispose(bool disposing) 87 | { 88 | if (_isDisposed) 89 | { 90 | return; 91 | } 92 | 93 | lock (_disposeSync) 94 | { 95 | _isDisposed = true; 96 | 97 | if (disposing) 98 | { 99 | // Discard blocks in pool 100 | while (_blocks.TryDequeue(out _)) 101 | { 102 | 103 | } 104 | } 105 | } 106 | } 107 | 108 | KcpRentedBuffer IKcpBufferPool.Rent(KcpBufferPoolRentOptions options) => options.Size <= _blockSize ? KcpRentedBuffer.FromMemoryOwner(Rent(_blockSize)) : KcpRentedBuffer.FromSharedArrayPool(options.Size); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /tests/KcpSharp.Tests/Utils/KcpRawDuplexChannel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Channels; 4 | using System.Threading.Tasks; 5 | 6 | namespace KcpSharp.Tests 7 | { 8 | internal static class KcpRawDuplexChannelFactory 9 | { 10 | public static KcpRawDuplexChannel CreateDuplexChannel(uint conversationId) 11 | { 12 | return new KcpRawDuplexChannel(conversationId, null, null); 13 | } 14 | 15 | public static KcpRawDuplexChannel CreateDuplexChannel(uint conversationId, KcpRawChannelOptions? options) 16 | { 17 | return new KcpRawDuplexChannel(conversationId, options, options); 18 | } 19 | 20 | public static KcpRawDuplexChannel CreateDuplexChannel() 21 | { 22 | return new KcpRawDuplexChannel(null, null, null); 23 | } 24 | 25 | public static KcpRawDuplexChannel CreateDuplexChannel(KcpRawChannelOptions? options) 26 | { 27 | return new KcpRawDuplexChannel(null, options, options); 28 | } 29 | } 30 | 31 | internal class KcpRawDuplexChannel : IDisposable 32 | { 33 | private readonly KcpRawOneWayChannel _alice; 34 | private readonly KcpRawOneWayChannel _bob; 35 | private readonly Channel _aliceToBobChannel; 36 | private readonly Channel _bobToAliceChannel; 37 | private readonly CancellationTokenSource _cts; 38 | 39 | public KcpRawChannel Alice => _alice.Conversation; 40 | public KcpRawChannel Bob => _bob.Conversation; 41 | 42 | public KcpRawDuplexChannel(uint? conversationId, KcpRawChannelOptions? aliceOptions, KcpRawChannelOptions? bobOptions) 43 | { 44 | _aliceToBobChannel = Channel.CreateUnbounded(); 45 | _bobToAliceChannel = Channel.CreateUnbounded(); 46 | _alice = new KcpRawOneWayChannel(conversationId, _aliceToBobChannel.Writer, aliceOptions); 47 | _bob = new KcpRawOneWayChannel(conversationId, _bobToAliceChannel.Writer, bobOptions); 48 | _cts = new CancellationTokenSource(); 49 | _ = Task.Run(() => PipeFromAliceToBob(_cts.Token)); 50 | _ = Task.Run(() => PipeFromBobToAlice(_cts.Token)); 51 | } 52 | 53 | private async Task PipeFromAliceToBob(CancellationToken cancellationToken) 54 | { 55 | while (!cancellationToken.IsCancellationRequested) 56 | { 57 | byte[] packet = await _aliceToBobChannel.Reader.ReadAsync(cancellationToken).ConfigureAwait(false); 58 | await _bob.PutPacketAsync(packet, cancellationToken).ConfigureAwait(false); 59 | } 60 | } 61 | 62 | private async Task PipeFromBobToAlice(CancellationToken cancellationToken) 63 | { 64 | while (!cancellationToken.IsCancellationRequested) 65 | { 66 | byte[] packet = await _bobToAliceChannel.Reader.ReadAsync(cancellationToken).ConfigureAwait(false); 67 | await _alice.PutPacketAsync(packet, cancellationToken).ConfigureAwait(false); 68 | } 69 | } 70 | 71 | public void Dispose() 72 | { 73 | _cts.Cancel(); 74 | _alice.CloseConnection(); 75 | _bob.CloseConnection(); 76 | } 77 | } 78 | 79 | internal class KcpRawOneWayChannel : IKcpTransport 80 | { 81 | private readonly KcpRawChannel _conversation; 82 | private readonly ChannelWriter _output; 83 | 84 | public KcpRawOneWayChannel(uint? conversationId, ChannelWriter output, KcpRawChannelOptions? options = null) 85 | { 86 | _conversation = conversationId.HasValue ? new KcpRawChannel(this, (int)conversationId.GetValueOrDefault(), options) : new KcpRawChannel(this, options); 87 | _output = output; 88 | } 89 | 90 | public KcpRawChannel Conversation => _conversation; 91 | 92 | public void CloseConnection() 93 | { 94 | _conversation.SetTransportClosed(); 95 | } 96 | 97 | async ValueTask IKcpTransport.SendPacketAsync(Memory packet, CancellationToken cancellationToken) 98 | { 99 | await _output.WriteAsync(packet.ToArray(), cancellationToken).ConfigureAwait(false); 100 | } 101 | 102 | public async Task PutPacketAsync(byte[] packet, CancellationToken cancellationToken) 103 | { 104 | await ((IKcpConversation)_conversation).InputPakcetAsync(packet, cancellationToken).ConfigureAwait(false); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /tests/KcpSharp.Benchmarks/PerfectKcpConversationPipe.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace KcpSharp.Benchmarks 7 | { 8 | internal sealed class PerfectKcpConversationPipe : IDisposable 9 | { 10 | private readonly PerfectOneWayConnection _alice; 11 | private readonly PerfectOneWayConnection _bob; 12 | private readonly PreallocatedQueue<(KcpRentedBuffer Owner, int Length)> _aliceToBobChannel; 13 | private readonly PreallocatedQueue<(KcpRentedBuffer Owner, int Length)> _bobToAliceChannel; 14 | private readonly CancellationTokenSource _cts; 15 | 16 | public KcpConversation Alice => _alice.Conversation; 17 | public KcpConversation Bob => _bob.Conversation; 18 | 19 | public PerfectKcpConversationPipe(IKcpBufferPool bufferPool, int mtu, int capacity, KcpConversationOptions? options) 20 | { 21 | _aliceToBobChannel = new PreallocatedQueue<(KcpRentedBuffer Owner, int Length)>(capacity); 22 | _bobToAliceChannel = new PreallocatedQueue<(KcpRentedBuffer Owner, int Length)>(capacity); 23 | _alice = new PerfectOneWayConnection(bufferPool, mtu, _aliceToBobChannel, options); 24 | _bob = new PerfectOneWayConnection(bufferPool, mtu, _bobToAliceChannel, options); 25 | _cts = new CancellationTokenSource(); 26 | _ = Task.Run(() => PipeFromAliceToBob(_cts.Token)); 27 | _ = Task.Run(() => PipeFromBobToAlice(_cts.Token)); 28 | } 29 | 30 | private async Task PipeFromAliceToBob(CancellationToken cancellationToken) 31 | { 32 | while (!cancellationToken.IsCancellationRequested) 33 | { 34 | (KcpRentedBuffer owner, int length) = await _aliceToBobChannel.ReadAsync(cancellationToken).ConfigureAwait(false); 35 | await _bob.PutPacketAsync(owner.Memory.Slice(0, length), cancellationToken).ConfigureAwait(false); 36 | owner.Dispose(); 37 | } 38 | } 39 | 40 | private async Task PipeFromBobToAlice(CancellationToken cancellationToken) 41 | { 42 | while (!cancellationToken.IsCancellationRequested) 43 | { 44 | (KcpRentedBuffer owner, int length) = await _bobToAliceChannel.ReadAsync(cancellationToken).ConfigureAwait(false); 45 | await _alice.PutPacketAsync(owner.Memory.Slice(0, length), cancellationToken).ConfigureAwait(false); 46 | owner.Dispose(); 47 | } 48 | } 49 | 50 | public void Dispose() 51 | { 52 | _cts.Cancel(); 53 | _alice.CloseConnection(); 54 | _bob.CloseConnection(); 55 | } 56 | } 57 | 58 | internal class PerfectOneWayConnection : IKcpTransport 59 | { 60 | private readonly IKcpBufferPool _bufferPool; 61 | private readonly int _mtu; 62 | private readonly PreallocatedQueue<(KcpRentedBuffer Owner, int Length)> _channel; 63 | private readonly KcpConversation _conversation; 64 | 65 | public PerfectOneWayConnection(IKcpBufferPool bufferPool, int mtu, PreallocatedQueue<(KcpRentedBuffer Owner, int Length)> channel, KcpConversationOptions? options = null) 66 | { 67 | _bufferPool = bufferPool; 68 | _mtu = mtu; 69 | _channel = channel; 70 | _conversation = new KcpConversation(this, options); 71 | } 72 | 73 | public KcpConversation Conversation => _conversation; 74 | 75 | public void CloseConnection() 76 | { 77 | _conversation.SetTransportClosed(); 78 | _conversation.Dispose(); 79 | } 80 | 81 | [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] 82 | async ValueTask IKcpTransport.SendPacketAsync(Memory packet, CancellationToken cancellationToken) 83 | { 84 | await Task.Yield(); // The purpose is to simulate async writing so that we can validate our customized async method builder works. 85 | if (packet.Length > _mtu) 86 | { 87 | return; 88 | } 89 | KcpRentedBuffer owner = _bufferPool.Rent(new KcpBufferPoolRentOptions(_mtu, false)); 90 | packet.CopyTo(owner.Memory); 91 | if (!_channel.TryWrite((owner, packet.Length))) 92 | { 93 | owner.Dispose(); 94 | } 95 | } 96 | 97 | public ValueTask PutPacketAsync(ReadOnlyMemory packet, CancellationToken cancellationToken) 98 | => _conversation.InputPakcetAsync(packet, cancellationToken); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /tests/KcpSharp.Tests/SendAndFlushTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Xunit; 5 | 6 | namespace KcpSharp.Tests 7 | { 8 | public class SendAndFlushTests 9 | { 10 | [Fact] 11 | public Task FlushEmptyQueueAsync() 12 | { 13 | return TestHelper.RunWithTimeout(TimeSpan.FromSeconds(10), async cancellationToken => 14 | { 15 | using KcpConversationPipe pipe = KcpConversationFactory.CreatePerfectPipe(0x12345678); 16 | Assert.True(await pipe.Alice.FlushAsync(cancellationToken)); 17 | }); 18 | } 19 | 20 | [Fact] 21 | public Task FlushAfterDispose() 22 | { 23 | return TestHelper.RunWithTimeout(TimeSpan.FromSeconds(10), async cancellationToken => 24 | { 25 | using KcpConversationPipe pipe = KcpConversationFactory.CreatePerfectPipe(0x12345678); 26 | pipe.Alice.Dispose(); 27 | Assert.False(await pipe.Alice.FlushAsync(cancellationToken)); 28 | }); 29 | } 30 | 31 | [Fact] 32 | public Task FlushAfterTransportClosed() 33 | { 34 | return TestHelper.RunWithTimeout(TimeSpan.FromSeconds(10), async cancellationToken => 35 | { 36 | using KcpConversationPipe pipe = KcpConversationFactory.CreatePerfectPipe(0x12345678); 37 | pipe.Alice.SetTransportClosed(); 38 | Assert.False(await pipe.Alice.FlushAsync(cancellationToken)); 39 | }); 40 | } 41 | 42 | [Fact] 43 | public Task FlushWithLargeWindowSize() 44 | { 45 | return TestHelper.RunWithTimeout(TimeSpan.FromSeconds(10), async cancellationToken => 46 | { 47 | using KcpConversationPipe pipe = KcpConversationFactory.CreatePerfectPipe(0x12345678); 48 | Assert.Equal(0, pipe.Alice.UnflushedBytes); 49 | for (int i = 0; i < 6; i++) 50 | { 51 | await pipe.Alice.SendAsync(new byte[100], cancellationToken); 52 | } 53 | Assert.True(await pipe.Alice.FlushAsync(cancellationToken)); 54 | Assert.Equal(0, pipe.Alice.UnflushedBytes); 55 | }); 56 | } 57 | 58 | [Fact] 59 | public Task FlushWithSmallWindowSize() 60 | { 61 | return TestHelper.RunWithTimeout(TimeSpan.FromSeconds(15), async cancellationToken => 62 | { 63 | using KcpConversationPipe pipe = KcpConversationFactory.CreatePerfectPipe(0x12345678, new KcpConversationOptions { SendWindow = 2, ReceiveWindow = 2, RemoteReceiveWindow = 2, SendQueueSize = 2, ReceiveQueueSize = 2, UpdateInterval = 10, NoDelay = true }); 64 | 65 | await SendPackets(pipe.Alice, 6, cancellationToken); 66 | Task flushTask = pipe.Alice.FlushAsync(cancellationToken).AsTask(); 67 | Assert.False(flushTask.IsCompleted); 68 | Assert.True(pipe.Alice.UnflushedBytes > 0); 69 | 70 | await ReceiveAllAsync(pipe.Bob, 1, cancellationToken); 71 | await Task.Delay(200, cancellationToken); 72 | Assert.False(flushTask.IsCompleted); 73 | Assert.True(pipe.Alice.UnflushedBytes > 0); 74 | 75 | await ReceiveAllAsync(pipe.Bob, 1, cancellationToken); 76 | Assert.False(flushTask.IsCompleted); 77 | Assert.True(pipe.Alice.UnflushedBytes > 0); 78 | 79 | await ReceiveAllAsync(pipe.Bob, 1, cancellationToken); 80 | Assert.False(flushTask.IsCompleted); 81 | Assert.True(pipe.Alice.UnflushedBytes > 0); 82 | 83 | await ReceiveAllAsync(pipe.Bob, 1, cancellationToken); 84 | Assert.False(flushTask.IsCompleted); 85 | Assert.True(pipe.Alice.UnflushedBytes > 0); 86 | 87 | await ReceiveAllAsync(pipe.Bob, 2, cancellationToken); 88 | await Task.Delay(200, cancellationToken); 89 | Assert.True(flushTask.IsCompleted); 90 | await flushTask; 91 | Assert.Equal(0, pipe.Alice.UnflushedBytes); 92 | }); 93 | 94 | static async Task SendPackets(KcpConversation conversation, int count, CancellationToken cancellationToken) 95 | { 96 | for (int i = 0; i < count; i++) 97 | { 98 | Assert.True(await conversation.SendAsync(new byte[100], cancellationToken)); 99 | } 100 | } 101 | 102 | static async Task ReceiveAllAsync(KcpConversation conversation, int count, CancellationToken cancellationToken) 103 | { 104 | byte[] buffer = new byte[100]; 105 | for (int i = 0; i < count; i++) 106 | { 107 | await conversation.ReceiveAsync(buffer, cancellationToken); 108 | } 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /samples/KcpTunnel/KcpTunnelClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Net.Sockets; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using KcpSharp; 7 | 8 | namespace KcpTunnel 9 | { 10 | internal static class KcpTunnelClientProgram 11 | { 12 | public static async Task RunAsync(string endpoint, string tcpListen, int mtu, CancellationToken cancellationToken) 13 | { 14 | if (string.IsNullOrEmpty(endpoint) || !IPEndPoint.TryParse(endpoint, out IPEndPoint? connectEndPoint)) 15 | { 16 | throw new ArgumentException("endpoint is not a valid IPEndPoint.", nameof(endpoint)); 17 | } 18 | if (string.IsNullOrEmpty(tcpListen) || !IPEndPoint.TryParse(tcpListen, out IPEndPoint? tcpListenEndPoint)) 19 | { 20 | throw new ArgumentException("tcpForward is not a valid IPEndPoint.", nameof(tcpListen)); 21 | } 22 | if (mtu < 64 || mtu > ushort.MaxValue) 23 | { 24 | throw new ArgumentOutOfRangeException(nameof(mtu)); 25 | } 26 | 27 | var client = new KcpTunnelClient(connectEndPoint, tcpListenEndPoint, new KcpTunnelClientOptions { Mtu = mtu }); 28 | try 29 | { 30 | await client.RunAsync(cancellationToken); 31 | } 32 | catch (OperationCanceledException) 33 | { 34 | // Ignore 35 | } 36 | } 37 | } 38 | 39 | internal class KcpTunnelClient 40 | { 41 | private readonly ConnectionIdPool _idPool; 42 | private readonly IPEndPoint _connectEndPoint; 43 | private readonly IPEndPoint _listenEndPoint; 44 | private readonly KcpTunnelClientOptions _options; 45 | 46 | public KcpTunnelClient(IPEndPoint connectEndPoint, IPEndPoint listenEndPoint, KcpTunnelClientOptions options) 47 | { 48 | _idPool = new ConnectionIdPool(); 49 | _connectEndPoint = connectEndPoint; 50 | _listenEndPoint = listenEndPoint; 51 | _options = options; 52 | } 53 | 54 | public async Task RunAsync(CancellationToken cancellationToken) 55 | { 56 | using var socket = new Socket(_connectEndPoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp); 57 | SocketHelper.PatchSocket(socket); 58 | await socket.ConnectAsync(_connectEndPoint, cancellationToken); 59 | using IKcpTransport> connection = KcpSocketTransport.CreateMultiplexConnection(socket, _connectEndPoint, _options.Mtu, state => state?.Dispose()); 60 | connection.Start(); 61 | 62 | using (cancellationToken.UnsafeRegister(state => ((IDisposable?)state)!.Dispose(), connection)) 63 | { 64 | _ = Task.Run(() => SendHeartbeatLoop(connection.Connection, cancellationToken)); 65 | await AcceptLoop(connection.Connection, cancellationToken); 66 | } 67 | } 68 | 69 | private async Task AcceptLoop(IKcpMultiplexConnection connection, CancellationToken cancellationToken) 70 | { 71 | using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp); 72 | socket.Bind(_listenEndPoint); 73 | socket.Listen(); 74 | using (cancellationToken.UnsafeRegister(s => ((Socket?)s)!.Dispose(), socket)) 75 | { 76 | try 77 | { 78 | while (!cancellationToken.IsCancellationRequested) 79 | { 80 | Socket acceptSocket = await socket.AcceptAsync(); 81 | ProcessNewConnection(connection, acceptSocket); 82 | } 83 | } 84 | catch (Exception ex) 85 | { 86 | Console.WriteLine(ex); 87 | } 88 | } 89 | } 90 | 91 | private void ProcessNewConnection(IKcpMultiplexConnection connection, Socket socket) 92 | { 93 | TcpSourceConnection.Start(connection, socket, _idPool, _options); 94 | } 95 | 96 | private async Task SendHeartbeatLoop(IKcpMultiplexConnection connection, CancellationToken cancellationToken) 97 | { 98 | try 99 | { 100 | KcpRawChannel channel = connection.CreateRawChannel(0, new KcpRawChannelOptions { ReceiveQueueSize = 1 }); 101 | while (!cancellationToken.IsCancellationRequested) 102 | { 103 | if (!await channel.SendAsync(default, cancellationToken)) 104 | { 105 | break; 106 | } 107 | Console.WriteLine("Heartbeat sent. " + _connectEndPoint); 108 | await Task.Delay(TimeSpan.FromSeconds(30), cancellationToken); 109 | } 110 | } 111 | catch (OperationCanceledException) 112 | { 113 | // Ignore 114 | } 115 | } 116 | 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /tests/KcpSharp.Benchmarks/PreallocatedQueue.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using System.Threading.Tasks.Sources; 6 | 7 | namespace KcpSharp.Benchmarks 8 | { 9 | internal sealed class PreallocatedQueue 10 | { 11 | private readonly LinkedList _queue; 12 | private readonly LinkedList _cache; 13 | private readonly DequeueOperation _dequeueOperation; 14 | 15 | public PreallocatedQueue(int size) 16 | { 17 | _queue = new LinkedList(); 18 | _cache = new LinkedList(); 19 | _dequeueOperation = new DequeueOperation(_queue, _cache); 20 | 21 | for (int i = 0; i < size; i++) 22 | { 23 | _cache.AddLast(default(T)!); 24 | } 25 | } 26 | 27 | public bool TryWrite(T value) 28 | { 29 | lock (_queue) 30 | { 31 | if (_dequeueOperation.TryComplete(value)) 32 | { 33 | return true; 34 | } 35 | LinkedListNode? node = _cache.First; 36 | if (node is null) 37 | { 38 | return false; 39 | } 40 | _cache.Remove(node); 41 | node.ValueRef = value; 42 | _queue.AddLast(node); 43 | } 44 | return true; 45 | } 46 | 47 | public ValueTask ReadAsync(CancellationToken cancellationToken) 48 | => _dequeueOperation.DequeueAsync(cancellationToken); 49 | 50 | class DequeueOperation : IValueTaskSource 51 | { 52 | private readonly LinkedList _queue; 53 | private readonly LinkedList _cache; 54 | private ManualResetValueTaskSourceCore _mrvtsc; 55 | 56 | private bool _isActive; 57 | private CancellationToken _cancellationToken; 58 | private CancellationTokenRegistration _cancellationRegistration; 59 | 60 | public DequeueOperation(LinkedList queue, LinkedList cache) 61 | { 62 | _queue = queue; 63 | _cache = cache; 64 | _mrvtsc = new ManualResetValueTaskSourceCore 65 | { 66 | RunContinuationsAsynchronously = true 67 | }; 68 | } 69 | 70 | T IValueTaskSource.GetResult(short token) => _mrvtsc.GetResult(token); 71 | ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) => _mrvtsc.GetStatus(token); 72 | void IValueTaskSource.OnCompleted(Action continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) 73 | => _mrvtsc.OnCompleted(continuation, state, token, flags); 74 | 75 | public ValueTask DequeueAsync(CancellationToken cancellationToken) 76 | { 77 | short token; 78 | lock (_queue) 79 | { 80 | if (_isActive) 81 | { 82 | return ValueTask.FromException(new InvalidOperationException()); 83 | } 84 | if (cancellationToken.IsCancellationRequested) 85 | { 86 | return ValueTask.FromCanceled(cancellationToken); 87 | } 88 | LinkedListNode? node = _queue.First; 89 | if (node is not null) 90 | { 91 | T value = node.ValueRef; 92 | node.ValueRef = default!; 93 | _queue.Remove(node); 94 | _cache.AddLast(node); 95 | return new ValueTask(value); 96 | } 97 | 98 | _mrvtsc.Reset(); 99 | _isActive = true; 100 | _cancellationToken = cancellationToken; 101 | token = _mrvtsc.Version; 102 | } 103 | 104 | _cancellationRegistration = cancellationToken.UnsafeRegister(state => ((DequeueOperation?)state)!.SetCanceled(), this); 105 | 106 | return new ValueTask(this, token); 107 | } 108 | 109 | private void SetCanceled() 110 | { 111 | lock (_queue) 112 | { 113 | if (_isActive) 114 | { 115 | CancellationToken cancellationToken = _cancellationToken; 116 | ClearPreviousOperation(); 117 | _mrvtsc.SetException(new OperationCanceledException(cancellationToken)); 118 | } 119 | } 120 | } 121 | 122 | private void ClearPreviousOperation() 123 | { 124 | _isActive = false; 125 | _cancellationToken = default; 126 | _cancellationRegistration.Dispose(); 127 | _cancellationRegistration = default; 128 | } 129 | 130 | internal bool TryComplete(T value) 131 | { 132 | if (_isActive) 133 | { 134 | ClearPreviousOperation(); 135 | _mrvtsc.SetResult(value); 136 | return true; 137 | } 138 | return false; 139 | } 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /tests/KcpSharp.Tests/Utils/BadKcpConversationPipe.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Channels; 4 | using System.Threading.Tasks; 5 | 6 | namespace KcpSharp.Tests 7 | { 8 | internal sealed class BadKcpConversationPipe : KcpConversationPipe 9 | { 10 | private readonly BadOneWayConnection _alice; 11 | private readonly BadOneWayConnection _bob; 12 | private readonly Channel _aliceToBobChannel; 13 | private readonly Channel _bobToAliceChannel; 14 | private readonly CancellationTokenSource _cts; 15 | 16 | public override KcpConversation Alice => _alice.Conversation; 17 | public override KcpConversation Bob => _bob.Conversation; 18 | 19 | public BadKcpConversationPipe(uint conversationId, BadOneWayConnectionOptions connectionOptions, KcpConversationOptions? aliceOptions, KcpConversationOptions? bobOptions) 20 | { 21 | _aliceToBobChannel = Channel.CreateBounded(new BoundedChannelOptions(connectionOptions.ConcurrentCount) 22 | { 23 | SingleReader = true, 24 | FullMode = BoundedChannelFullMode.DropWrite, 25 | }); 26 | _bobToAliceChannel = Channel.CreateBounded(new BoundedChannelOptions(connectionOptions.ConcurrentCount) 27 | { 28 | SingleReader = true, 29 | FullMode = BoundedChannelFullMode.DropWrite, 30 | }); 31 | _alice = new BadOneWayConnection((int)conversationId, _aliceToBobChannel.Writer, connectionOptions, aliceOptions); 32 | _bob = new BadOneWayConnection((int)conversationId, _bobToAliceChannel.Writer, connectionOptions, bobOptions); 33 | _cts = new CancellationTokenSource(); 34 | _ = Task.Run(() => PipeFromAliceToBob(_cts.Token)); 35 | _ = Task.Run(() => PipeFromBobToAlice(_cts.Token)); 36 | } 37 | 38 | private async Task PipeFromAliceToBob(CancellationToken cancellationToken) 39 | { 40 | while (!cancellationToken.IsCancellationRequested) 41 | { 42 | byte[] packet = await _aliceToBobChannel.Reader.ReadAsync(cancellationToken).ConfigureAwait(false); 43 | await _bob.PutPacketAsync(packet, cancellationToken).ConfigureAwait(false); 44 | } 45 | } 46 | 47 | private async Task PipeFromBobToAlice(CancellationToken cancellationToken) 48 | { 49 | while (!cancellationToken.IsCancellationRequested) 50 | { 51 | byte[] packet = await _bobToAliceChannel.Reader.ReadAsync(cancellationToken).ConfigureAwait(false); 52 | await _alice.PutPacketAsync(packet, cancellationToken).ConfigureAwait(false); 53 | } 54 | } 55 | 56 | public override void Dispose() 57 | { 58 | _cts.Cancel(); 59 | _alice.CloseConnection(); 60 | _bob.CloseConnection(); 61 | } 62 | 63 | } 64 | 65 | internal class BadOneWayConnectionOptions 66 | { 67 | public double DropProbability { get; set; } 68 | public int BaseLatency { get; set; } 69 | public int RandomRelay { get; set; } 70 | public int ConcurrentCount { get; set; } = 4; 71 | public Random Random { get; set; } = Random.Shared; 72 | } 73 | 74 | internal class BadOneWayConnection : IKcpTransport 75 | { 76 | private KcpConversation _conversation; 77 | private readonly ChannelWriter _output; 78 | private readonly BadOneWayConnectionOptions _connectionOptions; 79 | private bool _closed; 80 | 81 | public BadOneWayConnection(int conversationId, ChannelWriter output, BadOneWayConnectionOptions connectionOptions, KcpConversationOptions? options = null) 82 | { 83 | _conversation = new KcpConversation(this, conversationId, options); 84 | _output = output; 85 | _connectionOptions = connectionOptions; 86 | } 87 | 88 | public KcpConversation Conversation => _conversation; 89 | 90 | public void CloseConnection() 91 | { 92 | _closed = true; 93 | _conversation.SetTransportClosed(); 94 | _conversation.Dispose(); 95 | _output.Complete(); 96 | } 97 | 98 | ValueTask IKcpTransport.SendPacketAsync(Memory packet, CancellationToken cancellationToken) 99 | { 100 | double drop = _connectionOptions.Random.NextDouble(); 101 | if (drop < _connectionOptions.DropProbability) 102 | { 103 | //Console.WriteLine("Pakcet dropped intentionally."); 104 | return default; 105 | } 106 | 107 | byte[] arr = packet.ToArray(); 108 | _ = Task.Run(() => SendAsync(arr, cancellationToken)); 109 | return default; 110 | } 111 | 112 | private async Task SendAsync(byte[] packet, CancellationToken cancellationToken) 113 | { 114 | await Task.Delay(_connectionOptions.BaseLatency + _connectionOptions.Random.Next(0, _connectionOptions.RandomRelay), cancellationToken); 115 | if (!_closed) 116 | { 117 | await _output.WriteAsync(packet, cancellationToken).ConfigureAwait(false); 118 | } 119 | } 120 | 121 | public async Task PutPacketAsync(byte[] packet, CancellationToken cancellationToken) 122 | { 123 | await _conversation.InputPakcetAsync(packet, cancellationToken).ConfigureAwait(false); 124 | } 125 | } 126 | 127 | 128 | } 129 | -------------------------------------------------------------------------------- /src/KcpSharp/KcpExceptionProducerExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace KcpSharp 4 | { 5 | /// 6 | /// Helper methods for . 7 | /// 8 | public static class KcpExceptionProducerExtensions 9 | { 10 | /// 11 | /// 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. 12 | /// 13 | /// The producer instance. 14 | /// The exception handler. 15 | public static void SetExceptionHandler(this IKcpExceptionProducer producer, Func handler) 16 | { 17 | if (producer is null) 18 | { 19 | throw new ArgumentNullException(nameof(producer)); 20 | } 21 | if (handler is null) 22 | { 23 | throw new ArgumentNullException(nameof(handler)); 24 | } 25 | 26 | producer.SetExceptionHandler( 27 | (ex, conv, state) => ((Func?)state)!.Invoke(ex, conv), 28 | handler 29 | ); 30 | } 31 | 32 | /// 33 | /// 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. 34 | /// 35 | /// The producer instance. 36 | /// The exception handler. 37 | public static void SetExceptionHandler(this IKcpExceptionProducer producer, Func handler) 38 | { 39 | if (producer is null) 40 | { 41 | throw new ArgumentNullException(nameof(producer)); 42 | } 43 | if (handler is null) 44 | { 45 | throw new ArgumentNullException(nameof(handler)); 46 | } 47 | 48 | producer.SetExceptionHandler( 49 | (ex, conv, state) => ((Func?)state)!.Invoke(ex), 50 | handler 51 | ); 52 | } 53 | 54 | /// 55 | /// Set the handler to invoke when exception is thrown. 56 | /// 57 | /// The producer instance. 58 | /// The exception handler. 59 | /// The state object to pass into the exception handler. 60 | public static void SetExceptionHandler(this IKcpExceptionProducer producer, Action handler, object? state) 61 | { 62 | if (producer is null) 63 | { 64 | throw new ArgumentNullException(nameof(producer)); 65 | } 66 | if (handler is null) 67 | { 68 | throw new ArgumentNullException(nameof(handler)); 69 | } 70 | 71 | producer.SetExceptionHandler( 72 | (ex, conv, state) => 73 | { 74 | var tuple = (Tuple, object?>)state!; 75 | tuple.Item1.Invoke(ex, conv, tuple.Item2); 76 | return false; 77 | }, 78 | Tuple.Create(handler, state) 79 | ); 80 | } 81 | 82 | /// 83 | /// Set the handler to invoke when exception is thrown. 84 | /// 85 | /// The producer instance. 86 | /// The exception handler. 87 | public static void SetExceptionHandler(this IKcpExceptionProducer producer, Action handler) 88 | { 89 | if (producer is null) 90 | { 91 | throw new ArgumentNullException(nameof(producer)); 92 | } 93 | if (handler is null) 94 | { 95 | throw new ArgumentNullException(nameof(handler)); 96 | } 97 | 98 | producer.SetExceptionHandler( 99 | (ex, conv, state) => 100 | { 101 | var handler = (Action)state!; 102 | handler.Invoke(ex, conv); 103 | return false; 104 | }, 105 | handler 106 | ); 107 | } 108 | 109 | /// 110 | /// Set the handler to invoke when exception is thrown. 111 | /// 112 | /// The producer instance. 113 | /// The exception handler. 114 | public static void SetExceptionHandler(this IKcpExceptionProducer producer, Action handler) 115 | { 116 | if (producer is null) 117 | { 118 | throw new ArgumentNullException(nameof(producer)); 119 | } 120 | if (handler is null) 121 | { 122 | throw new ArgumentNullException(nameof(handler)); 123 | } 124 | 125 | producer.SetExceptionHandler( 126 | (ex, conv, state) => 127 | { 128 | var handler = (Action)state!; 129 | handler.Invoke(ex); 130 | return false; 131 | }, 132 | handler 133 | ); 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /samples/KcpChatWasm/App.razor: -------------------------------------------------------------------------------- 1 | @using System; 2 | @using System.Buffers; 3 | @using System.Text; 4 | @using System.Threading; 5 | @using Microsoft.AspNetCore.Components.Web 6 | @using KcpSharp; 7 | 8 | @implements IDisposable 9 | 10 |
11 |
12 | This demo uses a in-memory bidirectional channel to pump packets produced by KcpSharp from one side of the client to the other. It demonstrates that KcpSharp works asynchronously and can function in a single-thread environment (like WebAssembly). 13 |
14 |
15 |
16 |
    17 | @foreach ((int key, string message) in MessageList1) 18 | { 19 |
  • @message
  • 20 | } 21 |
22 |
23 | 24 | 25 |
26 |
27 |
28 |
    29 | @foreach ((int key, string message) in MessageList2) 30 | { 31 |
  • @message
  • 32 | } 33 |
34 |
35 | 36 | 37 |
38 |
39 |
40 |
41 | 42 | @code { 43 | 44 | private string Input1 { get; set; } 45 | private string Input2 { get; set; } 46 | private bool Input1Busy { get; set; } 47 | private bool Input2Busy { get; set; } 48 | private List<(int, string)> MessageList1 { get; } = new List<(int, string)>(); 49 | private List<(int, string)> MessageList2 { get; } = new List<(int, string)>(); 50 | 51 | private PerfectKcpConversationPipe _pipe; 52 | 53 | protected override Task OnInitializedAsync() 54 | { 55 | _pipe = new PerfectKcpConversationPipe(0, null, null); 56 | _ = Task.Run(() => ReceiveLoop(_pipe.Alice, MessageList1)); 57 | _ = Task.Run(() => ReceiveLoop(_pipe.Bob, MessageList2)); 58 | 59 | return base.OnInitializedAsync(); 60 | } 61 | 62 | private async Task ReceiveLoop(KcpConversation conversation, List<(int, string)> list) 63 | { 64 | int index = 0; 65 | while (true) 66 | { 67 | KcpConversationReceiveResult result = await conversation.WaitToReceiveAsync(); 68 | if (result.TransportClosed) 69 | { 70 | break; 71 | } 72 | 73 | byte[] buffer = ArrayPool.Shared.Rent(result.BytesReceived); 74 | try 75 | { 76 | result = await conversation.ReceiveAsync(buffer); 77 | if (result.TransportClosed) 78 | { 79 | break; 80 | } 81 | 82 | try 83 | { 84 | string message = Encoding.UTF8.GetString(buffer.AsSpan(0, result.BytesReceived)); 85 | list.Add((index++, "Received: " + message)); 86 | } 87 | catch 88 | { 89 | list.Add((index++, "Error: Failed to decode message.")); 90 | } 91 | 92 | StateHasChanged(); 93 | } 94 | finally 95 | { 96 | ArrayPool.Shared.Return(buffer); 97 | } 98 | } 99 | } 100 | 101 | private async Task SendInput1() 102 | { 103 | PerfectKcpConversationPipe pipe = _pipe; 104 | if (Input1Busy || pipe is null) 105 | { 106 | return; 107 | } 108 | try 109 | { 110 | Input1Busy = true; 111 | 112 | string message = Input1 ?? string.Empty; 113 | 114 | await SendMessage(pipe.Alice, message); 115 | } 116 | catch (Exception ex) 117 | { 118 | Console.WriteLine(ex); 119 | } 120 | finally 121 | { 122 | Input1Busy = _pipe is null; 123 | StateHasChanged(); 124 | } 125 | } 126 | 127 | private async Task SendInput2() 128 | { 129 | PerfectKcpConversationPipe pipe = _pipe; 130 | if (Input2Busy || pipe is null) 131 | { 132 | return; 133 | } 134 | try 135 | { 136 | Input2Busy = true; 137 | 138 | string message = Input2 ?? string.Empty; 139 | 140 | await SendMessage(pipe.Bob, message); 141 | } 142 | catch (Exception ex) 143 | { 144 | Console.WriteLine(ex); 145 | } 146 | finally 147 | { 148 | Input2Busy = _pipe is null; 149 | StateHasChanged(); 150 | } 151 | } 152 | 153 | private static async Task SendMessage(KcpConversation conversation, string message) 154 | { 155 | int length = Encoding.UTF8.GetByteCount(message); 156 | byte[] buffer = ArrayPool.Shared.Rent(length); 157 | try 158 | { 159 | length = Encoding.UTF8.GetBytes(message, buffer); 160 | 161 | await conversation.SendAsync(buffer.AsMemory(0, length)); 162 | } 163 | finally 164 | { 165 | ArrayPool.Shared.Return(buffer); 166 | } 167 | } 168 | 169 | public void Dispose() 170 | { 171 | Input1Busy = true; 172 | Input2Busy = true; 173 | Interlocked.Exchange(ref _pipe, null)?.Dispose(); 174 | } 175 | 176 | } 177 | -------------------------------------------------------------------------------- /tests/KcpSharp.ThroughputBanchmarks/StreamThroughputBenchmark/StreamThroughputBenchmarkClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Net.Sockets; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace KcpSharp.ThroughputBanchmarks.StreamThroughputBenchmark 8 | { 9 | internal class StreamThroughputBenchmarkClient 10 | { 11 | private long _bytesTransmitted; 12 | 13 | public async Task RunAsync(string endpoint, int mtu, int concurrency, int bufferSize, int windowSize, int queueSize, int updateInterval, bool noDelay, CancellationToken cancellationToken) 14 | { 15 | if (!IPEndPoint.TryParse(endpoint, out IPEndPoint? ipEndPoint)) 16 | { 17 | throw new ArgumentException("endpoint is not a valid IPEndPoint.", nameof(endpoint)); 18 | } 19 | if (mtu < 50 || mtu > ushort.MaxValue) 20 | { 21 | throw new ArgumentOutOfRangeException(nameof(mtu), "mtu is not valid."); 22 | } 23 | if (concurrency <= 0) 24 | { 25 | throw new ArgumentOutOfRangeException(nameof(concurrency), "concurrency is not valid."); 26 | } 27 | if (bufferSize < 0 || bufferSize > ushort.MaxValue) 28 | { 29 | throw new ArgumentOutOfRangeException(nameof(bufferSize), "packetSize is not valid."); 30 | } 31 | if (windowSize <= 0 || windowSize >= ushort.MaxValue) 32 | { 33 | throw new ArgumentOutOfRangeException(nameof(windowSize), "windowSize is not valid."); 34 | } 35 | if (queueSize <= 0 || queueSize >= ushort.MaxValue) 36 | { 37 | throw new ArgumentOutOfRangeException(nameof(queueSize), "queueSize is not valid."); 38 | } 39 | if (updateInterval <= 0 || updateInterval > 1000) 40 | { 41 | throw new ArgumentOutOfRangeException(nameof(updateInterval), "updateInterval is not valid."); 42 | } 43 | 44 | var allocator = new PinnedBlockMemoryPool(mtu); 45 | var options = new KcpConversationOptions() 46 | { 47 | BufferPool = allocator, 48 | Mtu = mtu, 49 | SendQueueSize = queueSize, 50 | SendWindow = windowSize, 51 | RemoteReceiveWindow = windowSize, 52 | UpdateInterval = updateInterval, 53 | NoDelay = noDelay, 54 | StreamMode = true 55 | }; 56 | 57 | _ = Task.Run(() => DisplayStats(cancellationToken)); 58 | 59 | var tasks = new Task[concurrency]; 60 | for (int i = 0; i < tasks.Length; i++) 61 | { 62 | tasks[i] = RunSingleAsync(ipEndPoint, bufferSize, options, cancellationToken); 63 | } 64 | 65 | Console.WriteLine($"Started {concurrency} tasks concurrently."); 66 | try 67 | { 68 | await Task.WhenAll(tasks); 69 | } 70 | catch (OperationCanceledException) 71 | { 72 | // Do nothing 73 | } 74 | catch (Exception ex) 75 | { 76 | Console.WriteLine(ex); 77 | } 78 | } 79 | 80 | private async Task RunSingleAsync(IPEndPoint ipEndPoint, int bufferSize, KcpConversationOptions options, CancellationToken cancellationToken) 81 | { 82 | var socket = new Socket(ipEndPoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp); 83 | SocketHelper.PatchSocket(socket); 84 | await socket.ConnectAsync(ipEndPoint, cancellationToken); 85 | 86 | IKcpTransport transport = KcpSocketTransport.CreateConversation(socket, ipEndPoint, 0, options); 87 | transport.Start(); 88 | KcpConversation conversation = transport.Connection; 89 | 90 | byte[] packet = new byte[bufferSize]; 91 | while (!cancellationToken.IsCancellationRequested) 92 | { 93 | if (!await conversation.SendAsync(packet, cancellationToken)) 94 | { 95 | break; 96 | } 97 | Interlocked.Add(ref _bytesTransmitted, bufferSize); 98 | } 99 | } 100 | 101 | private async Task DisplayStats(CancellationToken cancellationToken) 102 | { 103 | while (!cancellationToken.IsCancellationRequested) 104 | { 105 | await Task.Delay(30 * 1000, cancellationToken); 106 | 107 | long packetsTransmitted = Interlocked.Exchange(ref _bytesTransmitted, 0); 108 | Console.WriteLine($"{DateTime.Now:O}: {SizeSuffix(packetsTransmitted)} bytes transmitted. speed: {SizeSuffix(packetsTransmitted / 30)}/s."); 109 | } 110 | } 111 | 112 | // https://stackoverflow.com/questions/14488796/does-net-provide-an-easy-way-convert-bytes-to-kb-mb-gb-etc/14488941#14488941 113 | static readonly string[] SizeSuffixes = 114 | { "bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" }; 115 | static string SizeSuffix(long value, int decimalPlaces = 1) 116 | { 117 | if (decimalPlaces < 0) { throw new ArgumentOutOfRangeException("decimalPlaces"); } 118 | if (value < 0) { return "-" + SizeSuffix(-value, decimalPlaces); } 119 | if (value == 0) { return string.Format("{0:n" + decimalPlaces + "} bytes", 0); } 120 | 121 | // mag is 0 for bytes, 1 for KB, 2, for MB, etc. 122 | int mag = (int)Math.Log(value, 1024); 123 | 124 | // 1L << (mag * 10) == 2 ^ (10 * mag) 125 | // [i.e. the number of bytes in the unit corresponding to mag] 126 | decimal adjustedSize = (decimal)value / (1L << (mag * 10)); 127 | 128 | // make adjustment when the value is large enough that 129 | // it would round up to 1000 or more 130 | if (Math.Round(adjustedSize, decimalPlaces) >= 1000) 131 | { 132 | mag += 1; 133 | adjustedSize /= 1024; 134 | } 135 | 136 | return string.Format("{0:n" + decimalPlaces + "} {1}", 137 | adjustedSize, 138 | SizeSuffixes[mag]); 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /tests/KcpSharp.ThroughputBanchmarks/PacketsThroughputBenchmark/PacketsThroughputBenchmarkClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Net.Sockets; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace KcpSharp.ThroughputBanchmarks.PacketsThroughputBenchmark 8 | { 9 | internal class PacketsThroughputBenchmarkClient 10 | { 11 | private long _packetsTransmitted; 12 | 13 | public async Task RunAsync(string endpoint, int mtu, int concurrency, int packetSize, int windowSize, int queueSize, int updateInterval, bool noDelay, CancellationToken cancellationToken) 14 | { 15 | if (!IPEndPoint.TryParse(endpoint, out IPEndPoint? ipEndPoint)) 16 | { 17 | throw new ArgumentException("endpoint is not a valid IPEndPoint.", nameof(endpoint)); 18 | } 19 | if (mtu < 50 || mtu > ushort.MaxValue) 20 | { 21 | throw new ArgumentOutOfRangeException(nameof(mtu), "mtu is not valid."); 22 | } 23 | if (concurrency <= 0) 24 | { 25 | throw new ArgumentOutOfRangeException(nameof(concurrency), "concurrency is not valid."); 26 | } 27 | if (packetSize < 0 || packetSize > (mtu - 24)) 28 | { 29 | throw new ArgumentOutOfRangeException(nameof(packetSize), "packetSize is not valid."); 30 | } 31 | if (windowSize <= 0 || windowSize >= ushort.MaxValue) 32 | { 33 | throw new ArgumentOutOfRangeException(nameof(windowSize), "windowSize is not valid."); 34 | } 35 | if (queueSize <= 0 || queueSize >= ushort.MaxValue) 36 | { 37 | throw new ArgumentOutOfRangeException(nameof(queueSize), "queueSize is not valid."); 38 | } 39 | if (updateInterval <= 0 || updateInterval > 1000) 40 | { 41 | throw new ArgumentOutOfRangeException(nameof(updateInterval), "updateInterval is not valid."); 42 | } 43 | 44 | var allocator = new PinnedBlockMemoryPool(mtu); 45 | var options = new KcpConversationOptions() 46 | { 47 | BufferPool = allocator, 48 | Mtu = mtu, 49 | SendQueueSize = queueSize, 50 | SendWindow = windowSize, 51 | RemoteReceiveWindow = windowSize, 52 | UpdateInterval = updateInterval, 53 | NoDelay = noDelay, 54 | }; 55 | 56 | _ = Task.Run(() => DisplayStats(packetSize, cancellationToken)); 57 | 58 | var tasks = new Task[concurrency]; 59 | for (int i = 0; i < tasks.Length; i++) 60 | { 61 | tasks[i] = RunSingleAsync(ipEndPoint, packetSize, options, cancellationToken); 62 | } 63 | 64 | Console.WriteLine($"Started {concurrency} tasks concurrently."); 65 | try 66 | { 67 | await Task.WhenAll(tasks); 68 | } 69 | catch (OperationCanceledException) 70 | { 71 | // Do nothing 72 | } 73 | catch (Exception ex) 74 | { 75 | Console.WriteLine(ex); 76 | } 77 | } 78 | 79 | private async Task RunSingleAsync(IPEndPoint ipEndPoint, int packetSize, KcpConversationOptions options, CancellationToken cancellationToken) 80 | { 81 | var socket = new Socket(ipEndPoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp); 82 | SocketHelper.PatchSocket(socket); 83 | await socket.ConnectAsync(ipEndPoint, cancellationToken); 84 | 85 | IKcpTransport transport = KcpSocketTransport.CreateConversation(socket, ipEndPoint, 0, options); 86 | transport.Start(); 87 | KcpConversation conversation = transport.Connection; 88 | 89 | byte[] packet = new byte[packetSize]; 90 | while (!cancellationToken.IsCancellationRequested) 91 | { 92 | if (!await conversation.SendAsync(packet, cancellationToken)) 93 | { 94 | break; 95 | } 96 | Interlocked.Increment(ref _packetsTransmitted); 97 | } 98 | } 99 | 100 | private async Task DisplayStats(int packetSize, CancellationToken cancellationToken) 101 | { 102 | while (!cancellationToken.IsCancellationRequested) 103 | { 104 | await Task.Delay(30 * 1000, cancellationToken); 105 | 106 | long packetsTransmitted = Interlocked.Exchange(ref _packetsTransmitted, 0); 107 | long bytesTransferred = packetsTransmitted * packetSize; 108 | Console.WriteLine($"{DateTime.Now:O}: {packetsTransmitted} packets transmitted. size: {SizeSuffix(bytesTransferred)}. speed: {SizeSuffix(bytesTransferred / 30)}/s."); 109 | } 110 | } 111 | 112 | // https://stackoverflow.com/questions/14488796/does-net-provide-an-easy-way-convert-bytes-to-kb-mb-gb-etc/14488941#14488941 113 | static readonly string[] SizeSuffixes = 114 | { "bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" }; 115 | static string SizeSuffix(long value, int decimalPlaces = 1) 116 | { 117 | if (decimalPlaces < 0) { throw new ArgumentOutOfRangeException("decimalPlaces"); } 118 | if (value < 0) { return "-" + SizeSuffix(-value, decimalPlaces); } 119 | if (value == 0) { return string.Format("{0:n" + decimalPlaces + "} bytes", 0); } 120 | 121 | // mag is 0 for bytes, 1 for KB, 2, for MB, etc. 122 | int mag = (int)Math.Log(value, 1024); 123 | 124 | // 1L << (mag * 10) == 2 ^ (10 * mag) 125 | // [i.e. the number of bytes in the unit corresponding to mag] 126 | decimal adjustedSize = (decimal)value / (1L << (mag * 10)); 127 | 128 | // make adjustment when the value is large enough that 129 | // it would round up to 1000 or more 130 | if (Math.Round(adjustedSize, decimalPlaces) >= 1000) 131 | { 132 | mag += 1; 133 | adjustedSize /= 1024; 134 | } 135 | 136 | return string.Format("{0:n" + decimalPlaces + "} {1}", 137 | adjustedSize, 138 | SizeSuffixes[mag]); 139 | } 140 | } 141 | } 142 | --------------------------------------------------------------------------------