mirror of
https://github.com/DerTyp7/defrain-shooter-unity.git
synced 2025-10-30 13:07:10 +01:00
CHANGED TO MIRROR
This commit is contained in:
8
Assets/Mirror/Runtime/Transport/KCP.meta
Normal file
8
Assets/Mirror/Runtime/Transport/KCP.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 953bb5ec5ab2346a092f58061e01ba65
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Mirror/Runtime/Transport/KCP/MirrorTransport.meta
Normal file
8
Assets/Mirror/Runtime/Transport/KCP/MirrorTransport.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7bdb797750d0a490684410110bf48192
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -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:
|
||||
8
Assets/Mirror/Runtime/Transport/KCP/kcp2k.meta
Normal file
8
Assets/Mirror/Runtime/Transport/KCP/kcp2k.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 71a1c8e8c022d4731a481c1808f37e5d
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
24
Assets/Mirror/Runtime/Transport/KCP/kcp2k/LICENSE
Normal file
24
Assets/Mirror/Runtime/Transport/KCP/kcp2k/LICENSE
Normal 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.
|
||||
7
Assets/Mirror/Runtime/Transport/KCP/kcp2k/LICENSE.meta
Normal file
7
Assets/Mirror/Runtime/Transport/KCP/kcp2k/LICENSE.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9a3e8369060cf4e94ac117603de47aa6
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
94
Assets/Mirror/Runtime/Transport/KCP/kcp2k/VERSION
Normal file
94
Assets/Mirror/Runtime/Transport/KCP/kcp2k/VERSION
Normal 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
|
||||
7
Assets/Mirror/Runtime/Transport/KCP/kcp2k/VERSION.meta
Normal file
7
Assets/Mirror/Runtime/Transport/KCP/kcp2k/VERSION.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ed3f2cf1bbf1b4d53a6f2c103d311f71
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel.meta
Normal file
8
Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5a54d18b954cb4407a28b633fc32ea6d
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9e852b2532fb248d19715cfebe371db3
|
||||
timeCreated: 1610081248
|
||||
120
Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/KcpClient.cs
Normal file
120
Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/KcpClient.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6aa069a28ed24fedb533c102d9742b36
|
||||
timeCreated: 1603786960
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 96512e74aa8214a6faa8a412a7a07877
|
||||
timeCreated: 1602601237
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3915c7c62b72d4dc2a9e4e76c94fc484
|
||||
timeCreated: 1602600432
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 91b5edac31224a49bd76f960ae018942
|
||||
timeCreated: 1610081248
|
||||
337
Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/KcpServer.cs
Normal file
337
Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/KcpServer.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9759159c6589494a9037f5e130a867ed
|
||||
timeCreated: 1603787747
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 80a9b1ce9a6f14abeb32bfa9921d097b
|
||||
timeCreated: 1602601483
|
||||
14
Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/Log.cs
Normal file
14
Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/Log.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7b5e1de98d6d84c3793a61cf7d8da9a4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0b320ff06046474eae7bce7240ea478c
|
||||
timeCreated: 1626430641
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4c1b235bbe054706bef6d092f361006e
|
||||
timeCreated: 1626430539
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2cf0ccf7d551480bb5af08fcbe169f84
|
||||
timeCreated: 1626435264
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4e1b74cc224b4c83a0f6c8d8da9090ab
|
||||
timeCreated: 1626430608
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 54b8398dcd544c8a93bcad846214cc40
|
||||
timeCreated: 1626432191
|
||||
8
Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp.meta
Normal file
8
Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5cafb8851a0084f3e94a580c207b3923
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,3 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("kcp2k.Tests")]
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aec6a15ac7bd43129317ea1f01f19782
|
||||
timeCreated: 1602665988
|
||||
1042
Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Kcp.cs
Normal file
1042
Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Kcp.cs
Normal file
File diff suppressed because it is too large
Load Diff
11
Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Kcp.cs.meta
Normal file
11
Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Kcp.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a59b1cae10a334faf807432ab472f212
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
46
Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Pool.cs
Normal file
46
Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Pool.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
11
Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Pool.cs.meta
Normal file
11
Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Pool.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 35c07818fc4784bb4ba472c8e5029002
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
62
Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Segment.cs
Normal file
62
Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Segment.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fc58706a05dd3442c8fde858d5266855
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
76
Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Utils.cs
Normal file
76
Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Utils.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Utils.cs.meta
Normal file
11
Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Utils.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ef959eb716205bd48b050f010a9a35ae
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
15
Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp2k.asmdef
Normal file
15
Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp2k.asmdef
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "kcp2k",
|
||||
"references": [
|
||||
"GUID:63c380d6dae6946209ed0832388a657c"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": true,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6806a62c384838046a3c66c44f06d75f
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e9de45e025f26411bbb52d1aefc8d5a5
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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.
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a857d4e863bbf4a7dba70bc2cd1b5949
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6b7f3f8e8fa16475bbe48a8e9fbe800b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,3 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("where-allocations.Tests")]
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 158a96a7489b450485a8b06a13328871
|
||||
timeCreated: 1622356221
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9e801942544d44d65808fb250623fe25
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: af0279d15e39b484792394f1d3cad4d9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "where-allocations",
|
||||
"references": [],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 63c380d6dae6946209ed0832388a657c
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,2 @@
|
||||
V0.1 [2021-06-01]
|
||||
- initial release
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f1256cadc037546ccb66071784fce137
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
280
Assets/Mirror/Runtime/Transport/LatencySimulation.cs
Normal file
280
Assets/Mirror/Runtime/Transport/LatencySimulation.cs
Normal 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}";
|
||||
}
|
||||
}
|
||||
11
Assets/Mirror/Runtime/Transport/LatencySimulation.cs.meta
Normal file
11
Assets/Mirror/Runtime/Transport/LatencySimulation.cs.meta
Normal 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:
|
||||
54
Assets/Mirror/Runtime/Transport/MiddlewareTransport.cs
Normal file
54
Assets/Mirror/Runtime/Transport/MiddlewareTransport.cs
Normal 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
|
||||
}
|
||||
}
|
||||
11
Assets/Mirror/Runtime/Transport/MiddlewareTransport.cs.meta
Normal file
11
Assets/Mirror/Runtime/Transport/MiddlewareTransport.cs.meta
Normal 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:
|
||||
306
Assets/Mirror/Runtime/Transport/MultiplexTransport.cs
Normal file
306
Assets/Mirror/Runtime/Transport/MultiplexTransport.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Mirror/Runtime/Transport/MultiplexTransport.cs.meta
Normal file
11
Assets/Mirror/Runtime/Transport/MultiplexTransport.cs.meta
Normal 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:
|
||||
8
Assets/Mirror/Runtime/Transport/SimpleWebTransport.meta
Normal file
8
Assets/Mirror/Runtime/Transport/SimpleWebTransport.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a3ba68af305d809418d6c6a804939290
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,4 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("SimpleWebTransport.Tests.Runtime")]
|
||||
[assembly: InternalsVisibleTo("SimpleWebTransport.Tests.Editor")]
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ee9e76201f7665244bd6ab8ea343a83f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5faa957b8d9fc314ab7596ccf14750d9
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 13131761a0bf5a64dadeccd700fe26e5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a9c19d05220a87c4cbbe4d1e422da0aa
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3ffdcabc9e28f764a94fc4efc82d3e8b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 46055a75559a79849a750f39a766db61
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 05a9c87dea309e241a9185e5aa0d72ab
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7142349d566213c4abc763afaf4d91a1
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 97b96a0b65c104443977473323c2ff35
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 015c5b1915fd1a64cbe36444d16b2f7d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1999985791b91b9458059e88404885a7
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
@@ -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:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 564d2cd3eee5b21419553c0528739d1b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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})");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 94ae50f3ec35667469b861b12cd72f92
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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}]";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a13073c2b49d39943888df45174851bd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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' };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 85d110a089d6ad348abf2d073ebce7cd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Mirror.SimpleWeb
|
||||
{
|
||||
public enum EventType
|
||||
{
|
||||
Connected,
|
||||
Data,
|
||||
Disconnected,
|
||||
Error
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2d9cd7d2b5229ab42a12e82ae17d0347
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
115
Assets/Mirror/Runtime/Transport/SimpleWebTransport/Common/Log.cs
Normal file
115
Assets/Mirror/Runtime/Transport/SimpleWebTransport/Common/Log.cs
Normal 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user