CHANGED TO MIRROR

This commit is contained in:
DerTyp187
2021-10-25 09:20:01 +02:00
parent bd712107b7
commit e509a919b6
611 changed files with 38291 additions and 1216 deletions

View File

@@ -0,0 +1,10 @@
namespace kcp2k
{
// channel type and header for raw messages
public enum KcpChannel : byte
{
// don't react on 0x00. might help to filter out random noise.
Reliable = 0x01,
Unreliable = 0x02
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 9e852b2532fb248d19715cfebe371db3
timeCreated: 1610081248

View File

@@ -0,0 +1,120 @@
// kcp client logic abstracted into a class.
// for use in Mirror, DOTSNET, testing, etc.
using System;
namespace kcp2k
{
public class KcpClient
{
// events
public Action OnConnected;
public Action<ArraySegment<byte>> OnData;
public Action OnDisconnected;
// state
public KcpClientConnection connection;
public bool connected;
public KcpClient(Action OnConnected, Action<ArraySegment<byte>> OnData, Action OnDisconnected)
{
this.OnConnected = OnConnected;
this.OnData = OnData;
this.OnDisconnected = OnDisconnected;
}
// CreateConnection can be overwritten for where-allocation:
// https://github.com/vis2k/where-allocation
protected virtual KcpClientConnection CreateConnection() =>
new KcpClientConnection();
public void Connect(string address, ushort port, bool noDelay, uint interval, int fastResend = 0, bool congestionWindow = true, uint sendWindowSize = Kcp.WND_SND, uint receiveWindowSize = Kcp.WND_RCV, int timeout = KcpConnection.DEFAULT_TIMEOUT)
{
if (connected)
{
Log.Warning("KCP: client already connected!");
return;
}
// create connection
connection = CreateConnection();
// setup events
connection.OnAuthenticated = () =>
{
Log.Info($"KCP: OnClientConnected");
connected = true;
OnConnected.Invoke();
};
connection.OnData = (message) =>
{
//Log.Debug($"KCP: OnClientData({BitConverter.ToString(message.Array, message.Offset, message.Count)})");
OnData.Invoke(message);
};
connection.OnDisconnected = () =>
{
Log.Info($"KCP: OnClientDisconnected");
connected = false;
connection = null;
OnDisconnected.Invoke();
};
// connect
connection.Connect(address, port, noDelay, interval, fastResend, congestionWindow, sendWindowSize, receiveWindowSize, timeout);
}
public void Send(ArraySegment<byte> segment, KcpChannel channel)
{
if (connected)
{
connection.SendData(segment, channel);
}
else Log.Warning("KCP: can't send because client not connected!");
}
public void Disconnect()
{
// only if connected
// otherwise we end up in a deadlock because of an open Mirror bug:
// https://github.com/vis2k/Mirror/issues/2353
if (connected)
{
// call Disconnect and let the connection handle it.
// DO NOT set it to null yet. it needs to be updated a few more
// times first. let the connection handle it!
connection?.Disconnect();
}
}
// process incoming messages. should be called before updating the world.
public void TickIncoming()
{
// recv on socket first, then process incoming
// (even if we didn't receive anything. need to tick ping etc.)
// (connection is null if not active)
connection?.RawReceive();
connection?.TickIncoming();
}
// process outgoing messages. should be called after updating the world.
public void TickOutgoing()
{
// process outgoing
// (connection is null if not active)
connection?.TickOutgoing();
}
// process incoming and outgoing for convenience
// => ideally call ProcessIncoming() before updating the world and
// ProcessOutgoing() after updating the world for minimum latency
public void Tick()
{
TickIncoming();
TickOutgoing();
}
// pause/unpause to safely support mirror scene handling and to
// immediately pause the receive while loop if needed.
public void Pause() => connection?.Pause();
public void Unpause() => connection?.Unpause();
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 6aa069a28ed24fedb533c102d9742b36
timeCreated: 1603786960

View File

@@ -0,0 +1,109 @@
using System.Net;
using System.Net.Sockets;
namespace kcp2k
{
public class KcpClientConnection : KcpConnection
{
// IMPORTANT: raw receive buffer always needs to be of 'MTU' size, even
// if MaxMessageSize is larger. kcp always sends in MTU
// segments and having a buffer smaller than MTU would
// silently drop excess data.
// => we need the MTU to fit channel + message!
readonly byte[] rawReceiveBuffer = new byte[Kcp.MTU_DEF];
// helper function to resolve host to IPAddress
public static bool ResolveHostname(string hostname, out IPAddress[] addresses)
{
try
{
addresses = Dns.GetHostAddresses(hostname);
return addresses.Length >= 1;
}
catch (SocketException)
{
Log.Info($"Failed to resolve host: {hostname}");
addresses = null;
return false;
}
}
// EndPoint & Receive functions can be overwritten for where-allocation:
// https://github.com/vis2k/where-allocation
// NOTE: Client's SendTo doesn't allocate, don't need a virtual.
protected virtual void CreateRemoteEndPoint(IPAddress[] addresses, ushort port) =>
remoteEndPoint = new IPEndPoint(addresses[0], port);
protected virtual int ReceiveFrom(byte[] buffer) =>
socket.ReceiveFrom(buffer, ref remoteEndPoint);
public void Connect(string host, ushort port, bool noDelay, uint interval = Kcp.INTERVAL, int fastResend = 0, bool congestionWindow = true, uint sendWindowSize = Kcp.WND_SND, uint receiveWindowSize = Kcp.WND_RCV, int timeout = DEFAULT_TIMEOUT)
{
Log.Info($"KcpClient: connect to {host}:{port}");
// try resolve host name
if (ResolveHostname(host, out IPAddress[] addresses))
{
// create remote endpoint
CreateRemoteEndPoint(addresses, port);
// create socket
socket = new Socket(remoteEndPoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp);
socket.Connect(remoteEndPoint);
// set up kcp
SetupKcp(noDelay, interval, fastResend, congestionWindow, sendWindowSize, receiveWindowSize, timeout);
// client should send handshake to server as very first message
SendHandshake();
RawReceive();
}
// otherwise call OnDisconnected to let the user know.
else OnDisconnected();
}
// call from transport update
public void RawReceive()
{
try
{
if (socket != null)
{
while (socket.Poll(0, SelectMode.SelectRead))
{
int msgLength = ReceiveFrom(rawReceiveBuffer);
// IMPORTANT: detect if buffer was too small for the
// received msgLength. otherwise the excess
// data would be silently lost.
// (see ReceiveFrom documentation)
if (msgLength <= rawReceiveBuffer.Length)
{
//Log.Debug($"KCP: client raw recv {msgLength} bytes = {BitConverter.ToString(buffer, 0, msgLength)}");
RawInput(rawReceiveBuffer, msgLength);
}
else
{
Log.Error($"KCP ClientConnection: message of size {msgLength} does not fit into buffer of size {rawReceiveBuffer.Length}. The excess was silently dropped. Disconnecting.");
Disconnect();
}
}
}
}
// this is fine, the socket might have been closed in the other end
catch (SocketException) {}
}
protected override void Dispose()
{
socket.Close();
socket = null;
}
protected override void RawSend(byte[] data, int length)
{
socket.Send(data, length, SocketFlags.None);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 96512e74aa8214a6faa8a412a7a07877
timeCreated: 1602601237

View File

@@ -0,0 +1,674 @@
using System;
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
namespace kcp2k
{
enum KcpState { Connected, Authenticated, Disconnected }
public abstract class KcpConnection
{
protected Socket socket;
protected EndPoint remoteEndPoint;
internal Kcp kcp;
// kcp can have several different states, let's use a state machine
KcpState state = KcpState.Disconnected;
public Action OnAuthenticated;
public Action<ArraySegment<byte>> OnData;
public Action OnDisconnected;
// Mirror needs a way to stop the kcp message processing while loop
// immediately after a scene change message. Mirror can't process any
// other messages during a scene change.
// (could be useful for others too)
bool paused;
// If we don't receive anything these many milliseconds
// then consider us disconnected
public const int DEFAULT_TIMEOUT = 10000;
public int timeout = DEFAULT_TIMEOUT;
uint lastReceiveTime;
// internal time.
// StopWatch offers ElapsedMilliSeconds and should be more precise than
// Unity's time.deltaTime over long periods.
readonly Stopwatch refTime = new Stopwatch();
// we need to subtract the channel byte from every MaxMessageSize
// calculation.
// we also need to tell kcp to use MTU-1 to leave space for the byte.
const int CHANNEL_HEADER_SIZE = 1;
// reliable channel (= kcp) MaxMessageSize so the outside knows largest
// allowed message to send the calculation in Send() is not obvious at
// all, so let's provide the helper here.
//
// kcp does fragmentation, so max message is way larger than MTU.
//
// -> runtime MTU changes are disabled: mss is always MTU_DEF-OVERHEAD
// -> Send() checks if fragment count < WND_RCV, so we use WND_RCV - 1.
// note that Send() checks WND_RCV instead of wnd_rcv which may or
// may not be a bug in original kcp. but since it uses the define, we
// can use that here too.
// -> we add 1 byte KcpHeader enum to each message, so -1
//
// IMPORTANT: max message is MTU * WND_RCV, in other words it completely
// fills the receive window! due to head of line blocking,
// all other messages have to wait while a maxed size message
// is being delivered.
// => in other words, DO NOT use max size all the time like
// for batching.
// => sending UNRELIABLE max message size most of the time is
// best for performance (use that one for batching!)
public const int ReliableMaxMessageSize = (Kcp.MTU_DEF - Kcp.OVERHEAD - CHANNEL_HEADER_SIZE) * (Kcp.WND_RCV - 1) - 1;
// unreliable max message size is simply MTU - channel header size
public const int UnreliableMaxMessageSize = Kcp.MTU_DEF - CHANNEL_HEADER_SIZE;
// buffer to receive kcp's processed messages (avoids allocations).
// IMPORTANT: this is for KCP messages. so it needs to be of size:
// 1 byte header + MaxMessageSize content
byte[] kcpMessageBuffer = new byte[1 + ReliableMaxMessageSize];
// send buffer for handing user messages to kcp for processing.
// (avoids allocations).
// IMPORTANT: needs to be of size:
// 1 byte header + MaxMessageSize content
byte[] kcpSendBuffer = new byte[1 + ReliableMaxMessageSize];
// raw send buffer is exactly MTU.
byte[] rawSendBuffer = new byte[Kcp.MTU_DEF];
// send a ping occasionally so we don't time out on the other end.
// for example, creating a character in an MMO could easily take a
// minute of no data being sent. which doesn't mean we want to time out.
// same goes for slow paced card games etc.
public const int PING_INTERVAL = 1000;
uint lastPingTime;
// if we send more than kcp can handle, we will get ever growing
// send/recv buffers and queues and minutes of latency.
// => if a connection can't keep up, it should be disconnected instead
// to protect the server under heavy load, and because there is no
// point in growing to gigabytes of memory or minutes of latency!
// => 2k isn't enough. we reach 2k when spawning 4k monsters at once
// easily, but it does recover over time.
// => 10k seems safe.
//
// note: we have a ChokeConnectionAutoDisconnects test for this too!
internal const int QueueDisconnectThreshold = 10000;
// getters for queue and buffer counts, used for debug info
public int SendQueueCount => kcp.snd_queue.Count;
public int ReceiveQueueCount => kcp.rcv_queue.Count;
public int SendBufferCount => kcp.snd_buf.Count;
public int ReceiveBufferCount => kcp.rcv_buf.Count;
// maximum send rate per second can be calculated from kcp parameters
// source: https://translate.google.com/translate?sl=auto&tl=en&u=https://wetest.qq.com/lab/view/391.html
//
// KCP can send/receive a maximum of WND*MTU per interval.
// multiple by 1000ms / interval to get the per-second rate.
//
// example:
// WND(32) * MTU(1400) = 43.75KB
// => 43.75KB * 1000 / INTERVAL(10) = 4375KB/s
//
// returns bytes/second!
public uint MaxSendRate =>
kcp.snd_wnd * kcp.mtu * 1000 / kcp.interval;
public uint MaxReceiveRate =>
kcp.rcv_wnd * kcp.mtu * 1000 / kcp.interval;
// SetupKcp creates and configures a new KCP instance.
// => useful to start from a fresh state every time the client connects
// => NoDelay, interval, wnd size are the most important configurations.
// let's force require the parameters so we don't forget it anywhere.
protected void SetupKcp(bool noDelay, uint interval = Kcp.INTERVAL, int fastResend = 0, bool congestionWindow = true, uint sendWindowSize = Kcp.WND_SND, uint receiveWindowSize = Kcp.WND_RCV, int timeout = DEFAULT_TIMEOUT)
{
// set up kcp over reliable channel (that's what kcp is for)
kcp = new Kcp(0, RawSendReliable);
// set nodelay.
// note that kcp uses 'nocwnd' internally so we negate the parameter
kcp.SetNoDelay(noDelay ? 1u : 0u, interval, fastResend, !congestionWindow);
kcp.SetWindowSize(sendWindowSize, receiveWindowSize);
// IMPORTANT: high level needs to add 1 channel byte to each raw
// message. so while Kcp.MTU_DEF is perfect, we actually need to
// tell kcp to use MTU-1 so we can still put the header into the
// message afterwards.
kcp.SetMtu(Kcp.MTU_DEF - CHANNEL_HEADER_SIZE);
this.timeout = timeout;
state = KcpState.Connected;
refTime.Start();
}
void HandleTimeout(uint time)
{
// note: we are also sending a ping regularly, so timeout should
// only ever happen if the connection is truly gone.
if (time >= lastReceiveTime + timeout)
{
Log.Warning($"KCP: Connection timed out after not receiving any message for {timeout}ms. Disconnecting.");
Disconnect();
}
}
void HandleDeadLink()
{
// kcp has 'dead_link' detection. might as well use it.
if (kcp.state == -1)
{
Log.Warning("KCP Connection dead_link detected. Disconnecting.");
Disconnect();
}
}
// send a ping occasionally in order to not time out on the other end.
void HandlePing(uint time)
{
// enough time elapsed since last ping?
if (time >= lastPingTime + PING_INTERVAL)
{
// ping again and reset time
//Log.Debug("KCP: sending ping...");
SendPing();
lastPingTime = time;
}
}
void HandleChoked()
{
// disconnect connections that can't process the load.
// see QueueSizeDisconnect comments.
// => include all of kcp's buffers and the unreliable queue!
int total = kcp.rcv_queue.Count + kcp.snd_queue.Count +
kcp.rcv_buf.Count + kcp.snd_buf.Count;
if (total >= QueueDisconnectThreshold)
{
Log.Warning($"KCP: disconnecting connection because it can't process data fast enough.\n" +
$"Queue total {total}>{QueueDisconnectThreshold}. rcv_queue={kcp.rcv_queue.Count} snd_queue={kcp.snd_queue.Count} rcv_buf={kcp.rcv_buf.Count} snd_buf={kcp.snd_buf.Count}\n" +
$"* Try to Enable NoDelay, decrease INTERVAL, disable Congestion Window (= enable NOCWND!), increase SEND/RECV WINDOW or compress data.\n" +
$"* Or perhaps the network is simply too slow on our end, or on the other end.\n");
// let's clear all pending sends before disconnting with 'Bye'.
// otherwise a single Flush in Disconnect() won't be enough to
// flush thousands of messages to finally deliver 'Bye'.
// this is just faster and more robust.
kcp.snd_queue.Clear();
Disconnect();
}
}
// reads the next reliable message type & content from kcp.
// -> to avoid buffering, unreliable messages call OnData directly.
bool ReceiveNextReliable(out KcpHeader header, out ArraySegment<byte> message)
{
int msgSize = kcp.PeekSize();
if (msgSize > 0)
{
// only allow receiving up to buffer sized messages.
// otherwise we would get BlockCopy ArgumentException anyway.
if (msgSize <= kcpMessageBuffer.Length)
{
// receive from kcp
int received = kcp.Receive(kcpMessageBuffer, msgSize);
if (received >= 0)
{
// extract header & content without header
header = (KcpHeader)kcpMessageBuffer[0];
message = new ArraySegment<byte>(kcpMessageBuffer, 1, msgSize - 1);
lastReceiveTime = (uint)refTime.ElapsedMilliseconds;
return true;
}
else
{
// if receive failed, close everything
Log.Warning($"Receive failed with error={received}. closing connection.");
Disconnect();
}
}
// we don't allow sending messages > Max, so this must be an
// attacker. let's disconnect to avoid allocation attacks etc.
else
{
Log.Warning($"KCP: possible allocation attack for msgSize {msgSize} > buffer {kcpMessageBuffer.Length}. Disconnecting the connection.");
Disconnect();
}
}
message = default;
header = KcpHeader.Disconnect;
return false;
}
void TickIncoming_Connected(uint time)
{
// detect common events & ping
HandleTimeout(time);
HandleDeadLink();
HandlePing(time);
HandleChoked();
// any reliable kcp message received?
if (ReceiveNextReliable(out KcpHeader header, out ArraySegment<byte> message))
{
// message type FSM. no default so we never miss a case.
switch (header)
{
case KcpHeader.Handshake:
{
// we were waiting for a handshake.
// it proves that the other end speaks our protocol.
Log.Info("KCP: received handshake");
state = KcpState.Authenticated;
OnAuthenticated?.Invoke();
break;
}
case KcpHeader.Ping:
{
// ping keeps kcp from timing out. do nothing.
break;
}
case KcpHeader.Data:
case KcpHeader.Disconnect:
{
// everything else is not allowed during handshake!
Log.Warning($"KCP: received invalid header {header} while Connected. Disconnecting the connection.");
Disconnect();
break;
}
}
}
}
void TickIncoming_Authenticated(uint time)
{
// detect common events & ping
HandleTimeout(time);
HandleDeadLink();
HandlePing(time);
HandleChoked();
// process all received messages
//
// Mirror scene changing requires transports to immediately stop
// processing any more messages after a scene message was
// received. and since we are in a while loop here, we need this
// extra check.
//
// note while that this is mainly for Mirror, but might be
// useful in other applications too.
//
// note that we check it BEFORE ever calling ReceiveNext. otherwise
// we would silently eat the received message and never process it.
while (!paused &&
ReceiveNextReliable(out KcpHeader header, out ArraySegment<byte> message))
{
// message type FSM. no default so we never miss a case.
switch (header)
{
case KcpHeader.Handshake:
{
// should never receive another handshake after auth
Log.Warning($"KCP: received invalid header {header} while Authenticated. Disconnecting the connection.");
Disconnect();
break;
}
case KcpHeader.Data:
{
// call OnData IF the message contained actual data
if (message.Count > 0)
{
//Log.Warning($"Kcp recv msg: {BitConverter.ToString(message.Array, message.Offset, message.Count)}");
OnData?.Invoke(message);
}
// empty data = attacker, or something went wrong
else
{
Log.Warning("KCP: received empty Data message while Authenticated. Disconnecting the connection.");
Disconnect();
}
break;
}
case KcpHeader.Ping:
{
// ping keeps kcp from timing out. do nothing.
break;
}
case KcpHeader.Disconnect:
{
// disconnect might happen
Log.Info("KCP: received disconnect message");
Disconnect();
break;
}
}
}
}
public void TickIncoming()
{
uint time = (uint)refTime.ElapsedMilliseconds;
try
{
switch (state)
{
case KcpState.Connected:
{
TickIncoming_Connected(time);
break;
}
case KcpState.Authenticated:
{
TickIncoming_Authenticated(time);
break;
}
case KcpState.Disconnected:
{
// do nothing while disconnected
break;
}
}
}
catch (SocketException exception)
{
// this is ok, the connection was closed
Log.Info($"KCP Connection: Disconnecting because {exception}. This is fine.");
Disconnect();
}
catch (ObjectDisposedException exception)
{
// fine, socket was closed
Log.Info($"KCP Connection: Disconnecting because {exception}. This is fine.");
Disconnect();
}
catch (Exception ex)
{
// unexpected
Log.Error(ex.ToString());
Disconnect();
}
}
public void TickOutgoing()
{
uint time = (uint)refTime.ElapsedMilliseconds;
try
{
switch (state)
{
case KcpState.Connected:
case KcpState.Authenticated:
{
// update flushes out messages
kcp.Update(time);
break;
}
case KcpState.Disconnected:
{
// do nothing while disconnected
break;
}
}
}
catch (SocketException exception)
{
// this is ok, the connection was closed
Log.Info($"KCP Connection: Disconnecting because {exception}. This is fine.");
Disconnect();
}
catch (ObjectDisposedException exception)
{
// fine, socket was closed
Log.Info($"KCP Connection: Disconnecting because {exception}. This is fine.");
Disconnect();
}
catch (Exception ex)
{
// unexpected
Log.Error(ex.ToString());
Disconnect();
}
}
public void RawInput(byte[] buffer, int msgLength)
{
// parse channel
if (msgLength > 0)
{
byte channel = buffer[0];
switch (channel)
{
case (byte)KcpChannel.Reliable:
{
// input into kcp, but skip channel byte
int input = kcp.Input(buffer, 1, msgLength - 1);
if (input != 0)
{
Log.Warning($"Input failed with error={input} for buffer with length={msgLength - 1}");
}
break;
}
case (byte)KcpChannel.Unreliable:
{
// ideally we would queue all unreliable messages and
// then process them in ReceiveNext() together with the
// reliable messages, but:
// -> queues/allocations/pools are slow and complex.
// -> DOTSNET 10k is actually slower if we use pooled
// unreliable messages for transform messages.
//
// DOTSNET 10k benchmark:
// reliable-only: 170 FPS
// unreliable queued: 130-150 FPS
// unreliable direct: 183 FPS(!)
//
// DOTSNET 50k benchmark:
// reliable-only: FAILS (queues keep growing)
// unreliable direct: 18-22 FPS(!)
//
// -> all unreliable messages are DATA messages anyway.
// -> let's skip the magic and call OnData directly if
// the current state allows it.
if (state == KcpState.Authenticated)
{
// only process messages while not paused for Mirror
// scene switching etc.
// -> if an unreliable message comes in while
// paused, simply drop it. it's unreliable!
if (!paused)
{
ArraySegment<byte> message = new ArraySegment<byte>(buffer, 1, msgLength - 1);
OnData?.Invoke(message);
}
// set last receive time to avoid timeout.
// -> we do this in ANY case even if not enabled.
// a message is a message.
// -> we set last receive time for both reliable and
// unreliable messages. both count.
// otherwise a connection might time out even
// though unreliable were received, but no
// reliable was received.
lastReceiveTime = (uint)refTime.ElapsedMilliseconds;
}
else
{
// should never
Log.Warning($"KCP: received unreliable message in state {state}. Disconnecting the connection.");
Disconnect();
}
break;
}
default:
{
// not a valid channel. random data or attacks.
Log.Info($"Disconnecting connection because of invalid channel header: {channel}");
Disconnect();
break;
}
}
}
}
// raw send puts the data into the socket
protected abstract void RawSend(byte[] data, int length);
// raw send called by kcp
void RawSendReliable(byte[] data, int length)
{
// copy channel header, data into raw send buffer, then send
rawSendBuffer[0] = (byte)KcpChannel.Reliable;
Buffer.BlockCopy(data, 0, rawSendBuffer, 1, length);
RawSend(rawSendBuffer, length + 1);
}
void SendReliable(KcpHeader header, ArraySegment<byte> content)
{
// 1 byte header + content needs to fit into send buffer
if (1 + content.Count <= kcpSendBuffer.Length) // TODO
{
// copy header, content (if any) into send buffer
kcpSendBuffer[0] = (byte)header;
if (content.Count > 0)
Buffer.BlockCopy(content.Array, content.Offset, kcpSendBuffer, 1, content.Count);
// send to kcp for processing
int sent = kcp.Send(kcpSendBuffer, 0, 1 + content.Count);
if (sent < 0)
{
Log.Warning($"Send failed with error={sent} for content with length={content.Count}");
}
}
// otherwise content is larger than MaxMessageSize. let user know!
else Log.Error($"Failed to send reliable message of size {content.Count} because it's larger than ReliableMaxMessageSize={ReliableMaxMessageSize}");
}
void SendUnreliable(ArraySegment<byte> message)
{
// message size needs to be <= unreliable max size
if (message.Count <= UnreliableMaxMessageSize)
{
// copy channel header, data into raw send buffer, then send
rawSendBuffer[0] = (byte)KcpChannel.Unreliable;
Buffer.BlockCopy(message.Array, 0, rawSendBuffer, 1, message.Count);
RawSend(rawSendBuffer, message.Count + 1);
}
// otherwise content is larger than MaxMessageSize. let user know!
else Log.Error($"Failed to send unreliable message of size {message.Count} because it's larger than UnreliableMaxMessageSize={UnreliableMaxMessageSize}");
}
// server & client need to send handshake at different times, so we need
// to expose the function.
// * client should send it immediately.
// * server should send it as reply to client's handshake, not before
// (server should not reply to random internet messages with handshake)
// => handshake info needs to be delivered, so it goes over reliable.
public void SendHandshake()
{
Log.Info("KcpConnection: sending Handshake to other end!");
SendReliable(KcpHeader.Handshake, default);
}
public void SendData(ArraySegment<byte> data, KcpChannel channel)
{
// sending empty segments is not allowed.
// nobody should ever try to send empty data.
// it means that something went wrong, e.g. in Mirror/DOTSNET.
// let's make it obvious so it's easy to debug.
if (data.Count == 0)
{
Log.Warning("KcpConnection: tried sending empty message. This should never happen. Disconnecting.");
Disconnect();
return;
}
switch (channel)
{
case KcpChannel.Reliable:
SendReliable(KcpHeader.Data, data);
break;
case KcpChannel.Unreliable:
SendUnreliable(data);
break;
}
}
// ping goes through kcp to keep it from timing out, so it goes over the
// reliable channel.
void SendPing() => SendReliable(KcpHeader.Ping, default);
// disconnect info needs to be delivered, so it goes over reliable
void SendDisconnect() => SendReliable(KcpHeader.Disconnect, default);
protected virtual void Dispose() {}
// disconnect this connection
public void Disconnect()
{
// only if not disconnected yet
if (state == KcpState.Disconnected)
return;
// send a disconnect message
if (socket.Connected)
{
try
{
SendDisconnect();
kcp.Flush();
}
catch (SocketException)
{
// this is ok, the connection was already closed
}
catch (ObjectDisposedException)
{
// this is normal when we stop the server
// the socket is stopped so we can't send anything anymore
// to the clients
// the clients will eventually timeout and realize they
// were disconnected
}
}
// set as Disconnected, call event
Log.Info("KCP Connection: Disconnected.");
state = KcpState.Disconnected;
OnDisconnected?.Invoke();
}
// get remote endpoint
public EndPoint GetRemoteEndPoint() => remoteEndPoint;
// pause/unpause to safely support mirror scene handling and to
// immediately pause the receive while loop if needed.
public void Pause() => paused = true;
public void Unpause()
{
// unpause
paused = false;
// reset the timeout.
// we have likely been paused for > timeout seconds, but that
// doesn't mean we should disconnect. for example, Mirror pauses
// kcp during scene changes which could easily take > 10s timeout:
// see also: https://github.com/vis2k/kcp2k/issues/8
// => Unpause completely resets the timeout instead of restoring the
// time difference when we started pausing. it's more simple and
// it's a good idea to start counting from 0 after we unpaused!
lastReceiveTime = (uint)refTime.ElapsedMilliseconds;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 3915c7c62b72d4dc2a9e4e76c94fc484
timeCreated: 1602600432

View File

@@ -0,0 +1,19 @@
namespace kcp2k
{
// header for messages processed by kcp.
// this is NOT for the raw receive messages(!) because handshake/disconnect
// need to be sent reliably. it's not enough to have those in rawreceive
// because those messages might get lost without being resent!
public enum KcpHeader : byte
{
// don't react on 0x00. might help to filter out random noise.
Handshake = 0x01,
// ping goes over reliable & KcpHeader for now. could go over reliable
// too. there is no real difference except that this is easier because
// we already have a KcpHeader for reliable messages.
// ping is only used to keep it alive, so latency doesn't matter.
Ping = 0x02,
Data = 0x03,
Disconnect = 0x04
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 91b5edac31224a49bd76f960ae018942
timeCreated: 1610081248

View File

@@ -0,0 +1,337 @@
// kcp server logic abstracted into a class.
// for use in Mirror, DOTSNET, testing, etc.
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
namespace kcp2k
{
public class KcpServer
{
// events
public Action<int> OnConnected;
public Action<int, ArraySegment<byte>> OnData;
public Action<int> OnDisconnected;
// configuration
// DualMode uses both IPv6 and IPv4. not all platforms support it.
// (Nintendo Switch, etc.)
public bool DualMode;
// NoDelay is recommended to reduce latency. This also scales better
// without buffers getting full.
public bool NoDelay;
// KCP internal update interval. 100ms is KCP default, but a lower
// interval is recommended to minimize latency and to scale to more
// networked entities.
public uint Interval;
// KCP fastresend parameter. Faster resend for the cost of higher
// bandwidth.
public int FastResend;
// KCP 'NoCongestionWindow' is false by default. here we negate it for
// ease of use. This can be disabled for high scale games if connections
// choke regularly.
public bool CongestionWindow;
// KCP window size can be modified to support higher loads.
// for example, Mirror Benchmark requires:
// 128, 128 for 4k monsters
// 512, 512 for 10k monsters
// 8192, 8192 for 20k monsters
public uint SendWindowSize;
public uint ReceiveWindowSize;
// timeout in milliseconds
public int Timeout;
// state
protected Socket socket;
EndPoint newClientEP;
// IMPORTANT: raw receive buffer always needs to be of 'MTU' size, even
// if MaxMessageSize is larger. kcp always sends in MTU
// segments and having a buffer smaller than MTU would
// silently drop excess data.
// => we need the mtu to fit channel + message!
readonly byte[] rawReceiveBuffer = new byte[Kcp.MTU_DEF];
// connections <connectionId, connection> where connectionId is EndPoint.GetHashCode
public Dictionary<int, KcpServerConnection> connections = new Dictionary<int, KcpServerConnection>();
public KcpServer(Action<int> OnConnected,
Action<int, ArraySegment<byte>> OnData,
Action<int> OnDisconnected,
bool DualMode,
bool NoDelay,
uint Interval,
int FastResend = 0,
bool CongestionWindow = true,
uint SendWindowSize = Kcp.WND_SND,
uint ReceiveWindowSize = Kcp.WND_RCV,
int Timeout = KcpConnection.DEFAULT_TIMEOUT)
{
this.OnConnected = OnConnected;
this.OnData = OnData;
this.OnDisconnected = OnDisconnected;
this.DualMode = DualMode;
this.NoDelay = NoDelay;
this.Interval = Interval;
this.FastResend = FastResend;
this.CongestionWindow = CongestionWindow;
this.SendWindowSize = SendWindowSize;
this.ReceiveWindowSize = ReceiveWindowSize;
this.Timeout = Timeout;
// create newClientEP either IPv4 or IPv6
newClientEP = DualMode
? new IPEndPoint(IPAddress.IPv6Any, 0)
: new IPEndPoint(IPAddress.Any, 0);
}
public bool IsActive() => socket != null;
public void Start(ushort port)
{
// only start once
if (socket != null)
{
Log.Warning("KCP: server already started!");
}
// listen
if (DualMode)
{
// IPv6 socket with DualMode
socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp);
socket.DualMode = true;
socket.Bind(new IPEndPoint(IPAddress.IPv6Any, port));
}
else
{
// IPv4 socket
socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
socket.Bind(new IPEndPoint(IPAddress.Any, port));
}
}
public void Send(int connectionId, ArraySegment<byte> segment, KcpChannel channel)
{
if (connections.TryGetValue(connectionId, out KcpServerConnection connection))
{
connection.SendData(segment, channel);
}
}
public void Disconnect(int connectionId)
{
if (connections.TryGetValue(connectionId, out KcpServerConnection connection))
{
connection.Disconnect();
}
}
public string GetClientAddress(int connectionId)
{
if (connections.TryGetValue(connectionId, out KcpServerConnection connection))
{
return (connection.GetRemoteEndPoint() as IPEndPoint).Address.ToString();
}
return "";
}
// EndPoint & Receive functions can be overwritten for where-allocation:
// https://github.com/vis2k/where-allocation
protected virtual int ReceiveFrom(byte[] buffer, out int connectionHash)
{
// NOTE: ReceiveFrom allocates.
// we pass our IPEndPoint to ReceiveFrom.
// receive from calls newClientEP.Create(socketAddr).
// IPEndPoint.Create always returns a new IPEndPoint.
// https://github.com/mono/mono/blob/f74eed4b09790a0929889ad7fc2cf96c9b6e3757/mcs/class/System/System.Net.Sockets/Socket.cs#L1761
int read = socket.ReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref newClientEP);
// calculate connectionHash from endpoint
// NOTE: IPEndPoint.GetHashCode() allocates.
// it calls m_Address.GetHashCode().
// m_Address is an IPAddress.
// GetHashCode() allocates for IPv6:
// https://github.com/mono/mono/blob/bdd772531d379b4e78593587d15113c37edd4a64/mcs/class/referencesource/System/net/System/Net/IPAddress.cs#L699
//
// => using only newClientEP.Port wouldn't work, because
// different connections can have the same port.
connectionHash = newClientEP.GetHashCode();
return read;
}
protected virtual KcpServerConnection CreateConnection() =>
new KcpServerConnection(socket, newClientEP, NoDelay, Interval, FastResend, CongestionWindow, SendWindowSize, ReceiveWindowSize, Timeout);
// process incoming messages. should be called before updating the world.
HashSet<int> connectionsToRemove = new HashSet<int>();
public void TickIncoming()
{
while (socket != null && socket.Poll(0, SelectMode.SelectRead))
{
try
{
// receive
int msgLength = ReceiveFrom(rawReceiveBuffer, out int connectionId);
//Log.Info($"KCP: server raw recv {msgLength} bytes = {BitConverter.ToString(buffer, 0, msgLength)}");
// IMPORTANT: detect if buffer was too small for the received
// msgLength. otherwise the excess data would be
// silently lost.
// (see ReceiveFrom documentation)
if (msgLength <= rawReceiveBuffer.Length)
{
// is this a new connection?
if (!connections.TryGetValue(connectionId, out KcpServerConnection connection))
{
// create a new KcpConnection based on last received
// EndPoint. can be overwritten for where-allocation.
connection = CreateConnection();
// DO NOT add to connections yet. only if the first message
// is actually the kcp handshake. otherwise it's either:
// * random data from the internet
// * or from a client connection that we just disconnected
// but that hasn't realized it yet, still sending data
// from last session that we should absolutely ignore.
//
//
// TODO this allocates a new KcpConnection for each new
// internet connection. not ideal, but C# UDP Receive
// already allocated anyway.
//
// expecting a MAGIC byte[] would work, but sending the raw
// UDP message without kcp's reliability will have low
// probability of being received.
//
// for now, this is fine.
// setup authenticated event that also adds to connections
connection.OnAuthenticated = () =>
{
// only send handshake to client AFTER we received his
// handshake in OnAuthenticated.
// we don't want to reply to random internet messages
// with handshakes each time.
connection.SendHandshake();
// add to connections dict after being authenticated.
connections.Add(connectionId, connection);
Log.Info($"KCP: server added connection({connectionId})");
// setup Data + Disconnected events only AFTER the
// handshake. we don't want to fire OnServerDisconnected
// every time we receive invalid random data from the
// internet.
// setup data event
connection.OnData = (message) =>
{
// call mirror event
//Log.Info($"KCP: OnServerDataReceived({connectionId}, {BitConverter.ToString(message.Array, message.Offset, message.Count)})");
OnData.Invoke(connectionId, message);
};
// setup disconnected event
connection.OnDisconnected = () =>
{
// flag for removal
// (can't remove directly because connection is updated
// and event is called while iterating all connections)
connectionsToRemove.Add(connectionId);
// call mirror event
Log.Info($"KCP: OnServerDisconnected({connectionId})");
OnDisconnected.Invoke(connectionId);
};
// finally, call mirror OnConnected event
Log.Info($"KCP: OnServerConnected({connectionId})");
OnConnected.Invoke(connectionId);
};
// now input the message & process received ones
// connected event was set up.
// tick will process the first message and adds the
// connection if it was the handshake.
connection.RawInput(rawReceiveBuffer, msgLength);
connection.TickIncoming();
// again, do not add to connections.
// if the first message wasn't the kcp handshake then
// connection will simply be garbage collected.
}
// existing connection: simply input the message into kcp
else
{
connection.RawInput(rawReceiveBuffer, msgLength);
}
}
else
{
Log.Error($"KCP Server: message of size {msgLength} does not fit into buffer of size {rawReceiveBuffer.Length}. The excess was silently dropped. Disconnecting connectionId={connectionId}.");
Disconnect(connectionId);
}
}
// this is fine, the socket might have been closed in the other end
catch (SocketException) {}
}
// process inputs for all server connections
// (even if we didn't receive anything. need to tick ping etc.)
foreach (KcpServerConnection connection in connections.Values)
{
connection.TickIncoming();
}
// remove disconnected connections
// (can't do it in connection.OnDisconnected because Tick is called
// while iterating connections)
foreach (int connectionId in connectionsToRemove)
{
connections.Remove(connectionId);
}
connectionsToRemove.Clear();
}
// process outgoing messages. should be called after updating the world.
public void TickOutgoing()
{
// flush all server connections
foreach (KcpServerConnection connection in connections.Values)
{
connection.TickOutgoing();
}
}
// process incoming and outgoing for convenience.
// => ideally call ProcessIncoming() before updating the world and
// ProcessOutgoing() after updating the world for minimum latency
public void Tick()
{
TickIncoming();
TickOutgoing();
}
public void Stop()
{
socket?.Close();
socket = null;
}
// pause/unpause to safely support mirror scene handling and to
// immediately pause the receive while loop if needed.
public void Pause()
{
foreach (KcpServerConnection connection in connections.Values)
connection.Pause();
}
public void Unpause()
{
foreach (KcpServerConnection connection in connections.Values)
connection.Unpause();
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 9759159c6589494a9037f5e130a867ed
timeCreated: 1603787747

View File

@@ -0,0 +1,22 @@
using System.Net;
using System.Net.Sockets;
namespace kcp2k
{
public class KcpServerConnection : KcpConnection
{
// Constructor & Send functions can be overwritten for where-allocation:
// https://github.com/vis2k/where-allocation
public KcpServerConnection(Socket socket, EndPoint remoteEndPoint, bool noDelay, uint interval = Kcp.INTERVAL, int fastResend = 0, bool congestionWindow = true, uint sendWindowSize = Kcp.WND_SND, uint receiveWindowSize = Kcp.WND_RCV, int timeout = DEFAULT_TIMEOUT)
{
this.socket = socket;
this.remoteEndPoint = remoteEndPoint;
SetupKcp(noDelay, interval, fastResend, congestionWindow, sendWindowSize, receiveWindowSize, timeout);
}
protected override void RawSend(byte[] data, int length)
{
socket.SendTo(data, 0, length, SocketFlags.None, remoteEndPoint);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 80a9b1ce9a6f14abeb32bfa9921d097b
timeCreated: 1602601483

View File

@@ -0,0 +1,14 @@
// A simple logger class that uses Console.WriteLine by default.
// Can also do Logger.LogMethod = Debug.Log for Unity etc.
// (this way we don't have to depend on UnityEngine)
using System;
namespace kcp2k
{
public static class Log
{
public static Action<string> Info = Console.WriteLine;
public static Action<string> Warning = Console.WriteLine;
public static Action<string> Error = Console.Error.WriteLine;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7b5e1de98d6d84c3793a61cf7d8da9a4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 0b320ff06046474eae7bce7240ea478c
timeCreated: 1626430641

View File

@@ -0,0 +1,24 @@
// where-allocation version of KcpClientConnection.
// may not be wanted on all platforms, so it's an extra optional class.
using System.Net;
using WhereAllocation;
namespace kcp2k
{
public class KcpClientConnectionNonAlloc : KcpClientConnection
{
IPEndPointNonAlloc reusableEP;
protected override void CreateRemoteEndPoint(IPAddress[] addresses, ushort port)
{
// create reusableEP with same address family as remoteEndPoint.
// otherwise ReceiveFrom_NonAlloc couldn't use it.
reusableEP = new IPEndPointNonAlloc(addresses[0], port);
base.CreateRemoteEndPoint(addresses, port);
}
// where-allocation nonalloc recv
protected override int ReceiveFrom(byte[] buffer) =>
socket.ReceiveFrom_NonAlloc(buffer, reusableEP);
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 4c1b235bbe054706bef6d092f361006e
timeCreated: 1626430539

View File

@@ -0,0 +1,17 @@
// where-allocation version of KcpClientConnectionNonAlloc.
// may not be wanted on all platforms, so it's an extra optional class.
using System;
namespace kcp2k
{
public class KcpClientNonAlloc : KcpClient
{
public KcpClientNonAlloc(Action OnConnected, Action<ArraySegment<byte>> OnData, Action OnDisconnected)
: base(OnConnected, OnData, OnDisconnected)
{
}
protected override KcpClientConnection CreateConnection() =>
new KcpClientConnectionNonAlloc();
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2cf0ccf7d551480bb5af08fcbe169f84
timeCreated: 1626435264

View File

@@ -0,0 +1,25 @@
// where-allocation version of KcpServerConnection.
// may not be wanted on all platforms, so it's an extra optional class.
using System.Net;
using System.Net.Sockets;
using WhereAllocation;
namespace kcp2k
{
public class KcpServerConnectionNonAlloc : KcpServerConnection
{
IPEndPointNonAlloc reusableSendEndPoint;
public KcpServerConnectionNonAlloc(Socket socket, EndPoint remoteEndpoint, IPEndPointNonAlloc reusableSendEndPoint, bool noDelay, uint interval = Kcp.INTERVAL, int fastResend = 0, bool congestionWindow = true, uint sendWindowSize = Kcp.WND_SND, uint receiveWindowSize = Kcp.WND_RCV, int timeout = DEFAULT_TIMEOUT)
: base(socket, remoteEndpoint, noDelay, interval, fastResend, congestionWindow, sendWindowSize, receiveWindowSize, timeout)
{
this.reusableSendEndPoint = reusableSendEndPoint;
}
protected override void RawSend(byte[] data, int length)
{
// where-allocation nonalloc send
socket.SendTo_NonAlloc(data, 0, length, SocketFlags.None, reusableSendEndPoint);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 4e1b74cc224b4c83a0f6c8d8da9090ab
timeCreated: 1626430608

View File

@@ -0,0 +1,51 @@
// where-allocation version of KcpServer.
// may not be wanted on all platforms, so it's an extra optional class.
using System;
using System.Net;
using System.Net.Sockets;
using WhereAllocation;
namespace kcp2k
{
public class KcpServerNonAlloc : KcpServer
{
IPEndPointNonAlloc reusableClientEP;
public KcpServerNonAlloc(Action<int> OnConnected, Action<int, ArraySegment<byte>> OnData, Action<int> OnDisconnected, bool DualMode, bool NoDelay, uint Interval, int FastResend = 0, bool CongestionWindow = true, uint SendWindowSize = Kcp.WND_SND, uint ReceiveWindowSize = Kcp.WND_RCV, int Timeout = KcpConnection.DEFAULT_TIMEOUT)
: base(OnConnected, OnData, OnDisconnected, DualMode, NoDelay, Interval, FastResend, CongestionWindow, SendWindowSize, ReceiveWindowSize, Timeout)
{
// create reusableClientEP either IPv4 or IPv6
reusableClientEP = DualMode
? new IPEndPointNonAlloc(IPAddress.IPv6Any, 0)
: new IPEndPointNonAlloc(IPAddress.Any, 0);
}
protected override int ReceiveFrom(byte[] buffer, out int connectionHash)
{
// where-allocation nonalloc ReceiveFrom.
int read = socket.ReceiveFrom_NonAlloc(buffer, 0, buffer.Length, SocketFlags.None, reusableClientEP);
SocketAddress remoteAddress = reusableClientEP.temp;
// where-allocation nonalloc GetHashCode
connectionHash = remoteAddress.GetHashCode();
return read;
}
protected override KcpServerConnection CreateConnection()
{
// IPEndPointNonAlloc is reused all the time.
// we can't store that as the connection's endpoint.
// we need a new copy!
IPEndPoint newClientEP = reusableClientEP.DeepCopyIPEndPoint();
// for allocation free sending, we also need another
// IPEndPointNonAlloc...
IPEndPointNonAlloc reusableSendEP = new IPEndPointNonAlloc(newClientEP.Address, newClientEP.Port);
// create a new KcpConnection NonAlloc version
// -> where-allocation IPEndPointNonAlloc is reused.
// need to create a new one from the temp address.
return new KcpServerConnectionNonAlloc(socket, newClientEP, reusableSendEP, NoDelay, Interval, FastResend, CongestionWindow, SendWindowSize, ReceiveWindowSize, Timeout);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 54b8398dcd544c8a93bcad846214cc40
timeCreated: 1626432191