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,8 @@
fileFormatVersion: 2
guid: 953bb5ec5ab2346a092f58061e01ba65
folderAsset: yes
DefaultImporter:
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,346 @@
//#if MIRROR <- commented out because MIRROR isn't defined on first import yet
using System;
using System.Linq;
using System.Net;
using UnityEngine;
using Mirror;
namespace kcp2k
{
[HelpURL("https://mirror-networking.gitbook.io/docs/transports/kcp-transport")]
[DisallowMultipleComponent]
public class KcpTransport : Transport
{
// scheme used by this transport
public const string Scheme = "kcp";
// common
[Header("Transport Configuration")]
public ushort Port = 7777;
[Tooltip("DualMode listens to IPv6 and IPv4 simultaneously. Disable if the platform only supports IPv4.")]
public bool DualMode = true;
[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;
[Tooltip("KCP timeout in milliseconds. Note that KCP sends a ping automatically.")]
public int Timeout = 10000;
[Header("Advanced")]
[Tooltip("KCP fastresend parameter. Faster resend for the cost of higher bandwidth. 0 in normal mode, 2 in turbo mode.")]
public int FastResend = 2;
[Tooltip("KCP congestion window. Enabled in normal mode, disabled in turbo mode. Disable this for high scale games if connections get choked regularly.")]
public bool CongestionWindow = false; // KCP 'NoCongestionWindow' is false by default. here we negate it for ease of use.
[Tooltip("KCP window size can be modified to support higher loads.")]
public uint SendWindowSize = 4096; //Kcp.WND_SND; 32 by default. Mirror sends a lot, so we need a lot more.
[Tooltip("KCP window size can be modified to support higher loads.")]
public uint ReceiveWindowSize = 4096; //Kcp.WND_RCV; 128 by default. Mirror sends a lot, so we need a lot more.
[Tooltip("Enable to use where-allocation NonAlloc KcpServer/Client/Connection versions. Highly recommended on all Unity platforms.")]
public bool NonAlloc = true;
// server & client (where-allocation NonAlloc versions)
KcpServer server;
KcpClient client;
// debugging
[Header("Debug")]
public bool debugLog;
// show statistics in OnGUI
public bool statisticsGUI;
// log statistics for headless servers that can't show them in GUI
public bool statisticsLog;
void Awake()
{
// logging
// Log.Info should use Debug.Log if enabled, or nothing otherwise
// (don't want to spam the console on headless servers)
if (debugLog)
Log.Info = Debug.Log;
else
Log.Info = _ => {};
Log.Warning = Debug.LogWarning;
Log.Error = Debug.LogError;
// client
client = NonAlloc
? new KcpClientNonAlloc(
() => OnClientConnected.Invoke(),
(message) => OnClientDataReceived.Invoke(message, Channels.Reliable),
() => OnClientDisconnected.Invoke())
: new KcpClient(
() => OnClientConnected.Invoke(),
(message) => OnClientDataReceived.Invoke(message, Channels.Reliable),
() => OnClientDisconnected.Invoke());
// server
server = NonAlloc
? new KcpServerNonAlloc(
(connectionId) => OnServerConnected.Invoke(connectionId),
(connectionId, message) => OnServerDataReceived.Invoke(connectionId, message, Channels.Reliable),
(connectionId) => OnServerDisconnected.Invoke(connectionId),
DualMode,
NoDelay,
Interval,
FastResend,
CongestionWindow,
SendWindowSize,
ReceiveWindowSize,
Timeout)
: new KcpServer(
(connectionId) => OnServerConnected.Invoke(connectionId),
(connectionId, message) => OnServerDataReceived.Invoke(connectionId, message, Channels.Reliable),
(connectionId) => OnServerDisconnected.Invoke(connectionId),
DualMode,
NoDelay,
Interval,
FastResend,
CongestionWindow,
SendWindowSize,
ReceiveWindowSize,
Timeout);
if (statisticsLog)
InvokeRepeating(nameof(OnLogStatistics), 1, 1);
Debug.Log("KcpTransport initialized!");
}
// all except WebGL
public override bool Available() =>
Application.platform != RuntimePlatform.WebGLPlayer;
// client
public override bool ClientConnected() => client.connected;
public override void ClientConnect(string address)
{
client.Connect(address, Port, NoDelay, Interval, FastResend, CongestionWindow, SendWindowSize, ReceiveWindowSize, Timeout);
}
public override void ClientSend(ArraySegment<byte> segment, int channelId)
{
// switch to kcp channel.
// unreliable or reliable.
// default to reliable just to be sure.
switch (channelId)
{
case Channels.Unreliable:
client.Send(segment, KcpChannel.Unreliable);
break;
default:
client.Send(segment, KcpChannel.Reliable);
break;
}
}
public override void ClientDisconnect() => client.Disconnect();
// process incoming in early update
public override void ClientEarlyUpdate()
{
// scene change messages disable transports to stop them from
// processing while changing the scene.
// -> we need to check enabled here
// -> and in kcp's internal loops, see Awake() OnCheckEnabled setup!
// (see also: https://github.com/vis2k/Mirror/pull/379)
if (enabled) client.TickIncoming();
}
// process outgoing in late update
public override void ClientLateUpdate() => client.TickOutgoing();
// scene change message will disable transports.
// kcp processes messages in an internal loop which should be
// stopped immediately after scene change (= after disabled)
// => kcp has tests to guaranteed that calling .Pause() during the
// receive loop stops the receive loop immediately, not after.
void OnEnable()
{
// unpause when enabled again
client?.Unpause();
server?.Unpause();
}
void OnDisable()
{
// pause immediately when not enabled anymore
client?.Pause();
server?.Pause();
}
// server
public override Uri ServerUri()
{
UriBuilder builder = new UriBuilder();
builder.Scheme = Scheme;
builder.Host = Dns.GetHostName();
builder.Port = Port;
return builder.Uri;
}
public override bool ServerActive() => server.IsActive();
public override void ServerStart() => server.Start(Port);
public override void ServerSend(int connectionId, ArraySegment<byte> segment, int channelId)
{
// switch to kcp channel.
// unreliable or reliable.
// default to reliable just to be sure.
switch (channelId)
{
case Channels.Unreliable:
server.Send(connectionId, segment, KcpChannel.Unreliable);
break;
default:
server.Send(connectionId, segment, KcpChannel.Reliable);
break;
}
}
public override void ServerDisconnect(int connectionId) => server.Disconnect(connectionId);
public override string ServerGetClientAddress(int connectionId) => server.GetClientAddress(connectionId);
public override void ServerStop() => server.Stop();
public override void ServerEarlyUpdate()
{
// scene change messages disable transports to stop them from
// processing while changing the scene.
// -> we need to check enabled here
// -> and in kcp's internal loops, see Awake() OnCheckEnabled setup!
// (see also: https://github.com/vis2k/Mirror/pull/379)
if (enabled) server.TickIncoming();
}
// process outgoing in late update
public override void ServerLateUpdate() => server.TickOutgoing();
// common
public override void Shutdown() {}
// max message size
public override int GetMaxPacketSize(int channelId = Channels.Reliable)
{
// switch to kcp channel.
// unreliable or reliable.
// default to reliable just to be sure.
switch (channelId)
{
case Channels.Unreliable:
return KcpConnection.UnreliableMaxMessageSize;
default:
return KcpConnection.ReliableMaxMessageSize;
}
}
// kcp reliable channel max packet size is MTU * WND_RCV
// this allows 144kb messages. but due to head of line blocking, all
// other messages would have to wait until the maxed size one is
// delivered. batching 144kb messages each time would be EXTREMELY slow
// and fill the send queue nearly immediately when using it over the
// network.
// => instead we always use MTU sized batches.
// => people can still send maxed size if needed.
public override int GetBatchThreshold(int channelId) =>
KcpConnection.UnreliableMaxMessageSize;
// server statistics
// LONG to avoid int overflows with connections.Sum.
// see also: https://github.com/vis2k/Mirror/pull/2777
public long GetAverageMaxSendRate() =>
server.connections.Count > 0
? server.connections.Values.Sum(conn => (long)conn.MaxSendRate) / server.connections.Count
: 0;
public long GetAverageMaxReceiveRate() =>
server.connections.Count > 0
? server.connections.Values.Sum(conn => (long)conn.MaxReceiveRate) / server.connections.Count
: 0;
long GetTotalSendQueue() =>
server.connections.Values.Sum(conn => conn.SendQueueCount);
long GetTotalReceiveQueue() =>
server.connections.Values.Sum(conn => conn.ReceiveQueueCount);
long GetTotalSendBuffer() =>
server.connections.Values.Sum(conn => conn.SendBufferCount);
long GetTotalReceiveBuffer() =>
server.connections.Values.Sum(conn => conn.ReceiveBufferCount);
// PrettyBytes function from DOTSNET
// pretty prints bytes as KB/MB/GB/etc.
// long to support > 2GB
// divides by floats to return "2.5MB" etc.
public static string PrettyBytes(long bytes)
{
// bytes
if (bytes < 1024)
return $"{bytes} B";
// kilobytes
else if (bytes < 1024L * 1024L)
return $"{(bytes / 1024f):F2} KB";
// megabytes
else if (bytes < 1024 * 1024L * 1024L)
return $"{(bytes / (1024f * 1024f)):F2} MB";
// gigabytes
return $"{(bytes / (1024f * 1024f * 1024f)):F2} GB";
}
// OnGUI allocates even if it does nothing. avoid in release.
#if UNITY_EDITOR || DEVELOPMENT_BUILD
void OnGUI()
{
if (!statisticsGUI) return;
GUILayout.BeginArea(new Rect(5, 110, 300, 300));
if (ServerActive())
{
GUILayout.BeginVertical("Box");
GUILayout.Label("SERVER");
GUILayout.Label($" connections: {server.connections.Count}");
GUILayout.Label($" MaxSendRate (avg): {PrettyBytes(GetAverageMaxSendRate())}/s");
GUILayout.Label($" MaxRecvRate (avg): {PrettyBytes(GetAverageMaxReceiveRate())}/s");
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($" MaxSendRate: {PrettyBytes(client.connection.MaxSendRate)}/s");
GUILayout.Label($" MaxRecvRate: {PrettyBytes(client.connection.MaxReceiveRate)}/s");
GUILayout.Label($" SendQueue: {client.connection.SendQueueCount}");
GUILayout.Label($" ReceiveQueue: {client.connection.ReceiveQueueCount}");
GUILayout.Label($" SendBuffer: {client.connection.SendBufferCount}");
GUILayout.Label($" ReceiveBuffer: {client.connection.ReceiveBufferCount}");
GUILayout.EndVertical();
}
GUILayout.EndArea();
}
#endif
void OnLogStatistics()
{
if (ServerActive())
{
string log = "kcp SERVER @ time: " + NetworkTime.localTime + "\n";
log += $" connections: {server.connections.Count}\n";
log += $" MaxSendRate (avg): {PrettyBytes(GetAverageMaxSendRate())}/s\n";
log += $" MaxRecvRate (avg): {PrettyBytes(GetAverageMaxReceiveRate())}/s\n";
log += $" SendQueue: {GetTotalSendQueue()}\n";
log += $" ReceiveQueue: {GetTotalReceiveQueue()}\n";
log += $" SendBuffer: {GetTotalSendBuffer()}\n";
log += $" ReceiveBuffer: {GetTotalReceiveBuffer()}\n\n";
Debug.Log(log);
}
if (ClientConnected())
{
string log = "kcp CLIENT @ time: " + NetworkTime.localTime + "\n";
log += $" MaxSendRate: {PrettyBytes(client.connection.MaxSendRate)}/s\n";
log += $" MaxRecvRate: {PrettyBytes(client.connection.MaxReceiveRate)}/s\n";
log += $" SendQueue: {client.connection.SendQueueCount}\n";
log += $" ReceiveQueue: {client.connection.ReceiveQueueCount}\n";
log += $" SendBuffer: {client.connection.SendBufferCount}\n";
log += $" ReceiveBuffer: {client.connection.ReceiveBufferCount}\n\n";
Debug.Log(log);
}
}
public override string ToString() => "KCP";
}
}
//#endif MIRROR <- commented out because MIRROR isn't defined on first import yet

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6b0fecffa3f624585964b0d0eb21b18e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 71a1c8e8c022d4731a481c1808f37e5d
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,94 @@
V1.12 [2021-07-16]
- where-allocation removed. will be optional in the future.
- Tests: don't depend on Unity anymore
- fix: #26 - Kcp now catches exception if host couldn't be resolved, and calls
OnDisconnected to let the user now.
- fix: KcpServer.DualMode is now configurable in the constructor instead of
using #if UNITY_SWITCH. makes it run on all other non dual mode platforms too.
V1.11 rollback [2021-06-01]
- perf: Segment MemoryStream initial capacity set to MTU to avoid early runtime
resizing/allocations
V1.10 [2021-05-28]
- feature: configurable Timeout
- allocations explained with comments (C# ReceiveFrom / IPEndPoint.GetHashCode)
- fix: #17 KcpConnection.ReceiveNextReliable now assigns message default so it
works in .net too
- fix: Segment pool is not static anymore. Each kcp instance now has it's own
Pool<Segment>. fixes #18 concurrency issues
V1.9 [2021-03-02]
- Tick() split into TickIncoming()/TickOutgoing() to use in Mirror's new update
functions. allows to minimize latency.
=> original Tick() is still supported for convenience. simply processes both!
V1.8 [2021-02-14]
- fix: Unity IPv6 errors on Nintendo Switch
- fix: KcpConnection now disconnects if data message was received without content.
previously it would call OnData with an empty ArraySegment, causing all kinds of
weird behaviour in Mirror/DOTSNET. Added tests too.
- fix: KcpConnection.SendData: don't allow sending empty messages anymore. disconnect
and log a warning to make it completely obvious.
V1.7 [2021-01-13]
- fix: unreliable messages reset timeout now too
- perf: KcpConnection OnCheckEnabled callback changed to a simple 'paused' boolean.
This is faster than invoking a Func<bool> every time and allows us to fix #8 more
easily later by calling .Pause/.Unpause from OnEnable/OnDisable in MirrorTransport.
- fix #8: Unpause now resets timeout to fix a bug where Mirror would pause kcp,
change the scene which took >10s, then unpause and kcp would detect the lack of
any messages for >10s as timeout. Added test to make sure it never happens again.
- MirrorTransport: statistics logging for headless servers
- Mirror Transport: Send/Receive window size increased once more from 2048 to 4096.
V1.6 [2021-01-10]
- Unreliable channel added!
- perf: KcpHeader byte added to every kcp message to indicate
Handshake/Data/Ping/Disconnect instead of scanning each message for Hello/Byte/Ping
content via SegmentEquals. It's a lot cleaner, should be faster and should avoid
edge cases where a message content would equal Hello/Ping/Bye sequence accidentally.
- Kcp.Input: offset moved to parameters for cases where it's needed
- Kcp.SetMtu from original Kcp.c
V1.5 [2021-01-07]
- KcpConnection.MaxSend/ReceiveRate calculation based on the article
- MirrorTransport: large send/recv window size defaults to avoid high latencies caused
by packets not being processed fast enough
- MirrorTransport: show MaxSend/ReceiveRate in debug gui
- MirrorTransport: don't Log.Info to console in headless mode if debug log is disabled
V1.4 [2020-11-27]
- fix: OnCheckEnabled added. KcpConnection message processing while loop can now
be interrupted immediately. fixes Mirror Transport scene changes which need to stop
processing any messages immediately after a scene message)
- perf: Mirror KcpTransport: FastResend enabled by default. turbo mode according to:
https://github.com/skywind3000/kcp/blob/master/README.en.md#protocol-configuration
- perf: Mirror KcpTransport: CongestionControl disabled by default (turbo mode)
V1.3 [2020-11-17]
- Log.Info/Warning/Error so logging doesn't depend on UnityEngine anymore
- fix: Server.Tick catches SocketException which happens if Android client is killed
- MirrorTransport: debugLog option added that can be checked in Unity Inspector
- Utils.Clamp so Kcp.cs doesn't depend on UnityEngine
- Utils.SegmentsEqual: use Linq SequenceEqual so it doesn't depend on UnityEngine
=> kcp2k can now be used in any C# project even without Unity
V1.2 [2020-11-10]
- more tests added
- fix: raw receive buffers are now all of MTU size
- fix: raw receive detects error where buffer was too small for msgLength and
result in excess data being dropped silently
- KcpConnection.MaxMessageSize added for use in high level
- KcpConnection.MaxMessageSize increased from 1200 bytes to to maximum allowed
message size of 145KB for kcp (based on mtu, overhead, wnd_rcv)
V1.1 [2020-10-30]
- high level cleanup, fixes, improvements
V1.0 [2020-10-22]
- 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: 5a54d18b954cb4407a28b633fc32ea6d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

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

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5cafb8851a0084f3e94a580c207b3923
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

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,46 @@
// Pool to avoid allocations (from libuv2k & Mirror)
using System;
using System.Collections.Generic;
namespace kcp2k
{
public class Pool<T>
{
// Mirror is single threaded, no need for concurrent collections
readonly Stack<T> objects = new Stack<T>();
// some types might need additional parameters in their constructor, so
// we use a Func<T> generator
readonly Func<T> objectGenerator;
// some types might need additional cleanup for returned objects
readonly Action<T> objectResetter;
public Pool(Func<T> objectGenerator, Action<T> objectResetter, int initialCapacity)
{
this.objectGenerator = objectGenerator;
this.objectResetter = objectResetter;
// allocate an initial pool so we have fewer (if any)
// allocations in the first few frames (or seconds).
for (int i = 0; i < initialCapacity; ++i)
objects.Push(objectGenerator());
}
// take an element from the pool, or create a new one if empty
public T Take() => objects.Count > 0 ? objects.Pop() : objectGenerator();
// return an element to the pool
public void Return(T item)
{
objectResetter(item);
objects.Push(item);
}
// clear the pool
public void Clear() => objects.Clear();
// count to see how many objects are in the pool. useful for tests.
public int Count => objects.Count;
}
}

View File

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

View File

@@ -0,0 +1,62 @@
using System.IO;
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 size that the receive can currently receive
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;
// we need an auto scaling byte[] with a WriteBytes function.
// MemoryStream does that perfectly, no need to reinvent the wheel.
// note: no need to pool it, because Segment is already pooled.
// -> MTU as initial capacity to avoid most runtime resizing/allocations
internal MemoryStream data = new MemoryStream(Kcp.MTU_DEF);
// 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 length (= bytes written)
data.SetLength(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,76 @@
using System.Runtime.CompilerServices;
namespace kcp2k
{
public static partial class Utils
{
// Clamp so we don't have to depend on UnityEngine
public static int Clamp(int value, int min, int max)
{
if (value < min) return min;
if (value > max) return max;
return value;
}
// 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:

View File

@@ -0,0 +1,15 @@
{
"name": "kcp2k",
"references": [
"GUID:63c380d6dae6946209ed0832388a657c"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": true,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

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

View File

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

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Mirror Networking (vis2k, FakeByte)
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: a857d4e863bbf4a7dba70bc2cd1b5949
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

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

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 158a96a7489b450485a8b06a13328871
timeCreated: 1622356221

View File

@@ -0,0 +1,58 @@
using System.Net;
using System.Net.Sockets;
namespace WhereAllocation
{
public static class Extensions
{
// always pass the same IPEndPointNonAlloc instead of allocating a new
// one each time.
//
// use IPEndPointNonAlloc.temp to get the latest SocketAdddress written
// by ReceiveFrom_Internal!
//
// IMPORTANT: .temp will be overwritten in next call!
// hash or manually copy it if you need to store it, e.g.
// when adding a new connection.
public static int ReceiveFrom_NonAlloc(
this Socket socket,
byte[] buffer,
int offset,
int size,
SocketFlags socketFlags,
IPEndPointNonAlloc remoteEndPoint)
{
// call ReceiveFrom with IPEndPointNonAlloc.
// need to wrap this in ReceiveFrom_NonAlloc because it's not
// obvious that IPEndPointNonAlloc.Create does NOT create a new
// IPEndPoint. it saves the result in IPEndPointNonAlloc.temp!
EndPoint casted = remoteEndPoint;
return socket.ReceiveFrom(buffer, offset, size, socketFlags, ref casted);
}
// same as above, different parameters
public static int ReceiveFrom_NonAlloc(this Socket socket, byte[] buffer, IPEndPointNonAlloc remoteEndPoint)
{
EndPoint casted = remoteEndPoint;
return socket.ReceiveFrom(buffer, ref casted);
}
// SendTo allocates too:
// https://github.com/mono/mono/blob/f74eed4b09790a0929889ad7fc2cf96c9b6e3757/mcs/class/System/System.Net.Sockets/Socket.cs#L2240
// -> the allocation is in EndPoint.Serialize()
// NOTE: technically this function isn't necessary.
// could just pass IPEndPointNonAlloc.
// still good for strong typing.
public static int SendTo_NonAlloc(
this Socket socket,
byte[] buffer,
int offset,
int size,
SocketFlags socketFlags,
IPEndPointNonAlloc remoteEndPoint)
{
EndPoint casted = remoteEndPoint;
return socket.SendTo(buffer, offset, size, socketFlags, casted);
}
}
}

View File

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

View File

@@ -0,0 +1,208 @@
using System;
using System.Net;
using System.Net.Sockets;
namespace WhereAllocation
{
public class IPEndPointNonAlloc : IPEndPoint
{
// Two steps to remove allocations in ReceiveFrom_Internal:
//
// 1.) remoteEndPoint.Serialize():
// https://github.com/mono/mono/blob/f74eed4b09790a0929889ad7fc2cf96c9b6e3757/mcs/class/System/System.Net.Sockets/Socket.cs#L1733
// -> creates an EndPoint for ReceiveFrom_Internal to write into
// -> it's never read from:
// ReceiveFrom_Internal passes it to native:
// https://github.com/mono/mono/blob/f74eed4b09790a0929889ad7fc2cf96c9b6e3757/mcs/class/System/System.Net.Sockets/Socket.cs#L1885
// native recv populates 'sockaddr* from' with the remote address:
// https://docs.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-recvfrom
// -> can NOT be null. bricks both Unity and Unity Hub otherwise.
// -> it seems as if Serialize() is only called to avoid allocating
// a 'new SocketAddress' in ReceiveFrom. it's up to the EndPoint.
//
// 2.) EndPoint.Create(SocketAddress):
// https://github.com/mono/mono/blob/f74eed4b09790a0929889ad7fc2cf96c9b6e3757/mcs/class/System/System.Net.Sockets/Socket.cs#L1761
// -> SocketAddress is the remote's address that we want to return
// -> to avoid 'new EndPoint(SocketAddress), it seems up to the user
// to decide how to create a new EndPoint via .Create
// -> SocketAddress is the object that was returned by Serialize()
//
// in other words, all we need is an extra SocketAddress field that we
// can pass to ReceiveFrom_Internal to write the result into.
// => callers can then get the result from the extra field!
// => no allocations
//
// IMPORTANT: remember that IPEndPointNonAlloc is always the same object
// and never changes. only the helper field is changed.
public SocketAddress temp;
// constructors simply create the field once by calling the base method.
// (our overwritten method would create anything new)
public IPEndPointNonAlloc(long address, int port) : base(address, port)
{
temp = base.Serialize();
}
public IPEndPointNonAlloc(IPAddress address, int port) : base(address, port)
{
temp = base.Serialize();
}
// Serialize simply returns it
public override SocketAddress Serialize() => temp;
// Create doesn't need to create anything.
// SocketAddress object is already the one we returned in Serialize().
// ReceiveFrom_Internal simply wrote into it.
public override EndPoint Create(SocketAddress socketAddress)
{
// original IPEndPoint.Create validates:
if (socketAddress.Family != AddressFamily)
throw new ArgumentException($"Unsupported socketAddress.AddressFamily: {socketAddress.Family}. Expected: {AddressFamily}");
if (socketAddress.Size < 8)
throw new ArgumentException($"Unsupported socketAddress.Size: {socketAddress.Size}. Expected: <8");
// double check to guarantee that ReceiveFrom actually did write
// into our 'temp' field. just in case that's ever changed.
if (socketAddress != temp)
{
// well this is fun.
// in the latest mono from the above github links,
// the result of Serialize() is passed as 'ref' so ReceiveFrom
// does in fact write into it.
//
// in Unity 2019 LTS's mono version, it does create a new one
// each time. this is from ILSpy Receive_From:
//
// SocketPal.CheckDualModeReceiveSupport(this);
// ValidateBlockingMode();
// if (NetEventSource.IsEnabled)
// {
// NetEventSource.Info(this, $"SRC{LocalEndPoint} size:{size} remoteEP:{remoteEP}", "ReceiveFrom");
// }
// EndPoint remoteEP2 = remoteEP;
// System.Net.Internals.SocketAddress socketAddress = SnapshotAndSerialize(ref remoteEP2);
// System.Net.Internals.SocketAddress socketAddress2 = IPEndPointExtensions.Serialize(remoteEP2);
// int bytesTransferred;
// SocketError socketError = SocketPal.ReceiveFrom(_handle, buffer, offset, size, socketFlags, socketAddress.Buffer, ref socketAddress.InternalSize, out bytesTransferred);
// SocketException ex = null;
// if (socketError != 0)
// {
// ex = new SocketException((int)socketError);
// UpdateStatusAfterSocketError(ex);
// if (NetEventSource.IsEnabled)
// {
// NetEventSource.Error(this, ex, "ReceiveFrom");
// }
// if (ex.SocketErrorCode != SocketError.MessageSize)
// {
// throw ex;
// }
// }
// if (!socketAddress2.Equals(socketAddress))
// {
// try
// {
// remoteEP = remoteEP2.Create(socketAddress);
// }
// catch
// {
// }
// if (_rightEndPoint == null)
// {
// _rightEndPoint = remoteEP2;
// }
// }
// if (ex != null)
// {
// throw ex;
// }
// if (NetEventSource.IsEnabled)
// {
// NetEventSource.DumpBuffer(this, buffer, offset, size, "ReceiveFrom");
// NetEventSource.Exit(this, bytesTransferred, "ReceiveFrom");
// }
// return bytesTransferred;
//
// so until they upgrade their mono version, we are stuck with
// some allocations.
//
// for now, let's pass the newly created on to our temp so at
// least we reuse it next time.
temp = socketAddress;
// SocketAddress.GetHashCode() depends on SocketAddress.m_changed.
// ReceiveFrom only sets the buffer, it does not seem to set m_changed.
// we need to reset m_changed for two reasons:
// * if m_changed is false, GetHashCode() returns the cahced m_hash
// which is '0'. that would be a problem.
// https://github.com/mono/mono/blob/bdd772531d379b4e78593587d15113c37edd4a64/mcs/class/referencesource/System/net/System/Net/SocketAddress.cs#L262
// * if we have a cached m_hash, but ReceiveFrom modified the buffer
// then the GetHashCode() should change too. so we need to reset
// either way.
//
// the only way to do that is by _actually_ modifying the buffer:
// https://github.com/mono/mono/blob/bdd772531d379b4e78593587d15113c37edd4a64/mcs/class/referencesource/System/net/System/Net/SocketAddress.cs#L99
// so let's do that.
// -> unchecked in case it's byte.Max
unchecked
{
temp[0] += 1;
temp[0] -= 1;
}
// make sure this worked.
// at least throw an Exception to make it obvious if the trick does
// not work anymore, in case ReceiveFrom is ever changed.
if (temp.GetHashCode() == 0)
throw new Exception($"SocketAddress GetHashCode() is 0 after ReceiveFrom. Does the m_changed trick not work anymore?");
// in the future, enable this again:
//throw new Exception($"Socket.ReceiveFrom(): passed SocketAddress={socketAddress} but expected {temp}. This should never happen. Did ReceiveFrom() change?");
}
// ReceiveFrom sets seed_endpoint to the result of Create():
// https://github.com/mono/mono/blob/f74eed4b09790a0929889ad7fc2cf96c9b6e3757/mcs/class/System/System.Net.Sockets/Socket.cs#L1764
// so let's return ourselves at least.
// (seed_endpoint only seems to matter for BeginSend etc.)
return this;
}
// we need to overwrite GetHashCode() for two reasons.
// https://github.com/mono/mono/blob/bdd772531d379b4e78593587d15113c37edd4a64/mcs/class/referencesource/System/net/System/Net/IPEndPoint.cs#L160
// * it uses m_Address. but our true SocketAddress is in m_temp.
// m_Address might not be set at all.
// * m_Address.GetHashCode() allocates:
// https://github.com/mono/mono/blob/bdd772531d379b4e78593587d15113c37edd4a64/mcs/class/referencesource/System/net/System/Net/IPAddress.cs#L699
public override int GetHashCode() => temp.GetHashCode();
// helper function to create an ACTUAL new IPEndPoint from this.
// server needs it to store new connections as unique IPEndPoints.
public IPEndPoint DeepCopyIPEndPoint()
{
// we need to create a new IPEndPoint from 'temp' SocketAddress.
// there is no 'new IPEndPoint(SocketAddress) constructor.
// so we need to be a bit creative...
// allocate a placeholder IPAddress to copy
// our SocketAddress into.
// -> needs to be the same address family.
IPAddress ipAddress;
if (temp.Family == AddressFamily.InterNetworkV6)
ipAddress = IPAddress.IPv6Any;
else if (temp.Family == AddressFamily.InterNetwork)
ipAddress = IPAddress.Any;
else
throw new Exception($"Unexpected SocketAddress family: {temp.Family}");
// allocate a placeholder IPEndPoint
// with the needed size form IPAddress.
// (the real class. not NonAlloc)
IPEndPoint placeholder = new IPEndPoint(ipAddress, 0);
// the real IPEndPoint's .Create function can create a new IPEndPoint
// copy from a SocketAddress.
return (IPEndPoint)placeholder.Create(temp);
}
}
}

View File

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

View File

@@ -0,0 +1,13 @@
{
"name": "where-allocations",
"references": [],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

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

View File

@@ -0,0 +1,2 @@
V0.1 [2021-06-01]
- initial release

View File

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

View File

@@ -0,0 +1,280 @@
// wraps around a transport and adds latency/loss/scramble simulation.
//
// reliable: latency
// unreliable: latency, loss, scramble (unreliable isn't ordered so we scramble)
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Mirror
{
struct QueuedMessage
{
public int connectionId;
public byte[] bytes;
public float time;
}
[HelpURL("https://mirror-networking.gitbook.io/docs/transports/latency-simulaton-transport")]
[DisallowMultipleComponent]
public class LatencySimulation : Transport
{
public Transport wrap;
[Header("Common")]
[Tooltip("Spike latency via perlin(Time * speedMultiplier) * spikeMultiplier")]
[Range(0, 1)] public float latencySpikeMultiplier;
[Tooltip("Spike latency via perlin(Time * speedMultiplier) * spikeMultiplier")]
public float latencySpikeSpeedMultiplier = 1;
[Header("Reliable Messages")]
[Tooltip("Reliable latency in seconds")]
public float reliableLatency;
// note: packet loss over reliable manifests itself in latency.
// don't need (and can't add) a loss option here.
// note: reliable is ordered by definition. no need to scramble.
[Header("Unreliable Messages")]
[Tooltip("Packet loss in %")]
[Range(0, 1)] public float unreliableLoss;
[Tooltip("Unreliable latency in seconds")]
public float unreliableLatency;
[Tooltip("Scramble % of unreliable messages, just like over the real network. Mirror unreliable is unordered.")]
[Range(0, 1)] public float unreliableScramble;
// message queues
// list so we can insert randomly (scramble)
List<QueuedMessage> reliableClientToServer = new List<QueuedMessage>();
List<QueuedMessage> reliableServerToClient = new List<QueuedMessage>();
List<QueuedMessage> unreliableClientToServer = new List<QueuedMessage>();
List<QueuedMessage> unreliableServerToClient = new List<QueuedMessage>();
// random
// UnityEngine.Random.value is [0, 1] with both upper and lower bounds inclusive
// but we need the upper bound to be exclusive, so using System.Random instead.
// => NextDouble() is NEVER < 0 so loss=0 never drops!
// => NextDouble() is ALWAYS < 1 so loss=1 always drops!
System.Random random = new System.Random();
public void Awake()
{
if (wrap == null)
throw new Exception("PressureDrop requires an underlying transport to wrap around.");
}
// forward enable/disable to the wrapped transport
void OnEnable() { wrap.enabled = true; }
void OnDisable() { wrap.enabled = false; }
// noise function can be replaced if needed
protected virtual float Noise(float time) => Mathf.PerlinNoise(time, time);
// helper function to simulate latency
float SimulateLatency(int channeldId)
{
// spike over perlin noise.
// no spikes isn't realistic.
// sin is too predictable / no realistic.
// perlin is still deterministic and random enough.
float spike = Noise(Time.time * latencySpikeSpeedMultiplier) * latencySpikeMultiplier;
// base latency
switch (channeldId)
{
case Channels.Reliable:
return reliableLatency + spike;
case Channels.Unreliable:
return unreliableLatency + spike;
default:
return 0;
}
}
// helper function to simulate a send with latency/loss/scramble
void SimulateSend(int connectionId, ArraySegment<byte> segment, int channelId, float latency, List<QueuedMessage> reliableQueue, List<QueuedMessage> unreliableQueue)
{
// segment is only valid after returning. copy it.
// (allocates for now. it's only for testing anyway.)
byte[] bytes = new byte[segment.Count];
Buffer.BlockCopy(segment.Array, segment.Offset, bytes, 0, segment.Count);
// enqueue message. send after latency interval.
QueuedMessage message = new QueuedMessage
{
connectionId = connectionId,
bytes = bytes,
time = Time.time + latency
};
switch (channelId)
{
case Channels.Reliable:
// simulate latency
reliableQueue.Add(message);
break;
case Channels.Unreliable:
// simulate packet loss
bool drop = random.NextDouble() < unreliableLoss;
if (!drop)
{
// simulate scramble (Random.Next is < max, so +1)
bool scramble = random.NextDouble() < unreliableScramble;
int last = unreliableQueue.Count;
int index = scramble ? random.Next(0, last + 1) : last;
// simulate latency
unreliableQueue.Insert(index, message);
}
break;
default:
Debug.LogError($"{nameof(LatencySimulation)} unexpected channelId: {channelId}");
break;
}
}
public override bool Available() => wrap.Available();
public override void ClientConnect(string address)
{
wrap.OnClientConnected = OnClientConnected;
wrap.OnClientDataReceived = OnClientDataReceived;
wrap.OnClientError = OnClientError;
wrap.OnClientDisconnected = OnClientDisconnected;
wrap.ClientConnect(address);
}
public override void ClientConnect(Uri uri)
{
wrap.OnClientConnected = OnClientConnected;
wrap.OnClientDataReceived = OnClientDataReceived;
wrap.OnClientError = OnClientError;
wrap.OnClientDisconnected = OnClientDisconnected;
wrap.ClientConnect(uri);
}
public override bool ClientConnected() => wrap.ClientConnected();
public override void ClientDisconnect()
{
wrap.ClientDisconnect();
reliableClientToServer.Clear();
unreliableClientToServer.Clear();
}
public override void ClientSend(ArraySegment<byte> segment, int channelId)
{
float latency = SimulateLatency(channelId);
SimulateSend(0, segment, channelId, latency, reliableClientToServer, unreliableClientToServer);
}
public override Uri ServerUri() => wrap.ServerUri();
public override bool ServerActive() => wrap.ServerActive();
public override string ServerGetClientAddress(int connectionId) => wrap.ServerGetClientAddress(connectionId);
public override void ServerDisconnect(int connectionId) => wrap.ServerDisconnect(connectionId);
public override void ServerSend(int connectionId, ArraySegment<byte> segment, int channelId)
{
float latency = SimulateLatency(channelId);
SimulateSend(connectionId, segment, channelId, latency, reliableServerToClient, unreliableServerToClient);
}
public override void ServerStart()
{
wrap.OnServerConnected = OnServerConnected;
wrap.OnServerDataReceived = OnServerDataReceived;
wrap.OnServerError = OnServerError;
wrap.OnServerDisconnected = OnServerDisconnected;
wrap.ServerStart();
}
public override void ServerStop()
{
wrap.ServerStop();
reliableServerToClient.Clear();
unreliableServerToClient.Clear();
}
public override void ClientEarlyUpdate() => wrap.ClientEarlyUpdate();
public override void ServerEarlyUpdate() => wrap.ServerEarlyUpdate();
public override void ClientLateUpdate()
{
// flush reliable messages after latency
while (reliableClientToServer.Count > 0)
{
// check the first message time
QueuedMessage message = reliableClientToServer[0];
if (message.time <= Time.time)
{
// send and eat
wrap.ClientSend(new ArraySegment<byte>(message.bytes), Channels.Reliable);
reliableClientToServer.RemoveAt(0);
}
// not enough time elapsed yet
break;
}
// flush unreliable messages after latency
while (unreliableClientToServer.Count > 0)
{
// check the first message time
QueuedMessage message = unreliableClientToServer[0];
if (message.time <= Time.time)
{
// send and eat
wrap.ClientSend(new ArraySegment<byte>(message.bytes), Channels.Unreliable);
unreliableClientToServer.RemoveAt(0);
}
// not enough time elapsed yet
break;
}
// update wrapped transport too
wrap.ClientLateUpdate();
}
public override void ServerLateUpdate()
{
// flush reliable messages after latency
while (reliableServerToClient.Count > 0)
{
// check the first message time
QueuedMessage message = reliableServerToClient[0];
if (message.time <= Time.time)
{
// send and eat
wrap.ServerSend(message.connectionId, new ArraySegment<byte>(message.bytes), Channels.Reliable);
reliableServerToClient.RemoveAt(0);
}
// not enough time elapsed yet
break;
}
// flush unreliable messages after latency
while (unreliableServerToClient.Count > 0)
{
// check the first message time
QueuedMessage message = unreliableServerToClient[0];
if (message.time <= Time.time)
{
// send and eat
wrap.ServerSend(message.connectionId, new ArraySegment<byte>(message.bytes), Channels.Unreliable);
unreliableServerToClient.RemoveAt(0);
}
// not enough time elapsed yet
break;
}
// update wrapped transport too
wrap.ServerLateUpdate();
}
public override int GetBatchThreshold(int channelId) => wrap.GetBatchThreshold(channelId);
public override int GetMaxPacketSize(int channelId = 0) => wrap.GetMaxPacketSize(channelId);
public override void Shutdown() => wrap.Shutdown();
public override string ToString() => $"{nameof(LatencySimulation)} {wrap}";
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 96b149f511061407fb54895c057b7736
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,54 @@
using System;
using UnityEngine;
namespace Mirror
{
/// <summary>
/// Allows Middleware to override some of the transport methods or let the inner transport handle them.
/// </summary>
[DisallowMultipleComponent]
public abstract class MiddlewareTransport : Transport
{
/// <summary>
/// Transport to call to after middleware
/// </summary>
public Transport inner;
public override bool Available() => inner.Available();
public override int GetMaxPacketSize(int channelId = 0) => inner.GetMaxPacketSize(channelId);
public override void Shutdown() => inner.Shutdown();
#region Client
public override void ClientConnect(string address)
{
inner.OnClientConnected = OnClientConnected;
inner.OnClientDataReceived = OnClientDataReceived;
inner.OnClientDisconnected = OnClientDisconnected;
inner.OnClientError = OnClientError;
inner.ClientConnect(address);
}
public override bool ClientConnected() => inner.ClientConnected();
public override void ClientDisconnect() => inner.ClientDisconnect();
public override void ClientSend(ArraySegment<byte> segment, int channelId) => inner.ClientSend(segment, channelId);
#endregion
#region Server
public override bool ServerActive() => inner.ServerActive();
public override void ServerStart()
{
inner.OnServerConnected = OnServerConnected;
inner.OnServerDataReceived = OnServerDataReceived;
inner.OnServerDisconnected = OnServerDisconnected;
inner.OnServerError = OnServerError;
inner.ServerStart();
}
public override void ServerStop() => inner.ServerStop();
public override void ServerSend(int connectionId, ArraySegment<byte> segment, int channelId) => inner.ServerSend(connectionId, segment, channelId);
public override void ServerDisconnect(int connectionId) => inner.ServerDisconnect(connectionId);
public override string ServerGetClientAddress(int connectionId) => inner.ServerGetClientAddress(connectionId);
public override Uri ServerUri() => inner.ServerUri();
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 46f20ede74658e147a1af57172710de2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,306 @@
using System;
using System.Text;
using UnityEngine;
namespace Mirror
{
// a transport that can listen to multiple underlying transport at the same time
[DisallowMultipleComponent]
public class MultiplexTransport : Transport
{
public Transport[] transports;
Transport available;
public void Awake()
{
if (transports == null || transports.Length == 0)
{
Debug.LogError("Multiplex transport requires at least 1 underlying transport");
}
}
public override void ClientEarlyUpdate()
{
foreach (Transport transport in transports)
{
transport.ClientEarlyUpdate();
}
}
public override void ServerEarlyUpdate()
{
foreach (Transport transport in transports)
{
transport.ServerEarlyUpdate();
}
}
public override void ClientLateUpdate()
{
foreach (Transport transport in transports)
{
transport.ClientLateUpdate();
}
}
public override void ServerLateUpdate()
{
foreach (Transport transport in transports)
{
transport.ServerLateUpdate();
}
}
void OnEnable()
{
foreach (Transport transport in transports)
{
transport.enabled = true;
}
}
void OnDisable()
{
foreach (Transport transport in transports)
{
transport.enabled = false;
}
}
public override bool Available()
{
// available if any of the transports is available
foreach (Transport transport in transports)
{
if (transport.Available())
{
return true;
}
}
return false;
}
#region Client
public override void ClientConnect(string address)
{
foreach (Transport transport in transports)
{
if (transport.Available())
{
available = transport;
transport.OnClientConnected = OnClientConnected;
transport.OnClientDataReceived = OnClientDataReceived;
transport.OnClientError = OnClientError;
transport.OnClientDisconnected = OnClientDisconnected;
transport.ClientConnect(address);
return;
}
}
throw new ArgumentException("No transport suitable for this platform");
}
public override void ClientConnect(Uri uri)
{
foreach (Transport transport in transports)
{
if (transport.Available())
{
try
{
available = transport;
transport.OnClientConnected = OnClientConnected;
transport.OnClientDataReceived = OnClientDataReceived;
transport.OnClientError = OnClientError;
transport.OnClientDisconnected = OnClientDisconnected;
transport.ClientConnect(uri);
return;
}
catch (ArgumentException)
{
// transport does not support the schema, just move on to the next one
}
}
}
throw new ArgumentException("No transport suitable for this platform");
}
public override bool ClientConnected()
{
return (object)available != null && available.ClientConnected();
}
public override void ClientDisconnect()
{
if ((object)available != null)
available.ClientDisconnect();
}
public override void ClientSend(ArraySegment<byte> segment, int channelId)
{
available.ClientSend(segment, channelId);
}
#endregion
#region Server
// connection ids get mapped to base transports
// if we have 3 transports, then
// transport 0 will produce connection ids [0, 3, 6, 9, ...]
// transport 1 will produce connection ids [1, 4, 7, 10, ...]
// transport 2 will produce connection ids [2, 5, 8, 11, ...]
int FromBaseId(int transportId, int connectionId)
{
return connectionId * transports.Length + transportId;
}
int ToBaseId(int connectionId)
{
return connectionId / transports.Length;
}
int ToTransportId(int connectionId)
{
return connectionId % transports.Length;
}
void AddServerCallbacks()
{
// wire all the base transports to my events
for (int i = 0; i < transports.Length; i++)
{
// this is required for the handlers, if I use i directly
// then all the handlers will use the last i
int locali = i;
Transport transport = transports[i];
transport.OnServerConnected = (baseConnectionId =>
{
OnServerConnected.Invoke(FromBaseId(locali, baseConnectionId));
});
transport.OnServerDataReceived = (baseConnectionId, data, channel) =>
{
OnServerDataReceived.Invoke(FromBaseId(locali, baseConnectionId), data, channel);
};
transport.OnServerError = (baseConnectionId, error) =>
{
OnServerError.Invoke(FromBaseId(locali, baseConnectionId), error);
};
transport.OnServerDisconnected = baseConnectionId =>
{
OnServerDisconnected.Invoke(FromBaseId(locali, baseConnectionId));
};
}
}
// for now returns the first uri,
// should we return all available uris?
public override Uri ServerUri()
{
return transports[0].ServerUri();
}
public override bool ServerActive()
{
// avoid Linq.All allocations
foreach (Transport transport in transports)
{
if (!transport.ServerActive())
{
return false;
}
}
return true;
}
public override string ServerGetClientAddress(int connectionId)
{
int baseConnectionId = ToBaseId(connectionId);
int transportId = ToTransportId(connectionId);
return transports[transportId].ServerGetClientAddress(baseConnectionId);
}
public override void ServerDisconnect(int connectionId)
{
int baseConnectionId = ToBaseId(connectionId);
int transportId = ToTransportId(connectionId);
transports[transportId].ServerDisconnect(baseConnectionId);
}
public override void ServerSend(int connectionId, ArraySegment<byte> segment, int channelId)
{
int baseConnectionId = ToBaseId(connectionId);
int transportId = ToTransportId(connectionId);
for (int i = 0; i < transports.Length; ++i)
{
if (i == transportId)
{
transports[i].ServerSend(baseConnectionId, segment, channelId);
}
}
}
public override void ServerStart()
{
foreach (Transport transport in transports)
{
AddServerCallbacks();
transport.ServerStart();
}
}
public override void ServerStop()
{
foreach (Transport transport in transports)
{
transport.ServerStop();
}
}
#endregion
public override int GetMaxPacketSize(int channelId = 0)
{
// finding the max packet size in a multiplex environment has to be
// done very carefully:
// * servers run multiple transports at the same time
// * different clients run different transports
// * there should only ever be ONE true max packet size for everyone,
// otherwise a spawn message might be sent to all tcp sockets, but
// be too big for some udp sockets. that would be a debugging
// nightmare and allow for possible exploits and players on
// different platforms seeing a different game state.
// => the safest solution is to use the smallest max size for all
// transports. that will never fail.
int mininumAllowedSize = int.MaxValue;
foreach (Transport transport in transports)
{
int size = transport.GetMaxPacketSize(channelId);
mininumAllowedSize = Mathf.Min(size, mininumAllowedSize);
}
return mininumAllowedSize;
}
public override void Shutdown()
{
foreach (Transport transport in transports)
{
transport.Shutdown();
}
}
public override string ToString()
{
StringBuilder builder = new StringBuilder();
foreach (Transport transport in transports)
{
builder.AppendLine(transport.ToString());
}
return builder.ToString().Trim();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 929e3234c7db540b899f00183fc2b1fe
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,86 @@
using System;
using System.Collections.Concurrent;
using UnityEngine;
namespace Mirror.SimpleWeb
{
public enum ClientState
{
NotConnected = 0,
Connecting = 1,
Connected = 2,
Disconnecting = 3,
}
/// <summary>
/// Client used to control websockets
/// <para>Base class used by WebSocketClientWebGl and WebSocketClientStandAlone</para>
/// </summary>
public abstract class SimpleWebClient
{
public static SimpleWebClient Create(int maxMessageSize, int maxMessagesPerTick, TcpConfig tcpConfig)
{
#if UNITY_WEBGL && !UNITY_EDITOR
return new WebSocketClientWebGl(maxMessageSize, maxMessagesPerTick);
#else
return new WebSocketClientStandAlone(maxMessageSize, maxMessagesPerTick, tcpConfig);
#endif
}
readonly int maxMessagesPerTick;
protected readonly int maxMessageSize;
protected readonly ConcurrentQueue<Message> receiveQueue = new ConcurrentQueue<Message>();
protected readonly BufferPool bufferPool;
protected ClientState state;
protected SimpleWebClient(int maxMessageSize, int maxMessagesPerTick)
{
this.maxMessageSize = maxMessageSize;
this.maxMessagesPerTick = maxMessagesPerTick;
bufferPool = new BufferPool(5, 20, maxMessageSize);
}
public ClientState ConnectionState => state;
public event Action onConnect;
public event Action onDisconnect;
public event Action<ArraySegment<byte>> onData;
public event Action<Exception> onError;
public void ProcessMessageQueue(MonoBehaviour behaviour)
{
int processedCount = 0;
// check enabled every time in case behaviour was disabled after data
while (
behaviour.enabled &&
processedCount < maxMessagesPerTick &&
// Dequeue last
receiveQueue.TryDequeue(out Message next)
)
{
processedCount++;
switch (next.type)
{
case EventType.Connected:
onConnect?.Invoke();
break;
case EventType.Data:
onData?.Invoke(next.data.ToSegment());
next.data.Release();
break;
case EventType.Disconnected:
onDisconnect?.Invoke();
break;
case EventType.Error:
onError?.Invoke(next.exception);
break;
}
}
}
public abstract void Connect(Uri serverAddress);
public abstract void Disconnect();
public abstract void Send(ArraySegment<byte> segment);
}
}

View File

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

View File

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

View File

@@ -0,0 +1,77 @@
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
namespace Mirror.SimpleWeb
{
/// <summary>
/// Handles Handshake to the server when it first connects
/// <para>The client handshake does not need buffers to reduce allocations since it only happens once</para>
/// </summary>
internal class ClientHandshake
{
public bool TryHandshake(Connection conn, Uri uri)
{
try
{
Stream stream = conn.stream;
byte[] keyBuffer = new byte[16];
using (RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider())
{
rng.GetBytes(keyBuffer);
}
string key = Convert.ToBase64String(keyBuffer);
string keySum = key + Constants.HandshakeGUID;
byte[] keySumBytes = Encoding.ASCII.GetBytes(keySum);
Log.Verbose($"Handshake Hashing {Encoding.ASCII.GetString(keySumBytes)}");
byte[] keySumHash = SHA1.Create().ComputeHash(keySumBytes);
string expectedResponse = Convert.ToBase64String(keySumHash);
string handshake =
$"GET {uri.PathAndQuery} HTTP/1.1\r\n" +
$"Host: {uri.Host}:{uri.Port}\r\n" +
$"Upgrade: websocket\r\n" +
$"Connection: Upgrade\r\n" +
$"Sec-WebSocket-Key: {key}\r\n" +
$"Sec-WebSocket-Version: 13\r\n" +
"\r\n";
byte[] encoded = Encoding.ASCII.GetBytes(handshake);
stream.Write(encoded, 0, encoded.Length);
byte[] responseBuffer = new byte[1000];
int? lengthOrNull = ReadHelper.SafeReadTillMatch(stream, responseBuffer, 0, responseBuffer.Length, Constants.endOfHandshake);
if (!lengthOrNull.HasValue)
{
Log.Error("Connected closed before handshake");
return false;
}
string responseString = Encoding.ASCII.GetString(responseBuffer, 0, lengthOrNull.Value);
string acceptHeader = "Sec-WebSocket-Accept: ";
int startIndex = responseString.IndexOf(acceptHeader) + acceptHeader.Length;
int endIndex = responseString.IndexOf("\r\n", startIndex);
string responseKey = responseString.Substring(startIndex, endIndex - startIndex);
if (responseKey != expectedResponse)
{
Log.Error($"Response key incorrect, Response:{responseKey} Expected:{expectedResponse}");
return false;
}
return true;
}
catch (Exception e)
{
Log.Exception(e);
return false;
}
}
}
}

View File

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

View File

@@ -0,0 +1,47 @@
using System;
using System.IO;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Cryptography.X509Certificates;
namespace Mirror.SimpleWeb
{
internal class ClientSslHelper
{
internal bool TryCreateStream(Connection conn, Uri uri)
{
NetworkStream stream = conn.client.GetStream();
if (uri.Scheme != "wss")
{
conn.stream = stream;
return true;
}
try
{
conn.stream = CreateStream(stream, uri);
return true;
}
catch (Exception e)
{
Log.Error($"Create SSLStream Failed: {e}", false);
return false;
}
}
Stream CreateStream(NetworkStream stream, Uri uri)
{
SslStream sslStream = new SslStream(stream, true, ValidateServerCertificate);
sslStream.AuthenticateAsClient(uri.Host);
return sslStream;
}
static bool ValidateServerCertificate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
// Do not allow this client to communicate with unauthenticated servers.
// only accept if no errors
return sslPolicyErrors == SslPolicyErrors.None;
}
}
}

View File

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

View File

@@ -0,0 +1,139 @@
using System;
using System.Net.Sockets;
using System.Threading;
namespace Mirror.SimpleWeb
{
public class WebSocketClientStandAlone : SimpleWebClient
{
readonly ClientSslHelper sslHelper;
readonly ClientHandshake handshake;
readonly TcpConfig tcpConfig;
Connection conn;
internal WebSocketClientStandAlone(int maxMessageSize, int maxMessagesPerTick, TcpConfig tcpConfig) : base(maxMessageSize, maxMessagesPerTick)
{
#if UNITY_WEBGL && !UNITY_EDITOR
throw new NotSupportedException();
#else
sslHelper = new ClientSslHelper();
handshake = new ClientHandshake();
this.tcpConfig = tcpConfig;
#endif
}
public override void Connect(Uri serverAddress)
{
state = ClientState.Connecting;
Thread receiveThread = new Thread(() => ConnectAndReceiveLoop(serverAddress));
receiveThread.IsBackground = true;
receiveThread.Start();
}
void ConnectAndReceiveLoop(Uri serverAddress)
{
try
{
TcpClient client = new TcpClient();
tcpConfig.ApplyTo(client);
// create connection object here so dispose correctly disconnects on failed connect
conn = new Connection(client, AfterConnectionDisposed);
conn.receiveThread = Thread.CurrentThread;
try
{
client.Connect(serverAddress.Host, serverAddress.Port);
}
catch (SocketException)
{
client.Dispose();
throw;
}
bool success = sslHelper.TryCreateStream(conn, serverAddress);
if (!success)
{
Log.Warn("Failed to create Stream");
conn.Dispose();
return;
}
success = handshake.TryHandshake(conn, serverAddress);
if (!success)
{
Log.Warn("Failed Handshake");
conn.Dispose();
return;
}
Log.Info("HandShake Successful");
state = ClientState.Connected;
receiveQueue.Enqueue(new Message(EventType.Connected));
Thread sendThread = new Thread(() =>
{
SendLoop.Config sendConfig = new SendLoop.Config(
conn,
bufferSize: Constants.HeaderSize + Constants.MaskSize + maxMessageSize,
setMask: true);
SendLoop.Loop(sendConfig);
});
conn.sendThread = sendThread;
sendThread.IsBackground = true;
sendThread.Start();
ReceiveLoop.Config config = new ReceiveLoop.Config(conn,
maxMessageSize,
false,
receiveQueue,
bufferPool);
ReceiveLoop.Loop(config);
}
catch (ThreadInterruptedException e) { Log.InfoException(e); }
catch (ThreadAbortException e) { Log.InfoException(e); }
catch (Exception e) { Log.Exception(e); }
finally
{
// close here in case connect fails
conn?.Dispose();
}
}
void AfterConnectionDisposed(Connection conn)
{
state = ClientState.NotConnected;
// make sure Disconnected event is only called once
receiveQueue.Enqueue(new Message(EventType.Disconnected));
}
public override void Disconnect()
{
state = ClientState.Disconnecting;
Log.Info("Disconnect Called");
if (conn == null)
{
state = ClientState.NotConnected;
}
else
{
conn?.Dispose();
}
}
public override void Send(ArraySegment<byte> segment)
{
ArrayBuffer buffer = bufferPool.Take(segment.Count);
buffer.CopyFrom(segment);
conn.sendQueue.Enqueue(buffer);
conn.sendPending.Set();
}
}
}

View File

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

View File

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

View File

@@ -0,0 +1,34 @@
using System;
#if UNITY_WEBGL
using System.Runtime.InteropServices;
#endif
namespace Mirror.SimpleWeb
{
internal static class SimpleWebJSLib
{
#if UNITY_WEBGL
[DllImport("__Internal")]
internal static extern bool IsConnected(int index);
#pragma warning disable CA2101 // Specify marshaling for P/Invoke string arguments
[DllImport("__Internal")]
#pragma warning restore CA2101 // Specify marshaling for P/Invoke string arguments
internal static extern int Connect(string address, Action<int> openCallback, Action<int> closeCallBack, Action<int, IntPtr, int> messageCallback, Action<int> errorCallback);
[DllImport("__Internal")]
internal static extern void Disconnect(int index);
[DllImport("__Internal")]
internal static extern bool Send(int index, byte[] array, int offset, int length);
#else
internal static bool IsConnected(int index) => throw new NotSupportedException();
internal static int Connect(string address, Action<int> openCallback, Action<int> closeCallBack, Action<int, IntPtr, int> messageCallback, Action<int> errorCallback) => throw new NotSupportedException();
internal static void Disconnect(int index) => throw new NotSupportedException();
internal static bool Send(int index, byte[] array, int offset, int length) => throw new NotSupportedException();
#endif
}
}

View File

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

View File

@@ -0,0 +1,99 @@
using System;
using System.Collections.Generic;
using AOT;
namespace Mirror.SimpleWeb
{
public class WebSocketClientWebGl : SimpleWebClient
{
static readonly Dictionary<int, WebSocketClientWebGl> instances = new Dictionary<int, WebSocketClientWebGl>();
/// <summary>
/// key for instances sent between c# and js
/// </summary>
int index;
internal WebSocketClientWebGl(int maxMessageSize, int maxMessagesPerTick) : base(maxMessageSize, maxMessagesPerTick)
{
#if !UNITY_WEBGL || UNITY_EDITOR
throw new NotSupportedException();
#endif
}
public bool CheckJsConnected() => SimpleWebJSLib.IsConnected(index);
public override void Connect(Uri serverAddress)
{
index = SimpleWebJSLib.Connect(serverAddress.ToString(), OpenCallback, CloseCallBack, MessageCallback, ErrorCallback);
instances.Add(index, this);
state = ClientState.Connecting;
}
public override void Disconnect()
{
state = ClientState.Disconnecting;
// disconnect should cause closeCallback and OnDisconnect to be called
SimpleWebJSLib.Disconnect(index);
}
public override void Send(ArraySegment<byte> segment)
{
if (segment.Count > maxMessageSize)
{
Log.Error($"Cant send message with length {segment.Count} because it is over the max size of {maxMessageSize}");
return;
}
SimpleWebJSLib.Send(index, segment.Array, 0, segment.Count);
}
void onOpen()
{
receiveQueue.Enqueue(new Message(EventType.Connected));
state = ClientState.Connected;
}
void onClose()
{
// this code should be last in this class
receiveQueue.Enqueue(new Message(EventType.Disconnected));
state = ClientState.NotConnected;
instances.Remove(index);
}
void onMessage(IntPtr bufferPtr, int count)
{
try
{
ArrayBuffer buffer = bufferPool.Take(count);
buffer.CopyFrom(bufferPtr, count);
receiveQueue.Enqueue(new Message(buffer));
}
catch (Exception e)
{
Log.Error($"onData {e.GetType()}: {e.Message}\n{e.StackTrace}");
receiveQueue.Enqueue(new Message(e));
}
}
void onErr()
{
receiveQueue.Enqueue(new Message(new Exception("Javascript Websocket error")));
Disconnect();
}
[MonoPInvokeCallback(typeof(Action<int>))]
static void OpenCallback(int index) => instances[index].onOpen();
[MonoPInvokeCallback(typeof(Action<int>))]
static void CloseCallBack(int index) => instances[index].onClose();
[MonoPInvokeCallback(typeof(Action<int, IntPtr, int>))]
static void MessageCallback(int index, IntPtr bufferPtr, int count) => instances[index].onMessage(bufferPtr, count);
[MonoPInvokeCallback(typeof(Action<int>))]
static void ErrorCallback(int index) => instances[index].onErr();
}
}

View File

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

View File

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

View File

@@ -0,0 +1,105 @@
// this will create a global object
const SimpleWeb = {
webSockets: [],
next: 1,
GetWebSocket: function (index) {
return SimpleWeb.webSockets[index]
},
AddNextSocket: function (webSocket) {
var index = SimpleWeb.next;
SimpleWeb.next++;
SimpleWeb.webSockets[index] = webSocket;
return index;
},
RemoveSocket: function (index) {
SimpleWeb.webSockets[index] = undefined;
},
};
function IsConnected(index) {
var webSocket = SimpleWeb.GetWebSocket(index);
if (webSocket) {
return webSocket.readyState === webSocket.OPEN;
}
else {
return false;
}
}
function Connect(addressPtr, openCallbackPtr, closeCallBackPtr, messageCallbackPtr, errorCallbackPtr) {
const address = Pointer_stringify(addressPtr);
console.log("Connecting to " + address);
// Create webSocket connection.
webSocket = new WebSocket(address);
webSocket.binaryType = 'arraybuffer';
const index = SimpleWeb.AddNextSocket(webSocket);
// Connection opened
webSocket.addEventListener('open', function (event) {
console.log("Connected to " + address);
Runtime.dynCall('vi', openCallbackPtr, [index]);
});
webSocket.addEventListener('close', function (event) {
console.log("Disconnected from " + address);
Runtime.dynCall('vi', closeCallBackPtr, [index]);
});
// Listen for messages
webSocket.addEventListener('message', function (event) {
if (event.data instanceof ArrayBuffer) {
// TODO dont alloc each time
var array = new Uint8Array(event.data);
var arrayLength = array.length;
var bufferPtr = _malloc(arrayLength);
var dataBuffer = new Uint8Array(HEAPU8.buffer, bufferPtr, arrayLength);
dataBuffer.set(array);
Runtime.dynCall('viii', messageCallbackPtr, [index, bufferPtr, arrayLength]);
_free(bufferPtr);
}
else {
console.error("message type not supported")
}
});
webSocket.addEventListener('error', function (event) {
console.error('Socket Error', event);
Runtime.dynCall('vi', errorCallbackPtr, [index]);
});
return index;
}
function Disconnect(index) {
var webSocket = SimpleWeb.GetWebSocket(index);
if (webSocket) {
webSocket.close(1000, "Disconnect Called by Mirror");
}
SimpleWeb.RemoveSocket(index);
}
function Send(index, arrayPtr, offset, length) {
var webSocket = SimpleWeb.GetWebSocket(index);
if (webSocket) {
const start = arrayPtr + offset;
const end = start + length;
const data = HEAPU8.buffer.slice(start, end);
webSocket.send(data);
return true;
}
return false;
}
const SimpleWebLib = {
$SimpleWeb: SimpleWeb,
IsConnected,
Connect,
Disconnect,
Send
};
autoAddDeps(SimpleWebLib, '$SimpleWeb');
mergeInto(LibraryManager.library, SimpleWebLib);

View File

@@ -0,0 +1,37 @@
fileFormatVersion: 2
guid: 54452a8c6d2ca9b49a8c79f81b50305c
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
DefaultValueInitialized: true
- first:
Facebook: WebGL
second:
enabled: 1
settings: {}
- first:
WebGL: WebGL
second:
enabled: 1
settings: {}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@@ -0,0 +1,265 @@
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
namespace Mirror.SimpleWeb
{
public interface IBufferOwner
{
void Return(ArrayBuffer buffer);
}
public sealed class ArrayBuffer : IDisposable
{
readonly IBufferOwner owner;
public readonly byte[] array;
/// <summary>
/// number of bytes writen to buffer
/// </summary>
internal int count;
/// <summary>
/// How many times release needs to be called before buffer is returned to pool
/// <para>This allows the buffer to be used in multiple places at the same time</para>
/// </summary>
public void SetReleasesRequired(int required)
{
releasesRequired = required;
}
/// <summary>
/// How many times release needs to be called before buffer is returned to pool
/// <para>This allows the buffer to be used in multiple places at the same time</para>
/// </summary>
/// <remarks>
/// This value is normally 0, but can be changed to require release to be called multiple times
/// </remarks>
int releasesRequired;
public ArrayBuffer(IBufferOwner owner, int size)
{
this.owner = owner;
array = new byte[size];
}
public void Release()
{
int newValue = Interlocked.Decrement(ref releasesRequired);
if (newValue <= 0)
{
count = 0;
owner.Return(this);
}
}
public void Dispose()
{
Release();
}
public void CopyTo(byte[] target, int offset)
{
if (count > (target.Length + offset)) throw new ArgumentException($"{nameof(count)} was greater than {nameof(target)}.length", nameof(target));
Buffer.BlockCopy(array, 0, target, offset, count);
}
public void CopyFrom(ArraySegment<byte> segment)
{
CopyFrom(segment.Array, segment.Offset, segment.Count);
}
public void CopyFrom(byte[] source, int offset, int length)
{
if (length > array.Length) throw new ArgumentException($"{nameof(length)} was greater than {nameof(array)}.length", nameof(length));
count = length;
Buffer.BlockCopy(source, offset, array, 0, length);
}
public void CopyFrom(IntPtr bufferPtr, int length)
{
if (length > array.Length) throw new ArgumentException($"{nameof(length)} was greater than {nameof(array)}.length", nameof(length));
count = length;
Marshal.Copy(bufferPtr, array, 0, length);
}
public ArraySegment<byte> ToSegment()
{
return new ArraySegment<byte>(array, 0, count);
}
[Conditional("UNITY_ASSERTIONS")]
internal void Validate(int arraySize)
{
if (array.Length != arraySize)
{
Log.Error("Buffer that was returned had an array of the wrong size");
}
}
}
internal class BufferBucket : IBufferOwner
{
public readonly int arraySize;
readonly ConcurrentQueue<ArrayBuffer> buffers;
/// <summary>
/// keeps track of how many arrays are taken vs returned
/// </summary>
internal int _current = 0;
public BufferBucket(int arraySize)
{
this.arraySize = arraySize;
buffers = new ConcurrentQueue<ArrayBuffer>();
}
public ArrayBuffer Take()
{
IncrementCreated();
if (buffers.TryDequeue(out ArrayBuffer buffer))
{
return buffer;
}
else
{
Log.Verbose($"BufferBucket({arraySize}) create new");
return new ArrayBuffer(this, arraySize);
}
}
public void Return(ArrayBuffer buffer)
{
DecrementCreated();
buffer.Validate(arraySize);
buffers.Enqueue(buffer);
}
[Conditional("DEBUG")]
void IncrementCreated()
{
int next = Interlocked.Increment(ref _current);
Log.Verbose($"BufferBucket({arraySize}) count:{next}");
}
[Conditional("DEBUG")]
void DecrementCreated()
{
int next = Interlocked.Decrement(ref _current);
Log.Verbose($"BufferBucket({arraySize}) count:{next}");
}
}
/// <summary>
/// Collection of different sized buffers
/// </summary>
/// <remarks>
/// <para>
/// Problem: <br/>
/// * Need to cached byte[] so that new ones aren't created each time <br/>
/// * Arrays sent are multiple different sizes <br/>
/// * Some message might be big so need buffers to cover that size <br/>
/// * Most messages will be small compared to max message size <br/>
/// </para>
/// <br/>
/// <para>
/// Solution: <br/>
/// * Create multiple groups of buffers covering the range of allowed sizes <br/>
/// * Split range exponentially (using math.log) so that there are more groups for small buffers <br/>
/// </para>
/// </remarks>
public class BufferPool
{
internal readonly BufferBucket[] buckets;
readonly int bucketCount;
readonly int smallest;
readonly int largest;
public BufferPool(int bucketCount, int smallest, int largest)
{
if (bucketCount < 2) throw new ArgumentException("Count must be at least 2");
if (smallest < 1) throw new ArgumentException("Smallest must be at least 1");
if (largest < smallest) throw new ArgumentException("Largest must be greater than smallest");
this.bucketCount = bucketCount;
this.smallest = smallest;
this.largest = largest;
// split range over log scale (more buckets for smaller sizes)
double minLog = Math.Log(this.smallest);
double maxLog = Math.Log(this.largest);
double range = maxLog - minLog;
double each = range / (bucketCount - 1);
buckets = new BufferBucket[bucketCount];
for (int i = 0; i < bucketCount; i++)
{
double size = smallest * Math.Pow(Math.E, each * i);
buckets[i] = new BufferBucket((int)Math.Ceiling(size));
}
Validate();
// Example
// 5 count
// 20 smallest
// 16400 largest
// 3.0 log 20
// 9.7 log 16400
// 6.7 range 9.7 - 3
// 1.675 each 6.7 / (5-1)
// 20 e^ (3 + 1.675 * 0)
// 107 e^ (3 + 1.675 * 1)
// 572 e^ (3 + 1.675 * 2)
// 3056 e^ (3 + 1.675 * 3)
// 16,317 e^ (3 + 1.675 * 4)
// perceision wont be lose when using doubles
}
[Conditional("UNITY_ASSERTIONS")]
void Validate()
{
if (buckets[0].arraySize != smallest)
{
Log.Error($"BufferPool Failed to create bucket for smallest. bucket:{buckets[0].arraySize} smallest{smallest}");
}
int largestBucket = buckets[bucketCount - 1].arraySize;
// rounded using Ceiling, so allowed to be 1 more that largest
if (largestBucket != largest && largestBucket != largest + 1)
{
Log.Error($"BufferPool Failed to create bucket for largest. bucket:{largestBucket} smallest{largest}");
}
}
public ArrayBuffer Take(int size)
{
if (size > largest) { throw new ArgumentException($"Size ({size}) is greatest that largest ({largest})"); }
for (int i = 0; i < bucketCount; i++)
{
if (size <= buckets[i].arraySize)
{
return buckets[i].Take();
}
}
throw new ArgumentException($"Size ({size}) is greatest that largest ({largest})");
}
}
}

View File

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

View File

@@ -0,0 +1,90 @@
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Net.Sockets;
using System.Threading;
namespace Mirror.SimpleWeb
{
internal sealed class Connection : IDisposable
{
public const int IdNotSet = -1;
readonly object disposedLock = new object();
public TcpClient client;
public int connId = IdNotSet;
public Stream stream;
public Thread receiveThread;
public Thread sendThread;
public ManualResetEventSlim sendPending = new ManualResetEventSlim(false);
public ConcurrentQueue<ArrayBuffer> sendQueue = new ConcurrentQueue<ArrayBuffer>();
public Action<Connection> onDispose;
volatile bool hasDisposed;
public Connection(TcpClient client, Action<Connection> onDispose)
{
this.client = client ?? throw new ArgumentNullException(nameof(client));
this.onDispose = onDispose;
}
/// <summary>
/// disposes client and stops threads
/// </summary>
public void Dispose()
{
Log.Verbose($"Dispose {ToString()}");
// check hasDisposed first to stop ThreadInterruptedException on lock
if (hasDisposed) { return; }
Log.Info($"Connection Close: {ToString()}");
lock (disposedLock)
{
// check hasDisposed again inside lock to make sure no other object has called this
if (hasDisposed) { return; }
hasDisposed = true;
// stop threads first so they don't try to use disposed objects
receiveThread.Interrupt();
sendThread?.Interrupt();
try
{
// stream
stream?.Dispose();
stream = null;
client.Dispose();
client = null;
}
catch (Exception e)
{
Log.Exception(e);
}
sendPending.Dispose();
// release all buffers in send queue
while (sendQueue.TryDequeue(out ArrayBuffer buffer))
{
buffer.Release();
}
onDispose.Invoke(this);
}
}
public override string ToString()
{
System.Net.EndPoint endpoint = client?.Client?.RemoteEndPoint;
return $"[Conn:{connId}, endPoint:{endpoint}]";
}
}
}

View File

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

View File

@@ -0,0 +1,72 @@
using System.Text;
namespace Mirror.SimpleWeb
{
/// <summary>
/// Constant values that should never change
/// <para>
/// Some values are from https://tools.ietf.org/html/rfc6455
/// </para>
/// </summary>
internal static class Constants
{
/// <summary>
/// Header is at most 4 bytes
/// <para>
/// If message is less than 125 then header is 2 bytes, else header is 4 bytes
/// </para>
/// </summary>
public const int HeaderSize = 4;
/// <summary>
/// Smallest size of header
/// <para>
/// If message is less than 125 then header is 2 bytes, else header is 4 bytes
/// </para>
/// </summary>
public const int HeaderMinSize = 2;
/// <summary>
/// bytes for short length
/// </summary>
public const int ShortLength = 2;
/// <summary>
/// Message mask is always 4 bytes
/// </summary>
public const int MaskSize = 4;
/// <summary>
/// Max size of a message for length to be 1 byte long
/// <para>
/// payload length between 0-125
/// </para>
/// </summary>
public const int BytePayloadLength = 125;
/// <summary>
/// if payload length is 126 when next 2 bytes will be the length
/// </summary>
public const int UshortPayloadLength = 126;
/// <summary>
/// if payload length is 127 when next 8 bytes will be the length
/// </summary>
public const int UlongPayloadLength = 127;
/// <summary>
/// Guid used for WebSocket Protocol
/// </summary>
public const string HandshakeGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
public static readonly int HandshakeGUIDLength = HandshakeGUID.Length;
public static readonly byte[] HandshakeGUIDBytes = Encoding.ASCII.GetBytes(HandshakeGUID);
/// <summary>
/// Handshake messages will end with \r\n\r\n
/// </summary>
public static readonly byte[] endOfHandshake = new byte[4] { (byte)'\r', (byte)'\n', (byte)'\r', (byte)'\n' };
}
}

View File

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

View File

@@ -0,0 +1,10 @@
namespace Mirror.SimpleWeb
{
public enum EventType
{
Connected,
Data,
Disconnected,
Error
}
}

View File

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

View File

@@ -0,0 +1,115 @@
using System;
using UnityEngine;
using Conditional = System.Diagnostics.ConditionalAttribute;
namespace Mirror.SimpleWeb
{
public static class Log
{
// used for Conditional
const string SIMPLEWEB_LOG_ENABLED = nameof(SIMPLEWEB_LOG_ENABLED);
const string DEBUG = nameof(DEBUG);
public enum Levels
{
none = 0,
error = 1,
warn = 2,
info = 3,
verbose = 4,
}
public static Levels level = Levels.none;
public static string BufferToString(byte[] buffer, int offset = 0, int? length = null)
{
return BitConverter.ToString(buffer, offset, length ?? buffer.Length);
}
[Conditional(SIMPLEWEB_LOG_ENABLED)]
public static void DumpBuffer(string label, byte[] buffer, int offset, int length)
{
if (level < Levels.verbose)
return;
Debug.Log($"VERBOSE: <color=blue>{label}: {BufferToString(buffer, offset, length)}</color>");
}
[Conditional(SIMPLEWEB_LOG_ENABLED)]
public static void DumpBuffer(string label, ArrayBuffer arrayBuffer)
{
if (level < Levels.verbose)
return;
Debug.Log($"VERBOSE: <color=blue>{label}: {BufferToString(arrayBuffer.array, 0, arrayBuffer.count)}</color>");
}
[Conditional(SIMPLEWEB_LOG_ENABLED)]
public static void Verbose(string msg, bool showColor = true)
{
if (level < Levels.verbose)
return;
if (showColor)
Debug.Log($"VERBOSE: <color=blue>{msg}</color>");
else
Debug.Log($"VERBOSE: {msg}");
}
[Conditional(SIMPLEWEB_LOG_ENABLED)]
public static void Info(string msg, bool showColor = true)
{
if (level < Levels.info)
return;
if (showColor)
Debug.Log($"INFO: <color=blue>{msg}</color>");
else
Debug.Log($"INFO: {msg}");
}
/// <summary>
/// An expected Exception was caught, useful for debugging but not important
/// </summary>
/// <param name="msg"></param>
/// <param name="showColor"></param>
[Conditional(SIMPLEWEB_LOG_ENABLED)]
public static void InfoException(Exception e)
{
if (level < Levels.info)
return;
Debug.Log($"INFO_EXCEPTION: <color=blue>{e.GetType().Name}</color> Message: {e.Message}");
}
[Conditional(SIMPLEWEB_LOG_ENABLED), Conditional(DEBUG)]
public static void Warn(string msg, bool showColor = true)
{
if (level < Levels.warn)
return;
if (showColor)
Debug.LogWarning($"WARN: <color=orange>{msg}</color>");
else
Debug.LogWarning($"WARN: {msg}");
}
[Conditional(SIMPLEWEB_LOG_ENABLED), Conditional(DEBUG)]
public static void Error(string msg, bool showColor = true)
{
if (level < Levels.error)
return;
if (showColor)
Debug.LogError($"ERROR: <color=red>{msg}</color>");
else
Debug.LogError($"ERROR: {msg}");
}
public static void Exception(Exception e)
{
// always log Exceptions
Debug.LogError($"EXCEPTION: <color=red>{e.GetType().Name}</color> Message: {e.Message}");
}
}
}

View File

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

Some files were not shown because too many files have changed in this diff Show More