kcp2k V1.0

This commit is contained in:
vis2k 2020-10-21 20:44:28 +02:00
parent 85be663b13
commit ceb5dceacd
30 changed files with 2086 additions and 7 deletions

View File

@ -268,8 +268,8 @@ GameObject:
- component: {fileID: 1282001518} - component: {fileID: 1282001518}
- component: {fileID: 1282001520} - component: {fileID: 1282001520}
- component: {fileID: 1282001519} - component: {fileID: 1282001519}
- component: {fileID: 1282001521}
- component: {fileID: 1282001522} - component: {fileID: 1282001522}
- component: {fileID: 1282001521}
m_Layer: 0 m_Layer: 0
m_Name: NetworkManager m_Name: NetworkManager
m_TagString: Untagged m_TagString: Untagged
@ -347,7 +347,7 @@ MonoBehaviour:
m_GameObject: {fileID: 1282001517} m_GameObject: {fileID: 1282001517}
m_Enabled: 1 m_Enabled: 1
m_EditorHideFlags: 0 m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: c7424c1070fad4ba2a7a96b02fbeb4bb, type: 3} m_Script: {fileID: 11500000, guid: 6b0fecffa3f624585964b0d0eb21b18e, type: 3}
m_Name: m_Name:
m_EditorClassIdentifier: m_EditorClassIdentifier:
OnClientConnected: OnClientConnected:
@ -374,12 +374,9 @@ MonoBehaviour:
OnServerDisconnected: OnServerDisconnected:
m_PersistentCalls: m_PersistentCalls:
m_Calls: [] m_Calls: []
port: 7777 Port: 7777
NoDelay: 1 NoDelay: 1
serverMaxMessageSize: 16384 Interval: 10
serverMaxReceivesPerTick: 10000
clientMaxMessageSize: 16384
clientMaxReceivesPerTick: 1000
--- !u!114 &1282001522 --- !u!114 &1282001522
MonoBehaviour: MonoBehaviour:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 8f38345a2b94c4b5ca63a775eaad5584
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,16 @@
{
"name": "KCP",
"references": [
"GUID:30817c1a0e6d646d99c048fc403f5979",
"GUID:f51ebe6a0ceec4240a699833d6309b23"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": true,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: f34a216d4b31d4eb4872b2f30c5a6a11
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7bdb797750d0a490684410110bf48192
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,321 @@
#if MIRROR
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using UnityEngine;
using kcp2k;
namespace Mirror.KCP
{
public class KcpTransport : Transport
{
// common
[Header("Transport Configuration")]
public ushort Port = 7777;
[Tooltip("NoDelay is recommended to reduce latency. This also scales better without buffers getting full.")]
public bool NoDelay = true;
[Tooltip("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 = 10;
readonly byte[] buffer = new byte[Kcp.MTU_DEF];
// server
Socket serverSocket;
EndPoint serverNewClientEP = new IPEndPoint(IPAddress.IPv6Any, 0);
// connections <connectionId, connection> where connectionId is EndPoint.GetHashCode
Dictionary<int, KcpServerConnection> connections = new Dictionary<int, KcpServerConnection>();
// client
KcpClientConnection clientConnection;
bool clientConnected;
void Awake()
{
Debug.Log("KcpTransport initialized!");
}
// all except WebGL
public override bool Available() =>
Application.platform != RuntimePlatform.WebGLPlayer;
// use same Kcp configuration on server and client
void ConfigureKcpConnection(KcpConnection connection)
{
// TODO consider lower interval IF interval matters in nodelay mode
// we did this in previous test
connection.kcp.SetNoDelay(1, 10, 2, true);
// this works for 4k:
//connection.kcp.SetWindowSize(128, 128);
// this works for 10k:
connection.kcp.SetWindowSize(512, 512);
// this works for 20k:
//connection.kcp.SetWindowSize(8192, 8192);
}
// client
public override bool ClientConnected() => clientConnection != null;
public override void ClientConnect(string address)
{
if (clientConnected)
{
Debug.LogWarning("KCP: client already connected!");
return;
}
clientConnection = new KcpClientConnection();
// setup events
clientConnection.OnConnected += () =>
{
Debug.Log($"KCP: OnClientConnected");
clientConnected = true;
OnClientConnected.Invoke();
};
clientConnection.OnData += (message) =>
{
//Debug.Log($"KCP: OnClientDataReceived({BitConverter.ToString(message.Array, message.Offset, message.Count)})");
OnClientDataReceived.Invoke(message);
};
clientConnection.OnDisconnected += () =>
{
Debug.Log($"KCP: OnClientDisconnected");
clientConnected = false;
OnClientDisconnected.Invoke();
};
// connect
clientConnection.Connect(address, Port, NoDelay, Interval);
// configure connection for max scale
ConfigureKcpConnection(clientConnection);
}
public override bool ClientSend(int channelId, ArraySegment<byte> segment)
{
if (clientConnection != null)
{
clientConnection.Send(segment);
return true;
}
Debug.LogWarning("KCP: can't send because client not connected!");
return false;
}
public override void ClientDisconnect()
{
// only if connected
// otherwise we end up in a deadlock because of an open Mirror bug:
// https://github.com/vis2k/Mirror/issues/2353
if (clientConnected)
{
clientConnection?.Disconnect();
clientConnection = null;
}
}
HashSet<int> connectionsToRemove = new HashSet<int>();
void UpdateServer()
{
while (serverSocket != null && serverSocket.Poll(0, SelectMode.SelectRead))
{
int msgLength = serverSocket.ReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref serverNewClientEP);
//Debug.Log($"KCP: server raw recv {msgLength} bytes = {BitConverter.ToString(buffer, 0, msgLength)}");
// calculate connectionId from endpoint
int connectionId = serverNewClientEP.GetHashCode();
// is this a new connection?
if (!connections.TryGetValue(connectionId, out KcpServerConnection connection))
{
// add it to a queue
connection = new KcpServerConnection(serverSocket, serverNewClientEP, NoDelay, Interval);
// configure connection for max scale
ConfigureKcpConnection(connection);
//acceptedConnections.Writer.TryWrite(connection);
connections.Add(connectionId, connection);
Debug.Log($"KCP: server added connection {serverNewClientEP}");
// setup connected event
connection.OnConnected += () =>
{
// call mirror event
Debug.Log($"KCP: OnServerConnected({connectionId})");
OnServerConnected.Invoke(connectionId);
};
// setup data event
connection.OnData += (message) =>
{
// call mirror event
//Debug.Log($"KCP: OnServerDataReceived({connectionId}, {BitConverter.ToString(message.Array, message.Offset, message.Count)})");
OnServerDataReceived.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
Debug.Log($"KCP: OnServerDisconnected({connectionId})");
OnServerDisconnected.Invoke(connectionId);
};
// send handshake
connection.Handshake();
}
connection.RawInput(buffer, msgLength);
}
// tick all server connections
foreach (KcpServerConnection connection in connections.Values)
{
connection.Tick();
connection.Receive();
}
// 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();
}
void UpdateClient()
{
// tick client connection
if (clientConnection != null)
{
clientConnection.Tick();
// recv on socket
clientConnection.RawReceive();
// recv on kcp
clientConnection.Receive();
}
}
// IMPORTANT: set script execution order to >1000 to call Transport's
// LateUpdate after all others. Fixes race condition where
// e.g. in uSurvival Transport would apply Cmds before
// ShoulderRotation.LateUpdate, resulting in projectile
// spawns at the point before shoulder rotation.
public void LateUpdate()
{
// note: we need to check enabled in case we set it to false
// when LateUpdate already started.
// (https://github.com/vis2k/Mirror/pull/379)
if (!enabled)
return;
UpdateServer();
UpdateClient();
}
// server
public override bool ServerActive() => serverSocket != null;
public override void ServerStart()
{
// only start once
if (serverSocket != null)
{
Debug.LogWarning("KCP: server already started!");
}
// listen
serverSocket = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp);
serverSocket.DualMode = true;
serverSocket.Bind(new IPEndPoint(IPAddress.IPv6Any, Port));
}
public override bool ServerSend(int connectionId, int channelId, ArraySegment<byte> segment)
{
if (connections.TryGetValue(connectionId, out KcpServerConnection connection))
{
connection.Send(segment);
return true;
}
return false;
}
public override bool ServerDisconnect(int connectionId)
{
if (connections.TryGetValue(connectionId, out KcpServerConnection connection))
{
connection.Disconnect();
return true;
}
return false;
}
public override string ServerGetClientAddress(int connectionId)
{
if (connections.TryGetValue(connectionId, out KcpServerConnection connection))
{
return (connection.GetRemoteEndPoint() as IPEndPoint).Address.ToString();
}
return "";
}
public override void ServerStop()
{
serverSocket?.Close();
serverSocket = null;
}
// common
public override void Shutdown() {}
// MTU
public override ushort GetMaxPacketSize() => Kcp.MTU_DEF;
public override string ToString()
{
return "KCP";
}
int GetTotalSendQueue() =>
connections.Values.Sum(conn => conn.kcp.snd_queue.Count);
int GetTotalReceiveQueue() =>
connections.Values.Sum(conn => conn.kcp.rcv_queue.Count);
int GetTotalSendBuffer() =>
connections.Values.Sum(conn => conn.kcp.snd_buf.Count);
int GetTotalReceiveBuffer() =>
connections.Values.Sum(conn => conn.kcp.rcv_buf.Count);
void OnGUI()
{
GUILayout.BeginArea(new Rect(5, 100, 300, 300));
if (ServerActive())
{
GUILayout.BeginVertical("Box");
GUILayout.Label("SERVER");
GUILayout.Label(" SendQueue: " + GetTotalSendQueue());
GUILayout.Label(" ReceiveQueue: " + GetTotalReceiveQueue());
GUILayout.Label(" SendBuffer: " + GetTotalSendBuffer());
GUILayout.Label(" ReceiveBuffer: " + GetTotalReceiveBuffer());
GUILayout.EndVertical();
}
if (ClientConnected())
{
GUILayout.BeginVertical("Box");
GUILayout.Label("CLIENT");
GUILayout.Label(" SendQueue: " + clientConnection.kcp.snd_queue.Count);
GUILayout.Label(" ReceiveQueue: " + clientConnection.kcp.rcv_queue.Count);
GUILayout.Label(" SendBuffer: " + clientConnection.kcp.snd_buf.Count);
GUILayout.Label(" ReceiveBuffer: " + clientConnection.kcp.rcv_buf.Count);
GUILayout.EndVertical();
}
GUILayout.EndArea();
}
}
}
#endif

View File

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

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b6fcfb8bb66ef4cac9bb90b51a0e932f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,24 @@
MIT License
Copyright (c) 2016 limpo1989
Copyright (c) 2020 Paul Pacheco
Copyright (c) 2020 Lymdun
Copyright (c) 2020 vis2k
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 9a3e8369060cf4e94ac117603de47aa6
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,6 @@
V1.0
- Kcp.cs now mirrors original Kcp.c behaviour
(this fixes dozens of bugs)
V0.1
- initial kcp-csharp based version

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: ed3f2cf1bbf1b4d53a6f2c103d311f71
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 50a0273b27ec14f789a8e3c676e0bff4
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,73 @@
using UnityEngine;
using System.Net;
using System.Net.Sockets;
namespace kcp2k
{
public class KcpClientConnection : KcpConnection
{
readonly byte[] buffer = new byte[1500];
/// <summary>
/// Client connection, does not share the UDP client with anyone
/// so we can set up our own read loop
/// </summary>
/// <param name="host"></param>
/// <param name="port"></param>
public KcpClientConnection() : base()
{
}
public void Connect(string host, ushort port, bool noDelay, uint interval = Kcp.INTERVAL)
{
Debug.Log($"KcpClient: connect to {host}:{port}");
IPAddress[] ipAddress = Dns.GetHostAddresses(host);
if (ipAddress.Length < 1)
throw new SocketException((int)SocketError.HostNotFound);
remoteEndpoint = new IPEndPoint(ipAddress[0], port);
socket = new Socket(remoteEndpoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp);
socket.Connect(remoteEndpoint);
SetupKcp(noDelay, interval);
RawReceive();
Handshake();
}
// TODO call from transport update
public void RawReceive()
{
try
{
if (socket != null)
{
while (socket.Poll(0, SelectMode.SelectRead))
{
int msgLength = socket.ReceiveFrom(buffer, ref remoteEndpoint);
//Debug.Log($"KCP: client raw recv {msgLength} bytes = {BitConverter.ToString(buffer, 0, msgLength)}");
RawInput(buffer, msgLength);
}
// wait a few MS to poll again
//await UniTask.Delay(2);
}
}
catch (SocketException)
{
// this is fine, the socket might have been closed in the other end
}
}
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,282 @@
using System;
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using Unity.Collections.LowLevel.Unsafe;
using Debug = UnityEngine.Debug;
namespace kcp2k
{
public abstract class KcpConnection
{
protected Socket socket;
protected EndPoint remoteEndpoint;
internal Kcp kcp;
volatile bool open;
public event Action OnConnected;
public event Action<ArraySegment<byte>> OnData;
public event Action OnDisconnected;
// If we don't receive anything these many milliseconds
// then consider us disconnected
public const int TIMEOUT = 3000;
// internal time.
// StopWatch offers ElapsedMilliSeconds and should be more precise than
// Unity's time.deltaTime over long periods.
readonly Stopwatch refTime = new Stopwatch();
// recv buffer to avoid allocations
byte[] buffer = new byte[Kcp.MTU_DEF];
volatile uint lastReceived;
internal static readonly ArraySegment<byte> Hello = new ArraySegment<byte>(new byte[] { 0 });
private static readonly ArraySegment<byte> Goodby = new ArraySegment<byte>(new byte[] { 1 });
// a connection is authenticated after sending the correct handshake.
// useful to protect against random data from the internet.
bool authenticated;
protected KcpConnection()
{
}
// NoDelay & interval 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)
{
kcp = new Kcp(0, RawSend);
kcp.SetNoDelay(noDelay ? 1u : 0u, interval);
refTime.Start();
open = true;
Tick();
}
public void Tick()
{
try
{
uint time = (uint)refTime.ElapsedMilliseconds;
// TODO MirorrNG KCP used to set lastReceived here. but this
// would make the below time check always true.
// should we set lastReceived after updating instead?
//lastReceived = time;
if (open && time < lastReceived + TIMEOUT)
{
kcp.Update(time);
// check can be used to skip updates IF:
// - time < what check returned
// - AND send / recv haven't been called in that time
// (see Check() comments)
//
// for now, let's just always update and not call check.
//uint check = kcp.Check();
}
}
catch (SocketException)
{
// this is ok, the connection was closed
open = false;
}
catch (ObjectDisposedException)
{
// fine, socket was closed, no more ticking needed
open = false;
}
catch (Exception ex)
{
open = false;
Debug.LogException(ex);
}
}
public void RawInput(byte[] buffer, int msgLength)
{
int input = kcp.Input(buffer, msgLength);
if (input == 0)
{
lastReceived = (uint)refTime.ElapsedMilliseconds;
}
else Debug.LogWarning($"Input failed with error={input} for buffer with length={msgLength}");
}
protected abstract void RawSend(byte[] data, int length);
public void Send(ArraySegment<byte> data)
{
// only allow sending up to MaxMessageSize sized messages.
// other end won't process bigger messages anyway.
if (data.Count <= Kcp.MTU_DEF)
{
int sent = kcp.Send(data.Array, data.Offset, data.Count);
if (sent < 0)
{
Debug.LogWarning($"Send failed with error={sent} for segment with length={data.Count}");
}
}
else Debug.LogError($"Failed to send message of size {data.Count} because it's larger than MaxMessageSize={Kcp.MTU_DEF}");
}
protected virtual void Dispose()
{
}
// ArraySegment content comparison without Linq
public static unsafe bool SegmentsEqual(ArraySegment<byte> a, ArraySegment<byte> b)
{
if (a.Count == b.Count)
{
fixed (byte* aPtr = &a.Array[a.Offset],
bPtr = &b.Array[b.Offset])
{
return UnsafeUtility.MemCmp(aPtr, bPtr, a.Count) == 0;
}
}
return false;
}
/// <summary>
/// reads a message from connection
/// </summary>
/// <param name="buffer">buffer where the message will be written</param>
/// <returns>true if we got a message, false if we got disconnected</returns>
public void Receive()
{
if (!open)
{
OnDisconnected?.Invoke();
Debug.LogWarning("DISCO a");
return;
}
// read as long as we have things to read
int msgSize;
while ((msgSize = kcp.PeekSize()) > 0)
{
// only allow receiving up to MaxMessageSize sized messages.
// otherwise we would get BlockCopy ArgumentException anyway.
if (msgSize <= Kcp.MTU_DEF)
{
int received = kcp.Receive(buffer, msgSize);
if (received >= 0)
{
ArraySegment<byte> dataSegment = new ArraySegment<byte>(buffer, 0, msgSize);
// not authenticated yet?
if (!authenticated)
{
// handshake message?
if (SegmentsEqual(dataSegment, Hello))
{
// we are only connected if we received the handshake.
// not just after receiving any first data.
authenticated = true;
//Debug.Log("KCP: received handshake");
OnConnected?.Invoke();
}
// otherwise it's random data from the internet, not
// from a legitimate player.
else
{
Debug.LogWarning("KCP: received random data before handshake. Disconnecting the connection.");
open = false;
OnDisconnected?.Invoke();
break;
}
}
// authenticated.
else
{
// disconnect message?
if (SegmentsEqual(dataSegment, Goodby))
{
// if we receive a disconnect message, then close everything
//Debug.Log("KCP: received disconnect message");
open = false;
OnDisconnected?.Invoke();
break;
}
// otherwise regular message
else
{
// only accept regular messages
//Debug.LogWarning($"Kcp recv msg: {BitConverter.ToString(buffer, 0, msgSize)}");
OnData?.Invoke(dataSegment);
}
}
}
else
{
// if receive failed, close everything
Debug.LogWarning($"Receive failed with error={received}. closing connection.");
open = false;
OnDisconnected?.Invoke();
break;
}
}
// we don't allow sending messages > Max, so this must be an
// attacker. let's disconnect to avoid allocation attacks etc.
else
{
Debug.LogWarning($"KCP: possible allocation attack for msgSize {msgSize} > max {Kcp.MTU_DEF}. Disconnecting the connection.");
open = false;
OnDisconnected?.Invoke();
break;
}
}
}
public void Handshake()
{
// send a greeting and see if the server replies
Debug.LogWarning("KcpConnection: sending Handshake to other end!");
Send(Hello);
}
/// <summary>
/// Disconnect this connection
/// </summary>
public virtual void Disconnect()
{
// send a disconnect message and disconnect
if (open && socket.Connected)
{
try
{
Send(Goodby);
kcp.Flush();
// call OnDisconnected event, even if we manually
// disconnected
OnDisconnected?.Invoke();
}
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
}
}
open = false;
// EOF is now available
//dataAvailable?.TrySetResult();
}
// get remote endpoint
public EndPoint GetRemoteEndPoint() => remoteEndpoint;
}
}

View File

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

View File

@ -0,0 +1,20 @@
using System.Net;
using System.Net.Sockets;
namespace kcp2k
{
public class KcpServerConnection : KcpConnection
{
public KcpServerConnection(Socket socket, EndPoint remoteEndpoint, bool noDelay, uint interval = Kcp.INTERVAL)
{
this.socket = socket;
this.remoteEndpoint = remoteEndpoint;
SetupKcp(noDelay, interval);
}
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,8 @@
fileFormatVersion: 2
guid: da30be07fda954844b621fec7c727b6a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("kcp2k.Tests")]

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: aec6a15ac7bd43129317ea1f01f19782
timeCreated: 1602665988

View File

@ -0,0 +1,36 @@
// byte[] buffer with Position, resizes automatically.
// There is no size limit because we will only use it with ~MTU sized arrays.
using System;
using System.Runtime.CompilerServices;
namespace kcp2k
{
public class ByteBuffer
{
public int Position;
internal const int InitialCapacity = 1200;
public byte[] RawBuffer = new byte[InitialCapacity];
// resize to 'value' capacity if needed
[MethodImpl(MethodImplOptions.AggressiveInlining)]
void EnsureCapacity(int value)
{
if (RawBuffer.Length < value)
{
int capacity = Math.Max(value, RawBuffer.Length * 2);
Array.Resize(ref RawBuffer, capacity);
}
}
// write bytes at offset
public void WriteBytes(byte[] bytes, int offset, int count)
{
if (offset >= 0 && count > 0)
{
EnsureCapacity(Position + count);
Buffer.BlockCopy(bytes, offset, RawBuffer, Position, count);
Position += count;
}
}
}
}

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -0,0 +1,84 @@
using System.Collections.Generic;
namespace kcp2k
{
// KCP Segment Definition
internal class Segment
{
internal uint conv; // conversation
internal uint cmd; // command, e.g. Kcp.CMD_ACK etc.
internal uint frg; // fragment
internal uint wnd; // window
internal uint ts; // timestamp
internal uint sn; // serial number
internal uint una;
internal uint resendts; // resend timestamp
internal int rto;
internal uint fastack;
internal uint xmit;
internal ByteBuffer data;
// pool ////////////////////////////////////////////////////////////////
internal static readonly Stack<Segment> Pool = new Stack<Segment>(32);
public static Segment Take()
{
if (Pool.Count > 0)
{
Segment seg = Pool.Pop();
return seg;
}
return new Segment();
}
public static void Return(Segment seg)
{
seg.Reset();
Pool.Push(seg);
}
////////////////////////////////////////////////////////////////////////
internal Segment()
{
// allocate the ByteBuffer once.
// note that we don't need to pool ByteBuffer, because Segment is
// already pooled.
data = new ByteBuffer();
}
// ikcp_encode_seg
// encode a segment into buffer
internal int Encode(byte[] ptr, int offset)
{
int offset_ = offset;
offset += Utils.Encode32U(ptr, offset, conv);
offset += Utils.Encode8u(ptr, offset, (byte)cmd);
offset += Utils.Encode8u(ptr, offset, (byte)frg);
offset += Utils.Encode16U(ptr, offset, (ushort)wnd);
offset += Utils.Encode32U(ptr, offset, ts);
offset += Utils.Encode32U(ptr, offset, sn);
offset += Utils.Encode32U(ptr, offset, una);
offset += Utils.Encode32U(ptr, offset, (uint)data.Position);
return offset - offset_;
}
// reset to return a fresh segment to the pool
internal void Reset()
{
conv = 0;
cmd = 0;
frg = 0;
wnd = 0;
ts = 0;
sn = 0;
una = 0;
rto = 0;
xmit = 0;
resendts = 0;
fastack = 0;
// keep buffer for next pool usage, but reset position
data.Position = 0;
}
}
}

View File

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

View File

@ -0,0 +1,68 @@
using System.Runtime.CompilerServices;
namespace kcp2k
{
public static class Utils
{
// encode 8 bits unsigned int
public static int Encode8u(byte[] p, int offset, byte c)
{
p[0 + offset] = c;
return 1;
}
// decode 8 bits unsigned int
public static int Decode8u(byte[] p, int offset, ref byte c)
{
c = p[0 + offset];
return 1;
}
/* encode 16 bits unsigned int (lsb) */
public static int Encode16U(byte[] p, int offset, ushort w)
{
p[0 + offset] = (byte)(w >> 0);
p[1 + offset] = (byte)(w >> 8);
return 2;
}
/* decode 16 bits unsigned int (lsb) */
public static int Decode16U(byte[] p, int offset, ref ushort c)
{
ushort result = 0;
result |= p[0 + offset];
result |= (ushort)(p[1 + offset] << 8);
c = result;
return 2;
}
/* encode 32 bits unsigned int (lsb) */
public static int Encode32U(byte[] p, int offset, uint l)
{
p[0 + offset] = (byte)(l >> 0);
p[1 + offset] = (byte)(l >> 8);
p[2 + offset] = (byte)(l >> 16);
p[3 + offset] = (byte)(l >> 24);
return 4;
}
/* decode 32 bits unsigned int (lsb) */
public static int Decode32U(byte[] p, int offset, ref uint c)
{
uint result = 0;
result |= p[0 + offset];
result |= (uint)(p[1 + offset] << 8);
result |= (uint)(p[2 + offset] << 16);
result |= (uint)(p[3 + offset] << 24);
c = result;
return 4;
}
// timediff was a macro in original Kcp. let's inline it if possible.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int TimeDiff(uint later, uint earlier)
{
return (int)(later - earlier);
}
}
}

View File

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