KcpTransport is a Pure C# implementation of RUDP for high-performance real-time network communication. Similar to the implementation of System.Net.Quic, it provides KcpListener
, KcpConnection
, and KcpStream
. All Read/Write Operations are handled in a Stream-based manner, just like NetworkStream in TCP, providing an easy-to-use and modern asynchronous API that supports async/await. Furthermore, by implementing the ASP.NET Kestrel Transport in the future, the goal is to enable the replacement of the transport layer of gRPC and MagicOnion with KCP.
Caution
This library is currently in alpha preview. It cannot be used for production.
- Variations of RUDP have been widely adopted in applications that require real-time performance, which is difficult to achieve with TCP like gaming.
- QUIC is the future, but currently, it has difficulties with multi-platform support (especially for use on game consoles).
- KCP has a proven track record in games, audio, video, and more, with Genshin Impact being a notable example of its adoption.
- KCP itself has a simple implementation without any system calls, allowing it to be implemented in Pure C# while leveraging the latest UDP Socket Improvements and async/await support in .NET.
KcpTransport is built on top of KCP ported to Pure C#, with implementations of Syn Cookie handshake, connection management, Unreliable communication, and KeepAlive. In the future, encryption will also be supported.
This library is distributed via NuGet. Currently, it only supports .NET 8
as it is in preview, but in the future, it plans to support .NET Standard 2.1
and Unity
.
PM> Install-Package KcpTransport
On the server side, KcpListener.ListenAsync
is used to generate the connection, while on the client side, KcpConnection.ConnectAsync
is used. The Stream
for performing Read/Write operations is obtained using OpenOutboundStreamAsync
.
using KcpTransport;
using System.Text;
var server = RunEchoServer();
var client = RunEchoClient();
await await Task.WhenAny(server, client);
static async Task RunEchoServer()
{
// Create KCP Server
var listener = await KcpListener.ListenAsync("127.0.0.1", 11000);
// Accept client connection loop
while (true)
{
var connection = await listener.AcceptConnectionAsync();
ConsumeClient(connection);
}
static async void ConsumeClient(KcpConnection connection)
{
using (connection)
using (var stream = await connection.OpenOutboundStreamAsync())
{
try
{
var buffer = new byte[1024];
while (true)
{
// Wait incoming data
var len = await stream.ReadAsync(buffer);
var str = Encoding.UTF8.GetString(buffer, 0, len);
Console.WriteLine("Server Request Received: " + str);
// Send to Client(KCP, Reliable)
await stream.WriteAsync(Encoding.UTF8.GetBytes(str));
// Send to Client(Unreliable)
//await stream.WriteUnreliableAsync(Encoding.UTF8.GetBytes(str));
}
}
catch (KcpDisconnectedException)
{
// when client has been disconnected, ReadAsync will throw KcpDisconnectedException
Console.WriteLine($"Disconnected, Id:{connection.ConnectionId}");
}
}
}
}
static async Task RunEchoClient()
{
// Create KCP Client
using var connection = await KcpConnection.ConnectAsync("127.0.0.1", 11000);
using var stream = await connection.OpenOutboundStreamAsync();
var buffer = new byte[1024];
while (true)
{
Console.Write("Input Text:");
var inputText = Console.ReadLine();
// Send to Server(KCP, Reliable), or use WriteUnreliableAsync
await stream.WriteAsync(Encoding.UTF8.GetBytes(inputText!));
// Wait server response
var len = await stream.ReadAsync(buffer);
var str = Encoding.UTF8.GetString(buffer, 0, len);
Console.WriteLine($"Client Response Received: " + str);
}
}
KcpListener
and KcpConnection
can each be passed options when they are created.
var listener = await KcpListener.ListenAsync(new KcpListenerOptions
{
ListenEndPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), listenPort),
EventLoopCount = 1,
KeepAliveDelay = TimeSpan.FromSeconds(10),
ConnectionTimeout = TimeSpan.FromSeconds(20),
});
Currently, the default values are as follows:
public abstract record class KcpOptions
{
public bool EnableNoDelay { get; set; } = true;
public int IntervalMilliseconds { get; set; } = 10; // ikcp_nodelay min is 10.
public int Resend { get; set; } = 2;
public bool EnableFlowControl { get; set; } = false;
public (int SendWindow, int ReceiveWindow) WindowSize { get; set; } = ((int)KcpMethods.IKCP_WND_SND, (int)KcpMethods.IKCP_WND_RCV);
public int MaximumTransmissionUnit { get; set; } = (int)KcpMethods.IKCP_MTU_DEF;
}
public sealed record class KcpListenerOptions : KcpOptions
{
public required IPEndPoint ListenEndPoint { get; set; }
public TimeSpan UpdatePeriod { get; set; } = TimeSpan.FromMilliseconds(5);
public int EventLoopCount { get; set; } = Math.Max(1, Environment.ProcessorCount / 2);
public bool ConfigureAwait { get; set; } = false;
public TimeSpan KeepAliveDelay { get; set; } = TimeSpan.FromSeconds(20);
public TimeSpan ConnectionTimeout { get; set; } = TimeSpan.FromMinutes(1);
public TimeSpan HandshakeTimeout { get; set; } = TimeSpan.FromSeconds(30);
public HashFunc Handshake32bitHashKeyGenerator { get; set; } = KeyGenerator;
public Action<Socket, KcpListenerOptions, ListenerSocketType>? ConfigureSocket { get; set; }
}
public sealed record class KcpClientConnectionOptions : KcpOptions
{
public required EndPoint RemoteEndPoint { get; set; }
public TimeSpan UpdatePeriod { get; set; } = TimeSpan.FromMilliseconds(5);
public bool ConfigureAwait { get; set; } = false;
public TimeSpan KeepAliveDelay { get; set; } = TimeSpan.FromSeconds(20);
public TimeSpan ConnectionTimeout { get; set; } = TimeSpan.FromMinutes(1);
public Action<Socket, KcpClientConnectionOptions>? ConfigureSocket { get; set; }
}
This library is under the MIT License.