KCP protocol implemented in C#. (ported from https://github.com/skywind3000/kcp)
KCP is a fast and reliable Automatic Repeat reQuest (ARQ) protocol. It can be used to create a bidirectional reliable data channel over unreliable transport (such as UDP). According to the original author of KCP protocol, the average latency of the KCP protocol is lower than that of TCP at the cost of higher bandwidth usage. For more information, see the documentation in KCP's official repository.
- Implemented completely in C# code. No native dependencies.
- Runs on asynchronous APIs. Suitable for server-side applications.
- Supports both message mode and stream mode.
- Runtimes compatible with .NET Standard 2.0
- .NET 5
- .NET 6 (recommended)
The recommended method to install this package is from NuGet.
dotnet add package KcpSharp
KCP protocol support two modes: stream mode and message mode (non-stream mode). In the stream mode, the sender sends the data in the form of a stream of bytes and the also the receiver accepts the data in the form of a stream of bytes (much like a TCP stream). In the message mode, The sender packs bytes into messages and then sends these messages to the receiver. KCP protocol ensures that these messages are not lost nor duplicated, and arrive at the receiver in the same order when ther were sent. It is recommended but not required that both sides are in the same mode. KcpSharp uses message mode by default. Users can opt-in to stream mode when creating KcpConversation
instance.
using var conversation = new KcpConversation(transport, conversationId, new KcpConversationOptions { StreamMode = true });
// Or if you are using the built-in UDP socket transport
using var transport = KcpSocketTransport.CreateConversation(socket, endPoint, conversationId, new KcpConversationOptions { StreamMode = true });
See the official documentation for more details.
The KCP packet (aka. segment) structure is as following:
0 4 5 6 8 (BYTE)
+---------------+---+---+-------+
| conv |cmd|frg| wnd |
+---------------+---+---+-------+ 8
| ts | sn |
+---------------+---------------+ 16
| una | len |
+---------------+---------------+ 24
| |
| DATA (optional) |
| |
+-------------------------------+
A KCP conversation represents a reliable data channel over the underlying transport. The conversation ID (conv
) field is a 4-byte unique identifier which can be used to identify each KCP conversation. You can multiplex multiple conversation over the same transport by using different conversation IDs. The total size of the KCP packet header is 24 bytes.
If both the server side and also the client side uses KcpSharp library, and you don't need the ability to multiplex conversations, You can omit the conversation ID field when creating KcpConversation
instances. In this case, the conv
field is excluded from the packet header, and its size becomes 20 bytes.
KcpSharp library provides built-in support for UDP socket. The following code shows how to create a single conversation over an UDP socket.
public async Task ConnectAndProcessAsync(EndPoint endPoint, CancellationToken cancellationToken)
{
const int conversationId = 1;
using var socket = new Socket(SocketType.Dgram, ProtocolType.Udp);
await socket.ConnectAsync(endPoint, cancellationToken);
using var transport = KcpSocketTransport.CreateConversation(socket, endPoint, conversationId, null);
transport.Start();
KcpConversation conversation = transport.Connection;
// Do something with conversation
}
If you want to swap out UDP socket for other custom transport, you can implement it yourself using the instructions at docs/custom-transport.md.
KcpSharp by default works in message mode. The maximum size of a single message is 256 * mss
, where mss
is mtu - kcp_overhead
. mtu
defaults to 1400 and can be configured when creating KcpConversation
instance. kcp_overhead
is the size of the KCP packet header, which is 24 or 20, depending on whether conv
is specified when creating KcpConversation
instance.
In message mode, the most simple way to receive messages is to call RecevieAsync
in a loop and process every message received. The following code shows how to do that.
public async Task ReceiveLoop(KcpConversation conversation, CancellationToken cancellationToken)
{
const int mss = 1400 - 24;
byte[] buffer = new byte[256 * mss];
while (!cancellationToken.IsCancellationRequested)
{
// the size of the buffer passed into ReceiveAsync must be no less than the size of the message.
KcpConversationReceiveResult result = await conversation.ReceiveAsync(buffer, cancellationToken);
if (result.TransportClosed)
{
break;
}
// Your processing logic goes here.
await ProcessMessageAsync(buffer.AsMemory(0, result.BytesReceived), cancellationToken);
}
}
However in mose cases, messages sent be the remote host can never be this large. The receiving side may also want to avoid buffer allocation before messages are fully received. In this case, the receiving side may use the WaitToReceiveAsync
method to archive this.
public async Task ReceiveLoop(KcpConversation conversation, CancellationToken cancellationToken)
{
const int MaxMessageSize = 16384; // limit of the maximum message size.
while (!cancellationToken.IsCancellationRequested)
{
// WaitToReceiveAsync call completes when there is at least one message is received or the transport is closed.
KcpConversationReceiveResult result = await conversation.WaitToReceiveAsync(cancellationToken);
if (result.TransportClosed)
{
break;
}
if (result.BytesReceived > MaxMessageSize)
{
// The message is too large.
conversation.SetTransportClosed();
break;
}
byte[] buffer = ArrayPool<byte>.Shared.Rent(result.BytesReceived);
try
{
// TryReceive should not return false here, unless the transport is closed.
// So we don't need to check for result.TransportClosed.
if (!conversation.TryReceive(buffer, out result))
{
break;
}
// Your processing logic goes here.
await ProcessMessageAsync(buffer.AsMemory(0, result.BytesReceived), cancellationToken);
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
}
In stream mode, the buffer passed to ReceiveAsync
can be any size. KcpConversation
will try to fill the buffer before ReceiveAsync
completes. The following code shows how to do this. Besides, you can also use the same code shown above leveraging WaitToReceiveAsync
to wait for at least any data is available before buffer is allocated.
public async Task ReceiveLoop(KcpConversation conversation, CancellationToken cancellationToken)
{
byte[] buffer = new byte[16384]; // any size is OK.
while (!cancellationToken.IsCancellationRequested)
{
KcpConversationReceiveResult result = await conversation.ReceiveAsync(buffer, cancellationToken);
if (result.TransportClosed)
{
break;
}
// Your processing logic goes here.
await ProcessMessageAsync(buffer.AsMemory(0, result.BytesReceived), cancellationToken);
}
}
The following list shows all the methods which can be used on the receiving side. These methods should not be called concurrently with each other.
- WaitToReceiveAsync
- ReceiveAsync
- TryPeek
- TryReceive
Sending message is pretty straightforward. Simply call SendAsync
with your message to put the message into the send queue. If you want to make sure all the messages in the send queue have been received and acknowledged by the remote host, simply await on FlushAsync
.
byte[] message = new byte[500];
// Send this message.
bool transportClosed = await conversation.SendAsync(message, cancellationToken);
if (transportClosed)
{
// Handle this situation.
}
// optionally wait for all the messages to be acknowledged by the receiver
bool transportClosed = await conversation.FlushAsync(cancellationToken);
if (transportClosed)
{
// Handle this situation.
}
The following list shows all the methods which can be used on the sending side. These methods should not be called concurrently with each other.
- SendAsync
- FlushAsync
- TrySend
- TryGetSendQueueAvailableSpace
KcpSharp provides a Stream
adapter that derives from Stream
and wraps KcpConversation
. In other word, you can operate on KcpConversation
like it is a NetworkStream
. This adapter only works in stream mode.
using var conversation = new KcpConversation(transport, new KcpConversationOptions { StreamMode = true });
using Stream stream = new KcpStream(conversation, true);
// You can now use Stream APIs to operation on KcpConversation.
KcpRawChannel
is a thin wrapper over the overlying transport, but with conversation ID support and a simple receive queue. The class does not provide any reliability guarantee. It simply forwards messages into the underlying transport, or receives from the transport. This class is useful if you want to multiplex both reliable channels and unreliable channels over the same transport.
You can create multiple conversations over the same transport as long as they each have unique conversation ID. KcpSharp provides a built-in helper class KcpMultiplexConnection<T>
and its socket transport for this purpose. (T
is the type of the state object for each conversation. It is optional if you use the built-in transport.) You can create IKcpMultiplexConnection
instance over the UDP socket using the following code.
const int mtu = 1400;
using IKcpTransport<IKcpMultiplexConnection> transport = KcpSocketTransport.CreateMultiplexConnection(socket, endPoint, mtu);
transport.Start();
IKcpMultiplexConnection connection = transport.Connection;
Now that the multiplex connection object is created, you can create KcpConversation
or KcpRawChannel
on this connection.
// create a reliable KCP conversation.
using KcpConversation conversation = connection.CreateConversation(1, new KcpConversationOptions { Mtu = mtu, StreamMode = true });
// create a unreliable raw channel.
using KcpRawChannel channel = connection.CreateRawChannel(2, new KcpRawChannelOptions { Mtu = mtu });
For a detailed sample usage of IKcpMultiplexConnection
, see KcpTunnel
sample in ./samples/KcpTunnel.