alap
This commit is contained in:
8
Assets/Mirror/Transports/Edgegap.meta
Normal file
8
Assets/Mirror/Transports/Edgegap.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2d2959d363903444bae4333db12a9ea1
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
8
Assets/Mirror/Transports/Edgegap/EdgegapRelay.meta
Normal file
8
Assets/Mirror/Transports/Edgegap/EdgegapRelay.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 62c28e855fc644011b4079c268b46b71
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,141 @@
|
||||
// overwrite RawSend/Receive
|
||||
using System;
|
||||
using System.Net.Sockets;
|
||||
using Mirror;
|
||||
using UnityEngine;
|
||||
using kcp2k;
|
||||
|
||||
namespace Edgegap
|
||||
{
|
||||
public class EdgegapKcpClient : KcpClient
|
||||
{
|
||||
// need buffer larger than KcpClient.rawReceiveBuffer to add metadata
|
||||
readonly byte[] relayReceiveBuffer;
|
||||
|
||||
// authentication
|
||||
public uint userId;
|
||||
public uint sessionId;
|
||||
public ConnectionState connectionState = ConnectionState.Disconnected;
|
||||
|
||||
// ping
|
||||
double lastPingTime;
|
||||
|
||||
public EdgegapKcpClient(
|
||||
Action OnConnected,
|
||||
Action<ArraySegment<byte>, KcpChannel> OnData,
|
||||
Action OnDisconnected,
|
||||
Action<ErrorCode, string> OnError,
|
||||
KcpConfig config)
|
||||
: base(OnConnected, OnData, OnDisconnected, OnError, config)
|
||||
{
|
||||
relayReceiveBuffer = new byte[config.Mtu + Protocol.Overhead];
|
||||
}
|
||||
|
||||
// custom start function with relay parameters; connects udp client.
|
||||
public void Connect(string relayAddress, ushort relayPort, uint userId, uint sessionId)
|
||||
{
|
||||
// reset last state
|
||||
connectionState = ConnectionState.Checking;
|
||||
this.userId = userId;
|
||||
this.sessionId = sessionId;
|
||||
|
||||
// reuse base connect
|
||||
base.Connect(relayAddress, relayPort);
|
||||
}
|
||||
|
||||
// parse metadata, then pass to kcp
|
||||
protected override bool RawReceive(out ArraySegment<byte> segment)
|
||||
{
|
||||
segment = default;
|
||||
if (socket == null) return false;
|
||||
|
||||
try
|
||||
{
|
||||
if (socket.ReceiveNonBlocking(relayReceiveBuffer, out ArraySegment<byte> content))
|
||||
{
|
||||
using (NetworkReaderPooled reader = NetworkReaderPool.Get(content))
|
||||
{
|
||||
// parse message type
|
||||
if (reader.Remaining == 0)
|
||||
{
|
||||
Debug.LogWarning($"EdgegapClient: message of {content.Count} is too small to parse.");
|
||||
return false;
|
||||
}
|
||||
byte messageType = reader.ReadByte();
|
||||
|
||||
// handle message type
|
||||
switch (messageType)
|
||||
{
|
||||
case (byte)MessageType.Ping:
|
||||
{
|
||||
// parse state
|
||||
if (reader.Remaining < 1) return false;
|
||||
ConnectionState last = connectionState;
|
||||
connectionState = (ConnectionState)reader.ReadByte();
|
||||
|
||||
// log state changes for debugging.
|
||||
if (connectionState != last) Debug.Log($"EdgegapClient: state updated to: {connectionState}");
|
||||
|
||||
// return true indicates Mirror to keep checking
|
||||
// for further messages.
|
||||
return true;
|
||||
}
|
||||
case (byte)MessageType.Data:
|
||||
{
|
||||
segment = reader.ReadBytesSegment(reader.Remaining);
|
||||
return true;
|
||||
}
|
||||
// wrong message type. return false, don't throw.
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (SocketException e)
|
||||
{
|
||||
Log.Info($"EdgegapClient: looks like the other end has closed the connection. This is fine: {e}");
|
||||
Disconnect();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override void RawSend(ArraySegment<byte> data)
|
||||
{
|
||||
using (NetworkWriterPooled writer = NetworkWriterPool.Get())
|
||||
{
|
||||
writer.WriteUInt(userId);
|
||||
writer.WriteUInt(sessionId);
|
||||
writer.WriteByte((byte)MessageType.Data);
|
||||
writer.WriteBytes(data.Array, data.Offset, data.Count);
|
||||
base.RawSend(writer);
|
||||
}
|
||||
}
|
||||
|
||||
void SendPing()
|
||||
{
|
||||
using (NetworkWriterPooled writer = NetworkWriterPool.Get())
|
||||
{
|
||||
writer.WriteUInt(userId);
|
||||
writer.WriteUInt(sessionId);
|
||||
writer.WriteByte((byte)MessageType.Ping);
|
||||
base.RawSend(writer);
|
||||
}
|
||||
}
|
||||
|
||||
public override void TickOutgoing()
|
||||
{
|
||||
if (connected)
|
||||
{
|
||||
// ping every interval for keepalive & handshake
|
||||
if (NetworkTime.localTime >= lastPingTime + Protocol.PingInterval)
|
||||
{
|
||||
SendPing();
|
||||
lastPingTime = NetworkTime.localTime;
|
||||
}
|
||||
}
|
||||
|
||||
base.TickOutgoing();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a0d6fba7098f4ea3949d0195e8276adc
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,203 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using Mirror;
|
||||
using UnityEngine;
|
||||
using kcp2k;
|
||||
|
||||
namespace Edgegap
|
||||
{
|
||||
public class EdgegapKcpServer : KcpServer
|
||||
{
|
||||
// need buffer larger than KcpClient.rawReceiveBuffer to add metadata
|
||||
readonly byte[] relayReceiveBuffer;
|
||||
|
||||
// authentication
|
||||
public uint userId;
|
||||
public uint sessionId;
|
||||
public ConnectionState state = ConnectionState.Disconnected;
|
||||
|
||||
// server is an UDP client talking to relay
|
||||
protected Socket relaySocket;
|
||||
public EndPoint remoteEndPoint;
|
||||
|
||||
// ping
|
||||
double lastPingTime;
|
||||
|
||||
// custom 'active'. while connected to relay
|
||||
bool relayActive;
|
||||
|
||||
public EdgegapKcpServer(
|
||||
Action<int> OnConnected,
|
||||
Action<int, ArraySegment<byte>, KcpChannel> OnData,
|
||||
Action<int> OnDisconnected,
|
||||
Action<int, ErrorCode, string> OnError,
|
||||
KcpConfig config)
|
||||
// TODO don't call base. don't listen to local UdpServer at all?
|
||||
: base(OnConnected, OnData, OnDisconnected, OnError, config)
|
||||
{
|
||||
relayReceiveBuffer = new byte[config.Mtu + Protocol.Overhead];
|
||||
}
|
||||
|
||||
public override bool IsActive() => relayActive;
|
||||
|
||||
// custom start function with relay parameters; connects udp client.
|
||||
public void Start(string relayAddress, ushort relayPort, uint userId, uint sessionId)
|
||||
{
|
||||
// reset last state
|
||||
state = ConnectionState.Checking;
|
||||
this.userId = userId;
|
||||
this.sessionId = sessionId;
|
||||
|
||||
// try resolve host name
|
||||
if (!Common.ResolveHostname(relayAddress, out IPAddress[] addresses))
|
||||
{
|
||||
OnError(0, ErrorCode.DnsResolve, $"Failed to resolve host: {relayAddress}");
|
||||
return;
|
||||
}
|
||||
|
||||
// create socket
|
||||
remoteEndPoint = new IPEndPoint(addresses[0], relayPort);
|
||||
relaySocket = new Socket(remoteEndPoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp);
|
||||
relaySocket.Blocking = false;
|
||||
|
||||
// configure buffer sizes
|
||||
Common.ConfigureSocketBuffers(relaySocket, config.RecvBufferSize, config.SendBufferSize);
|
||||
|
||||
// bind to endpoint for Send/Receive instead of SendTo/ReceiveFrom
|
||||
relaySocket.Connect(remoteEndPoint);
|
||||
relayActive = true;
|
||||
}
|
||||
|
||||
public override void Stop()
|
||||
{
|
||||
relayActive = false;
|
||||
}
|
||||
|
||||
protected override bool RawReceiveFrom(out ArraySegment<byte> segment, out int connectionId)
|
||||
{
|
||||
segment = default;
|
||||
connectionId = 0;
|
||||
|
||||
if (relaySocket == null) return false;
|
||||
|
||||
try
|
||||
{
|
||||
// TODO need separate buffer. don't write into result yet. only payload
|
||||
|
||||
if (relaySocket.ReceiveNonBlocking(relayReceiveBuffer, out ArraySegment<byte> content))
|
||||
{
|
||||
using (NetworkReaderPooled reader = NetworkReaderPool.Get(content))
|
||||
{
|
||||
// parse message type
|
||||
if (reader.Remaining == 0)
|
||||
{
|
||||
Debug.LogWarning($"EdgegapServer: message of {content.Count} is too small to parse header.");
|
||||
return false;
|
||||
}
|
||||
byte messageType = reader.ReadByte();
|
||||
|
||||
// handle message type
|
||||
switch (messageType)
|
||||
{
|
||||
case (byte)MessageType.Ping:
|
||||
{
|
||||
// parse state
|
||||
if (reader.Remaining < 1) return false;
|
||||
ConnectionState last = state;
|
||||
state = (ConnectionState)reader.ReadByte();
|
||||
|
||||
// log state changes for debugging.
|
||||
if (state != last) Debug.Log($"EdgegapServer: state updated to: {state}");
|
||||
|
||||
// return true indicates Mirror to keep checking
|
||||
// for further messages.
|
||||
return true;
|
||||
}
|
||||
case (byte)MessageType.Data:
|
||||
{
|
||||
// parse connectionId and payload
|
||||
if (reader.Remaining <= 4)
|
||||
{
|
||||
Debug.LogWarning($"EdgegapServer: message of {content.Count} is too small to parse connId.");
|
||||
return false;
|
||||
}
|
||||
|
||||
connectionId = reader.ReadInt();
|
||||
segment = reader.ReadBytesSegment(reader.Remaining);
|
||||
// Debug.Log($"EdgegapServer: receiving from connId={connectionId}: {segment.ToHexString()}");
|
||||
return true;
|
||||
}
|
||||
// wrong message type. return false, don't throw.
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (SocketException e)
|
||||
{
|
||||
Log.Info($"EdgegapServer: looks like the other end has closed the connection. This is fine: {e}");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override void RawSend(int connectionId, ArraySegment<byte> data)
|
||||
{
|
||||
using (NetworkWriterPooled writer = NetworkWriterPool.Get())
|
||||
{
|
||||
// Debug.Log($"EdgegapServer: sending to connId={connectionId}: {data.ToHexString()}");
|
||||
writer.WriteUInt(userId);
|
||||
writer.WriteUInt(sessionId);
|
||||
writer.WriteByte((byte)MessageType.Data);
|
||||
writer.WriteInt(connectionId);
|
||||
writer.WriteBytes(data.Array, data.Offset, data.Count);
|
||||
ArraySegment<byte> message = writer;
|
||||
|
||||
try
|
||||
{
|
||||
relaySocket.SendNonBlocking(message);
|
||||
}
|
||||
catch (SocketException e)
|
||||
{
|
||||
Log.Error($"KcpRleayServer: RawSend failed: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SendPing()
|
||||
{
|
||||
using (NetworkWriterPooled writer = NetworkWriterPool.Get())
|
||||
{
|
||||
writer.WriteUInt(userId);
|
||||
writer.WriteUInt(sessionId);
|
||||
writer.WriteByte((byte)MessageType.Ping);
|
||||
ArraySegment<byte> message = writer;
|
||||
|
||||
try
|
||||
{
|
||||
relaySocket.SendNonBlocking(message);
|
||||
}
|
||||
catch (SocketException e)
|
||||
{
|
||||
Debug.LogWarning($"EdgegapServer: failed to ping. perhaps the relay isn't running? {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void TickOutgoing()
|
||||
{
|
||||
if (relayActive)
|
||||
{
|
||||
// ping every interval for keepalive & handshake
|
||||
if (NetworkTime.localTime >= lastPingTime + Protocol.PingInterval)
|
||||
{
|
||||
SendPing();
|
||||
lastPingTime = NetworkTime.localTime;
|
||||
}
|
||||
}
|
||||
|
||||
// base processing
|
||||
base.TickOutgoing();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fd8551078397248b0848950352c208ee
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,162 @@
|
||||
// edgegap relay transport.
|
||||
// reuses KcpTransport with custom KcpServer/Client.
|
||||
|
||||
//#if MIRROR <- commented out because MIRROR isn't defined on first import yet
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
using UnityEngine;
|
||||
using Mirror;
|
||||
using kcp2k;
|
||||
|
||||
namespace Edgegap
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
public class EdgegapKcpTransport : KcpTransport
|
||||
{
|
||||
[Header("Relay")]
|
||||
public string relayAddress = "127.0.0.1";
|
||||
public ushort relayGameServerPort = 8888;
|
||||
public ushort relayGameClientPort = 9999;
|
||||
|
||||
// mtu for kcp transport. respects relay overhead.
|
||||
public const int MaxPayload = Kcp.MTU_DEF - Protocol.Overhead;
|
||||
|
||||
[Header("Relay")]
|
||||
public bool relayGUI = true;
|
||||
public uint userId = 11111111;
|
||||
public uint sessionId = 22222222;
|
||||
|
||||
// helper
|
||||
internal static String ReParse(String cmd, String pattern, String defaultValue)
|
||||
{
|
||||
Match match = Regex.Match(cmd, pattern);
|
||||
return match.Success ? match.Groups[1].Value : defaultValue;
|
||||
}
|
||||
|
||||
protected override 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;
|
||||
|
||||
// create config from serialized settings.
|
||||
// with MaxPayload as max size to respect relay overhead.
|
||||
config = new KcpConfig(DualMode, RecvBufferSize, SendBufferSize, MaxPayload, NoDelay, Interval, FastResend, false, SendWindowSize, ReceiveWindowSize, Timeout, MaxRetransmit);
|
||||
|
||||
// client (NonAlloc version is not necessary anymore)
|
||||
client = new EdgegapKcpClient(
|
||||
() => OnClientConnected.Invoke(),
|
||||
(message, channel) => OnClientDataReceived.Invoke(message, FromKcpChannel(channel)),
|
||||
() => OnClientDisconnected.Invoke(),
|
||||
(error, reason) => OnClientError.Invoke(ToTransportError(error), reason),
|
||||
config
|
||||
);
|
||||
|
||||
// server
|
||||
server = new EdgegapKcpServer(
|
||||
(connectionId) => OnServerConnected.Invoke(connectionId),
|
||||
(connectionId, message, channel) => OnServerDataReceived.Invoke(connectionId, message, FromKcpChannel(channel)),
|
||||
(connectionId) => OnServerDisconnected.Invoke(connectionId),
|
||||
(connectionId, error, reason) => OnServerError.Invoke(connectionId, ToTransportError(error), reason),
|
||||
config);
|
||||
|
||||
if (statisticsLog)
|
||||
InvokeRepeating(nameof(OnLogStatistics), 1, 1);
|
||||
|
||||
Debug.Log("EdgegapTransport initialized!");
|
||||
}
|
||||
|
||||
protected override void OnValidate()
|
||||
{
|
||||
// show max message sizes in inspector for convenience.
|
||||
// 'config' isn't available in edit mode yet, so use MTU define.
|
||||
ReliableMaxMessageSize = KcpPeer.ReliableMaxMessageSize(MaxPayload, ReceiveWindowSize);
|
||||
UnreliableMaxMessageSize = KcpPeer.UnreliableMaxMessageSize(MaxPayload);
|
||||
}
|
||||
|
||||
// client overwrites to use EdgegapClient instead of KcpClient
|
||||
public override void ClientConnect(string address)
|
||||
{
|
||||
// connect to relay address:port instead of the expected server address
|
||||
EdgegapKcpClient client = (EdgegapKcpClient)this.client;
|
||||
client.userId = userId;
|
||||
client.sessionId = sessionId;
|
||||
client.connectionState = ConnectionState.Checking; // reset from last time
|
||||
client.Connect(relayAddress, relayGameClientPort);
|
||||
}
|
||||
public override void ClientConnect(Uri uri)
|
||||
{
|
||||
if (uri.Scheme != Scheme)
|
||||
throw new ArgumentException($"Invalid url {uri}, use {Scheme}://host:port instead", nameof(uri));
|
||||
|
||||
// connect to relay address:port instead of the expected server address
|
||||
EdgegapKcpClient client = (EdgegapKcpClient)this.client;
|
||||
client.Connect(relayAddress, relayGameClientPort, userId, sessionId);
|
||||
}
|
||||
|
||||
// server overwrites to use EdgegapServer instead of KcpServer
|
||||
public override void ServerStart()
|
||||
{
|
||||
// start the server
|
||||
EdgegapKcpServer server = (EdgegapKcpServer)this.server;
|
||||
server.Start(relayAddress, relayGameServerPort, userId, sessionId);
|
||||
}
|
||||
|
||||
void OnGUIRelay()
|
||||
{
|
||||
// if (server.IsActive()) return;
|
||||
|
||||
GUILayout.BeginArea(new Rect(300, 30, 200, 100));
|
||||
|
||||
GUILayout.BeginHorizontal();
|
||||
GUILayout.Label("SessionId:");
|
||||
sessionId = Convert.ToUInt32(GUILayout.TextField(sessionId.ToString()));
|
||||
GUILayout.EndHorizontal();
|
||||
|
||||
GUILayout.BeginHorizontal();
|
||||
GUILayout.Label("UserId:");
|
||||
userId = Convert.ToUInt32(GUILayout.TextField(userId.ToString()));
|
||||
GUILayout.EndHorizontal();
|
||||
|
||||
if (NetworkServer.active)
|
||||
{
|
||||
EdgegapKcpServer server = (EdgegapKcpServer)this.server;
|
||||
GUILayout.BeginHorizontal();
|
||||
GUILayout.Label("State:");
|
||||
GUILayout.Label(server.state.ToString());
|
||||
GUILayout.EndHorizontal();
|
||||
}
|
||||
else if (NetworkClient.active)
|
||||
{
|
||||
EdgegapKcpClient client = (EdgegapKcpClient)this.client;
|
||||
GUILayout.BeginHorizontal();
|
||||
GUILayout.Label("State:");
|
||||
GUILayout.Label(client.connectionState.ToString());
|
||||
GUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
GUILayout.EndArea();
|
||||
}
|
||||
|
||||
// base OnGUI only shows in editor & development builds.
|
||||
// here we always show it because we need the sessionid & userid buttons.
|
||||
#pragma warning disable CS0109
|
||||
new void OnGUI()
|
||||
{
|
||||
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
||||
base.OnGUI();
|
||||
#endif
|
||||
if (relayGUI) OnGUIRelay();
|
||||
}
|
||||
|
||||
public override string ToString() => "Edgegap Kcp Transport";
|
||||
}
|
||||
#pragma warning restore CS0109
|
||||
}
|
||||
//#endif MIRROR <- commented out because MIRROR isn't defined on first import yet
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c2d1e0e17f753449798fa27474d6b86b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
29
Assets/Mirror/Transports/Edgegap/EdgegapRelay/Protocol.cs
Normal file
29
Assets/Mirror/Transports/Edgegap/EdgegapRelay/Protocol.cs
Normal file
@ -0,0 +1,29 @@
|
||||
// relay protocol definitions
|
||||
namespace Edgegap
|
||||
{
|
||||
public enum ConnectionState : byte
|
||||
{
|
||||
Disconnected = 0, // until the user calls connect()
|
||||
Checking = 1, // recently connected, validation in progress
|
||||
Valid = 2, // validation succeeded
|
||||
Invalid = 3, // validation rejected by tower
|
||||
SessionTimeout = 4, // session owner timed out
|
||||
Error = 5, // other error
|
||||
}
|
||||
|
||||
public enum MessageType : byte
|
||||
{
|
||||
Ping = 1,
|
||||
Data = 2
|
||||
}
|
||||
|
||||
public static class Protocol
|
||||
{
|
||||
// MTU: relay adds up to 13 bytes of metadata in the worst case.
|
||||
public const int Overhead = 13;
|
||||
|
||||
// ping interval should be between 100 ms and 1 second.
|
||||
// faster ping gives faster authentication, but higher bandwidth.
|
||||
public const float PingInterval = 0.5f;
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: eac30312ba61470b849e368af3c3b0e9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
20
Assets/Mirror/Transports/Edgegap/EdgegapRelay/README.md
Normal file
20
Assets/Mirror/Transports/Edgegap/EdgegapRelay/README.md
Normal file
@ -0,0 +1,20 @@
|
||||
# Edgegap Relay for Mirror
|
||||
Documentation: https://docs.edgegap.com/docs/distributed-relay-manager/
|
||||
|
||||
## Prerequisites
|
||||
- Unity project set up with the Mirror networking library installed
|
||||
- Supported Versions: [Mirror](https://assetstore.unity.com/packages/tools/network/mirror-129321) and [Mirror LTS](https://assetstore.unity.com/packages/tools/network/mirror-lts-102631)
|
||||
- EdgegapTransport module downloaded and extracted
|
||||
|
||||
## Steps
|
||||
1. Open your Unity project and navigate to the "Assets" folder.
|
||||
2. Locate the "Mirror" folder within "Assets" and open it.
|
||||
3. Within the "Mirror" folder, open the "Transports" folder.
|
||||
4. Drag and drop the "Unity" folder from the extracted EdgegapTransport files into the "Transports" folder.
|
||||
5. Open your NetworkManager script in the Unity Editor and navigate to the "Inspector" panel.
|
||||
6. In the "Inspector" panel, locate the "Network Manager" component and click the "+" button next to the "Transport" property.
|
||||
7. In the "Add Component" menu that appears, select "Edgegap Transport" to add it to the NetworkManager.
|
||||
8. Drag the newly added "Edgegap Transport" component into the "Transport" property in the "Inspector" panel.
|
||||
|
||||
## Notes
|
||||
- The EdgegapTransport module is only compatible with Mirror and Mirror LTS versions.
|
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8ade7c960d8fe4e94970ddd88ede3bca
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,25 @@
|
||||
// parse session_id and user_id from command line args.
|
||||
// mac: "open mirror.app --args session_id=123 user_id=456"
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Edgegap
|
||||
{
|
||||
public class RelayCredentialsFromArgs : MonoBehaviour
|
||||
{
|
||||
void Awake()
|
||||
{
|
||||
String cmd = Environment.CommandLine;
|
||||
|
||||
// parse session_id via regex
|
||||
String sessionId = EdgegapKcpTransport.ReParse(cmd, "session_id=(\\d+)", "111111");
|
||||
String userID = EdgegapKcpTransport.ReParse(cmd, "user_id=(\\d+)", "222222");
|
||||
Debug.Log($"Parsed sessionId: {sessionId} user_id: {userID}");
|
||||
|
||||
// configure transport
|
||||
EdgegapKcpTransport transport = GetComponent<EdgegapKcpTransport>();
|
||||
transport.sessionId = UInt32.Parse(sessionId);
|
||||
transport.userId = UInt32.Parse(userID);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e9ec7091b26c4d3882f4b42f10f9b8c1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
BIN
Assets/Mirror/Transports/Edgegap/edgegap.png
Normal file
BIN
Assets/Mirror/Transports/Edgegap/edgegap.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
123
Assets/Mirror/Transports/Edgegap/edgegap.png.meta
Normal file
123
Assets/Mirror/Transports/Edgegap/edgegap.png.meta
Normal file
@ -0,0 +1,123 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3ea6ff15cda674a57b0c7c8b7dc1878c
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 12
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 1
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMasterTextureLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 0
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 1
|
||||
wrapV: 1
|
||||
wrapW: 1
|
||||
nPOTScale: 0
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 1
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 16
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 0
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 0
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
cookieLightType: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 3
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 0
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: Server
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID: 5e97eb03825dee720800000000000000
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
nameFileIdTable: {}
|
||||
spritePackingTag:
|
||||
pSDRemoveMatte: 0
|
||||
pSDShowRemoveMatteOption: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
3
Assets/Mirror/Transports/Encryption.meta
Normal file
3
Assets/Mirror/Transports/Encryption.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 741b3c7e5d0842049ff50a2f6e27ca12
|
||||
timeCreated: 1708015148
|
3
Assets/Mirror/Transports/Encryption/Editor.meta
Normal file
3
Assets/Mirror/Transports/Encryption/Editor.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0d3cd9d7d6e84a578f7e4b384ff813f1
|
||||
timeCreated: 1708793986
|
@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "EncryptionTransportEditor",
|
||||
"rootNamespace": "",
|
||||
"references": [
|
||||
"GUID:627104647b9c04b4ebb8978a92ecac63"
|
||||
],
|
||||
"includePlatforms": [
|
||||
"Editor"
|
||||
],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4c9c7b0ef83e6e945b276d644816a489
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,81 @@
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror.Transports.Encryption
|
||||
{
|
||||
[CustomEditor(typeof(EncryptionTransport), true)]
|
||||
public class EncryptionTransportInspector : UnityEditor.Editor
|
||||
{
|
||||
SerializedProperty innerProperty;
|
||||
SerializedProperty clientValidatesServerPubKeyProperty;
|
||||
SerializedProperty clientTrustedPubKeySignaturesProperty;
|
||||
SerializedProperty serverKeypairPathProperty;
|
||||
SerializedProperty serverLoadKeyPairFromFileProperty;
|
||||
|
||||
// Assuming proper SerializedProperty definitions for properties
|
||||
// Add more SerializedProperty fields related to different modes as needed
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
innerProperty = serializedObject.FindProperty("inner");
|
||||
clientValidatesServerPubKeyProperty = serializedObject.FindProperty("clientValidateServerPubKey");
|
||||
clientTrustedPubKeySignaturesProperty = serializedObject.FindProperty("clientTrustedPubKeySignatures");
|
||||
serverKeypairPathProperty = serializedObject.FindProperty("serverKeypairPath");
|
||||
serverLoadKeyPairFromFileProperty = serializedObject.FindProperty("serverLoadKeyPairFromFile");
|
||||
}
|
||||
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
serializedObject.Update();
|
||||
|
||||
EditorGUILayout.LabelField("Common", EditorStyles.boldLabel);
|
||||
EditorGUILayout.PropertyField(innerProperty);
|
||||
EditorGUILayout.Separator();
|
||||
// Client Section
|
||||
EditorGUILayout.LabelField("Client", EditorStyles.boldLabel);
|
||||
EditorGUILayout.HelpBox("Validating the servers public key is essential for complete man-in-the-middle (MITM) safety, but might not be feasible for all modes of hosting.", MessageType.Info);
|
||||
EditorGUILayout.PropertyField(clientValidatesServerPubKeyProperty, new GUIContent("Validate Server Public Key"));
|
||||
|
||||
EncryptionTransport.ValidationMode validationMode = (EncryptionTransport.ValidationMode)clientValidatesServerPubKeyProperty.enumValueIndex;
|
||||
|
||||
switch (validationMode)
|
||||
{
|
||||
case EncryptionTransport.ValidationMode.List:
|
||||
EditorGUILayout.PropertyField(clientTrustedPubKeySignaturesProperty);
|
||||
break;
|
||||
case EncryptionTransport.ValidationMode.Callback:
|
||||
EditorGUILayout.HelpBox("Please set the EncryptionTransport.onClientValidateServerPubKey at runtime.", MessageType.Info);
|
||||
break;
|
||||
}
|
||||
|
||||
EditorGUILayout.Separator();
|
||||
// Server Section
|
||||
EditorGUILayout.LabelField("Server", EditorStyles.boldLabel);
|
||||
EditorGUILayout.PropertyField(serverLoadKeyPairFromFileProperty, new GUIContent("Load Keypair From File"));
|
||||
if (serverLoadKeyPairFromFileProperty.boolValue)
|
||||
{
|
||||
EditorGUILayout.PropertyField(serverKeypairPathProperty, new GUIContent("Keypair File Path"));
|
||||
}
|
||||
if(GUILayout.Button("Generate Key Pair"))
|
||||
{
|
||||
EncryptionCredentials keyPair = EncryptionCredentials.Generate();
|
||||
string path = EditorUtility.SaveFilePanel("Select where to save the keypair", "", "server-keys.json", "json");
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
keyPair.SaveToFile(path);
|
||||
EditorUtility.DisplayDialog("KeyPair Saved", $"Successfully saved the keypair.\nThe fingerprint is {keyPair.PublicKeyFingerprint}, you can also retrieve it from the saved json file at any point.", "Ok");
|
||||
if (validationMode == EncryptionTransport.ValidationMode.List)
|
||||
{
|
||||
if (EditorUtility.DisplayDialog("Add key to trusted list?", "Do you also want to add the generated key to the trusted list?", "Yes", "No"))
|
||||
{
|
||||
clientTrustedPubKeySignaturesProperty.arraySize++;
|
||||
clientTrustedPubKeySignaturesProperty.GetArrayElementAtIndex(clientTrustedPubKeySignaturesProperty.arraySize - 1).stringValue = keyPair.PublicKeyFingerprint;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 871580d2094a46139279d651cec92b5d
|
||||
timeCreated: 1708794004
|
595
Assets/Mirror/Transports/Encryption/EncryptedConnection.cs
Normal file
595
Assets/Mirror/Transports/Encryption/EncryptedConnection.cs
Normal file
@ -0,0 +1,595 @@
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Org.BouncyCastle.Crypto;
|
||||
using Org.BouncyCastle.Crypto.Agreement;
|
||||
using Org.BouncyCastle.Crypto.Digests;
|
||||
using Org.BouncyCastle.Crypto.Generators;
|
||||
using Org.BouncyCastle.Crypto.Modes;
|
||||
using Org.BouncyCastle.Crypto.Parameters;
|
||||
using UnityEngine.Profiling;
|
||||
|
||||
namespace Mirror.Transports.Encryption
|
||||
{
|
||||
public class EncryptedConnection
|
||||
{
|
||||
// 256-bit key
|
||||
private const int KeyLength = 32;
|
||||
// 512-bit salt for the key derivation function
|
||||
private const int HkdfSaltSize = KeyLength * 2;
|
||||
|
||||
// Info tag for the HKDF, this just adds more entropy
|
||||
private static readonly byte[] HkdfInfo = Encoding.UTF8.GetBytes("Mirror/EncryptionTransport");
|
||||
|
||||
// fixed size of the unique per-packet nonce. Defaults to 12 bytes/96 bits (not recommended to be changed)
|
||||
private const int NonceSize = 12;
|
||||
|
||||
// this is the size of the "checksum" included in each encrypted payload
|
||||
// 16 bytes/128 bytes is the recommended value for best security
|
||||
// can be reduced to 12 bytes for a small space savings, but makes encryption slightly weaker.
|
||||
// Setting it lower than 12 bytes is not recommended
|
||||
private const int MacSizeBytes = 16;
|
||||
|
||||
private const int MacSizeBits = MacSizeBytes * 8;
|
||||
|
||||
// How much metadata overhead we have for regular packets
|
||||
public const int Overhead = sizeof(OpCodes) + MacSizeBytes + NonceSize;
|
||||
|
||||
// After how many seconds of not receiving a handshake packet we should time out
|
||||
private const double DurationTimeout = 2; // 2s
|
||||
|
||||
// After how many seconds to assume the last handshake packet got lost and to resend another one
|
||||
private const double DurationResend = 0.05; // 50ms
|
||||
|
||||
|
||||
// Static fields for allocation efficiency, makes this not thread safe
|
||||
// It'd be as easy as using ThreadLocal though to fix that
|
||||
|
||||
// Set up a global cipher instance, it is initialised/reset before use
|
||||
// (AesFastEngine used to exist, but was removed due to side channel issues)
|
||||
// use AesUtilities.CreateEngine here as it'll pick the hardware accelerated one if available (which is will not be unless on .net core)
|
||||
private static readonly GcmBlockCipher Cipher = new GcmBlockCipher(AesUtilities.CreateEngine());
|
||||
|
||||
// Set up a global HKDF with a SHA-256 digest
|
||||
private static readonly HkdfBytesGenerator Hkdf = new HkdfBytesGenerator(new Sha256Digest());
|
||||
|
||||
// Global byte array to store nonce sent by the remote side, they're used immediately after
|
||||
private static readonly byte[] ReceiveNonce = new byte[NonceSize];
|
||||
|
||||
// Buffer for the remote salt, as bouncycastle needs to take a byte[] *rolls eyes*
|
||||
private static byte[] _tmpRemoteSaltBuffer = new byte[HkdfSaltSize];
|
||||
// buffer for encrypt/decrypt operations, resized larger as needed
|
||||
// this is also the buffer that will be returned to mirror via ArraySegment
|
||||
// so any thread safety concerns would need to take extra care here
|
||||
private static byte[] _tmpCryptBuffer = new byte[2048];
|
||||
|
||||
// packet headers
|
||||
enum OpCodes : byte
|
||||
{
|
||||
// start at 1 to maybe filter out random noise
|
||||
Data = 1,
|
||||
HandshakeStart = 2,
|
||||
HandshakeAck = 3,
|
||||
HandshakeFin = 4,
|
||||
}
|
||||
|
||||
enum State
|
||||
{
|
||||
// Waiting for a handshake to arrive
|
||||
// this is for _sendsFirst:
|
||||
// - false: OpCodes.HandshakeStart
|
||||
// - true: Opcodes.HandshakeAck
|
||||
WaitingHandshake,
|
||||
|
||||
// Waiting for a handshake reply/acknowledgement to arrive
|
||||
// this is for _sendsFirst:
|
||||
// - false: OpCodes.HandshakeFine
|
||||
// - true: Opcodes.Data (implicitly)
|
||||
WaitingHandshakeReply,
|
||||
|
||||
// Both sides have confirmed the keys are exchanged and data can be sent freely
|
||||
Ready
|
||||
}
|
||||
|
||||
private State _state = State.WaitingHandshake;
|
||||
|
||||
// Key exchange confirmed and data can be sent freely
|
||||
public bool IsReady => _state == State.Ready;
|
||||
// Callback to send off encrypted data
|
||||
private Action<ArraySegment<byte>, int> _send;
|
||||
// Callback when received data has been decrypted
|
||||
private Action<ArraySegment<byte>, int> _receive;
|
||||
// Callback when the connection becomes ready
|
||||
private Action _ready;
|
||||
// On-error callback, disconnect expected
|
||||
private Action<TransportError, string> _error;
|
||||
// Optional callback to validate the remotes public key, validation on one side is necessary to ensure MITM resistance
|
||||
// (usually client validates the server key)
|
||||
private Func<PubKeyInfo, bool> _validateRemoteKey;
|
||||
// Our asymmetric credentials for the initial DH exchange
|
||||
private EncryptionCredentials _credentials;
|
||||
private byte[] _hkdfSalt;
|
||||
|
||||
// After no handshake packet in this many seconds, the handshake fails
|
||||
private double _handshakeTimeout;
|
||||
// When to assume the last handshake packet got lost and to resend another one
|
||||
private double _nextHandshakeResend;
|
||||
|
||||
|
||||
// we can reuse the _cipherParameters here since the nonce is stored as the byte[] reference we pass in
|
||||
// so we can update it without creating a new AeadParameters instance
|
||||
// this might break in the future! (will cause bad data)
|
||||
private byte[] _nonce = new byte[NonceSize];
|
||||
private AeadParameters _cipherParametersEncrypt;
|
||||
private AeadParameters _cipherParametersDecrypt;
|
||||
|
||||
|
||||
/*
|
||||
* Specifies if we send the first key, then receive ack, then send fin
|
||||
* Or the opposite if set to false
|
||||
*
|
||||
* The client does this, since the fin is not acked explicitly, but by receiving data to decrypt
|
||||
*/
|
||||
private readonly bool _sendsFirst;
|
||||
|
||||
public EncryptedConnection(EncryptionCredentials credentials,
|
||||
bool isClient,
|
||||
Action<ArraySegment<byte>, int> sendAction,
|
||||
Action<ArraySegment<byte>, int> receiveAction,
|
||||
Action readyAction,
|
||||
Action<TransportError, string> errorAction,
|
||||
Func<PubKeyInfo, bool> validateRemoteKey = null)
|
||||
{
|
||||
_credentials = credentials;
|
||||
_sendsFirst = isClient;
|
||||
if (!_sendsFirst)
|
||||
{
|
||||
// salt is controlled by the server
|
||||
_hkdfSalt = GenerateSecureBytes(HkdfSaltSize);
|
||||
}
|
||||
_send = sendAction;
|
||||
_receive = receiveAction;
|
||||
_ready = readyAction;
|
||||
_error = errorAction;
|
||||
_validateRemoteKey = validateRemoteKey;
|
||||
}
|
||||
|
||||
// Generates a random starting nonce
|
||||
private static byte[] GenerateSecureBytes(int size)
|
||||
{
|
||||
byte[] bytes = new byte[size];
|
||||
using (RandomNumberGenerator rng = RandomNumberGenerator.Create())
|
||||
{
|
||||
rng.GetBytes(bytes);
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public void OnReceiveRaw(ArraySegment<byte> data, int channel)
|
||||
{
|
||||
if (data.Count < 1)
|
||||
{
|
||||
_error(TransportError.Unexpected, "Received empty packet");
|
||||
return;
|
||||
}
|
||||
|
||||
using (NetworkReaderPooled reader = NetworkReaderPool.Get(data))
|
||||
{
|
||||
OpCodes opcode = (OpCodes)reader.ReadByte();
|
||||
switch (opcode)
|
||||
{
|
||||
case OpCodes.Data:
|
||||
// first sender ready is implicit when data is received
|
||||
if (_sendsFirst && _state == State.WaitingHandshakeReply)
|
||||
{
|
||||
SetReady();
|
||||
}
|
||||
else if (!IsReady)
|
||||
{
|
||||
_error(TransportError.Unexpected, "Unexpected data while not ready.");
|
||||
}
|
||||
|
||||
if (reader.Remaining < Overhead)
|
||||
{
|
||||
_error(TransportError.Unexpected, "received data packet smaller than metadata size");
|
||||
return;
|
||||
}
|
||||
|
||||
ArraySegment<byte> ciphertext = reader.ReadBytesSegment(reader.Remaining - NonceSize);
|
||||
reader.ReadBytes(ReceiveNonce, NonceSize);
|
||||
|
||||
Profiler.BeginSample("EncryptedConnection.Decrypt");
|
||||
ArraySegment<byte> plaintext = Decrypt(ciphertext);
|
||||
Profiler.EndSample();
|
||||
if (plaintext.Count == 0)
|
||||
{
|
||||
// error
|
||||
return;
|
||||
}
|
||||
_receive(plaintext, channel);
|
||||
break;
|
||||
case OpCodes.HandshakeStart:
|
||||
if (_sendsFirst)
|
||||
{
|
||||
_error(TransportError.Unexpected, "Received HandshakeStart packet, we don't expect this.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_state == State.WaitingHandshakeReply)
|
||||
{
|
||||
// this is fine, packets may arrive out of order
|
||||
return;
|
||||
}
|
||||
|
||||
_state = State.WaitingHandshakeReply;
|
||||
ResetTimeouts();
|
||||
CompleteExchange(reader.ReadBytesSegment(reader.Remaining), _hkdfSalt);
|
||||
SendHandshakeAndPubKey(OpCodes.HandshakeAck);
|
||||
break;
|
||||
case OpCodes.HandshakeAck:
|
||||
if (!_sendsFirst)
|
||||
{
|
||||
_error(TransportError.Unexpected, "Received HandshakeAck packet, we don't expect this.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsReady)
|
||||
{
|
||||
// this is fine, packets may arrive out of order
|
||||
return;
|
||||
}
|
||||
|
||||
if (_state == State.WaitingHandshakeReply)
|
||||
{
|
||||
// this is fine, packets may arrive out of order
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
_state = State.WaitingHandshakeReply;
|
||||
ResetTimeouts();
|
||||
reader.ReadBytes(_tmpRemoteSaltBuffer, HkdfSaltSize);
|
||||
CompleteExchange(reader.ReadBytesSegment(reader.Remaining), _tmpRemoteSaltBuffer);
|
||||
SendHandshakeFin();
|
||||
break;
|
||||
case OpCodes.HandshakeFin:
|
||||
if (_sendsFirst)
|
||||
{
|
||||
_error(TransportError.Unexpected, "Received HandshakeFin packet, we don't expect this.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsReady)
|
||||
{
|
||||
// this is fine, packets may arrive out of order
|
||||
return;
|
||||
}
|
||||
|
||||
if (_state != State.WaitingHandshakeReply)
|
||||
{
|
||||
_error(TransportError.Unexpected,
|
||||
"Received HandshakeFin packet, we didn't expect this yet.");
|
||||
return;
|
||||
}
|
||||
|
||||
SetReady();
|
||||
|
||||
break;
|
||||
default:
|
||||
_error(TransportError.InvalidReceive, $"Unhandled opcode {(byte)opcode:x}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
private void SetReady()
|
||||
{
|
||||
// done with credentials, null out the reference
|
||||
_credentials = null;
|
||||
|
||||
_state = State.Ready;
|
||||
_ready();
|
||||
}
|
||||
|
||||
private void ResetTimeouts()
|
||||
{
|
||||
_handshakeTimeout = 0;
|
||||
_nextHandshakeResend = -1;
|
||||
}
|
||||
|
||||
public void Send(ArraySegment<byte> data, int channel)
|
||||
{
|
||||
using (NetworkWriterPooled writer = NetworkWriterPool.Get())
|
||||
{
|
||||
writer.WriteByte((byte)OpCodes.Data);
|
||||
Profiler.BeginSample("EncryptedConnection.Encrypt");
|
||||
ArraySegment<byte> encrypted = Encrypt(data);
|
||||
Profiler.EndSample();
|
||||
|
||||
if (encrypted.Count == 0)
|
||||
{
|
||||
// error
|
||||
return;
|
||||
}
|
||||
writer.WriteBytes(encrypted.Array, 0, encrypted.Count);
|
||||
// write nonce after since Encrypt will update it
|
||||
writer.WriteBytes(_nonce, 0, NonceSize);
|
||||
_send(writer.ToArraySegment(), channel);
|
||||
}
|
||||
}
|
||||
|
||||
private ArraySegment<byte> Encrypt(ArraySegment<byte> plaintext)
|
||||
{
|
||||
if (plaintext.Count == 0)
|
||||
{
|
||||
// Invalid
|
||||
return new ArraySegment<byte>();
|
||||
}
|
||||
// Need to make the nonce unique again before encrypting another message
|
||||
UpdateNonce();
|
||||
// Re-initialize the cipher with our cached parameters
|
||||
Cipher.Init(true, _cipherParametersEncrypt);
|
||||
|
||||
// Calculate the expected output size, this should always be input size + mac size
|
||||
int outSize = Cipher.GetOutputSize(plaintext.Count);
|
||||
#if UNITY_EDITOR
|
||||
// expecting the outSize to be input size + MacSize
|
||||
if (outSize != plaintext.Count + MacSizeBytes)
|
||||
{
|
||||
throw new Exception($"Encrypt: Unexpected output size (Expected {plaintext.Count + MacSizeBytes}, got {outSize}");
|
||||
}
|
||||
#endif
|
||||
// Resize the static buffer to fit
|
||||
EnsureSize(ref _tmpCryptBuffer, outSize);
|
||||
int resultLen;
|
||||
try
|
||||
{
|
||||
// Run the plain text through the cipher, ProcessBytes will only process full blocks
|
||||
resultLen =
|
||||
Cipher.ProcessBytes(plaintext.Array, plaintext.Offset, plaintext.Count, _tmpCryptBuffer, 0);
|
||||
// Then run any potentially remaining partial blocks through with DoFinal (and calculate the mac)
|
||||
resultLen += Cipher.DoFinal(_tmpCryptBuffer, resultLen);
|
||||
}
|
||||
// catch all Exception's since BouncyCastle is fairly noisy with both standard and their own exception types
|
||||
//
|
||||
catch (Exception e)
|
||||
{
|
||||
_error(TransportError.Unexpected, $"Unexpected exception while encrypting {e.GetType()}: {e.Message}");
|
||||
return new ArraySegment<byte>();
|
||||
}
|
||||
#if UNITY_EDITOR
|
||||
// expecting the result length to match the previously calculated input size + MacSize
|
||||
if (resultLen != outSize)
|
||||
{
|
||||
throw new Exception($"Encrypt: resultLen did not match outSize (expected {outSize}, got {resultLen})");
|
||||
}
|
||||
#endif
|
||||
return new ArraySegment<byte>(_tmpCryptBuffer, 0, resultLen);
|
||||
}
|
||||
|
||||
private ArraySegment<byte> Decrypt(ArraySegment<byte> ciphertext)
|
||||
{
|
||||
if (ciphertext.Count <= MacSizeBytes)
|
||||
{
|
||||
_error(TransportError.Unexpected, $"Received too short data packet (min {{MacSizeBytes + 1}}, got {ciphertext.Count})");
|
||||
// Invalid
|
||||
return new ArraySegment<byte>();
|
||||
}
|
||||
// Re-initialize the cipher with our cached parameters
|
||||
Cipher.Init(false, _cipherParametersDecrypt);
|
||||
|
||||
// Calculate the expected output size, this should always be input size - mac size
|
||||
int outSize = Cipher.GetOutputSize(ciphertext.Count);
|
||||
#if UNITY_EDITOR
|
||||
// expecting the outSize to be input size - MacSize
|
||||
if (outSize != ciphertext.Count - MacSizeBytes)
|
||||
{
|
||||
throw new Exception($"Decrypt: Unexpected output size (Expected {ciphertext.Count - MacSizeBytes}, got {outSize}");
|
||||
}
|
||||
#endif
|
||||
// Resize the static buffer to fit
|
||||
EnsureSize(ref _tmpCryptBuffer, outSize);
|
||||
int resultLen;
|
||||
try
|
||||
{
|
||||
// Run the ciphertext through the cipher, ProcessBytes will only process full blocks
|
||||
resultLen =
|
||||
Cipher.ProcessBytes(ciphertext.Array, ciphertext.Offset, ciphertext.Count, _tmpCryptBuffer, 0);
|
||||
// Then run any potentially remaining partial blocks through with DoFinal (and calculate/check the mac)
|
||||
resultLen += Cipher.DoFinal(_tmpCryptBuffer, resultLen);
|
||||
}
|
||||
// catch all Exception's since BouncyCastle is fairly noisy with both standard and their own exception types
|
||||
catch (Exception e)
|
||||
{
|
||||
_error(TransportError.Unexpected, $"Unexpected exception while decrypting {e.GetType()}: {e.Message}. This usually signifies corrupt data");
|
||||
return new ArraySegment<byte>();
|
||||
}
|
||||
#if UNITY_EDITOR
|
||||
// expecting the result length to match the previously calculated input size + MacSize
|
||||
if (resultLen != outSize)
|
||||
{
|
||||
throw new Exception($"Decrypt: resultLen did not match outSize (expected {outSize}, got {resultLen})");
|
||||
}
|
||||
#endif
|
||||
return new ArraySegment<byte>(_tmpCryptBuffer, 0, resultLen);
|
||||
}
|
||||
|
||||
private void UpdateNonce()
|
||||
{
|
||||
// increment the nonce by one
|
||||
// we need to ensure the nonce is *always* unique and not reused
|
||||
// easiest way to do this is by simply incrementing it
|
||||
for (int i = 0; i < NonceSize; i++)
|
||||
{
|
||||
_nonce[i]++;
|
||||
if (_nonce[i] != 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnsureSize(ref byte[] buffer, int size)
|
||||
{
|
||||
if (buffer.Length < size)
|
||||
{
|
||||
// double buffer to avoid constantly resizing by a few bytes
|
||||
Array.Resize(ref buffer, Math.Max(size, buffer.Length * 2));
|
||||
}
|
||||
}
|
||||
|
||||
private void SendHandshakeAndPubKey(OpCodes opcode)
|
||||
{
|
||||
using (NetworkWriterPooled writer = NetworkWriterPool.Get())
|
||||
{
|
||||
writer.WriteByte((byte)opcode);
|
||||
if (opcode == OpCodes.HandshakeAck)
|
||||
{
|
||||
writer.WriteBytes(_hkdfSalt, 0, HkdfSaltSize);
|
||||
}
|
||||
writer.WriteBytes(_credentials.PublicKeySerialized, 0, _credentials.PublicKeySerialized.Length);
|
||||
_send(writer.ToArraySegment(), Channels.Unreliable);
|
||||
}
|
||||
}
|
||||
|
||||
private void SendHandshakeFin()
|
||||
{
|
||||
using (NetworkWriterPooled writer = NetworkWriterPool.Get())
|
||||
{
|
||||
writer.WriteByte((byte)OpCodes.HandshakeFin);
|
||||
_send(writer.ToArraySegment(), Channels.Unreliable);
|
||||
}
|
||||
}
|
||||
|
||||
private void CompleteExchange(ArraySegment<byte> remotePubKeyRaw, byte[] salt)
|
||||
{
|
||||
AsymmetricKeyParameter remotePubKey;
|
||||
try
|
||||
{
|
||||
remotePubKey = EncryptionCredentials.DeserializePublicKey(remotePubKeyRaw);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_error(TransportError.Unexpected, $"Failed to deserialize public key of remote. {e.GetType()}: {e.Message}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_validateRemoteKey != null)
|
||||
{
|
||||
PubKeyInfo info = new PubKeyInfo
|
||||
{
|
||||
Fingerprint = EncryptionCredentials.PubKeyFingerprint(remotePubKeyRaw),
|
||||
Serialized = remotePubKeyRaw,
|
||||
Key = remotePubKey
|
||||
};
|
||||
if (!_validateRemoteKey(info))
|
||||
{
|
||||
_error(TransportError.Unexpected, $"Remote public key (fingerprint: {info.Fingerprint}) failed validation. ");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate a common symmetric key from our private key and the remotes public key
|
||||
// This gives us the same key on the other side, with our public key and their remote
|
||||
// It's like magic, but with math!
|
||||
ECDHBasicAgreement ecdh = new ECDHBasicAgreement();
|
||||
ecdh.Init(_credentials.PrivateKey);
|
||||
byte[] sharedSecret;
|
||||
try
|
||||
{
|
||||
sharedSecret = ecdh.CalculateAgreement(remotePubKey).ToByteArrayUnsigned();
|
||||
}
|
||||
catch
|
||||
(Exception e)
|
||||
{
|
||||
_error(TransportError.Unexpected, $"Failed to calculate the ECDH key exchange. {e.GetType()}: {e.Message}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (salt.Length != HkdfSaltSize)
|
||||
{
|
||||
_error(TransportError.Unexpected, $"Salt is expected to be {HkdfSaltSize} bytes long, got {salt.Length}.");
|
||||
return;
|
||||
}
|
||||
|
||||
Hkdf.Init(new HkdfParameters(sharedSecret, salt, HkdfInfo));
|
||||
|
||||
// Allocate a buffer for the output key
|
||||
byte[] keyRaw = new byte[KeyLength];
|
||||
|
||||
// Generate the output keying material
|
||||
Hkdf.GenerateBytes(keyRaw, 0, keyRaw.Length);
|
||||
|
||||
KeyParameter key = new KeyParameter(keyRaw);
|
||||
|
||||
// generate a starting nonce
|
||||
_nonce = GenerateSecureBytes(NonceSize);
|
||||
|
||||
// we pass in the nonce array once (as it's stored by reference) so we can cache the AeadParameters instance
|
||||
// instead of creating a new one each encrypt/decrypt
|
||||
_cipherParametersEncrypt = new AeadParameters(key, MacSizeBits, _nonce);
|
||||
_cipherParametersDecrypt = new AeadParameters(key, MacSizeBits, ReceiveNonce);
|
||||
}
|
||||
|
||||
/**
|
||||
* non-ready connections need to be ticked for resending key data over unreliable
|
||||
*/
|
||||
public void TickNonReady(double time)
|
||||
{
|
||||
if (IsReady)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Timeout reset
|
||||
if (_handshakeTimeout == 0)
|
||||
{
|
||||
_handshakeTimeout = time + DurationTimeout;
|
||||
}
|
||||
else if (time > _handshakeTimeout)
|
||||
{
|
||||
_error?.Invoke(TransportError.Timeout, $"Timed out during {_state}, this probably just means the other side went away which is fine.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Timeout reset
|
||||
if (_nextHandshakeResend < 0)
|
||||
{
|
||||
_nextHandshakeResend = time + DurationResend;
|
||||
return;
|
||||
}
|
||||
|
||||
if (time < _nextHandshakeResend)
|
||||
{
|
||||
// Resend isn't due yet
|
||||
return;
|
||||
}
|
||||
|
||||
_nextHandshakeResend = time + DurationResend;
|
||||
switch (_state)
|
||||
{
|
||||
case State.WaitingHandshake:
|
||||
if (_sendsFirst)
|
||||
{
|
||||
SendHandshakeAndPubKey(OpCodes.HandshakeStart);
|
||||
}
|
||||
|
||||
break;
|
||||
case State.WaitingHandshakeReply:
|
||||
if (_sendsFirst)
|
||||
{
|
||||
SendHandshakeFin();
|
||||
}
|
||||
else
|
||||
{
|
||||
SendHandshakeAndPubKey(OpCodes.HandshakeAck);
|
||||
}
|
||||
|
||||
break;
|
||||
case State.Ready: // IsReady is checked above & early-returned
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 28f3ac4ff1d346a895d0b4ff714fb57b
|
||||
timeCreated: 1708111337
|
125
Assets/Mirror/Transports/Encryption/EncryptionCredentials.cs
Normal file
125
Assets/Mirror/Transports/Encryption/EncryptionCredentials.cs
Normal file
@ -0,0 +1,125 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Org.BouncyCastle.Asn1.Pkcs;
|
||||
using Org.BouncyCastle.Asn1.X509;
|
||||
using Org.BouncyCastle.Crypto;
|
||||
using Org.BouncyCastle.Crypto.Digests;
|
||||
using Org.BouncyCastle.Crypto.Generators;
|
||||
using Org.BouncyCastle.X509;
|
||||
using Org.BouncyCastle.Crypto.Parameters;
|
||||
using Org.BouncyCastle.Pkcs;
|
||||
using Org.BouncyCastle.Security;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror.Transports.Encryption
|
||||
{
|
||||
public class EncryptionCredentials
|
||||
{
|
||||
const int PrivateKeyBits = 256;
|
||||
// don't actually need to store this currently
|
||||
// but we'll need to for loading/saving from file maybe?
|
||||
// public ECPublicKeyParameters PublicKey;
|
||||
|
||||
// The serialized public key, in DER format
|
||||
public byte[] PublicKeySerialized;
|
||||
public ECPrivateKeyParameters PrivateKey;
|
||||
public string PublicKeyFingerprint;
|
||||
|
||||
EncryptionCredentials() {}
|
||||
|
||||
// TODO: load from file
|
||||
public static EncryptionCredentials Generate()
|
||||
{
|
||||
var generator = new ECKeyPairGenerator();
|
||||
generator.Init(new KeyGenerationParameters(new SecureRandom(), PrivateKeyBits));
|
||||
AsymmetricCipherKeyPair keyPair = generator.GenerateKeyPair();
|
||||
var serialized = SerializePublicKey((ECPublicKeyParameters)keyPair.Public);
|
||||
return new EncryptionCredentials
|
||||
{
|
||||
// see fields above
|
||||
// PublicKey = (ECPublicKeyParameters)keyPair.Public,
|
||||
PublicKeySerialized = serialized,
|
||||
PublicKeyFingerprint = PubKeyFingerprint(new ArraySegment<byte>(serialized)),
|
||||
PrivateKey = (ECPrivateKeyParameters)keyPair.Private
|
||||
};
|
||||
}
|
||||
|
||||
public static byte[] SerializePublicKey(AsymmetricKeyParameter publicKey)
|
||||
{
|
||||
// apparently the best way to transmit this public key over the network is to serialize it as a DER
|
||||
SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(publicKey);
|
||||
return publicKeyInfo.ToAsn1Object().GetDerEncoded();
|
||||
}
|
||||
|
||||
public static AsymmetricKeyParameter DeserializePublicKey(ArraySegment<byte> pubKey)
|
||||
{
|
||||
// And then we do this to deserialize from the DER (from above)
|
||||
// the "new MemoryStream" actually saves an allocation, since otherwise the ArraySegment would be converted
|
||||
// to a byte[] first and then shoved through a MemoryStream
|
||||
return PublicKeyFactory.CreateKey(new MemoryStream(pubKey.Array, pubKey.Offset, pubKey.Count, false));
|
||||
}
|
||||
|
||||
public static byte[] SerializePrivateKey(AsymmetricKeyParameter privateKey)
|
||||
{
|
||||
// Serialize privateKey as a DER
|
||||
PrivateKeyInfo privateKeyInfo = PrivateKeyInfoFactory.CreatePrivateKeyInfo(privateKey);
|
||||
return privateKeyInfo.ToAsn1Object().GetDerEncoded();
|
||||
}
|
||||
|
||||
public static AsymmetricKeyParameter DeserializePrivateKey(ArraySegment<byte> privateKey)
|
||||
{
|
||||
// And then we do this to deserialize from the DER (from above)
|
||||
// the "new MemoryStream" actually saves an allocation, since otherwise the ArraySegment would be converted
|
||||
// to a byte[] first and then shoved through a MemoryStream
|
||||
return PrivateKeyFactory.CreateKey(new MemoryStream(privateKey.Array, privateKey.Offset, privateKey.Count, false));
|
||||
}
|
||||
|
||||
public static string PubKeyFingerprint(ArraySegment<byte> publicKeyBytes)
|
||||
{
|
||||
Sha256Digest digest = new Sha256Digest();
|
||||
byte[] hash = new byte[digest.GetDigestSize()];
|
||||
digest.BlockUpdate(publicKeyBytes.Array, publicKeyBytes.Offset, publicKeyBytes.Count);
|
||||
digest.DoFinal(hash, 0);
|
||||
|
||||
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
|
||||
}
|
||||
|
||||
public void SaveToFile(string path)
|
||||
{
|
||||
string json = JsonUtility.ToJson(new SerializedPair
|
||||
{
|
||||
PublicKeyFingerprint = PublicKeyFingerprint,
|
||||
PublicKey = Convert.ToBase64String(PublicKeySerialized),
|
||||
PrivateKey= Convert.ToBase64String(SerializePrivateKey(PrivateKey)),
|
||||
});
|
||||
File.WriteAllText(path, json);
|
||||
}
|
||||
|
||||
public static EncryptionCredentials LoadFromFile(string path)
|
||||
{
|
||||
string json = File.ReadAllText(path);
|
||||
SerializedPair serializedPair = JsonUtility.FromJson<SerializedPair>(json);
|
||||
|
||||
byte[] publicKeyBytes = Convert.FromBase64String(serializedPair.PublicKey);
|
||||
byte[] privateKeyBytes = Convert.FromBase64String(serializedPair.PrivateKey);
|
||||
|
||||
if (serializedPair.PublicKeyFingerprint != PubKeyFingerprint(new ArraySegment<byte>(publicKeyBytes)))
|
||||
{
|
||||
throw new Exception("Saved public key fingerprint does not match public key.");
|
||||
}
|
||||
return new EncryptionCredentials
|
||||
{
|
||||
PublicKeySerialized = publicKeyBytes,
|
||||
PublicKeyFingerprint = serializedPair.PublicKeyFingerprint,
|
||||
PrivateKey = (ECPrivateKeyParameters) DeserializePrivateKey(new ArraySegment<byte>(privateKeyBytes))
|
||||
};
|
||||
}
|
||||
|
||||
private class SerializedPair
|
||||
{
|
||||
public string PublicKeyFingerprint;
|
||||
public string PublicKey;
|
||||
public string PrivateKey;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: af6ae5f74f9548588cba5731643fabaf
|
||||
timeCreated: 1708139579
|
268
Assets/Mirror/Transports/Encryption/EncryptionTransport.cs
Normal file
268
Assets/Mirror/Transports/Encryption/EncryptionTransport.cs
Normal file
@ -0,0 +1,268 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Profiling;
|
||||
using UnityEngine.Serialization;
|
||||
|
||||
namespace Mirror.Transports.Encryption
|
||||
{
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/manual/transports/encryption-transport")]
|
||||
public class EncryptionTransport : Transport
|
||||
{
|
||||
public Transport inner;
|
||||
|
||||
public enum ValidationMode
|
||||
{
|
||||
Off,
|
||||
List,
|
||||
Callback,
|
||||
}
|
||||
|
||||
public ValidationMode clientValidateServerPubKey;
|
||||
[Tooltip("List of public key fingerprints the client will accept")]
|
||||
public string[] clientTrustedPubKeySignatures;
|
||||
public Func<PubKeyInfo, bool> onClientValidateServerPubKey;
|
||||
public bool serverLoadKeyPairFromFile;
|
||||
public string serverKeypairPath = "./server-keys.json";
|
||||
|
||||
private EncryptedConnection _client;
|
||||
|
||||
private Dictionary<int, EncryptedConnection> _serverConnections = new Dictionary<int, EncryptedConnection>();
|
||||
|
||||
private List<EncryptedConnection> _serverPendingConnections =
|
||||
new List<EncryptedConnection>();
|
||||
|
||||
private EncryptionCredentials _credentials;
|
||||
public string EncryptionPublicKeyFingerprint => _credentials?.PublicKeyFingerprint;
|
||||
public byte[] EncryptionPublicKey => _credentials?.PublicKeySerialized;
|
||||
|
||||
private void ServerRemoveFromPending(EncryptedConnection con)
|
||||
{
|
||||
for (int i = 0; i < _serverPendingConnections.Count; i++)
|
||||
{
|
||||
if (_serverPendingConnections[i] == con)
|
||||
{
|
||||
// remove by swapping with last
|
||||
int lastIndex = _serverPendingConnections.Count - 1;
|
||||
_serverPendingConnections[i] = _serverPendingConnections[lastIndex];
|
||||
_serverPendingConnections.RemoveAt(lastIndex);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleInnerServerDisconnected(int connId)
|
||||
{
|
||||
if (_serverConnections.TryGetValue(connId, out EncryptedConnection con))
|
||||
{
|
||||
ServerRemoveFromPending(con);
|
||||
_serverConnections.Remove(connId);
|
||||
}
|
||||
OnServerDisconnected?.Invoke(connId);
|
||||
}
|
||||
|
||||
private void HandleInnerServerError(int connId, TransportError type, string msg)
|
||||
{
|
||||
OnServerError?.Invoke(connId, type, $"inner: {msg}");
|
||||
}
|
||||
|
||||
private void HandleInnerServerDataReceived(int connId, ArraySegment<byte> data, int channel)
|
||||
{
|
||||
if (_serverConnections.TryGetValue(connId, out EncryptedConnection c))
|
||||
{
|
||||
c.OnReceiveRaw(data, channel);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleInnerServerConnected(int connId)
|
||||
{
|
||||
Debug.Log($"[EncryptionTransport] New connection #{connId}");
|
||||
EncryptedConnection ec = null;
|
||||
ec = new EncryptedConnection(
|
||||
_credentials,
|
||||
false,
|
||||
(segment, channel) => inner.ServerSend(connId, segment, channel),
|
||||
(segment, channel) => OnServerDataReceived?.Invoke(connId, segment, channel),
|
||||
() =>
|
||||
{
|
||||
Debug.Log($"[EncryptionTransport] Connection #{connId} is ready");
|
||||
ServerRemoveFromPending(ec);
|
||||
OnServerConnected?.Invoke(connId);
|
||||
},
|
||||
(type, msg) =>
|
||||
{
|
||||
OnServerError?.Invoke(connId, type, msg);
|
||||
ServerDisconnect(connId);
|
||||
});
|
||||
_serverConnections.Add(connId, ec);
|
||||
_serverPendingConnections.Add(ec);
|
||||
}
|
||||
|
||||
private void HandleInnerClientDisconnected()
|
||||
{
|
||||
_client = null;
|
||||
OnClientDisconnected?.Invoke();
|
||||
}
|
||||
|
||||
private void HandleInnerClientError(TransportError arg1, string arg2)
|
||||
{
|
||||
OnClientError?.Invoke(arg1, $"inner: {arg2}");
|
||||
}
|
||||
|
||||
private void HandleInnerClientDataReceived(ArraySegment<byte> data, int channel)
|
||||
{
|
||||
_client?.OnReceiveRaw(data, channel);
|
||||
}
|
||||
|
||||
private void HandleInnerClientConnected()
|
||||
{
|
||||
_client = new EncryptedConnection(
|
||||
_credentials,
|
||||
true,
|
||||
(segment, channel) => inner.ClientSend(segment, channel),
|
||||
(segment, channel) => OnClientDataReceived?.Invoke(segment, channel),
|
||||
() =>
|
||||
{
|
||||
OnClientConnected?.Invoke();
|
||||
},
|
||||
(type, msg) =>
|
||||
{
|
||||
OnClientError?.Invoke(type, msg);
|
||||
ClientDisconnect();
|
||||
},
|
||||
HandleClientValidateServerPubKey);
|
||||
}
|
||||
|
||||
private bool HandleClientValidateServerPubKey(PubKeyInfo pubKeyInfo)
|
||||
{
|
||||
switch (clientValidateServerPubKey)
|
||||
{
|
||||
case ValidationMode.Off:
|
||||
return true;
|
||||
case ValidationMode.List:
|
||||
return Array.IndexOf(clientTrustedPubKeySignatures, pubKeyInfo.Fingerprint) >= 0;
|
||||
case ValidationMode.Callback:
|
||||
return onClientValidateServerPubKey(pubKeyInfo);
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
|
||||
public override bool Available() => inner.Available();
|
||||
|
||||
public override bool ClientConnected() => _client != null && _client.IsReady;
|
||||
|
||||
public override void ClientConnect(string address)
|
||||
{
|
||||
switch (clientValidateServerPubKey)
|
||||
{
|
||||
case ValidationMode.Off:
|
||||
break;
|
||||
case ValidationMode.List:
|
||||
if (clientTrustedPubKeySignatures == null || clientTrustedPubKeySignatures.Length == 0)
|
||||
{
|
||||
OnClientError?.Invoke(TransportError.Unexpected, "Validate Server Public Key is set to List, but the clientTrustedPubKeySignatures list is empty.");
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case ValidationMode.Callback:
|
||||
if (onClientValidateServerPubKey == null)
|
||||
{
|
||||
OnClientError?.Invoke(TransportError.Unexpected, "Validate Server Public Key is set to Callback, but the onClientValidateServerPubKey handler is not set");
|
||||
return;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
_credentials = EncryptionCredentials.Generate();
|
||||
inner.OnClientConnected = HandleInnerClientConnected;
|
||||
inner.OnClientDataReceived = HandleInnerClientDataReceived;
|
||||
inner.OnClientDataSent = (bytes, channel) => OnClientDataSent?.Invoke(bytes, channel);
|
||||
inner.OnClientError = HandleInnerClientError;
|
||||
inner.OnClientDisconnected = HandleInnerClientDisconnected;
|
||||
inner.ClientConnect(address);
|
||||
}
|
||||
|
||||
public override void ClientSend(ArraySegment<byte> segment, int channelId = Channels.Reliable) =>
|
||||
_client?.Send(segment, channelId);
|
||||
|
||||
public override void ClientDisconnect() => inner.ClientDisconnect();
|
||||
|
||||
public override Uri ServerUri() => inner.ServerUri();
|
||||
|
||||
public override bool ServerActive() => inner.ServerActive();
|
||||
|
||||
public override void ServerStart()
|
||||
{
|
||||
if (serverLoadKeyPairFromFile)
|
||||
{
|
||||
_credentials = EncryptionCredentials.LoadFromFile(serverKeypairPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
_credentials = EncryptionCredentials.Generate();
|
||||
}
|
||||
inner.OnServerConnected = HandleInnerServerConnected;
|
||||
inner.OnServerDataReceived = HandleInnerServerDataReceived;
|
||||
inner.OnServerDataSent = (connId, bytes, channel) => OnServerDataSent?.Invoke(connId, bytes, channel);
|
||||
inner.OnServerError = HandleInnerServerError;
|
||||
inner.OnServerDisconnected = HandleInnerServerDisconnected;
|
||||
inner.ServerStart();
|
||||
}
|
||||
|
||||
public override void ServerSend(int connectionId, ArraySegment<byte> segment, int channelId = Channels.Reliable)
|
||||
{
|
||||
if (_serverConnections.TryGetValue(connectionId, out EncryptedConnection connection) && connection.IsReady)
|
||||
{
|
||||
connection.Send(segment, channelId);
|
||||
}
|
||||
}
|
||||
|
||||
public override void ServerDisconnect(int connectionId)
|
||||
{
|
||||
// cleanup is done via inners disconnect event
|
||||
inner.ServerDisconnect(connectionId);
|
||||
}
|
||||
|
||||
public override string ServerGetClientAddress(int connectionId) => inner.ServerGetClientAddress(connectionId);
|
||||
|
||||
public override void ServerStop() => inner.ServerStop();
|
||||
|
||||
public override int GetMaxPacketSize(int channelId = Channels.Reliable) =>
|
||||
inner.GetMaxPacketSize(channelId) - EncryptedConnection.Overhead;
|
||||
|
||||
public override void Shutdown() => inner.Shutdown();
|
||||
|
||||
public override void ClientEarlyUpdate()
|
||||
{
|
||||
inner.ClientEarlyUpdate();
|
||||
}
|
||||
|
||||
public override void ClientLateUpdate()
|
||||
{
|
||||
inner.ClientLateUpdate();
|
||||
Profiler.BeginSample("EncryptionTransport.ServerLateUpdate");
|
||||
_client?.TickNonReady(NetworkTime.localTime);
|
||||
Profiler.EndSample();
|
||||
}
|
||||
|
||||
public override void ServerEarlyUpdate()
|
||||
{
|
||||
inner.ServerEarlyUpdate();
|
||||
}
|
||||
|
||||
public override void ServerLateUpdate()
|
||||
{
|
||||
inner.ServerLateUpdate();
|
||||
Profiler.BeginSample("EncryptionTransport.ServerLateUpdate");
|
||||
// Reverse iteration as entries can be removed while updating
|
||||
for (int i = _serverPendingConnections.Count - 1; i >= 0; i--)
|
||||
{
|
||||
_serverPendingConnections[i].TickNonReady(NetworkTime.time);
|
||||
}
|
||||
Profiler.EndSample();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0aa135acc32a4383ae9a5817f018cb06
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
9
Assets/Mirror/Transports/Encryption/PubKeyInfo.cs
Normal file
9
Assets/Mirror/Transports/Encryption/PubKeyInfo.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using System;
|
||||
using Org.BouncyCastle.Crypto;
|
||||
|
||||
public struct PubKeyInfo
|
||||
{
|
||||
public string Fingerprint;
|
||||
public ArraySegment<byte> Serialized;
|
||||
public AsymmetricKeyParameter Key;
|
||||
}
|
3
Assets/Mirror/Transports/Encryption/PubKeyInfo.cs.meta
Normal file
3
Assets/Mirror/Transports/Encryption/PubKeyInfo.cs.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e1e3744418024c02acf39f44c1d1bd20
|
||||
timeCreated: 1708874062
|
8
Assets/Mirror/Transports/KCP.meta
Normal file
8
Assets/Mirror/Transports/KCP.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ea4ea5d03df6a49449fa679ac2390773
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
371
Assets/Mirror/Transports/KCP/KcpTransport.cs
Normal file
371
Assets/Mirror/Transports/KCP/KcpTransport.cs
Normal file
@ -0,0 +1,371 @@
|
||||
//#if MIRROR <- commented out because MIRROR isn't defined on first import yet
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using Mirror;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Serialization;
|
||||
|
||||
namespace kcp2k
|
||||
{
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/transports/kcp-transport")]
|
||||
[DisallowMultipleComponent]
|
||||
public class KcpTransport : Transport, PortTransport
|
||||
{
|
||||
// scheme used by this transport
|
||||
public const string Scheme = "kcp";
|
||||
|
||||
// common
|
||||
[Header("Transport Configuration")]
|
||||
[FormerlySerializedAs("Port")]
|
||||
public ushort port = 7777;
|
||||
public ushort Port { get => port; set => port=value; }
|
||||
[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;
|
||||
[Tooltip("Socket receive buffer size. Large buffer helps support more connections. Increase operating system socket buffer size limits if needed.")]
|
||||
public int RecvBufferSize = 1024 * 1027 * 7;
|
||||
[Tooltip("Socket send buffer size. Large buffer helps support more connections. Increase operating system socket buffer size limits if needed.")]
|
||||
public int SendBufferSize = 1024 * 1027 * 7;
|
||||
|
||||
[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. Restricts window size to reduce congestion. Results in only 2-3 MTU messages per Flush even on loopback. Best to keept his disabled.")]
|
||||
/*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. This also increases max message size.")]
|
||||
public uint ReceiveWindowSize = 4096; //Kcp.WND_RCV; 128 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 SendWindowSize = 4096; //Kcp.WND_SND; 32 by default. Mirror sends a lot, so we need a lot more.
|
||||
[Tooltip("KCP will try to retransmit lost messages up to MaxRetransmit (aka dead_link) before disconnecting.")]
|
||||
public uint MaxRetransmit = Kcp.DEADLINK * 2; // default prematurely disconnects a lot of people (#3022). use 2x.
|
||||
[Tooltip("Enable to automatically set client & server send/recv buffers to OS limit. Avoids issues with too small buffers under heavy load, potentially dropping connections. Increase the OS limit if this is still too small.")]
|
||||
[FormerlySerializedAs("MaximizeSendReceiveBuffersToOSLimit")]
|
||||
public bool MaximizeSocketBuffers = true;
|
||||
|
||||
[Header("Allowed Max Message Sizes\nBased on Receive Window Size")]
|
||||
[Tooltip("KCP reliable max message size shown for convenience. Can be changed via ReceiveWindowSize.")]
|
||||
[ReadOnly] public int ReliableMaxMessageSize = 0; // readonly, displayed from OnValidate
|
||||
[Tooltip("KCP unreliable channel max message size for convenience. Not changeable.")]
|
||||
[ReadOnly] public int UnreliableMaxMessageSize = 0; // readonly, displayed from OnValidate
|
||||
|
||||
// config is created from the serialized properties above.
|
||||
// we can expose the config directly in the future.
|
||||
// for now, let's not break people's old settings.
|
||||
protected KcpConfig config;
|
||||
|
||||
// use default MTU for this transport.
|
||||
const int MTU = Kcp.MTU_DEF;
|
||||
|
||||
// server & client
|
||||
protected KcpServer server;
|
||||
protected 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;
|
||||
|
||||
// translate Kcp <-> Mirror channels
|
||||
public static int FromKcpChannel(KcpChannel channel) =>
|
||||
channel == KcpChannel.Reliable ? Channels.Reliable : Channels.Unreliable;
|
||||
|
||||
public static KcpChannel ToKcpChannel(int channel) =>
|
||||
channel == Channels.Reliable ? KcpChannel.Reliable : KcpChannel.Unreliable;
|
||||
|
||||
public static TransportError ToTransportError(ErrorCode error)
|
||||
{
|
||||
switch(error)
|
||||
{
|
||||
case ErrorCode.DnsResolve: return TransportError.DnsResolve;
|
||||
case ErrorCode.Timeout: return TransportError.Timeout;
|
||||
case ErrorCode.Congestion: return TransportError.Congestion;
|
||||
case ErrorCode.InvalidReceive: return TransportError.InvalidReceive;
|
||||
case ErrorCode.InvalidSend: return TransportError.InvalidSend;
|
||||
case ErrorCode.ConnectionClosed: return TransportError.ConnectionClosed;
|
||||
case ErrorCode.Unexpected: return TransportError.Unexpected;
|
||||
default: throw new InvalidCastException($"KCP: missing error translation for {error}");
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual 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;
|
||||
|
||||
// create config from serialized settings
|
||||
config = new KcpConfig(DualMode, RecvBufferSize, SendBufferSize, MTU, NoDelay, Interval, FastResend, CongestionWindow, SendWindowSize, ReceiveWindowSize, Timeout, MaxRetransmit);
|
||||
|
||||
// client (NonAlloc version is not necessary anymore)
|
||||
client = new KcpClient(
|
||||
() => OnClientConnected.Invoke(),
|
||||
(message, channel) => OnClientDataReceived.Invoke(message, FromKcpChannel(channel)),
|
||||
() => OnClientDisconnected?.Invoke(), // may be null in StopHost(): https://github.com/MirrorNetworking/Mirror/issues/3708
|
||||
(error, reason) => OnClientError.Invoke(ToTransportError(error), reason),
|
||||
config
|
||||
);
|
||||
|
||||
// server
|
||||
server = new KcpServer(
|
||||
(connectionId) => OnServerConnected.Invoke(connectionId),
|
||||
(connectionId, message, channel) => OnServerDataReceived.Invoke(connectionId, message, FromKcpChannel(channel)),
|
||||
(connectionId) => OnServerDisconnected.Invoke(connectionId),
|
||||
(connectionId, error, reason) => OnServerError.Invoke(connectionId, ToTransportError(error), reason),
|
||||
config
|
||||
);
|
||||
|
||||
if (statisticsLog)
|
||||
InvokeRepeating(nameof(OnLogStatistics), 1, 1);
|
||||
|
||||
Log.Info("KcpTransport initialized!");
|
||||
}
|
||||
|
||||
protected virtual void OnValidate()
|
||||
{
|
||||
// show max message sizes in inspector for convenience.
|
||||
// 'config' isn't available in edit mode yet, so use MTU define.
|
||||
ReliableMaxMessageSize = KcpPeer.ReliableMaxMessageSize(MTU, ReceiveWindowSize);
|
||||
UnreliableMaxMessageSize = KcpPeer.UnreliableMaxMessageSize(MTU);
|
||||
}
|
||||
|
||||
// all except WebGL
|
||||
// Do not change this back to using Application.platform
|
||||
// because that doesn't work in the Editor!
|
||||
public override bool Available() =>
|
||||
#if UNITY_WEBGL
|
||||
false;
|
||||
#else
|
||||
true;
|
||||
#endif
|
||||
|
||||
// client
|
||||
public override bool ClientConnected() => client.connected;
|
||||
public override void ClientConnect(string address)
|
||||
{
|
||||
client.Connect(address, Port);
|
||||
}
|
||||
public override void ClientConnect(Uri uri)
|
||||
{
|
||||
if (uri.Scheme != Scheme)
|
||||
throw new ArgumentException($"Invalid url {uri}, use {Scheme}://host:port instead", nameof(uri));
|
||||
|
||||
int serverPort = uri.IsDefaultPort ? Port : uri.Port;
|
||||
client.Connect(uri.Host, (ushort)serverPort);
|
||||
}
|
||||
public override void ClientSend(ArraySegment<byte> segment, int channelId)
|
||||
{
|
||||
client.Send(segment, ToKcpChannel(channelId));
|
||||
|
||||
// call event. might be null if no statistics are listening etc.
|
||||
OnClientDataSent?.Invoke(segment, channelId);
|
||||
}
|
||||
public override void ClientDisconnect() => client.Disconnect();
|
||||
// process incoming in early update
|
||||
public override void ClientEarlyUpdate()
|
||||
{
|
||||
// only process messages while transport is enabled.
|
||||
// scene change messsages disable it to stop processing.
|
||||
// (see also: https://github.com/vis2k/Mirror/pull/379)
|
||||
if (enabled) client.TickIncoming();
|
||||
}
|
||||
// process outgoing in late update
|
||||
public override void ClientLateUpdate() => client.TickOutgoing();
|
||||
|
||||
// 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)
|
||||
{
|
||||
server.Send(connectionId, segment, ToKcpChannel(channelId));
|
||||
|
||||
// call event. might be null if no statistics are listening etc.
|
||||
OnServerDataSent?.Invoke(connectionId, segment, channelId);
|
||||
}
|
||||
public override void ServerDisconnect(int connectionId) => server.Disconnect(connectionId);
|
||||
public override string ServerGetClientAddress(int connectionId)
|
||||
{
|
||||
IPEndPoint endPoint = server.GetClientEndPoint(connectionId);
|
||||
return endPoint != null
|
||||
// Map to IPv4 if "IsIPv4MappedToIPv6"
|
||||
// "::ffff:127.0.0.1" -> "127.0.0.1"
|
||||
? (endPoint.Address.IsIPv4MappedToIPv6
|
||||
? endPoint.Address.MapToIPv4().ToString()
|
||||
: endPoint.Address.ToString())
|
||||
: "";
|
||||
}
|
||||
public override void ServerStop() => server.Stop();
|
||||
public override void ServerEarlyUpdate()
|
||||
{
|
||||
// only process messages while transport is enabled.
|
||||
// scene change messsages disable it to stop processing.
|
||||
// (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 KcpPeer.UnreliableMaxMessageSize(config.Mtu);
|
||||
default:
|
||||
return KcpPeer.ReliableMaxMessageSize(config.Mtu, ReceiveWindowSize);
|
||||
}
|
||||
}
|
||||
|
||||
// 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) =>
|
||||
KcpPeer.UnreliableMaxMessageSize(config.Mtu);
|
||||
|
||||
// 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 => conn.MaxSendRate) / server.connections.Count
|
||||
: 0;
|
||||
public long GetAverageMaxReceiveRate() =>
|
||||
server.connections.Count > 0
|
||||
? server.connections.Values.Sum(conn => 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";
|
||||
}
|
||||
|
||||
protected virtual void OnGUIStatistics()
|
||||
{
|
||||
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.MaxSendRate)}/s");
|
||||
GUILayout.Label($" MaxRecvRate: {PrettyBytes(client.MaxReceiveRate)}/s");
|
||||
GUILayout.Label($" SendQueue: {client.SendQueueCount}");
|
||||
GUILayout.Label($" ReceiveQueue: {client.ReceiveQueueCount}");
|
||||
GUILayout.Label($" SendBuffer: {client.SendBufferCount}");
|
||||
GUILayout.Label($" ReceiveBuffer: {client.ReceiveBufferCount}");
|
||||
GUILayout.EndVertical();
|
||||
}
|
||||
|
||||
GUILayout.EndArea();
|
||||
}
|
||||
|
||||
// OnGUI allocates even if it does nothing. avoid in release.
|
||||
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
||||
protected virtual void OnGUI()
|
||||
{
|
||||
if (statisticsGUI) OnGUIStatistics();
|
||||
}
|
||||
#endif
|
||||
|
||||
protected virtual 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";
|
||||
Log.Info(log);
|
||||
}
|
||||
|
||||
if (ClientConnected())
|
||||
{
|
||||
string log = "kcp CLIENT @ time: " + NetworkTime.localTime + "\n";
|
||||
log += $" MaxSendRate: {PrettyBytes(client.MaxSendRate)}/s\n";
|
||||
log += $" MaxRecvRate: {PrettyBytes(client.MaxReceiveRate)}/s\n";
|
||||
log += $" SendQueue: {client.SendQueueCount}\n";
|
||||
log += $" ReceiveQueue: {client.ReceiveQueueCount}\n";
|
||||
log += $" SendBuffer: {client.SendBufferCount}\n";
|
||||
log += $" ReceiveBuffer: {client.ReceiveBufferCount}\n\n";
|
||||
Log.Info(log);
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString() => $"KCP [{port}]";
|
||||
}
|
||||
}
|
||||
//#endif MIRROR <- commented out because MIRROR isn't defined on first import yet
|
11
Assets/Mirror/Transports/KCP/KcpTransport.cs.meta
Normal file
11
Assets/Mirror/Transports/KCP/KcpTransport.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6b0fecffa3f624585964b0d0eb21b18e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
327
Assets/Mirror/Transports/KCP/ThreadedKcpTransport.cs
Normal file
327
Assets/Mirror/Transports/KCP/ThreadedKcpTransport.cs
Normal file
@ -0,0 +1,327 @@
|
||||
// Threaded version of our KCP transport.
|
||||
// Elevates a few milliseconds of transport computations into a worker thread.
|
||||
//
|
||||
//#if MIRROR <- commented out because MIRROR isn't defined on first import yet
|
||||
using System;
|
||||
using System.Net;
|
||||
using Mirror;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Serialization;
|
||||
|
||||
namespace kcp2k
|
||||
{
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/transports/kcp-transport")]
|
||||
[DisallowMultipleComponent]
|
||||
public class ThreadedKcpTransport : ThreadedTransport, PortTransport
|
||||
{
|
||||
// scheme used by this transport
|
||||
public const string Scheme = "kcp";
|
||||
|
||||
// common
|
||||
[Header("Transport Configuration")]
|
||||
[FormerlySerializedAs("Port")]
|
||||
public ushort port = 7777;
|
||||
public ushort Port { get => port; set => port=value; }
|
||||
[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;
|
||||
[Tooltip("Socket receive buffer size. Large buffer helps support more connections. Increase operating system socket buffer size limits if needed.")]
|
||||
public int RecvBufferSize = 1024 * 1027 * 7;
|
||||
[Tooltip("Socket send buffer size. Large buffer helps support more connections. Increase operating system socket buffer size limits if needed.")]
|
||||
public int SendBufferSize = 1024 * 1027 * 7;
|
||||
|
||||
[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. Restricts window size to reduce congestion. Results in only 2-3 MTU messages per Flush even on loopback. Best to keept his disabled.")]
|
||||
/*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. This also increases max message size.")]
|
||||
public uint ReceiveWindowSize = 4096; //Kcp.WND_RCV; 128 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 SendWindowSize = 4096; //Kcp.WND_SND; 32 by default. Mirror sends a lot, so we need a lot more.
|
||||
[Tooltip("KCP will try to retransmit lost messages up to MaxRetransmit (aka dead_link) before disconnecting.")]
|
||||
public uint MaxRetransmit = Kcp.DEADLINK * 2; // default prematurely disconnects a lot of people (#3022). use 2x.
|
||||
[Tooltip("Enable to automatically set client & server send/recv buffers to OS limit. Avoids issues with too small buffers under heavy load, potentially dropping connections. Increase the OS limit if this is still too small.")]
|
||||
[FormerlySerializedAs("MaximizeSendReceiveBuffersToOSLimit")]
|
||||
public bool MaximizeSocketBuffers = true;
|
||||
|
||||
[Header("Allowed Max Message Sizes\nBased on Receive Window Size")]
|
||||
[Tooltip("KCP reliable max message size shown for convenience. Can be changed via ReceiveWindowSize.")]
|
||||
[ReadOnly] public int ReliableMaxMessageSize = 0; // readonly, displayed from OnValidate
|
||||
[Tooltip("KCP unreliable channel max message size for convenience. Not changeable.")]
|
||||
[ReadOnly] public int UnreliableMaxMessageSize = 0; // readonly, displayed from OnValidate
|
||||
|
||||
// config is created from the serialized properties above.
|
||||
// we can expose the config directly in the future.
|
||||
// for now, let's not break people's old settings.
|
||||
protected KcpConfig config;
|
||||
|
||||
// use default MTU for this transport.
|
||||
const int MTU = Kcp.MTU_DEF;
|
||||
|
||||
// server & client
|
||||
KcpServer server; // USED IN WORKER THREAD. DON'T TOUCH FROM MAIN THREAD!
|
||||
KcpClient client; // USED IN WORKER THREAD. DON'T TOUCH FROM MAIN THREAD!
|
||||
|
||||
// copy MonoBehaviour.enabled for thread safe access
|
||||
volatile bool enabledCopy = true;
|
||||
|
||||
// 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;
|
||||
|
||||
protected override void Awake()
|
||||
{
|
||||
// logging
|
||||
// Log.Info should use Debug.Log if enabled, or nothing otherwise
|
||||
// (don't want to spam the console on headless servers)
|
||||
// THREAD SAFE thanks to ThreadLog.cs
|
||||
if (debugLog)
|
||||
Log.Info = Debug.Log;
|
||||
else
|
||||
Log.Info = _ => {};
|
||||
Log.Warning = Debug.LogWarning;
|
||||
Log.Error = Debug.LogError;
|
||||
|
||||
// create config from serialized settings
|
||||
config = new KcpConfig(DualMode, RecvBufferSize, SendBufferSize, MTU, NoDelay, Interval, FastResend, CongestionWindow, SendWindowSize, ReceiveWindowSize, Timeout, MaxRetransmit);
|
||||
|
||||
// client (NonAlloc version is not necessary anymore)
|
||||
client = new KcpClient(
|
||||
OnThreadedClientConnected,
|
||||
(message, channel) => OnThreadedClientReceive(message, KcpTransport.FromKcpChannel(channel)),
|
||||
OnThreadedClientDisconnected,
|
||||
(error, reason) => OnThreadedClientError(KcpTransport.ToTransportError(error), reason),
|
||||
config
|
||||
);
|
||||
|
||||
// server
|
||||
server = new KcpServer(
|
||||
OnThreadedServerConnected,
|
||||
(connectionId, message, channel) => OnThreadedServerReceive(connectionId, message, KcpTransport.FromKcpChannel(channel)),
|
||||
OnThreadedServerDisconnected,
|
||||
(connectionId, error, reason) => OnThreadedServerError(connectionId, KcpTransport.ToTransportError(error), reason),
|
||||
config
|
||||
);
|
||||
|
||||
if (statisticsLog)
|
||||
InvokeRepeating(nameof(OnLogStatistics), 1, 1);
|
||||
|
||||
// call base after creating kcp.
|
||||
// it'll be used by the created thread immediately.
|
||||
base.Awake();
|
||||
|
||||
Log.Info("ThreadedKcpTransport initialized!");
|
||||
}
|
||||
|
||||
protected virtual void OnValidate()
|
||||
{
|
||||
// show max message sizes in inspector for convenience.
|
||||
// 'config' isn't available in edit mode yet, so use MTU define.
|
||||
ReliableMaxMessageSize = KcpPeer.ReliableMaxMessageSize(MTU, ReceiveWindowSize);
|
||||
UnreliableMaxMessageSize = KcpPeer.UnreliableMaxMessageSize(MTU);
|
||||
}
|
||||
|
||||
// copy MonoBehaviour.enabled for thread safe use
|
||||
void OnEnable() => enabledCopy = true;
|
||||
void OnDisable() => enabledCopy = true;
|
||||
|
||||
// all except WebGL
|
||||
// Do not change this back to using Application.platform
|
||||
// because that doesn't work in the Editor!
|
||||
public override bool Available() =>
|
||||
#if UNITY_WEBGL
|
||||
false;
|
||||
#else
|
||||
true;
|
||||
#endif
|
||||
|
||||
protected override void ThreadedClientConnect(string address) => client.Connect(address, Port);
|
||||
protected override void ThreadedClientConnect(Uri uri)
|
||||
{
|
||||
if (uri.Scheme != Scheme)
|
||||
throw new ArgumentException($"Invalid url {uri}, use {Scheme}://host:port instead", nameof(uri));
|
||||
|
||||
int serverPort = uri.IsDefaultPort ? Port : uri.Port;
|
||||
client.Connect(uri.Host, (ushort)serverPort);
|
||||
}
|
||||
protected override void ThreadedClientSend(ArraySegment<byte> segment, int channelId)
|
||||
{
|
||||
client.Send(segment, KcpTransport.ToKcpChannel(channelId));
|
||||
|
||||
// thread safe version for statistics
|
||||
OnThreadedClientSend(segment, channelId);
|
||||
}
|
||||
protected override void ThreadedClientDisconnect() => client.Disconnect();
|
||||
// process incoming in early update
|
||||
protected override void ThreadedClientEarlyUpdate()
|
||||
{
|
||||
// only process messages while transport is enabled.
|
||||
// scene change messsages disable it to stop processing.
|
||||
// (see also: https://github.com/vis2k/Mirror/pull/379)
|
||||
// => enabledCopy for thread safe use
|
||||
if (enabledCopy) client.TickIncoming();
|
||||
}
|
||||
// process outgoing in late update
|
||||
protected override void ThreadedClientLateUpdate() => client.TickOutgoing();
|
||||
|
||||
// server thread overrides
|
||||
public override Uri ServerUri()
|
||||
{
|
||||
UriBuilder builder = new UriBuilder();
|
||||
builder.Scheme = Scheme;
|
||||
builder.Host = Dns.GetHostName();
|
||||
builder.Port = Port;
|
||||
return builder.Uri;
|
||||
}
|
||||
protected override void ThreadedServerStart() => server.Start(Port);
|
||||
protected override void ThreadedServerSend(int connectionId, ArraySegment<byte> segment, int channelId)
|
||||
{
|
||||
server.Send(connectionId, segment, KcpTransport.ToKcpChannel(channelId));
|
||||
|
||||
// thread safe version for statistics
|
||||
OnThreadedServerSend(connectionId, segment, channelId);
|
||||
}
|
||||
protected override void ThreadedServerDisconnect(int connectionId) => server.Disconnect(connectionId);
|
||||
/* NOT THREAD SAFE. ThreadedTransport version throws NotImplementedException for this.
|
||||
public override string ServerGetClientAddress(int connectionId)
|
||||
{
|
||||
IPEndPoint endPoint = server.GetClientEndPoint(connectionId);
|
||||
return endPoint != null
|
||||
// Map to IPv4 if "IsIPv4MappedToIPv6"
|
||||
// "::ffff:127.0.0.1" -> "127.0.0.1"
|
||||
? (endPoint.Address.IsIPv4MappedToIPv6
|
||||
? endPoint.Address.MapToIPv4().ToString()
|
||||
: endPoint.Address.ToString())
|
||||
: "";
|
||||
}
|
||||
*/
|
||||
protected override void ThreadedServerStop() => server.Stop();
|
||||
protected override void ThreadedServerEarlyUpdate()
|
||||
{
|
||||
// only process messages while transport is enabled.
|
||||
// scene change messsages disable it to stop processing.
|
||||
// (see also: https://github.com/vis2k/Mirror/pull/379)
|
||||
// => enabledCopy for thread safe use
|
||||
if (enabledCopy) server.TickIncoming();
|
||||
}
|
||||
// process outgoing in late update
|
||||
protected override void ThreadedServerLateUpdate() => server.TickOutgoing();
|
||||
|
||||
protected override void ThreadedShutdown() {}
|
||||
|
||||
// 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 KcpPeer.UnreliableMaxMessageSize(config.Mtu);
|
||||
default:
|
||||
return KcpPeer.ReliableMaxMessageSize(config.Mtu, ReceiveWindowSize);
|
||||
}
|
||||
}
|
||||
|
||||
// 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) =>
|
||||
KcpPeer.UnreliableMaxMessageSize(config.Mtu);
|
||||
|
||||
protected virtual void OnGUIStatistics()
|
||||
{
|
||||
// TODO not thread safe
|
||||
/*
|
||||
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): {KcpTransport.PrettyBytes(GetAverageMaxSendRate())}/s");
|
||||
GUILayout.Label($" MaxRecvRate (avg): {KcpTransport.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: {KcpTransport.PrettyBytes(client.peer.MaxSendRate)}/s");
|
||||
GUILayout.Label($" MaxRecvRate: {KcpTransport.PrettyBytes(client.peer.MaxReceiveRate)}/s");
|
||||
GUILayout.Label($" SendQueue: {client.peer.SendQueueCount}");
|
||||
GUILayout.Label($" ReceiveQueue: {client.peer.ReceiveQueueCount}");
|
||||
GUILayout.Label($" SendBuffer: {client.peer.SendBufferCount}");
|
||||
GUILayout.Label($" ReceiveBuffer: {client.peer.ReceiveBufferCount}");
|
||||
GUILayout.EndVertical();
|
||||
}
|
||||
|
||||
GUILayout.EndArea();
|
||||
*/
|
||||
}
|
||||
|
||||
// OnGUI allocates even if it does nothing. avoid in release.
|
||||
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
||||
protected virtual void OnGUI()
|
||||
{
|
||||
if (statisticsGUI) OnGUIStatistics();
|
||||
}
|
||||
#endif
|
||||
|
||||
protected virtual void OnLogStatistics()
|
||||
{
|
||||
// TODO not thread safe
|
||||
/*
|
||||
if (ServerActive())
|
||||
{
|
||||
string log = "kcp SERVER @ time: " + NetworkTime.localTime + "\n";
|
||||
log += $" connections: {server.connections.Count}\n";
|
||||
log += $" MaxSendRate (avg): {KcpTransport.PrettyBytes(GetAverageMaxSendRate())}/s\n";
|
||||
log += $" MaxRecvRate (avg): {KcpTransport.PrettyBytes(GetAverageMaxReceiveRate())}/s\n";
|
||||
log += $" SendQueue: {GetTotalSendQueue()}\n";
|
||||
log += $" ReceiveQueue: {GetTotalReceiveQueue()}\n";
|
||||
log += $" SendBuffer: {GetTotalSendBuffer()}\n";
|
||||
log += $" ReceiveBuffer: {GetTotalReceiveBuffer()}\n\n";
|
||||
Log.Info(log);
|
||||
}
|
||||
|
||||
if (ClientConnected())
|
||||
{
|
||||
string log = "kcp CLIENT @ time: " + NetworkTime.localTime + "\n";
|
||||
log += $" MaxSendRate: {KcpTransport.PrettyBytes(client.peer.MaxSendRate)}/s\n";
|
||||
log += $" MaxRecvRate: {KcpTransport.PrettyBytes(client.peer.MaxReceiveRate)}/s\n";
|
||||
log += $" SendQueue: {client.peer.SendQueueCount}\n";
|
||||
log += $" ReceiveQueue: {client.peer.ReceiveQueueCount}\n";
|
||||
log += $" SendBuffer: {client.peer.SendBufferCount}\n";
|
||||
log += $" ReceiveBuffer: {client.peer.ReceiveBufferCount}\n\n";
|
||||
Log.Info(log);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
public override string ToString() => $"ThreadedKCP {port}";
|
||||
}
|
||||
}
|
||||
//#endif MIRROR <- commented out because MIRROR isn't defined on first import yet
|
11
Assets/Mirror/Transports/KCP/ThreadedKcpTransport.cs.meta
Normal file
11
Assets/Mirror/Transports/KCP/ThreadedKcpTransport.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f7e416e0486524f0d9580be7e13388f4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
8
Assets/Mirror/Transports/KCP/kcp2k.meta
Normal file
8
Assets/Mirror/Transports/KCP/kcp2k.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 71a1c8e8c022d4731a481c1808f37e5d
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
16
Assets/Mirror/Transports/KCP/kcp2k/KCP.asmdef
Normal file
16
Assets/Mirror/Transports/KCP/kcp2k/KCP.asmdef
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "kcp2k",
|
||||
"rootNamespace": "",
|
||||
"references": [
|
||||
"GUID:30817c1a0e6d646d99c048fc403f5979"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": true,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
7
Assets/Mirror/Transports/KCP/kcp2k/KCP.asmdef.meta
Normal file
7
Assets/Mirror/Transports/KCP/kcp2k/KCP.asmdef.meta
Normal file
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6806a62c384838046a3c66c44f06d75f
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
24
Assets/Mirror/Transports/KCP/kcp2k/LICENSE.txt
Normal file
24
Assets/Mirror/Transports/KCP/kcp2k/LICENSE.txt
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/Transports/KCP/kcp2k/LICENSE.txt.meta
Normal file
7
Assets/Mirror/Transports/KCP/kcp2k/LICENSE.txt.meta
Normal file
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9a3e8369060cf4e94ac117603de47aa6
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
256
Assets/Mirror/Transports/KCP/kcp2k/VERSION.txt
Normal file
256
Assets/Mirror/Transports/KCP/kcp2k/VERSION.txt
Normal file
@ -0,0 +1,256 @@
|
||||
V1.40 [2024-01-03]
|
||||
- added [KCP] to all log messages
|
||||
- fix: #3704 remove old fix for #2353 which caused log spam and isn't needed anymore since the
|
||||
original Mirror issue is long gone
|
||||
- fix: KcpClient.RawSend now returns if socket wasn't created yet
|
||||
- fix: https://github.com/MirrorNetworking/Mirror/issues/3591 KcpPeer.SendDisconnect now rapid
|
||||
fires several unreliable messages instead of sending reliable. Fixes disconnect message not
|
||||
going through if the connection is closed & removed immediately after.
|
||||
|
||||
V1.39 [2023-10-31]
|
||||
- fix: https://github.com/MirrorNetworking/Mirror/issues/3611 Windows UDP socket exceptions
|
||||
on server if one of the clients died
|
||||
|
||||
V1.38 [2023-10-29]
|
||||
- fix: #54 mismatching cookie race condition. cookie is now included in all messages.
|
||||
- feature: Exposed local end point on KcpClient/Server
|
||||
- refactor: KcpPeer refactored as abstract class to remove KcpServer initialization workarounds
|
||||
|
||||
V1.37 [2023-07-31]
|
||||
- fix: #47 KcpServer.Stop now clears connections so they aren't carried over to the next session
|
||||
- fix: KcpPeer doesn't log 'received unreliable message while not authenticated' anymore.
|
||||
|
||||
V1.36 [2023-06-08]
|
||||
- fix: #49 KcpPeer.RawInput message size check now considers cookie as well
|
||||
- kcp.cs cleanups
|
||||
|
||||
V1.35 [2023-04-05]
|
||||
- fix: KcpClients now need to validate with a secure cookie in order to protect against
|
||||
UDP spoofing. fixes:
|
||||
https://github.com/MirrorNetworking/Mirror/issues/3286
|
||||
[disclosed by IncludeSec]
|
||||
- KcpClient/Server: change callbacks to protected so inheriting classes can use them too
|
||||
- KcpClient/Server: change config visibility to protected
|
||||
|
||||
V1.34 [2023-03-15]
|
||||
- Send/SendTo/Receive/ReceiveFrom NonBlocking extensions.
|
||||
to encapsulate WouldBlock allocations, exceptions, etc.
|
||||
allows for reuse when overwriting KcpServer/Client (i.e. for relays).
|
||||
|
||||
V1.33 [2023-03-14]
|
||||
- perf: KcpServer/Client RawReceive now call socket.Poll to avoid non-blocking
|
||||
socket's allocating a new SocketException in case they WouldBlock.
|
||||
fixes https://github.com/MirrorNetworking/Mirror/issues/3413
|
||||
- perf: KcpServer/Client RawSend now call socket.Poll to avoid non-blocking
|
||||
socket's allocating a new SocketException in case they WouldBlock.
|
||||
fixes https://github.com/MirrorNetworking/Mirror/issues/3413
|
||||
|
||||
V1.32 [2023-03-12]
|
||||
- fix: KcpPeer RawInput now doesn't disconnect in case of random internet noise
|
||||
|
||||
V1.31 [2023-03-05]
|
||||
- KcpClient: Tick/Incoming/Outgoing can now be overwritten (virtual)
|
||||
- breaking: KcpClient now takes KcpConfig in constructor instead of in Connect.
|
||||
cleaner, and prepares for KcpConfig.MTU setting.
|
||||
- KcpConfig now includes MTU; KcpPeer now works with KcpConfig's MTU, KcpServer/Client
|
||||
buffers are now created with config's MTU.
|
||||
|
||||
V1.30 [2023-02-20]
|
||||
- fix: set send/recv buffer sizes directly instead of iterating to find the limit.
|
||||
fixes: https://github.com/MirrorNetworking/Mirror/issues/3390
|
||||
- fix: server & client sockets are now always non-blocking to ensure main thread never
|
||||
blocks on socket.recv/send. Send() now also handles WouldBlock.
|
||||
- fix: socket.Receive/From directly with non-blocking sockets and handle WouldBlock,
|
||||
instead of socket.Poll. faster, more obvious, and fixes Poll() looping forever while
|
||||
socket is in error state. fixes: https://github.com/MirrorNetworking/Mirror/issues/2733
|
||||
|
||||
V1.29 [2023-01-28]
|
||||
- fix: KcpServer.CreateServerSocket now handles NotSupportedException when setting DualMode
|
||||
https://github.com/MirrorNetworking/Mirror/issues/3358
|
||||
|
||||
V1.28 [2023-01-28]
|
||||
- fix: KcpClient.Connect now resolves hostname before creating peer
|
||||
https://github.com/MirrorNetworking/Mirror/issues/3361
|
||||
|
||||
V1.27 [2023-01-08]
|
||||
- KcpClient.Connect: invoke own events directly instead of going through peer,
|
||||
which calls our own events anyway
|
||||
- fix: KcpPeer/Client/Server callbacks are readonly and assigned in constructor
|
||||
to ensure they are safe to use at all times.
|
||||
fixes https://github.com/MirrorNetworking/Mirror/issues/3337
|
||||
|
||||
V1.26 [2022-12-22]
|
||||
- KcpPeer.RawInput: fix compile error in old Unity Mono versions
|
||||
- fix: KcpServer sets up a new connection's OnError immediately.
|
||||
fixes KcpPeer throwing NullReferenceException when attempting to call OnError
|
||||
after authentication errors.
|
||||
- improved log messages
|
||||
|
||||
V1.25 [2022-12-14]
|
||||
- breaking: removed where-allocation. use IL2CPP on servers instead.
|
||||
- breaking: KcpConfig to simplify configuration
|
||||
- high level cleanups
|
||||
|
||||
V1.24 [2022-12-14]
|
||||
- KcpClient: fixed NullReferenceException when connection without a server.
|
||||
added test coverage to ensure this never happens again.
|
||||
|
||||
V1.23 [2022-12-07]
|
||||
- KcpClient: rawReceiveBuffer exposed
|
||||
- fix: KcpServer RawSend uses connection.remoteEndPoint instead of the helper
|
||||
'newClientEP'. fixes clients receiving the wrong messages meant for others.
|
||||
https://github.com/MirrorNetworking/Mirror/issues/3296
|
||||
|
||||
V1.22 [2022-11-30]
|
||||
- high level refactor, part two.
|
||||
|
||||
V1.21 [2022-11-24]
|
||||
- high level refactor, part one.
|
||||
- KcpPeer instead of KcpConnection, KcpClientConnection, KcpServerConnection
|
||||
- RawSend/Receive can now easily be overwritten in KcpClient/Server.
|
||||
for non-alloc, relays, etc.
|
||||
|
||||
V1.20 [2022-11-22]
|
||||
- perf: KcpClient receive allocation was removed entirely.
|
||||
reduces Mirror benchmark client sided allocations from 4.9 KB / 1.7 KB (non-alloc) to 0B.
|
||||
- fix: KcpConnection.Disconnect does not check socket.Connected anymore.
|
||||
UDP sockets don't have a connection.
|
||||
fixes Disconnects not being sent to clients in netcore.
|
||||
- KcpConnection.SendReliable: added OnError instead of logs
|
||||
|
||||
V1.19 [2022-05-12]
|
||||
- feature: OnError ErrorCodes
|
||||
|
||||
V1.18 [2022-05-08]
|
||||
- feature: OnError to allow higher level to show popups etc.
|
||||
- feature: KcpServer.GetClientAddress is now GetClientEndPoint in order to
|
||||
expose more details
|
||||
- ResolveHostname: include exception in log for easier debugging
|
||||
- fix: KcpClientConnection.RawReceive now logs the SocketException even if
|
||||
it was expected. makes debugging easier.
|
||||
- fix: KcpServer.TickIncoming now logs the SocketException even if it was
|
||||
expected. makes debugging easier.
|
||||
- fix: KcpClientConnection.RawReceive now calls Disconnect() if the other end
|
||||
has closed the connection. better than just remaining in a state with unusable
|
||||
sockets.
|
||||
|
||||
V1.17 [2022-01-09]
|
||||
- perf: server/client MaximizeSendReceiveBuffersToOSLimit option to set send/recv
|
||||
buffer sizes to OS limit. avoids drops due to small buffers under heavy load.
|
||||
|
||||
V1.16 [2022-01-06]
|
||||
- fix: SendUnreliable respects ArraySegment.Offset
|
||||
- fix: potential bug with negative length (see PR #2)
|
||||
- breaking: removed pause handling because it's not necessary for Mirror anymore
|
||||
|
||||
V1.15 [2021-12-11]
|
||||
- feature: feature: MaxRetransmits aka dead_link now configurable
|
||||
- dead_link disconnect message improved to show exact retransmit count
|
||||
|
||||
V1.14 [2021-11-30]
|
||||
- fix: Send() now throws an exception for messages which require > 255 fragments
|
||||
- fix: ReliableMaxMessageSize is now limited to messages which require <= 255 fragments
|
||||
|
||||
V1.13 [2021-11-28]
|
||||
- fix: perf: uncork max message size from 144 KB to as much as we want based on
|
||||
receive window size.
|
||||
fixes https://github.com/vis2k/kcp2k/issues/22
|
||||
fixes https://github.com/skywind3000/kcp/pull/291
|
||||
- feature: OnData now includes channel it was received on
|
||||
|
||||
V1.12 [2021-07-16]
|
||||
- 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.
|
||||
- fix: where-allocation made optional via virtuals and inheriting
|
||||
KcpServer/Client/Connection NonAlloc classes. fixes a bug where some platforms
|
||||
might not support where-allocation.
|
||||
|
||||
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/Transports/KCP/kcp2k/VERSION.txt.meta
Normal file
7
Assets/Mirror/Transports/KCP/kcp2k/VERSION.txt.meta
Normal file
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ed3f2cf1bbf1b4d53a6f2c103d311f71
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
3
Assets/Mirror/Transports/KCP/kcp2k/empty.meta
Normal file
3
Assets/Mirror/Transports/KCP/kcp2k/empty.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d9ce2267cb8a4a1c9632025287e8da88
|
||||
timeCreated: 1669162433
|
@ -0,0 +1 @@
|
||||
// removed 2022-12-13
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 54b8398dcd544c8a93bcad846214cc40
|
||||
timeCreated: 1626432191
|
8
Assets/Mirror/Transports/KCP/kcp2k/highlevel.meta
Normal file
8
Assets/Mirror/Transports/KCP/kcp2k/highlevel.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5a54d18b954cb4407a28b633fc32ea6d
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
75
Assets/Mirror/Transports/KCP/kcp2k/highlevel/Common.cs
Normal file
75
Assets/Mirror/Transports/KCP/kcp2k/highlevel/Common.cs
Normal file
@ -0,0 +1,75 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace kcp2k
|
||||
{
|
||||
public static class Common
|
||||
{
|
||||
// helper function to resolve host to IPAddress
|
||||
public static bool ResolveHostname(string hostname, out IPAddress[] addresses)
|
||||
{
|
||||
try
|
||||
{
|
||||
// NOTE: dns lookup is blocking. this can take a second.
|
||||
addresses = Dns.GetHostAddresses(hostname);
|
||||
return addresses.Length >= 1;
|
||||
}
|
||||
catch (SocketException exception)
|
||||
{
|
||||
Log.Info($"[KCP] Failed to resolve host: {hostname} reason: {exception}");
|
||||
addresses = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// if connections drop under heavy load, increase to OS limit.
|
||||
// if still not enough, increase the OS limit.
|
||||
public static void ConfigureSocketBuffers(Socket socket, int recvBufferSize, int sendBufferSize)
|
||||
{
|
||||
// log initial size for comparison.
|
||||
// remember initial size for log comparison
|
||||
int initialReceive = socket.ReceiveBufferSize;
|
||||
int initialSend = socket.SendBufferSize;
|
||||
|
||||
// set to configured size
|
||||
try
|
||||
{
|
||||
socket.ReceiveBufferSize = recvBufferSize;
|
||||
socket.SendBufferSize = sendBufferSize;
|
||||
}
|
||||
catch (SocketException)
|
||||
{
|
||||
Log.Warning($"[KCP] failed to set Socket RecvBufSize = {recvBufferSize} SendBufSize = {sendBufferSize}");
|
||||
}
|
||||
|
||||
|
||||
Log.Info($"[KCP] RecvBuf = {initialReceive}=>{socket.ReceiveBufferSize} ({socket.ReceiveBufferSize/initialReceive}x) SendBuf = {initialSend}=>{socket.SendBufferSize} ({socket.SendBufferSize/initialSend}x)");
|
||||
}
|
||||
|
||||
// generate a connection hash from IP+Port.
|
||||
//
|
||||
// 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.
|
||||
public static int ConnectionHash(EndPoint endPoint) =>
|
||||
endPoint.GetHashCode();
|
||||
|
||||
// cookies need to be generated with a secure random generator.
|
||||
// we don't want them to be deterministic / predictable.
|
||||
// RNG is cached to avoid runtime allocations.
|
||||
static readonly RNGCryptoServiceProvider cryptoRandom = new RNGCryptoServiceProvider();
|
||||
static readonly byte[] cryptoRandomBuffer = new byte[4];
|
||||
public static uint GenerateCookie()
|
||||
{
|
||||
cryptoRandom.GetBytes(cryptoRandomBuffer);
|
||||
return BitConverter.ToUInt32(cryptoRandomBuffer, 0);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9ced451c2954435f88cf718bcba020cb
|
||||
timeCreated: 1669135138
|
15
Assets/Mirror/Transports/KCP/kcp2k/highlevel/ErrorCode.cs
Normal file
15
Assets/Mirror/Transports/KCP/kcp2k/highlevel/ErrorCode.cs
Normal file
@ -0,0 +1,15 @@
|
||||
// kcp specific error codes to allow for error switching, localization,
|
||||
// translation to Mirror errors, etc.
|
||||
namespace kcp2k
|
||||
{
|
||||
public enum ErrorCode : byte
|
||||
{
|
||||
DnsResolve, // failed to resolve a host name
|
||||
Timeout, // ping timeout or dead link
|
||||
Congestion, // more messages than transport / network can process
|
||||
InvalidReceive, // recv invalid packet (possibly intentional attack)
|
||||
InvalidSend, // user tried to send invalid data
|
||||
ConnectionClosed, // connection closed voluntarily or lost involuntarily
|
||||
Unexpected // unexpected error / exception, requires fix.
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3abbeffc1d794f11a45b7fcf110353f5
|
||||
timeCreated: 1652320712
|
166
Assets/Mirror/Transports/KCP/kcp2k/highlevel/Extensions.cs
Normal file
166
Assets/Mirror/Transports/KCP/kcp2k/highlevel/Extensions.cs
Normal file
@ -0,0 +1,166 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace kcp2k
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
// ArraySegment as HexString for convenience
|
||||
public static string ToHexString(this ArraySegment<byte> segment) =>
|
||||
BitConverter.ToString(segment.Array, segment.Offset, segment.Count);
|
||||
|
||||
// non-blocking UDP send.
|
||||
// allows for reuse when overwriting KcpServer/Client (i.e. for relays).
|
||||
// => wrapped with Poll to avoid WouldBlock allocating new SocketException.
|
||||
// => wrapped with try-catch to ignore WouldBlock exception.
|
||||
// make sure to set socket.Blocking = false before using this!
|
||||
public static bool SendToNonBlocking(this Socket socket, ArraySegment<byte> data, EndPoint remoteEP)
|
||||
{
|
||||
try
|
||||
{
|
||||
// when using non-blocking sockets, SendTo may return WouldBlock.
|
||||
// in C#, WouldBlock throws a SocketException, which is expected.
|
||||
// unfortunately, creating the SocketException allocates in C#.
|
||||
// let's poll first to avoid the WouldBlock allocation.
|
||||
// note that this entirely to avoid allocations.
|
||||
// non-blocking UDP doesn't need Poll in other languages.
|
||||
// and the code still works without the Poll call.
|
||||
if (!socket.Poll(0, SelectMode.SelectWrite)) return false;
|
||||
|
||||
// send to the the endpoint.
|
||||
// do not send to 'newClientEP', as that's always reused.
|
||||
// fixes https://github.com/MirrorNetworking/Mirror/issues/3296
|
||||
socket.SendTo(data.Array, data.Offset, data.Count, SocketFlags.None, remoteEP);
|
||||
return true;
|
||||
}
|
||||
catch (SocketException e)
|
||||
{
|
||||
// for non-blocking sockets, SendTo may throw WouldBlock.
|
||||
// in that case, simply drop the message. it's UDP, it's fine.
|
||||
if (e.SocketErrorCode == SocketError.WouldBlock) return false;
|
||||
|
||||
// otherwise it's a real socket error. throw it.
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// non-blocking UDP send.
|
||||
// allows for reuse when overwriting KcpServer/Client (i.e. for relays).
|
||||
// => wrapped with Poll to avoid WouldBlock allocating new SocketException.
|
||||
// => wrapped with try-catch to ignore WouldBlock exception.
|
||||
// make sure to set socket.Blocking = false before using this!
|
||||
public static bool SendNonBlocking(this Socket socket, ArraySegment<byte> data)
|
||||
{
|
||||
try
|
||||
{
|
||||
// when using non-blocking sockets, SendTo may return WouldBlock.
|
||||
// in C#, WouldBlock throws a SocketException, which is expected.
|
||||
// unfortunately, creating the SocketException allocates in C#.
|
||||
// let's poll first to avoid the WouldBlock allocation.
|
||||
// note that this entirely to avoid allocations.
|
||||
// non-blocking UDP doesn't need Poll in other languages.
|
||||
// and the code still works without the Poll call.
|
||||
if (!socket.Poll(0, SelectMode.SelectWrite)) return false;
|
||||
|
||||
// SendTo allocates. we used bound Send.
|
||||
socket.Send(data.Array, data.Offset, data.Count, SocketFlags.None);
|
||||
return true;
|
||||
}
|
||||
catch (SocketException e)
|
||||
{
|
||||
// for non-blocking sockets, SendTo may throw WouldBlock.
|
||||
// in that case, simply drop the message. it's UDP, it's fine.
|
||||
if (e.SocketErrorCode == SocketError.WouldBlock) return false;
|
||||
|
||||
// otherwise it's a real socket error. throw it.
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// non-blocking UDP receive.
|
||||
// allows for reuse when overwriting KcpServer/Client (i.e. for relays).
|
||||
// => wrapped with Poll to avoid WouldBlock allocating new SocketException.
|
||||
// => wrapped with try-catch to ignore WouldBlock exception.
|
||||
// make sure to set socket.Blocking = false before using this!
|
||||
public static bool ReceiveFromNonBlocking(this Socket socket, byte[] recvBuffer, out ArraySegment<byte> data, ref EndPoint remoteEP)
|
||||
{
|
||||
data = default;
|
||||
|
||||
try
|
||||
{
|
||||
// when using non-blocking sockets, ReceiveFrom may return WouldBlock.
|
||||
// in C#, WouldBlock throws a SocketException, which is expected.
|
||||
// unfortunately, creating the SocketException allocates in C#.
|
||||
// let's poll first to avoid the WouldBlock allocation.
|
||||
// note that this entirely to avoid allocations.
|
||||
// non-blocking UDP doesn't need Poll in other languages.
|
||||
// and the code still works without the Poll call.
|
||||
if (!socket.Poll(0, SelectMode.SelectRead)) return false;
|
||||
|
||||
// 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
|
||||
//
|
||||
// throws SocketException if datagram was larger than buffer.
|
||||
// https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.socket.receive?view=net-6.0
|
||||
int size = socket.ReceiveFrom(recvBuffer, 0, recvBuffer.Length, SocketFlags.None, ref remoteEP);
|
||||
data = new ArraySegment<byte>(recvBuffer, 0, size);
|
||||
return true;
|
||||
}
|
||||
catch (SocketException e)
|
||||
{
|
||||
// for non-blocking sockets, Receive throws WouldBlock if there is
|
||||
// no message to read. that's okay. only log for other errors.
|
||||
if (e.SocketErrorCode == SocketError.WouldBlock) return false;
|
||||
|
||||
// otherwise it's a real socket error. throw it.
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// non-blocking UDP receive.
|
||||
// allows for reuse when overwriting KcpServer/Client (i.e. for relays).
|
||||
// => wrapped with Poll to avoid WouldBlock allocating new SocketException.
|
||||
// => wrapped with try-catch to ignore WouldBlock exception.
|
||||
// make sure to set socket.Blocking = false before using this!
|
||||
public static bool ReceiveNonBlocking(this Socket socket, byte[] recvBuffer, out ArraySegment<byte> data)
|
||||
{
|
||||
data = default;
|
||||
|
||||
try
|
||||
{
|
||||
// when using non-blocking sockets, ReceiveFrom may return WouldBlock.
|
||||
// in C#, WouldBlock throws a SocketException, which is expected.
|
||||
// unfortunately, creating the SocketException allocates in C#.
|
||||
// let's poll first to avoid the WouldBlock allocation.
|
||||
// note that this entirely to avoid allocations.
|
||||
// non-blocking UDP doesn't need Poll in other languages.
|
||||
// and the code still works without the Poll call.
|
||||
if (!socket.Poll(0, SelectMode.SelectRead)) return false;
|
||||
|
||||
// ReceiveFrom allocates. we used bound Receive.
|
||||
// returns amount of bytes written into buffer.
|
||||
// throws SocketException if datagram was larger than buffer.
|
||||
// https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.socket.receive?view=net-6.0
|
||||
//
|
||||
// throws SocketException if datagram was larger than buffer.
|
||||
// https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.socket.receive?view=net-6.0
|
||||
int size = socket.Receive(recvBuffer, 0, recvBuffer.Length, SocketFlags.None);
|
||||
data = new ArraySegment<byte>(recvBuffer, 0, size);
|
||||
return true;
|
||||
}
|
||||
catch (SocketException e)
|
||||
{
|
||||
// for non-blocking sockets, Receive throws WouldBlock if there is
|
||||
// no message to read. that's okay. only log for other errors.
|
||||
if (e.SocketErrorCode == SocketError.WouldBlock) return false;
|
||||
|
||||
// otherwise it's a real socket error. throw it.
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c0649195e5ba4fcf8e0e1231fee7d5f6
|
||||
timeCreated: 1641701011
|
10
Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpChannel.cs
Normal file
10
Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpChannel.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace kcp2k
|
||||
{
|
||||
// channel type and header for raw messages
|
||||
public enum KcpChannel : byte
|
||||
{
|
||||
// don't react on 0x00. might help to filter out random noise.
|
||||
Reliable = 1,
|
||||
Unreliable = 2
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9e852b2532fb248d19715cfebe371db3
|
||||
timeCreated: 1610081248
|
291
Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpClient.cs
Normal file
291
Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpClient.cs
Normal file
@ -0,0 +1,291 @@
|
||||
// kcp client logic abstracted into a class.
|
||||
// for use in Mirror, DOTSNET, testing, etc.
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace kcp2k
|
||||
{
|
||||
public class KcpClient : KcpPeer
|
||||
{
|
||||
// IO
|
||||
protected Socket socket;
|
||||
public EndPoint remoteEndPoint;
|
||||
|
||||
// expose local endpoint for users / relays / nat traversal etc.
|
||||
public EndPoint LocalEndPoint => socket?.LocalEndPoint;
|
||||
|
||||
// config
|
||||
protected readonly KcpConfig config;
|
||||
|
||||
// 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!
|
||||
// => protected because someone may overwrite RawReceive but still wants
|
||||
// to reuse the buffer.
|
||||
protected readonly byte[] rawReceiveBuffer;
|
||||
|
||||
// callbacks
|
||||
// even for errors, to allow liraries to show popups etc.
|
||||
// instead of logging directly.
|
||||
// (string instead of Exception for ease of use and to avoid user panic)
|
||||
//
|
||||
// events are readonly, set in constructor.
|
||||
// this ensures they are always initialized when used.
|
||||
// fixes https://github.com/MirrorNetworking/Mirror/issues/3337 and more
|
||||
protected readonly Action OnConnectedCallback;
|
||||
protected readonly Action<ArraySegment<byte>, KcpChannel> OnDataCallback;
|
||||
protected readonly Action OnDisconnectedCallback;
|
||||
protected readonly Action<ErrorCode, string> OnErrorCallback;
|
||||
|
||||
// state
|
||||
bool active = false; // active between when connect() and disconnect() are called
|
||||
public bool connected;
|
||||
|
||||
public KcpClient(Action OnConnected,
|
||||
Action<ArraySegment<byte>, KcpChannel> OnData,
|
||||
Action OnDisconnected,
|
||||
Action<ErrorCode, string> OnError,
|
||||
KcpConfig config)
|
||||
: base(config, 0) // client has no cookie yet
|
||||
{
|
||||
// initialize callbacks first to ensure they can be used safely.
|
||||
OnConnectedCallback = OnConnected;
|
||||
OnDataCallback = OnData;
|
||||
OnDisconnectedCallback = OnDisconnected;
|
||||
OnErrorCallback = OnError;
|
||||
this.config = config;
|
||||
|
||||
// create mtu sized receive buffer
|
||||
rawReceiveBuffer = new byte[config.Mtu];
|
||||
}
|
||||
|
||||
// callbacks ///////////////////////////////////////////////////////////
|
||||
// some callbacks need to wrapped with some extra logic
|
||||
protected override void OnAuthenticated()
|
||||
{
|
||||
Log.Info($"[KCP] Client: OnConnected");
|
||||
connected = true;
|
||||
OnConnectedCallback();
|
||||
}
|
||||
|
||||
protected override void OnData(ArraySegment<byte> message, KcpChannel channel) =>
|
||||
OnDataCallback(message, channel);
|
||||
|
||||
protected override void OnError(ErrorCode error, string message) =>
|
||||
OnErrorCallback(error, message);
|
||||
|
||||
protected override void OnDisconnected()
|
||||
{
|
||||
Log.Info($"[KCP] Client: OnDisconnected");
|
||||
connected = false;
|
||||
socket?.Close();
|
||||
socket = null;
|
||||
remoteEndPoint = null;
|
||||
OnDisconnectedCallback();
|
||||
active = false;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
public void Connect(string address, ushort port)
|
||||
{
|
||||
if (connected)
|
||||
{
|
||||
Log.Warning("[KCP] Client: already connected!");
|
||||
return;
|
||||
}
|
||||
|
||||
// resolve host name before creating peer.
|
||||
// fixes: https://github.com/MirrorNetworking/Mirror/issues/3361
|
||||
if (!Common.ResolveHostname(address, out IPAddress[] addresses))
|
||||
{
|
||||
// pass error to user callback. no need to log it manually.
|
||||
OnError(ErrorCode.DnsResolve, $"Failed to resolve host: {address}");
|
||||
OnDisconnectedCallback();
|
||||
return;
|
||||
}
|
||||
|
||||
// create fresh peer for each new session
|
||||
// client doesn't need secure cookie.
|
||||
Reset(config);
|
||||
|
||||
Log.Info($"[KCP] Client: connect to {address}:{port}");
|
||||
|
||||
// create socket
|
||||
remoteEndPoint = new IPEndPoint(addresses[0], port);
|
||||
socket = new Socket(remoteEndPoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp);
|
||||
active = true;
|
||||
|
||||
// recv & send are called from main thread.
|
||||
// need to ensure this never blocks.
|
||||
// even a 1ms block per connection would stop us from scaling.
|
||||
socket.Blocking = false;
|
||||
|
||||
// configure buffer sizes
|
||||
Common.ConfigureSocketBuffers(socket, config.RecvBufferSize, config.SendBufferSize);
|
||||
|
||||
// bind to endpoint so we can use send/recv instead of sendto/recvfrom.
|
||||
socket.Connect(remoteEndPoint);
|
||||
|
||||
// immediately send a hello message to the server.
|
||||
// server will call OnMessage and add the new connection.
|
||||
// note that this still has cookie=0 until we receive the server's hello.
|
||||
SendHello();
|
||||
}
|
||||
|
||||
// io - input.
|
||||
// virtual so it may be modified for relays, etc.
|
||||
// call this while it returns true, to process all messages this tick.
|
||||
// returned ArraySegment is valid until next call to RawReceive.
|
||||
protected virtual bool RawReceive(out ArraySegment<byte> segment)
|
||||
{
|
||||
segment = default;
|
||||
if (socket == null) return false;
|
||||
|
||||
try
|
||||
{
|
||||
return socket.ReceiveNonBlocking(rawReceiveBuffer, out segment);
|
||||
}
|
||||
// for non-blocking sockets, Receive throws WouldBlock if there is
|
||||
// no message to read. that's okay. only log for other errors.
|
||||
catch (SocketException e)
|
||||
{
|
||||
// the other end closing the connection is not an 'error'.
|
||||
// but connections should never just end silently.
|
||||
// at least log a message for easier debugging.
|
||||
// for example, his can happen when connecting without a server.
|
||||
// see test: ConnectWithoutServer().
|
||||
Log.Info($"[KCP] Client.RawReceive: looks like the other end has closed the connection. This is fine: {e}");
|
||||
base.Disconnect();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// io - output.
|
||||
// virtual so it may be modified for relays, etc.
|
||||
protected override void RawSend(ArraySegment<byte> data)
|
||||
{
|
||||
// only if socket was connected / created yet.
|
||||
// users may call send functions without having connected, causing NRE.
|
||||
if (socket == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
socket.SendNonBlocking(data);
|
||||
}
|
||||
catch (SocketException e)
|
||||
{
|
||||
// SendDisconnect() sometimes gets a SocketException with
|
||||
// 'Connection Refused' if the other end already closed.
|
||||
// this is not an 'error', it's expected to happen.
|
||||
// but connections should never just end silently.
|
||||
// at least log a message for easier debugging.
|
||||
Log.Info($"[KCP] Client.RawSend: looks like the other end has closed the connection. This is fine: {e}");
|
||||
// base.Disconnect(); <- don't call this, would deadlock if SendDisconnect() already throws
|
||||
}
|
||||
}
|
||||
|
||||
public void Send(ArraySegment<byte> segment, KcpChannel channel)
|
||||
{
|
||||
if (!connected)
|
||||
{
|
||||
Log.Warning("[KCP] Client: can't send because not connected!");
|
||||
return;
|
||||
}
|
||||
|
||||
SendData(segment, channel);
|
||||
}
|
||||
|
||||
// insert raw IO. usually from socket.Receive.
|
||||
// offset is useful for relays, where we may parse a header and then
|
||||
// feed the rest to kcp.
|
||||
public void RawInput(ArraySegment<byte> segment)
|
||||
{
|
||||
// ensure valid size: at least 1 byte for channel + 4 bytes for cookie
|
||||
if (segment.Count <= 5) return;
|
||||
|
||||
// parse channel
|
||||
// byte channel = segment[0]; ArraySegment[i] isn't supported in some older Unity Mono versions
|
||||
byte channel = segment.Array[segment.Offset + 0];
|
||||
|
||||
// server messages always contain the security cookie.
|
||||
// parse it, assign if not assigned, warn if suddenly different.
|
||||
Utils.Decode32U(segment.Array, segment.Offset + 1, out uint messageCookie);
|
||||
if (messageCookie == 0)
|
||||
{
|
||||
Log.Error($"[KCP] Client: received message with cookie=0, this should never happen. Server should always include the security cookie.");
|
||||
}
|
||||
|
||||
if (cookie == 0)
|
||||
{
|
||||
cookie = messageCookie;
|
||||
Log.Info($"[KCP] Client: received initial cookie: {cookie}");
|
||||
}
|
||||
else if (cookie != messageCookie)
|
||||
{
|
||||
Log.Warning($"[KCP] Client: dropping message with mismatching cookie: {messageCookie} expected: {cookie}.");
|
||||
return;
|
||||
}
|
||||
|
||||
// parse message
|
||||
ArraySegment<byte> message = new ArraySegment<byte>(segment.Array, segment.Offset + 1+4, segment.Count - 1-4);
|
||||
|
||||
switch (channel)
|
||||
{
|
||||
case (byte)KcpChannel.Reliable:
|
||||
{
|
||||
OnRawInputReliable(message);
|
||||
break;
|
||||
}
|
||||
case (byte)KcpChannel.Unreliable:
|
||||
{
|
||||
OnRawInputUnreliable(message);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
// invalid channel indicates random internet noise.
|
||||
// servers may receive random UDP data.
|
||||
// just ignore it, but log for easier debugging.
|
||||
Log.Warning($"[KCP] Client: invalid channel header: {channel}, likely internet noise");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// process incoming messages. should be called before updating the world.
|
||||
// virtual because relay may need to inject their own ping or similar.
|
||||
public override 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)
|
||||
if (active)
|
||||
{
|
||||
while (RawReceive(out ArraySegment<byte> segment))
|
||||
RawInput(segment);
|
||||
}
|
||||
|
||||
// RawReceive may have disconnected peer. active check again.
|
||||
if (active) base.TickIncoming();
|
||||
}
|
||||
|
||||
// process outgoing messages. should be called after updating the world.
|
||||
// virtual because relay may need to inject their own ping or similar.
|
||||
public override void TickOutgoing()
|
||||
{
|
||||
// process outgoing while active
|
||||
if (active) base.TickOutgoing();
|
||||
}
|
||||
|
||||
// process incoming and outgoing for convenience
|
||||
// => ideally call ProcessIncoming() before updating the world and
|
||||
// ProcessOutgoing() after updating the world for minimum latency
|
||||
public virtual void Tick()
|
||||
{
|
||||
TickIncoming();
|
||||
TickOutgoing();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6aa069a28ed24fedb533c102d9742b36
|
||||
timeCreated: 1603786960
|
97
Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpConfig.cs
Normal file
97
Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpConfig.cs
Normal file
@ -0,0 +1,97 @@
|
||||
// common config struct, instead of passing 10 parameters manually every time.
|
||||
using System;
|
||||
|
||||
namespace kcp2k
|
||||
{
|
||||
// [Serializable] to show it in Unity inspector.
|
||||
// 'class' so we can set defaults easily.
|
||||
[Serializable]
|
||||
public class KcpConfig
|
||||
{
|
||||
// socket configuration ////////////////////////////////////////////////
|
||||
// DualMode uses both IPv6 and IPv4. not all platforms support it.
|
||||
// (Nintendo Switch, etc.)
|
||||
public bool DualMode;
|
||||
|
||||
// UDP servers use only one socket.
|
||||
// maximize buffer to handle as many connections as possible.
|
||||
//
|
||||
// M1 mac pro:
|
||||
// recv buffer default: 786896 (771 KB)
|
||||
// send buffer default: 9216 (9 KB)
|
||||
// max configurable: ~7 MB
|
||||
public int RecvBufferSize;
|
||||
public int SendBufferSize;
|
||||
|
||||
// kcp configuration ///////////////////////////////////////////////////
|
||||
// configurable MTU in case kcp sits on top of other abstractions like
|
||||
// encrypted transports, relays, etc.
|
||||
public int Mtu;
|
||||
|
||||
// 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 congestion window heavily limits messages flushed per update.
|
||||
// congestion window may actually be broken in kcp:
|
||||
// - sending max sized message @ M1 mac flushes 2-3 messages per update
|
||||
// - even with super large send/recv window, it requires thousands of
|
||||
// update calls
|
||||
// best to leave this disabled, as it may significantly increase latency.
|
||||
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;
|
||||
|
||||
// maximum retransmission attempts until dead_link
|
||||
public uint MaxRetransmits;
|
||||
|
||||
// constructor /////////////////////////////////////////////////////////
|
||||
// constructor with defaults for convenience.
|
||||
// makes it easy to define "new KcpConfig(DualMode=false)" etc.
|
||||
public KcpConfig(
|
||||
bool DualMode = true,
|
||||
int RecvBufferSize = 1024 * 1024 * 7,
|
||||
int SendBufferSize = 1024 * 1024 * 7,
|
||||
int Mtu = Kcp.MTU_DEF,
|
||||
bool NoDelay = true,
|
||||
uint Interval = 10,
|
||||
int FastResend = 0,
|
||||
bool CongestionWindow = false,
|
||||
uint SendWindowSize = Kcp.WND_SND,
|
||||
uint ReceiveWindowSize = Kcp.WND_RCV,
|
||||
int Timeout = KcpPeer.DEFAULT_TIMEOUT,
|
||||
uint MaxRetransmits = Kcp.DEADLINK)
|
||||
{
|
||||
this.DualMode = DualMode;
|
||||
this.RecvBufferSize = RecvBufferSize;
|
||||
this.SendBufferSize = SendBufferSize;
|
||||
this.Mtu = Mtu;
|
||||
this.NoDelay = NoDelay;
|
||||
this.Interval = Interval;
|
||||
this.FastResend = FastResend;
|
||||
this.CongestionWindow = CongestionWindow;
|
||||
this.SendWindowSize = SendWindowSize;
|
||||
this.ReceiveWindowSize = ReceiveWindowSize;
|
||||
this.Timeout = Timeout;
|
||||
this.MaxRetransmits = MaxRetransmits;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 99692f99c45c4b47b0500e7abbfd12da
|
||||
timeCreated: 1670946969
|
26
Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpHeader.cs
Normal file
26
Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpHeader.cs
Normal file
@ -0,0 +1,26 @@
|
||||
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 KcpHeaderReliable : byte
|
||||
{
|
||||
// don't react on 0x00. might help to filter out random noise.
|
||||
Hello = 1,
|
||||
// ping goes over reliable & KcpHeader for now. could go over unreliable
|
||||
// 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 = 2,
|
||||
Data = 3,
|
||||
}
|
||||
|
||||
public enum KcpHeaderUnreliable : byte
|
||||
{
|
||||
// users may send unreliable messages
|
||||
Data = 4,
|
||||
// disconnect always goes through rapid fire unreliable (glenn fielder)
|
||||
Disconnect = 5,
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 91b5edac31224a49bd76f960ae018942
|
||||
timeCreated: 1610081248
|
775
Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpPeer.cs
Normal file
775
Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpPeer.cs
Normal file
@ -0,0 +1,775 @@
|
||||
// Kcp Peer, similar to UDP Peer but wrapped with reliability, channels,
|
||||
// timeouts, authentication, state, etc.
|
||||
//
|
||||
// still IO agnostic to work with udp, nonalloc, relays, native, etc.
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace kcp2k
|
||||
{
|
||||
public abstract class KcpPeer
|
||||
{
|
||||
// kcp reliability algorithm
|
||||
internal Kcp kcp;
|
||||
|
||||
// security cookie to prevent UDP spoofing.
|
||||
// credits to IncludeSec for disclosing the issue.
|
||||
//
|
||||
// server passes the expected cookie to the client's KcpPeer.
|
||||
// KcpPeer sends cookie to the connected client.
|
||||
// KcpPeer only accepts packets which contain the cookie.
|
||||
// => cookie can be a random number, but it needs to be cryptographically
|
||||
// secure random that can't be easily predicted.
|
||||
// => cookie can be hash(ip, port) BUT only if salted to be not predictable
|
||||
internal uint cookie;
|
||||
|
||||
// state: connected as soon as we create the peer.
|
||||
// leftover from KcpConnection. remove it after refactoring later.
|
||||
protected KcpState state = KcpState.Connected;
|
||||
|
||||
// If we don't receive anything these many milliseconds
|
||||
// then consider us disconnected
|
||||
public const int DEFAULT_TIMEOUT = 10000;
|
||||
public int timeout;
|
||||
uint lastReceiveTime;
|
||||
|
||||
// internal time.
|
||||
// StopWatch offers ElapsedMilliSeconds and should be more precise than
|
||||
// Unity's time.deltaTime over long periods.
|
||||
readonly Stopwatch watch = new Stopwatch();
|
||||
|
||||
// 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
|
||||
readonly 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
|
||||
readonly byte[] kcpSendBuffer;// = new byte[1 + ReliableMaxMessageSize];
|
||||
|
||||
// raw send buffer is exactly MTU.
|
||||
readonly byte[] rawSendBuffer;
|
||||
|
||||
// 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;
|
||||
|
||||
// we need to subtract the channel and cookie bytes from every
|
||||
// MaxMessageSize calculation.
|
||||
// we also need to tell kcp to use MTU-1 to leave space for the byte.
|
||||
public const int CHANNEL_HEADER_SIZE = 1;
|
||||
public const int COOKIE_HEADER_SIZE = 4;
|
||||
public const int METADATA_SIZE = CHANNEL_HEADER_SIZE + COOKIE_HEADER_SIZE;
|
||||
|
||||
// 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 < rcv_wnd, so we use rcv_wnd - 1.
|
||||
// NOTE that original kcp has a bug where WND_RCV default is used
|
||||
// instead of configured rcv_wnd, limiting max message size to 144 KB
|
||||
// https://github.com/skywind3000/kcp/pull/291
|
||||
// we fixed this in kcp2k.
|
||||
// -> we add 1 byte KcpHeader enum to each message, so -1
|
||||
//
|
||||
// IMPORTANT: max message is MTU * rcv_wnd, 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!)
|
||||
static int ReliableMaxMessageSize_Unconstrained(int mtu, uint rcv_wnd) =>
|
||||
(mtu - Kcp.OVERHEAD - METADATA_SIZE) * ((int)rcv_wnd - 1) - 1;
|
||||
|
||||
// kcp encodes 'frg' as 1 byte.
|
||||
// max message size can only ever allow up to 255 fragments.
|
||||
// WND_RCV gives 127 fragments.
|
||||
// WND_RCV * 2 gives 255 fragments.
|
||||
// so we can limit max message size by limiting rcv_wnd parameter.
|
||||
public static int ReliableMaxMessageSize(int mtu, uint rcv_wnd) =>
|
||||
ReliableMaxMessageSize_Unconstrained(mtu, Math.Min(rcv_wnd, Kcp.FRG_MAX));
|
||||
|
||||
// unreliable max message size is simply MTU - channel header - kcp header
|
||||
public static int UnreliableMaxMessageSize(int mtu) =>
|
||||
mtu - METADATA_SIZE - 1;
|
||||
|
||||
// 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;
|
||||
|
||||
// calculate max message sizes based on mtu and wnd only once
|
||||
public readonly int unreliableMax;
|
||||
public readonly int reliableMax;
|
||||
|
||||
// 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 KcpPeer(KcpConfig config, uint cookie)
|
||||
{
|
||||
// initialize variable state in extra function so we can reuse it
|
||||
// when reconnecting to reset state
|
||||
Reset(config);
|
||||
|
||||
// set the cookie after resetting state so it's not overwritten again.
|
||||
// with log message for debugging in case of cookie issues.
|
||||
this.cookie = cookie;
|
||||
Log.Info($"[KCP] {GetType()}: created with cookie={cookie}");
|
||||
|
||||
// create mtu sized send buffer
|
||||
rawSendBuffer = new byte[config.Mtu];
|
||||
|
||||
// calculate max message sizes once
|
||||
unreliableMax = UnreliableMaxMessageSize(config.Mtu);
|
||||
reliableMax = ReliableMaxMessageSize(config.Mtu, config.ReceiveWindowSize);
|
||||
|
||||
// create message buffers AFTER window size is set
|
||||
// see comments on buffer definition for the "+1" part
|
||||
kcpMessageBuffer = new byte[1 + reliableMax];
|
||||
kcpSendBuffer = new byte[1 + reliableMax];
|
||||
}
|
||||
|
||||
// Reset all state once.
|
||||
// useful for KcpClient to reconned with a fresh kcp state.
|
||||
protected void Reset(KcpConfig config)
|
||||
{
|
||||
// reset state
|
||||
cookie = 0;
|
||||
state = KcpState.Connected;
|
||||
lastReceiveTime = 0;
|
||||
lastPingTime = 0;
|
||||
watch.Restart(); // start at 0 each time
|
||||
|
||||
// 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(config.NoDelay ? 1u : 0u, config.Interval, config.FastResend, !config.CongestionWindow);
|
||||
kcp.SetWindowSize(config.SendWindowSize, config.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((uint)config.Mtu - METADATA_SIZE);
|
||||
|
||||
// set maximum retransmits (aka dead_link)
|
||||
kcp.dead_link = config.MaxRetransmits;
|
||||
timeout = config.Timeout;
|
||||
}
|
||||
|
||||
// callbacks ///////////////////////////////////////////////////////////
|
||||
// events are abstract, guaranteed to be implemented.
|
||||
// this ensures they are always initialized when used.
|
||||
// fixes https://github.com/MirrorNetworking/Mirror/issues/3337 and more
|
||||
protected abstract void OnAuthenticated();
|
||||
protected abstract void OnData(ArraySegment<byte> message, KcpChannel channel);
|
||||
protected abstract void OnDisconnected();
|
||||
|
||||
// error callback instead of logging.
|
||||
// allows libraries to show popups etc.
|
||||
// (string instead of Exception for ease of use and to avoid user panic)
|
||||
protected abstract void OnError(ErrorCode error, string message);
|
||||
protected abstract void RawSend(ArraySegment<byte> data);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
|
||||
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)
|
||||
{
|
||||
// pass error to user callback. no need to log it manually.
|
||||
// GetType() shows Server/ClientConn instead of just Connection.
|
||||
OnError(ErrorCode.Timeout, $"{GetType()}: 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)
|
||||
{
|
||||
// pass error to user callback. no need to log it manually.
|
||||
// GetType() shows Server/ClientConn instead of just Connection.
|
||||
OnError(ErrorCode.Timeout, $"{GetType()}: dead_link detected: a message was retransmitted {kcp.dead_link} times without ack. 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)
|
||||
{
|
||||
// pass error to user callback. no need to log it manually.
|
||||
// GetType() shows Server/ClientConn instead of just Connection.
|
||||
OnError(ErrorCode.Congestion,
|
||||
$"{GetType()}: 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.");
|
||||
|
||||
// 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 KcpHeaderReliable header, out ArraySegment<byte> message)
|
||||
{
|
||||
message = default;
|
||||
header = KcpHeaderReliable.Ping;
|
||||
|
||||
int msgSize = kcp.PeekSize();
|
||||
if (msgSize <= 0) return false;
|
||||
|
||||
// only allow receiving up to buffer sized messages.
|
||||
// otherwise we would get BlockCopy ArgumentException anyway.
|
||||
if (msgSize > kcpMessageBuffer.Length)
|
||||
{
|
||||
// we don't allow sending messages > Max, so this must be an
|
||||
// attacker. let's disconnect to avoid allocation attacks etc.
|
||||
// pass error to user callback. no need to log it manually.
|
||||
OnError(ErrorCode.InvalidReceive, $"{GetType()}: possible allocation attack for msgSize {msgSize} > buffer {kcpMessageBuffer.Length}. Disconnecting the connection.");
|
||||
Disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
// receive from kcp
|
||||
int received = kcp.Receive(kcpMessageBuffer, msgSize);
|
||||
if (received < 0)
|
||||
{
|
||||
// if receive failed, close everything
|
||||
// pass error to user callback. no need to log it manually.
|
||||
// GetType() shows Server/ClientConn instead of just Connection.
|
||||
OnError(ErrorCode.InvalidReceive, $"{GetType()}: Receive failed with error={received}. closing connection.");
|
||||
Disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
// extract header & content without header
|
||||
header = (KcpHeaderReliable)kcpMessageBuffer[0];
|
||||
message = new ArraySegment<byte>(kcpMessageBuffer, 1, msgSize - 1);
|
||||
lastReceiveTime = (uint)watch.ElapsedMilliseconds;
|
||||
return true;
|
||||
}
|
||||
|
||||
void TickIncoming_Connected(uint time)
|
||||
{
|
||||
// detect common events & ping
|
||||
HandleTimeout(time);
|
||||
HandleDeadLink();
|
||||
HandlePing(time);
|
||||
HandleChoked();
|
||||
|
||||
// any reliable kcp message received?
|
||||
if (ReceiveNextReliable(out KcpHeaderReliable header, out ArraySegment<byte> message))
|
||||
{
|
||||
// message type FSM. no default so we never miss a case.
|
||||
switch (header)
|
||||
{
|
||||
case KcpHeaderReliable.Hello:
|
||||
{
|
||||
// we were waiting for a Hello message.
|
||||
// it proves that the other end speaks our protocol.
|
||||
|
||||
// log with previously parsed cookie
|
||||
Log.Info($"[KCP] {GetType()}: received hello with cookie={cookie}");
|
||||
state = KcpState.Authenticated;
|
||||
OnAuthenticated();
|
||||
break;
|
||||
}
|
||||
case KcpHeaderReliable.Ping:
|
||||
{
|
||||
// ping keeps kcp from timing out. do nothing.
|
||||
break;
|
||||
}
|
||||
case KcpHeaderReliable.Data:
|
||||
{
|
||||
// everything else is not allowed during handshake!
|
||||
// pass error to user callback. no need to log it manually.
|
||||
// GetType() shows Server/ClientConn instead of just Connection.
|
||||
OnError(ErrorCode.InvalidReceive, $"[KCP] {GetType()}: 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
|
||||
while (ReceiveNextReliable(out KcpHeaderReliable header, out ArraySegment<byte> message))
|
||||
{
|
||||
// message type FSM. no default so we never miss a case.
|
||||
switch (header)
|
||||
{
|
||||
case KcpHeaderReliable.Hello:
|
||||
{
|
||||
// should never receive another hello after auth
|
||||
// GetType() shows Server/ClientConn instead of just Connection.
|
||||
Log.Warning($"{GetType()}: received invalid header {header} while Authenticated. Disconnecting the connection.");
|
||||
Disconnect();
|
||||
break;
|
||||
}
|
||||
case KcpHeaderReliable.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(message, KcpChannel.Reliable);
|
||||
}
|
||||
// empty data = attacker, or something went wrong
|
||||
else
|
||||
{
|
||||
// pass error to user callback. no need to log it manually.
|
||||
// GetType() shows Server/ClientConn instead of just Connection.
|
||||
OnError(ErrorCode.InvalidReceive, $"{GetType()}: received empty Data message while Authenticated. Disconnecting the connection.");
|
||||
Disconnect();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case KcpHeaderReliable.Ping:
|
||||
{
|
||||
// ping keeps kcp from timing out. do nothing.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void TickIncoming()
|
||||
{
|
||||
uint time = (uint)watch.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO KcpConnection is IO agnostic. move this to outside later.
|
||||
catch (SocketException exception)
|
||||
{
|
||||
// this is ok, the connection was closed
|
||||
// pass error to user callback. no need to log it manually.
|
||||
// GetType() shows Server/ClientConn instead of just Connection.
|
||||
OnError(ErrorCode.ConnectionClosed, $"{GetType()}: Disconnecting because {exception}. This is fine.");
|
||||
Disconnect();
|
||||
}
|
||||
catch (ObjectDisposedException exception)
|
||||
{
|
||||
// fine, socket was closed
|
||||
// pass error to user callback. no need to log it manually.
|
||||
// GetType() shows Server/ClientConn instead of just Connection.
|
||||
OnError(ErrorCode.ConnectionClosed, $"{GetType()}: Disconnecting because {exception}. This is fine.");
|
||||
Disconnect();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
// unexpected
|
||||
// pass error to user callback. no need to log it manually.
|
||||
// GetType() shows Server/ClientConn instead of just Connection.
|
||||
OnError(ErrorCode.Unexpected, $"{GetType()}: unexpected Exception: {exception}");
|
||||
Disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void TickOutgoing()
|
||||
{
|
||||
uint time = (uint)watch.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO KcpConnection is IO agnostic. move this to outside later.
|
||||
catch (SocketException exception)
|
||||
{
|
||||
// this is ok, the connection was closed
|
||||
// pass error to user callback. no need to log it manually.
|
||||
// GetType() shows Server/ClientConn instead of just Connection.
|
||||
OnError(ErrorCode.ConnectionClosed, $"{GetType()}: Disconnecting because {exception}. This is fine.");
|
||||
Disconnect();
|
||||
}
|
||||
catch (ObjectDisposedException exception)
|
||||
{
|
||||
// fine, socket was closed
|
||||
// pass error to user callback. no need to log it manually.
|
||||
// GetType() shows Server/ClientConn instead of just Connection.
|
||||
OnError(ErrorCode.ConnectionClosed, $"{GetType()}: Disconnecting because {exception}. This is fine.");
|
||||
Disconnect();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
// unexpected
|
||||
// pass error to user callback. no need to log it manually.
|
||||
// GetType() shows Server/ClientConn instead of just Connection.
|
||||
OnError(ErrorCode.Unexpected, $"{GetType()}: unexpected exception: {exception}");
|
||||
Disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
protected void OnRawInputReliable(ArraySegment<byte> message)
|
||||
{
|
||||
// input into kcp, but skip channel byte
|
||||
int input = kcp.Input(message.Array, message.Offset, message.Count);
|
||||
if (input != 0)
|
||||
{
|
||||
// GetType() shows Server/ClientConn instead of just Connection.
|
||||
Log.Warning($"[KCP] {GetType()}: Input failed with error={input} for buffer with length={message.Count - 1}");
|
||||
}
|
||||
}
|
||||
|
||||
protected void OnRawInputUnreliable(ArraySegment<byte> message)
|
||||
{
|
||||
// need at least one byte for the KcpHeader enum
|
||||
if (message.Count < 1) return;
|
||||
|
||||
// parse header and subtract it from message content.
|
||||
// (above we already ensure it's at least 1 byte long)
|
||||
KcpHeaderUnreliable header = (KcpHeaderUnreliable)message.Array[message.Offset + 0];
|
||||
message = new ArraySegment<byte>(message.Array, message.Offset + 1, message.Count - 1);
|
||||
|
||||
switch (header)
|
||||
{
|
||||
case KcpHeaderUnreliable.Data:
|
||||
{
|
||||
// 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)
|
||||
{
|
||||
OnData(message, KcpChannel.Unreliable);
|
||||
|
||||
// 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)watch.ElapsedMilliseconds;
|
||||
}
|
||||
else
|
||||
{
|
||||
// it's common to receive unreliable messages before being
|
||||
// authenticated, for example:
|
||||
// - random internet noise
|
||||
// - game server may send an unreliable message after authenticating,
|
||||
// and the unreliable message arrives on the client before the
|
||||
// 'auth_ok' message. this can be avoided by sending a final
|
||||
// 'ready' message after being authenticated, but this would
|
||||
// add another 'round trip time' of latency to the handshake.
|
||||
//
|
||||
// it's best to simply ignore invalid unreliable messages here.
|
||||
// Log.Info($"{GetType()}: received unreliable message while not authenticated.");
|
||||
}
|
||||
break;
|
||||
}
|
||||
case KcpHeaderUnreliable.Disconnect:
|
||||
{
|
||||
// GetType() shows Server/ClientConn instead of just Connection.
|
||||
Log.Info($"[KCP] {GetType()}: received disconnect message");
|
||||
Disconnect();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// raw send called by kcp
|
||||
void RawSendReliable(byte[] data, int length)
|
||||
{
|
||||
// write channel header
|
||||
// from 0, with 1 byte
|
||||
rawSendBuffer[0] = (byte)KcpChannel.Reliable;
|
||||
|
||||
// write handshake cookie to protect against UDP spoofing.
|
||||
// from 1, with 4 bytes
|
||||
Utils.Encode32U(rawSendBuffer, 1, cookie); // allocation free
|
||||
|
||||
// write data
|
||||
// from 5, with N bytes
|
||||
Buffer.BlockCopy(data, 0, rawSendBuffer, 1+4, length);
|
||||
|
||||
// IO send
|
||||
ArraySegment<byte> segment = new ArraySegment<byte>(rawSendBuffer, 0, length + 1+4);
|
||||
RawSend(segment);
|
||||
}
|
||||
|
||||
void SendReliable(KcpHeaderReliable header, ArraySegment<byte> content)
|
||||
{
|
||||
// 1 byte header + content needs to fit into send buffer
|
||||
if (1 + content.Count > kcpSendBuffer.Length) // TODO
|
||||
{
|
||||
// otherwise content is larger than MaxMessageSize. let user know!
|
||||
// GetType() shows Server/ClientConn instead of just Connection.
|
||||
OnError(ErrorCode.InvalidSend, $"{GetType()}: Failed to send reliable message of size {content.Count} because it's larger than ReliableMaxMessageSize={reliableMax}");
|
||||
return;
|
||||
}
|
||||
|
||||
// write channel header
|
||||
kcpSendBuffer[0] = (byte)header;
|
||||
|
||||
// write data (if any)
|
||||
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)
|
||||
{
|
||||
// GetType() shows Server/ClientConn instead of just Connection.
|
||||
OnError(ErrorCode.InvalidSend, $"{GetType()}: Send failed with error={sent} for content with length={content.Count}");
|
||||
}
|
||||
}
|
||||
|
||||
void SendUnreliable(KcpHeaderUnreliable header, ArraySegment<byte> content)
|
||||
{
|
||||
// message size needs to be <= unreliable max size
|
||||
if (content.Count > unreliableMax)
|
||||
{
|
||||
// otherwise content is larger than MaxMessageSize. let user know!
|
||||
// GetType() shows Server/ClientConn instead of just Connection.
|
||||
Log.Error($"[KCP] {GetType()}: Failed to send unreliable message of size {content.Count} because it's larger than UnreliableMaxMessageSize={unreliableMax}");
|
||||
return;
|
||||
}
|
||||
|
||||
// write channel header
|
||||
// from 0, with 1 byte
|
||||
rawSendBuffer[0] = (byte)KcpChannel.Unreliable;
|
||||
|
||||
// write handshake cookie to protect against UDP spoofing.
|
||||
// from 1, with 4 bytes
|
||||
Utils.Encode32U(rawSendBuffer, 1, cookie); // allocation free
|
||||
|
||||
// write kcp header
|
||||
rawSendBuffer[5] = (byte)header;
|
||||
|
||||
// write data (if any)
|
||||
// from 6, with N bytes
|
||||
if (content.Count > 0)
|
||||
Buffer.BlockCopy(content.Array, content.Offset, rawSendBuffer, 1 + 4 + 1, content.Count);
|
||||
|
||||
// IO send
|
||||
ArraySegment<byte> segment = new ArraySegment<byte>(rawSendBuffer, 0, content.Count + 1 + 4 + 1);
|
||||
RawSend(segment);
|
||||
}
|
||||
|
||||
// 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 SendHello()
|
||||
{
|
||||
// send an empty message with 'Hello' header.
|
||||
// cookie is automatically included in all messages.
|
||||
|
||||
// GetType() shows Server/ClientConn instead of just Connection.
|
||||
Log.Info($"[KCP] {GetType()}: sending handshake to other end with cookie={cookie}");
|
||||
SendReliable(KcpHeaderReliable.Hello, 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)
|
||||
{
|
||||
// pass error to user callback. no need to log it manually.
|
||||
// GetType() shows Server/ClientConn instead of just Connection.
|
||||
OnError(ErrorCode.InvalidSend, $"{GetType()}: tried sending empty message. This should never happen. Disconnecting.");
|
||||
Disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (channel)
|
||||
{
|
||||
case KcpChannel.Reliable:
|
||||
SendReliable(KcpHeaderReliable.Data, data);
|
||||
break;
|
||||
case KcpChannel.Unreliable:
|
||||
SendUnreliable(KcpHeaderUnreliable.Data, data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ping goes through kcp to keep it from timing out, so it goes over the
|
||||
// reliable channel.
|
||||
void SendPing() => SendReliable(KcpHeaderReliable.Ping, default);
|
||||
|
||||
// send disconnect message
|
||||
void SendDisconnect()
|
||||
{
|
||||
// sending over reliable to ensure delivery seems like a good idea:
|
||||
// but if we close the connection immediately, it often doesn't get
|
||||
// fully delivered: https://github.com/MirrorNetworking/Mirror/issues/3591
|
||||
// SendReliable(KcpHeader.Disconnect, default);
|
||||
//
|
||||
// instead, rapid fire a few unreliable messages.
|
||||
// they are sent immediately even if we close the connection after.
|
||||
// this way we don't need to keep the connection alive for a while.
|
||||
// (glenn fiedler method)
|
||||
for (int i = 0; i < 5; ++i)
|
||||
SendUnreliable(KcpHeaderUnreliable.Disconnect, default);
|
||||
}
|
||||
|
||||
// disconnect this connection
|
||||
public virtual void Disconnect()
|
||||
{
|
||||
// only if not disconnected yet
|
||||
if (state == KcpState.Disconnected)
|
||||
return;
|
||||
|
||||
// send a disconnect message
|
||||
try
|
||||
{
|
||||
SendDisconnect();
|
||||
}
|
||||
// TODO KcpConnection is IO agnostic. move this to outside later.
|
||||
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
|
||||
// GetType() shows Server/ClientConn instead of just Connection.
|
||||
Log.Info($"[KCP] {GetType()}: Disconnected.");
|
||||
state = KcpState.Disconnected;
|
||||
OnDisconnected();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3915c7c62b72d4dc2a9e4e76c94fc484
|
||||
timeCreated: 1602600432
|
411
Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpServer.cs
Normal file
411
Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpServer.cs
Normal file
@ -0,0 +1,411 @@
|
||||
// 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;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace kcp2k
|
||||
{
|
||||
public class KcpServer
|
||||
{
|
||||
// callbacks
|
||||
// even for errors, to allow liraries to show popups etc.
|
||||
// instead of logging directly.
|
||||
// (string instead of Exception for ease of use and to avoid user panic)
|
||||
//
|
||||
// events are readonly, set in constructor.
|
||||
// this ensures they are always initialized when used.
|
||||
// fixes https://github.com/MirrorNetworking/Mirror/issues/3337 and more
|
||||
protected readonly Action<int> OnConnected;
|
||||
protected readonly Action<int, ArraySegment<byte>, KcpChannel> OnData;
|
||||
protected readonly Action<int> OnDisconnected;
|
||||
protected readonly Action<int, ErrorCode, string> OnError;
|
||||
|
||||
// configuration
|
||||
protected readonly KcpConfig config;
|
||||
|
||||
// state
|
||||
protected Socket socket;
|
||||
EndPoint newClientEP;
|
||||
|
||||
// expose local endpoint for users / relays / nat traversal etc.
|
||||
public EndPoint LocalEndPoint => socket?.LocalEndPoint;
|
||||
|
||||
// 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!
|
||||
protected readonly byte[] rawReceiveBuffer;
|
||||
|
||||
// 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>, KcpChannel> OnData,
|
||||
Action<int> OnDisconnected,
|
||||
Action<int, ErrorCode, string> OnError,
|
||||
KcpConfig config)
|
||||
{
|
||||
// initialize callbacks first to ensure they can be used safely.
|
||||
this.OnConnected = OnConnected;
|
||||
this.OnData = OnData;
|
||||
this.OnDisconnected = OnDisconnected;
|
||||
this.OnError = OnError;
|
||||
this.config = config;
|
||||
|
||||
// create mtu sized receive buffer
|
||||
rawReceiveBuffer = new byte[config.Mtu];
|
||||
|
||||
// create newClientEP either IPv4 or IPv6
|
||||
newClientEP = config.DualMode
|
||||
? new IPEndPoint(IPAddress.IPv6Any, 0)
|
||||
: new IPEndPoint(IPAddress.Any, 0);
|
||||
}
|
||||
|
||||
public virtual bool IsActive() => socket != null;
|
||||
|
||||
static Socket CreateServerSocket(bool DualMode, ushort port)
|
||||
{
|
||||
if (DualMode)
|
||||
{
|
||||
// IPv6 socket with DualMode @ "::" : port
|
||||
Socket socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp);
|
||||
|
||||
// enabling DualMode may throw:
|
||||
// https://learn.microsoft.com/en-us/dotnet/api/System.Net.Sockets.Socket.DualMode?view=net-7.0
|
||||
// attempt it, otherwise log but continue
|
||||
// fixes: https://github.com/MirrorNetworking/Mirror/issues/3358
|
||||
try
|
||||
{
|
||||
socket.DualMode = true;
|
||||
}
|
||||
catch (NotSupportedException e)
|
||||
{
|
||||
Log.Warning($"[KCP] Failed to set Dual Mode, continuing with IPv6 without Dual Mode. Error: {e}");
|
||||
}
|
||||
|
||||
// for windows sockets, there's a rare issue where when using
|
||||
// a server socket with multiple clients, if one of the clients
|
||||
// is closed, the single server socket throws exceptions when
|
||||
// sending/receiving. even if the socket is made for N clients.
|
||||
//
|
||||
// this actually happened to one of our users:
|
||||
// https://github.com/MirrorNetworking/Mirror/issues/3611
|
||||
//
|
||||
// here's the in-depth explanation & solution:
|
||||
//
|
||||
// "As you may be aware, if a host receives a packet for a UDP
|
||||
// port that is not currently bound, it may send back an ICMP
|
||||
// "Port Unreachable" message. Whether or not it does this is
|
||||
// dependent on the firewall, private/public settings, etc.
|
||||
// On localhost, however, it will pretty much always send this
|
||||
// packet back.
|
||||
//
|
||||
// Now, on Windows (and only on Windows), by default, a received
|
||||
// ICMP Port Unreachable message will close the UDP socket that
|
||||
// sent it; hence, the next time you try to receive on the
|
||||
// socket, it will throw an exception because the socket has
|
||||
// been closed by the OS.
|
||||
//
|
||||
// Obviously, this causes a headache in the multi-client,
|
||||
// single-server socket set-up you have here, but luckily there
|
||||
// is a fix:
|
||||
//
|
||||
// You need to utilise the not-often-required SIO_UDP_CONNRESET
|
||||
// Winsock control code, which turns off this built-in behaviour
|
||||
// of automatically closing the socket.
|
||||
//
|
||||
// Note that this ioctl code is only supported on Windows
|
||||
// (XP and later), not on Linux, since it is provided by the
|
||||
// Winsock extensions. Of course, since the described behavior
|
||||
// is only the default behavior on Windows, this omission is not
|
||||
// a major loss. If you are attempting to create a
|
||||
// cross-platform library, you should cordon this off as
|
||||
// Windows-specific code."
|
||||
// https://stackoverflow.com/questions/74327225/why-does-sending-via-a-udpclient-cause-subsequent-receiving-to-fail
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
const uint IOC_IN = 0x80000000U;
|
||||
const uint IOC_VENDOR = 0x18000000U;
|
||||
const int SIO_UDP_CONNRESET = unchecked((int)(IOC_IN | IOC_VENDOR | 12));
|
||||
socket.IOControl(SIO_UDP_CONNRESET, new byte[] { 0x00 }, null);
|
||||
}
|
||||
|
||||
socket.Bind(new IPEndPoint(IPAddress.IPv6Any, port));
|
||||
return socket;
|
||||
}
|
||||
else
|
||||
{
|
||||
// IPv4 socket @ "0.0.0.0" : port
|
||||
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
|
||||
socket.Bind(new IPEndPoint(IPAddress.Any, port));
|
||||
return socket;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void Start(ushort port)
|
||||
{
|
||||
// only start once
|
||||
if (socket != null)
|
||||
{
|
||||
Log.Warning("[KCP] Server: already started!");
|
||||
return;
|
||||
}
|
||||
|
||||
// listen
|
||||
socket = CreateServerSocket(config.DualMode, port);
|
||||
|
||||
// recv & send are called from main thread.
|
||||
// need to ensure this never blocks.
|
||||
// even a 1ms block per connection would stop us from scaling.
|
||||
socket.Blocking = false;
|
||||
|
||||
// configure buffer sizes
|
||||
Common.ConfigureSocketBuffers(socket, config.RecvBufferSize, config.SendBufferSize);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
// expose the whole IPEndPoint, not just the IP address. some need it.
|
||||
public IPEndPoint GetClientEndPoint(int connectionId)
|
||||
{
|
||||
if (connections.TryGetValue(connectionId, out KcpServerConnection connection))
|
||||
{
|
||||
return connection.remoteEndPoint as IPEndPoint;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// io - input.
|
||||
// virtual so it may be modified for relays, nonalloc workaround, etc.
|
||||
// https://github.com/vis2k/where-allocation
|
||||
// bool return because not all receives may be valid.
|
||||
// for example, relay may expect a certain header.
|
||||
protected virtual bool RawReceiveFrom(out ArraySegment<byte> segment, out int connectionId)
|
||||
{
|
||||
segment = default;
|
||||
connectionId = 0;
|
||||
if (socket == null) return false;
|
||||
|
||||
try
|
||||
{
|
||||
if (socket.ReceiveFromNonBlocking(rawReceiveBuffer, out segment, ref newClientEP))
|
||||
{
|
||||
// set connectionId to hash from endpoint
|
||||
connectionId = Common.ConnectionHash(newClientEP);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (SocketException e)
|
||||
{
|
||||
// NOTE: SocketException is not a subclass of IOException.
|
||||
// the other end closing the connection is not an 'error'.
|
||||
// but connections should never just end silently.
|
||||
// at least log a message for easier debugging.
|
||||
Log.Info($"[KCP] Server: ReceiveFrom failed: {e}");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// io - out.
|
||||
// virtual so it may be modified for relays, nonalloc workaround, etc.
|
||||
// relays may need to prefix connId (and remoteEndPoint would be same for all)
|
||||
protected virtual void RawSend(int connectionId, ArraySegment<byte> data)
|
||||
{
|
||||
// get the connection's endpoint
|
||||
if (!connections.TryGetValue(connectionId, out KcpServerConnection connection))
|
||||
{
|
||||
Log.Warning($"[KCP] Server: RawSend invalid connectionId={connectionId}");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
socket.SendToNonBlocking(data, connection.remoteEndPoint);
|
||||
}
|
||||
catch (SocketException e)
|
||||
{
|
||||
Log.Error($"[KCP] Server: SendTo failed: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual KcpServerConnection CreateConnection(int connectionId)
|
||||
{
|
||||
// generate a random cookie for this connection to avoid UDP spoofing.
|
||||
// needs to be random, but without allocations to avoid GC.
|
||||
uint cookie = Common.GenerateCookie();
|
||||
|
||||
// create empty connection without peer first.
|
||||
// we need it to set up peer callbacks.
|
||||
// afterwards we assign the peer.
|
||||
// events need to be wrapped with connectionIds
|
||||
KcpServerConnection connection = new KcpServerConnection(
|
||||
OnConnectedCallback,
|
||||
(message, channel) => OnData(connectionId, message, channel),
|
||||
OnDisconnectedCallback,
|
||||
(error, reason) => OnError(connectionId, error, reason),
|
||||
(data) => RawSend(connectionId, data),
|
||||
config,
|
||||
cookie,
|
||||
newClientEP);
|
||||
|
||||
return connection;
|
||||
|
||||
// setup authenticated event that also adds to connections
|
||||
void OnConnectedCallback(KcpServerConnection conn)
|
||||
{
|
||||
// add to connections dict after being authenticated.
|
||||
connections.Add(connectionId, conn);
|
||||
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
|
||||
|
||||
// finally, call mirror OnConnected event
|
||||
Log.Info($"[KCP] Server: OnConnected({connectionId})");
|
||||
OnConnected(connectionId);
|
||||
}
|
||||
|
||||
void OnDisconnectedCallback()
|
||||
{
|
||||
// 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] Server: OnDisconnected({connectionId})");
|
||||
OnDisconnected(connectionId);
|
||||
}
|
||||
}
|
||||
|
||||
// receive + add + process once.
|
||||
// best to call this as long as there is more data to receive.
|
||||
void ProcessMessage(ArraySegment<byte> segment, int connectionId)
|
||||
{
|
||||
//Log.Info($"[KCP] server raw recv {msgLength} bytes = {BitConverter.ToString(buffer, 0, msgLength)}");
|
||||
|
||||
// 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(connectionId);
|
||||
|
||||
// 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.
|
||||
|
||||
|
||||
// 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(segment);
|
||||
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(segment);
|
||||
}
|
||||
}
|
||||
|
||||
// process incoming messages. should be called before updating the world.
|
||||
// virtual because relay may need to inject their own ping or similar.
|
||||
readonly HashSet<int> connectionsToRemove = new HashSet<int>();
|
||||
public virtual void TickIncoming()
|
||||
{
|
||||
// input all received messages into kcp
|
||||
while (RawReceiveFrom(out ArraySegment<byte> segment, out int connectionId))
|
||||
{
|
||||
ProcessMessage(segment, connectionId);
|
||||
}
|
||||
|
||||
// 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.
|
||||
// virtual because relay may need to inject their own ping or similar.
|
||||
public virtual 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 virtual void Tick()
|
||||
{
|
||||
TickIncoming();
|
||||
TickOutgoing();
|
||||
}
|
||||
|
||||
public virtual void Stop()
|
||||
{
|
||||
// need to clear connections, otherwise they are in next session.
|
||||
// fixes https://github.com/vis2k/kcp2k/pull/47
|
||||
connections.Clear();
|
||||
socket?.Close();
|
||||
socket = null;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9759159c6589494a9037f5e130a867ed
|
||||
timeCreated: 1603787747
|
@ -0,0 +1,124 @@
|
||||
// server needs to store a separate KcpPeer for each connection.
|
||||
// as well as remoteEndPoint so we know where to send data to.
|
||||
using System;
|
||||
using System.Net;
|
||||
|
||||
namespace kcp2k
|
||||
{
|
||||
public class KcpServerConnection : KcpPeer
|
||||
{
|
||||
public readonly EndPoint remoteEndPoint;
|
||||
|
||||
// callbacks
|
||||
// even for errors, to allow liraries to show popups etc.
|
||||
// instead of logging directly.
|
||||
// (string instead of Exception for ease of use and to avoid user panic)
|
||||
//
|
||||
// events are readonly, set in constructor.
|
||||
// this ensures they are always initialized when used.
|
||||
// fixes https://github.com/MirrorNetworking/Mirror/issues/3337 and more
|
||||
protected readonly Action<KcpServerConnection> OnConnectedCallback;
|
||||
protected readonly Action<ArraySegment<byte>, KcpChannel> OnDataCallback;
|
||||
protected readonly Action OnDisconnectedCallback;
|
||||
protected readonly Action<ErrorCode, string> OnErrorCallback;
|
||||
protected readonly Action<ArraySegment<byte>> RawSendCallback;
|
||||
|
||||
public KcpServerConnection(
|
||||
Action<KcpServerConnection> OnConnected,
|
||||
Action<ArraySegment<byte>, KcpChannel> OnData,
|
||||
Action OnDisconnected,
|
||||
Action<ErrorCode, string> OnError,
|
||||
Action<ArraySegment<byte>> OnRawSend,
|
||||
KcpConfig config,
|
||||
uint cookie,
|
||||
EndPoint remoteEndPoint)
|
||||
: base(config, cookie)
|
||||
{
|
||||
OnConnectedCallback = OnConnected;
|
||||
OnDataCallback = OnData;
|
||||
OnDisconnectedCallback = OnDisconnected;
|
||||
OnErrorCallback = OnError;
|
||||
RawSendCallback = OnRawSend;
|
||||
|
||||
this.remoteEndPoint = remoteEndPoint;
|
||||
}
|
||||
|
||||
// callbacks ///////////////////////////////////////////////////////////
|
||||
protected override void OnAuthenticated()
|
||||
{
|
||||
// once we receive the first client hello,
|
||||
// immediately reply with hello so the client knows the security cookie.
|
||||
SendHello();
|
||||
OnConnectedCallback(this);
|
||||
}
|
||||
|
||||
protected override void OnData(ArraySegment<byte> message, KcpChannel channel) =>
|
||||
OnDataCallback(message, channel);
|
||||
|
||||
protected override void OnDisconnected() =>
|
||||
OnDisconnectedCallback();
|
||||
|
||||
protected override void OnError(ErrorCode error, string message) =>
|
||||
OnErrorCallback(error, message);
|
||||
|
||||
protected override void RawSend(ArraySegment<byte> data) =>
|
||||
RawSendCallback(data);
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// insert raw IO. usually from socket.Receive.
|
||||
// offset is useful for relays, where we may parse a header and then
|
||||
// feed the rest to kcp.
|
||||
public void RawInput(ArraySegment<byte> segment)
|
||||
{
|
||||
// ensure valid size: at least 1 byte for channel + 4 bytes for cookie
|
||||
if (segment.Count <= 5) return;
|
||||
|
||||
// parse channel
|
||||
// byte channel = segment[0]; ArraySegment[i] isn't supported in some older Unity Mono versions
|
||||
byte channel = segment.Array[segment.Offset + 0];
|
||||
|
||||
// all server->client messages include the server's security cookie.
|
||||
// all client->server messages except for the initial 'hello' include it too.
|
||||
// parse the cookie and make sure it matches (except for initial hello).
|
||||
Utils.Decode32U(segment.Array, segment.Offset + 1, out uint messageCookie);
|
||||
|
||||
// compare cookie to protect against UDP spoofing.
|
||||
// messages won't have a cookie until after handshake.
|
||||
// so only compare if we are authenticated.
|
||||
// simply drop the message if the cookie doesn't match.
|
||||
if (state == KcpState.Authenticated)
|
||||
{
|
||||
if (messageCookie != cookie)
|
||||
{
|
||||
Log.Warning($"[KCP] ServerConnection: dropped message with invalid cookie: {messageCookie} expected: {cookie} state: {state}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// parse message
|
||||
ArraySegment<byte> message = new ArraySegment<byte>(segment.Array, segment.Offset + 1+4, segment.Count - 1-4);
|
||||
|
||||
switch (channel)
|
||||
{
|
||||
case (byte)KcpChannel.Reliable:
|
||||
{
|
||||
OnRawInputReliable(message);
|
||||
break;
|
||||
}
|
||||
case (byte)KcpChannel.Unreliable:
|
||||
{
|
||||
OnRawInputUnreliable(message);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
// invalid channel indicates random internet noise.
|
||||
// servers may receive random UDP data.
|
||||
// just ignore it, but log for easier debugging.
|
||||
Log.Warning($"[KCP] ServerConnection: invalid channel header: {channel}, likely internet noise");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 80a9b1ce9a6f14abeb32bfa9921d097b
|
||||
timeCreated: 1602601483
|
4
Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpState.cs
Normal file
4
Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpState.cs
Normal file
@ -0,0 +1,4 @@
|
||||
namespace kcp2k
|
||||
{
|
||||
public enum KcpState { Connected, Authenticated, Disconnected }
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 81a02c141a88d45d4a2f5ef68c6da75f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
14
Assets/Mirror/Transports/KCP/kcp2k/highlevel/Log.cs
Normal file
14
Assets/Mirror/Transports/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;
|
||||
}
|
||||
}
|
11
Assets/Mirror/Transports/KCP/kcp2k/highlevel/Log.cs.meta
Normal file
11
Assets/Mirror/Transports/KCP/kcp2k/highlevel/Log.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7b5e1de98d6d84c3793a61cf7d8da9a4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
8
Assets/Mirror/Transports/KCP/kcp2k/kcp.meta
Normal file
8
Assets/Mirror/Transports/KCP/kcp2k/kcp.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5cafb8851a0084f3e94a580c207b3923
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
8
Assets/Mirror/Transports/KCP/kcp2k/kcp/AckItem.cs
Normal file
8
Assets/Mirror/Transports/KCP/kcp2k/kcp/AckItem.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace kcp2k
|
||||
{
|
||||
internal struct AckItem
|
||||
{
|
||||
internal uint serialNumber;
|
||||
internal uint timestamp;
|
||||
}
|
||||
}
|
11
Assets/Mirror/Transports/KCP/kcp2k/kcp/AckItem.cs.meta
Normal file
11
Assets/Mirror/Transports/KCP/kcp2k/kcp/AckItem.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 71f47cb11125d429e84e188a150f3ae5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
3
Assets/Mirror/Transports/KCP/kcp2k/kcp/AssemblyInfo.cs
Normal file
3
Assets/Mirror/Transports/KCP/kcp2k/kcp/AssemblyInfo.cs
Normal file
@ -0,0 +1,3 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("kcp2k.Tests")]
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aec6a15ac7bd43129317ea1f01f19782
|
||||
timeCreated: 1602665988
|
1118
Assets/Mirror/Transports/KCP/kcp2k/kcp/Kcp.cs
Normal file
1118
Assets/Mirror/Transports/KCP/kcp2k/kcp/Kcp.cs
Normal file
File diff suppressed because it is too large
Load Diff
11
Assets/Mirror/Transports/KCP/kcp2k/kcp/Kcp.cs.meta
Normal file
11
Assets/Mirror/Transports/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/Transports/KCP/kcp2k/kcp/Pool.cs
Normal file
46
Assets/Mirror/Transports/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/Transports/KCP/kcp2k/kcp/Pool.cs.meta
Normal file
11
Assets/Mirror/Transports/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:
|
78
Assets/Mirror/Transports/KCP/kcp2k/kcp/Segment.cs
Normal file
78
Assets/Mirror/Transports/KCP/kcp2k/kcp/Segment.cs
Normal file
@ -0,0 +1,78 @@
|
||||
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.
|
||||
// fragment (sent as 1 byte).
|
||||
// 0 if unfragmented, otherwise fragment numbers in reverse: N,..,32,1,0
|
||||
// this way the first received segment tells us how many fragments there are.
|
||||
internal uint frg;
|
||||
internal uint wnd; // window size that the receive can currently receive
|
||||
internal uint ts; // timestamp
|
||||
internal uint sn; // sequence number
|
||||
internal uint una;
|
||||
internal uint resendts; // resend timestamp
|
||||
internal int rto;
|
||||
internal uint fastack;
|
||||
internal uint xmit; // retransmit count
|
||||
|
||||
// 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.
|
||||
// -> default MTU as initial capacity to avoid most runtime resizing/allocations
|
||||
//
|
||||
// .data is only used for Encode(), which always fits it into a buffer.
|
||||
// the buffer is always Kcp.buffer. Kcp ctor creates the buffer of size:
|
||||
// (mtu + OVERHEAD) * 3 bytes.
|
||||
// in other words, Encode only ever writes up to the above amount of bytes.
|
||||
internal MemoryStream data = new MemoryStream(Kcp.MTU_DEF);
|
||||
|
||||
// ikcp_encode_seg
|
||||
// encode a segment into buffer.
|
||||
// buffer is always Kcp.buffer. Kcp ctor creates the buffer of size:
|
||||
// (mtu + OVERHEAD) * 3 bytes.
|
||||
// in other words, Encode only ever writes up to the above amount of bytes.
|
||||
internal int Encode(byte[] ptr, int offset)
|
||||
{
|
||||
int previousPosition = offset;
|
||||
|
||||
offset += Utils.Encode32U(ptr, offset, conv);
|
||||
offset += Utils.Encode8u(ptr, offset, (byte)cmd);
|
||||
// IMPORTANT kcp encodes 'frg' as 1 byte.
|
||||
// so we can only support up to 255 fragments.
|
||||
// (which limits max message size to around 288 KB)
|
||||
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);
|
||||
|
||||
int written = offset - previousPosition;
|
||||
return written;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
11
Assets/Mirror/Transports/KCP/kcp2k/kcp/Segment.cs.meta
Normal file
11
Assets/Mirror/Transports/KCP/kcp2k/kcp/Segment.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fc58706a05dd3442c8fde858d5266855
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
76
Assets/Mirror/Transports/KCP/kcp2k/kcp/Utils.cs
Normal file
76
Assets/Mirror/Transports/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 value)
|
||||
{
|
||||
p[0 + offset] = value;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// decode 8 bits unsigned int
|
||||
public static int Decode8u(byte[] p, int offset, out byte value)
|
||||
{
|
||||
value = p[0 + offset];
|
||||
return 1;
|
||||
}
|
||||
|
||||
// encode 16 bits unsigned int (lsb)
|
||||
public static int Encode16U(byte[] p, int offset, ushort value)
|
||||
{
|
||||
p[0 + offset] = (byte)(value >> 0);
|
||||
p[1 + offset] = (byte)(value >> 8);
|
||||
return 2;
|
||||
}
|
||||
|
||||
// decode 16 bits unsigned int (lsb)
|
||||
public static int Decode16U(byte[] p, int offset, out ushort value)
|
||||
{
|
||||
ushort result = 0;
|
||||
result |= p[0 + offset];
|
||||
result |= (ushort)(p[1 + offset] << 8);
|
||||
value = result;
|
||||
return 2;
|
||||
}
|
||||
|
||||
// encode 32 bits unsigned int (lsb)
|
||||
public static int Encode32U(byte[] p, int offset, uint value)
|
||||
{
|
||||
p[0 + offset] = (byte)(value >> 0);
|
||||
p[1 + offset] = (byte)(value >> 8);
|
||||
p[2 + offset] = (byte)(value >> 16);
|
||||
p[3 + offset] = (byte)(value >> 24);
|
||||
return 4;
|
||||
}
|
||||
|
||||
// decode 32 bits unsigned int (lsb)
|
||||
public static int Decode32U(byte[] p, int offset, out uint value)
|
||||
{
|
||||
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);
|
||||
value = 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/Transports/KCP/kcp2k/kcp/Utils.cs.meta
Normal file
11
Assets/Mirror/Transports/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:
|
8
Assets/Mirror/Transports/Latency.meta
Normal file
8
Assets/Mirror/Transports/Latency.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 397bb578e2bb049ebac1e29effa9d298
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
323
Assets/Mirror/Transports/Latency/LatencySimulation.cs
Normal file
323
Assets/Mirror/Transports/Latency/LatencySimulation.cs
Normal file
@ -0,0 +1,323 @@
|
||||
// wraps around a transport and adds latency/loss/scramble simulation.
|
||||
//
|
||||
// reliable: latency
|
||||
// unreliable: latency, loss, scramble (unreliable isn't ordered so we scramble)
|
||||
//
|
||||
// IMPORTANT: use Time.unscaledTime instead of Time.time.
|
||||
// some games might have Time.timeScale modified.
|
||||
// see also: https://github.com/vis2k/Mirror/issues/2907
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Serialization;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
struct QueuedMessage
|
||||
{
|
||||
public int connectionId;
|
||||
public byte[] bytes;
|
||||
public double time;
|
||||
}
|
||||
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/transports/latency-simulaton-transport")]
|
||||
[DisallowMultipleComponent]
|
||||
public class LatencySimulation : Transport
|
||||
{
|
||||
public Transport wrap;
|
||||
|
||||
[Header("Common")]
|
||||
// latency always needs to be applied to both channels!
|
||||
// fixes a bug in prediction where predictedTime would have no latency, but [Command]s would have 100ms latency resulting in heavy, hard to debug jittering!
|
||||
// in real world, all UDP channels go over the same socket connection with the same latency.
|
||||
[Tooltip("Latency in milliseconds (1000 = 1 second). Always applied to both reliable and unreliable, otherwise unreliable NetworkTime may be behind reliable [SyncVars/Commands/Rpcs] or vice versa!")]
|
||||
[Range(0, 10000)] public float latency = 100;
|
||||
|
||||
[Tooltip("Jitter latency via perlin(Time * jitterSpeed) * jitter")]
|
||||
[FormerlySerializedAs("latencySpikeMultiplier")]
|
||||
[Range(0, 1)] public float jitter = 0.02f;
|
||||
|
||||
[Tooltip("Jitter latency via perlin(Time * jitterSpeed) * jitter")]
|
||||
[FormerlySerializedAs("latencySpikeSpeedMultiplier")]
|
||||
public float jitterSpeed = 1;
|
||||
|
||||
[Header("Reliable Messages")]
|
||||
// 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 %\n2% recommended for long term play testing, upto 5% for short bursts.\nAnything higher, or for a prolonged amount of time, suggests user has a connection fault.")]
|
||||
[Range(0, 100)] public float unreliableLoss = 2;
|
||||
|
||||
[Tooltip("Scramble % of unreliable messages, just like over the real network. Mirror unreliable is unordered.")]
|
||||
[Range(0, 100)] public float unreliableScramble = 2;
|
||||
|
||||
// message queues
|
||||
// list so we can insert randomly (scramble)
|
||||
readonly List<QueuedMessage> reliableClientToServer = new List<QueuedMessage>();
|
||||
readonly List<QueuedMessage> reliableServerToClient = new List<QueuedMessage>();
|
||||
readonly List<QueuedMessage> unreliableClientToServer = new List<QueuedMessage>();
|
||||
readonly 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!
|
||||
readonly System.Random random = new System.Random();
|
||||
|
||||
public void Awake()
|
||||
{
|
||||
if (wrap == null)
|
||||
throw new Exception("LatencySimulationTransport 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.
|
||||
#if !UNITY_2020_3_OR_NEWER
|
||||
float spike = Noise((float)NetworkTime.localTime * jitterSpeed) * jitter;
|
||||
#else
|
||||
float spike = Noise((float)Time.unscaledTimeAsDouble * jitterSpeed) * jitter;
|
||||
#endif
|
||||
|
||||
// base latency
|
||||
switch (channeldId)
|
||||
{
|
||||
case Channels.Reliable:
|
||||
return latency/1000 + spike;
|
||||
case Channels.Unreliable:
|
||||
return latency/1000 + 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,
|
||||
#if !UNITY_2020_3_OR_NEWER
|
||||
time = NetworkTime.localTime + latency
|
||||
#else
|
||||
time = Time.unscaledTimeAsDouble + latency
|
||||
#endif
|
||||
};
|
||||
|
||||
switch (channelId)
|
||||
{
|
||||
case Channels.Reliable:
|
||||
// simulate latency
|
||||
reliableQueue.Add(message);
|
||||
break;
|
||||
case Channels.Unreliable:
|
||||
// simulate packet loss
|
||||
bool drop = random.NextDouble() < unreliableLoss/100;
|
||||
if (!drop)
|
||||
{
|
||||
// simulate scramble (Random.Next is < max, so +1)
|
||||
bool scramble = random.NextDouble() < unreliableScramble/100;
|
||||
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.
|
||||
// need to iterate all, since queue isn't a sortedlist.
|
||||
for (int i = 0; i < reliableClientToServer.Count; ++i)
|
||||
{
|
||||
// message ready to be sent?
|
||||
QueuedMessage message = reliableClientToServer[i];
|
||||
#if !UNITY_2020_3_OR_NEWER
|
||||
if (message.time <= NetworkTime.localTime)
|
||||
#else
|
||||
if (message.time <= Time.unscaledTimeAsDouble)
|
||||
#endif
|
||||
{
|
||||
// send and eat
|
||||
wrap.ClientSend(new ArraySegment<byte>(message.bytes), Channels.Reliable);
|
||||
reliableClientToServer.RemoveAt(i);
|
||||
--i;
|
||||
}
|
||||
}
|
||||
|
||||
// flush unreliable messages after latency.
|
||||
// need to iterate all, since queue isn't a sortedlist.
|
||||
for (int i = 0; i < unreliableClientToServer.Count; ++i)
|
||||
{
|
||||
// message ready to be sent?
|
||||
QueuedMessage message = unreliableClientToServer[i];
|
||||
#if !UNITY_2020_3_OR_NEWER
|
||||
if (message.time <= NetworkTime.localTime)
|
||||
#else
|
||||
if (message.time <= Time.unscaledTimeAsDouble)
|
||||
#endif
|
||||
{
|
||||
// send and eat
|
||||
wrap.ClientSend(new ArraySegment<byte>(message.bytes), Channels.Reliable);
|
||||
unreliableClientToServer.RemoveAt(i);
|
||||
--i;
|
||||
}
|
||||
}
|
||||
|
||||
// update wrapped transport too
|
||||
wrap.ClientLateUpdate();
|
||||
}
|
||||
public override void ServerLateUpdate()
|
||||
{
|
||||
|
||||
// flush reliable messages after latency.
|
||||
// need to iterate all, since queue isn't a sortedlist.
|
||||
for (int i = 0; i < reliableServerToClient.Count; ++i)
|
||||
{
|
||||
// message ready to be sent?
|
||||
QueuedMessage message = reliableServerToClient[i];
|
||||
#if !UNITY_2020_3_OR_NEWER
|
||||
if (message.time <= NetworkTime.localTime)
|
||||
#else
|
||||
if (message.time <= Time.unscaledTimeAsDouble)
|
||||
#endif
|
||||
{
|
||||
// send and eat
|
||||
wrap.ServerSend(message.connectionId, new ArraySegment<byte>(message.bytes), Channels.Reliable);
|
||||
reliableServerToClient.RemoveAt(i);
|
||||
--i;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// flush unreliable messages after latency.
|
||||
// need to iterate all, since queue isn't a sortedlist.
|
||||
for (int i = 0; i < unreliableServerToClient.Count; ++i)
|
||||
{
|
||||
// message ready to be sent?
|
||||
QueuedMessage message = unreliableServerToClient[i];
|
||||
#if !UNITY_2020_3_OR_NEWER
|
||||
if (message.time <= NetworkTime.localTime)
|
||||
#else
|
||||
if (message.time <= Time.unscaledTimeAsDouble)
|
||||
#endif
|
||||
{
|
||||
// send and eat
|
||||
wrap.ServerSend(message.connectionId, new ArraySegment<byte>(message.bytes), Channels.Reliable);
|
||||
unreliableServerToClient.RemoveAt(i);
|
||||
--i;
|
||||
}
|
||||
}
|
||||
|
||||
// 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/Transports/Latency/LatencySimulation.cs.meta
Normal file
11
Assets/Mirror/Transports/Latency/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:
|
8
Assets/Mirror/Transports/Middleware.meta
Normal file
8
Assets/Mirror/Transports/Middleware.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 69344c174fdaf432e9ff0cdaf2e2ba21
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
61
Assets/Mirror/Transports/Middleware/MiddlewareTransport.cs
Normal file
61
Assets/Mirror/Transports/Middleware/MiddlewareTransport.cs
Normal file
@ -0,0 +1,61 @@
|
||||
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 int GetBatchThreshold(int channelId = Channels.Reliable) => inner.GetBatchThreshold(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);
|
||||
|
||||
public override void ClientEarlyUpdate() => inner.ClientEarlyUpdate();
|
||||
public override void ClientLateUpdate() => inner.ClientLateUpdate();
|
||||
#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();
|
||||
|
||||
public override void ServerEarlyUpdate() => inner.ServerEarlyUpdate();
|
||||
public override void ServerLateUpdate() => inner.ServerLateUpdate();
|
||||
#endregion
|
||||
}
|
||||
}
|
@ -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:
|
19
Assets/Mirror/Transports/Mirror.Transports.asmdef
Normal file
19
Assets/Mirror/Transports/Mirror.Transports.asmdef
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "Mirror.Transports",
|
||||
"rootNamespace": "",
|
||||
"references": [
|
||||
"GUID:30817c1a0e6d646d99c048fc403f5979",
|
||||
"GUID:6806a62c384838046a3c66c44f06d75f",
|
||||
"GUID:725ee7191c021de4dbf9269590ded755",
|
||||
"GUID:3b5390adca4e2bb4791cb930316d6f3e"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": true,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
7
Assets/Mirror/Transports/Mirror.Transports.asmdef.meta
Normal file
7
Assets/Mirror/Transports/Mirror.Transports.asmdef.meta
Normal file
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 627104647b9c04b4ebb8978a92ecac63
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
8
Assets/Mirror/Transports/Multiplex.meta
Normal file
8
Assets/Mirror/Transports/Multiplex.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 28d5150129fea43048bbdd23ea6e2446
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
440
Assets/Mirror/Transports/Multiplex/MultiplexTransport.cs
Normal file
440
Assets/Mirror/Transports/Multiplex/MultiplexTransport.cs
Normal file
@ -0,0 +1,440 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
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, PortTransport
|
||||
{
|
||||
public Transport[] transports;
|
||||
|
||||
Transport available;
|
||||
|
||||
// underlying transport connectionId to multiplexed connectionId lookup.
|
||||
//
|
||||
// originally we used a formula to map the connectionId:
|
||||
// connectionId * transportAmount + transportId
|
||||
//
|
||||
// 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, ...]
|
||||
//
|
||||
// however, some transports like kcp may give very large connectionIds.
|
||||
// if they are near int.max, then "* transprotAmount + transportIndex"
|
||||
// will overflow, resulting in connIds which can't be projected back.
|
||||
// https://github.com/vis2k/Mirror/issues/3280
|
||||
//
|
||||
// instead, use a simple lookup with 0-indexed ids.
|
||||
// with initial capacity to avoid runtime allocations.
|
||||
|
||||
// (original connectionId, transport#) to multiplexed connectionId
|
||||
readonly Dictionary<KeyValuePair<int, int>, int> originalToMultiplexedId =
|
||||
new Dictionary<KeyValuePair<int, int>, int>(100);
|
||||
|
||||
// multiplexed connectionId to (original connectionId, transport#)
|
||||
readonly Dictionary<int, KeyValuePair<int, int>> multiplexedToOriginalId =
|
||||
new Dictionary<int, KeyValuePair<int, int>>(100);
|
||||
|
||||
// next multiplexed id counter. start at 1 because 0 is reserved for host.
|
||||
int nextMultiplexedId = 1;
|
||||
|
||||
// prevent log flood from OnGUI or similar per-frame updates
|
||||
bool alreadyWarned;
|
||||
|
||||
public ushort Port
|
||||
{
|
||||
get
|
||||
{
|
||||
foreach (Transport transport in transports)
|
||||
if (transport.Available() && transport is PortTransport portTransport)
|
||||
return portTransport.Port;
|
||||
|
||||
return 0;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (Utils.IsHeadless() && !alreadyWarned)
|
||||
{
|
||||
// prevent log flood from OnGUI or similar per-frame updates
|
||||
alreadyWarned = true;
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine($"[Multiplexer] Server cannot set the same listen port for all transports! Set them directly instead.");
|
||||
Console.ResetColor();
|
||||
}
|
||||
else
|
||||
{
|
||||
// We can't set the same port for all transports because
|
||||
// listen ports have to be different for each transport
|
||||
// so we just set the first available one.
|
||||
// This depends on the selected build platform.
|
||||
foreach (Transport transport in transports)
|
||||
if (transport.Available() && transport is PortTransport portTransport)
|
||||
{
|
||||
portTransport.Port = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add to bidirection lookup. returns the multiplexed connectionId.
|
||||
public int AddToLookup(int originalConnectionId, int transportIndex)
|
||||
{
|
||||
// add to both
|
||||
KeyValuePair<int, int> pair = new KeyValuePair<int, int>(originalConnectionId, transportIndex);
|
||||
int multiplexedId = nextMultiplexedId++;
|
||||
|
||||
originalToMultiplexedId[pair] = multiplexedId;
|
||||
multiplexedToOriginalId[multiplexedId] = pair;
|
||||
|
||||
return multiplexedId;
|
||||
}
|
||||
|
||||
public void RemoveFromLookup(int originalConnectionId, int transportIndex)
|
||||
{
|
||||
// remove from both
|
||||
KeyValuePair<int, int> pair = new KeyValuePair<int, int>(originalConnectionId, transportIndex);
|
||||
if (originalToMultiplexedId.TryGetValue(pair, out int multiplexedId))
|
||||
{
|
||||
originalToMultiplexedId.Remove(pair);
|
||||
multiplexedToOriginalId.Remove(multiplexedId);
|
||||
}
|
||||
}
|
||||
|
||||
public bool OriginalId(int multiplexId, out int originalConnectionId, out int transportIndex)
|
||||
{
|
||||
if (!multiplexedToOriginalId.ContainsKey(multiplexId))
|
||||
{
|
||||
originalConnectionId = 0;
|
||||
transportIndex = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
KeyValuePair<int, int> pair = multiplexedToOriginalId[multiplexId];
|
||||
originalConnectionId = pair.Key;
|
||||
transportIndex = pair.Value;
|
||||
return true;
|
||||
}
|
||||
|
||||
public int MultiplexId(int originalConnectionId, int transportIndex)
|
||||
{
|
||||
KeyValuePair<int, int> pair = new KeyValuePair<int, int>(originalConnectionId, transportIndex);
|
||||
if (originalToMultiplexedId.TryGetValue(pair, out int multiplexedId))
|
||||
return multiplexedId;
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public void Awake()
|
||||
{
|
||||
if (transports == null || transports.Length == 0)
|
||||
{
|
||||
Debug.LogError("[Multiplexer] 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("[Multiplexer] 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("[Multiplexer] 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
|
||||
void AddServerCallbacks()
|
||||
{
|
||||
// all underlying transports should call the multiplex transport's 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 transportIndex = i;
|
||||
Transport transport = transports[i];
|
||||
|
||||
transport.OnServerConnected = (originalConnectionId =>
|
||||
{
|
||||
// invoke Multiplex event with multiplexed connectionId
|
||||
int multiplexedId = AddToLookup(originalConnectionId, transportIndex);
|
||||
OnServerConnected.Invoke(multiplexedId);
|
||||
});
|
||||
|
||||
transport.OnServerDataReceived = (originalConnectionId, data, channel) =>
|
||||
{
|
||||
// invoke Multiplex event with multiplexed connectionId
|
||||
int multiplexedId = MultiplexId(originalConnectionId, transportIndex);
|
||||
if (multiplexedId == 0)
|
||||
{
|
||||
if (Utils.IsHeadless())
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine($"[Multiplexer] Received data for unknown connectionId={originalConnectionId} on transport={transportIndex}");
|
||||
Console.ResetColor();
|
||||
}
|
||||
else
|
||||
Debug.LogWarning($"[Multiplexer] Received data for unknown connectionId={originalConnectionId} on transport={transportIndex}");
|
||||
|
||||
return;
|
||||
}
|
||||
OnServerDataReceived.Invoke(multiplexedId, data, channel);
|
||||
};
|
||||
|
||||
transport.OnServerError = (originalConnectionId, error, reason) =>
|
||||
{
|
||||
// invoke Multiplex event with multiplexed connectionId
|
||||
int multiplexedId = MultiplexId(originalConnectionId, transportIndex);
|
||||
if (multiplexedId == 0)
|
||||
{
|
||||
if (Utils.IsHeadless())
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.WriteLine($"[Multiplexer] Received error for unknown connectionId={originalConnectionId} on transport={transportIndex}");
|
||||
Console.ResetColor();
|
||||
}
|
||||
else
|
||||
Debug.LogError($"[Multiplexer] Received error for unknown connectionId={originalConnectionId} on transport={transportIndex}");
|
||||
|
||||
return;
|
||||
}
|
||||
OnServerError.Invoke(multiplexedId, error, reason);
|
||||
};
|
||||
|
||||
transport.OnServerDisconnected = originalConnectionId =>
|
||||
{
|
||||
// invoke Multiplex event with multiplexed connectionId
|
||||
int multiplexedId = MultiplexId(originalConnectionId, transportIndex);
|
||||
if (multiplexedId == 0)
|
||||
{
|
||||
if (Utils.IsHeadless())
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine($"[Multiplexer] Received disconnect for unknown connectionId={originalConnectionId} on transport={transportIndex}");
|
||||
Console.ResetColor();
|
||||
}
|
||||
else
|
||||
Debug.LogWarning($"[Multiplexer] Received disconnect for unknown connectionId={originalConnectionId} on transport={transportIndex}");
|
||||
|
||||
return;
|
||||
}
|
||||
OnServerDisconnected.Invoke(multiplexedId);
|
||||
RemoveFromLookup(originalConnectionId, transportIndex);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// for now returns the first uri,
|
||||
// should we return all available uris?
|
||||
public override Uri ServerUri() =>
|
||||
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)
|
||||
{
|
||||
// convert multiplexed connectionId to original id & transport index
|
||||
if (OriginalId(connectionId, out int originalConnectionId, out int transportIndex))
|
||||
return transports[transportIndex].ServerGetClientAddress(originalConnectionId);
|
||||
else
|
||||
return "";
|
||||
}
|
||||
|
||||
public override void ServerDisconnect(int connectionId)
|
||||
{
|
||||
// convert multiplexed connectionId to original id & transport index
|
||||
if (OriginalId(connectionId, out int originalConnectionId, out int transportIndex))
|
||||
transports[transportIndex].ServerDisconnect(originalConnectionId);
|
||||
}
|
||||
|
||||
public override void ServerSend(int connectionId, ArraySegment<byte> segment, int channelId)
|
||||
{
|
||||
// convert multiplexed connectionId to original transport + connId
|
||||
if (OriginalId(connectionId, out int originalConnectionId, out int transportIndex))
|
||||
transports[transportIndex].ServerSend(originalConnectionId, segment, channelId);
|
||||
}
|
||||
|
||||
public override void ServerStart()
|
||||
{
|
||||
AddServerCallbacks();
|
||||
|
||||
foreach (Transport transport in transports)
|
||||
{
|
||||
transport.ServerStart();
|
||||
|
||||
if (transport is PortTransport portTransport)
|
||||
{
|
||||
if (Utils.IsHeadless())
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Green;
|
||||
Console.WriteLine($"[Multiplexer]: Server listening on port {portTransport.Port} with {transport}");
|
||||
Console.ResetColor();
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log($"[Multiplexer]: Server listening on port {portTransport.Port} with {transport}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
builder.Append("Multiplexer:");
|
||||
|
||||
foreach (Transport transport in transports)
|
||||
builder.Append($" {transport}");
|
||||
|
||||
return builder.ToString().Trim();
|
||||
}
|
||||
}
|
||||
}
|
@ -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/Transports/SimpleWeb.meta
Normal file
8
Assets/Mirror/Transports/SimpleWeb.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a3ba68af305d809418d6c6a804939290
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
8
Assets/Mirror/Transports/SimpleWeb/Editor.meta
Normal file
8
Assets/Mirror/Transports/SimpleWeb/Editor.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a7a134b4d9eef45239a2d7caf7f52c3e
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,71 @@
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror.SimpleWeb.Editor
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
[CustomPropertyDrawer(typeof(ClientWebsocketSettings))]
|
||||
public class ClientWebsocketSettingsDrawer : PropertyDrawer
|
||||
{
|
||||
readonly string websocketPortOptionName = nameof(ClientWebsocketSettings.ClientPortOption);
|
||||
readonly string customPortName = nameof(ClientWebsocketSettings.CustomClientPort);
|
||||
readonly GUIContent portOptionLabel = new GUIContent("Client Port Option",
|
||||
"Specify what port the client websocket connection uses (default same as server port)");
|
||||
|
||||
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
|
||||
{
|
||||
property.isExpanded = true;
|
||||
return SumPropertyHeights(property, websocketPortOptionName, customPortName);
|
||||
}
|
||||
|
||||
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
|
||||
{
|
||||
DrawPortSettings(position, property);
|
||||
}
|
||||
|
||||
void DrawPortSettings(Rect position, SerializedProperty property)
|
||||
{
|
||||
SerializedProperty portOptionProp = property.FindPropertyRelative(websocketPortOptionName);
|
||||
SerializedProperty portProp = property.FindPropertyRelative(customPortName);
|
||||
float portOptionHeight = EditorGUI.GetPropertyHeight(portOptionProp);
|
||||
float portHeight = EditorGUI.GetPropertyHeight(portProp);
|
||||
float spacing = EditorGUIUtility.standardVerticalSpacing;
|
||||
bool wasEnabled = GUI.enabled;
|
||||
|
||||
position.height = portOptionHeight;
|
||||
|
||||
EditorGUI.PropertyField(position, portOptionProp, portOptionLabel);
|
||||
position.y += spacing + portOptionHeight;
|
||||
position.height = portHeight;
|
||||
|
||||
WebsocketPortOption portOption = (WebsocketPortOption)portOptionProp.enumValueIndex;
|
||||
if (portOption == WebsocketPortOption.MatchWebpageProtocol || portOption == WebsocketPortOption.DefaultSameAsServer)
|
||||
{
|
||||
int port = 0;
|
||||
if (property.serializedObject.targetObject is SimpleWebTransport swt)
|
||||
if (portOption == WebsocketPortOption.MatchWebpageProtocol)
|
||||
port = swt.clientUseWss ? 443 : 80;
|
||||
else
|
||||
port = swt.port;
|
||||
|
||||
GUI.enabled = false;
|
||||
EditorGUI.IntField(position, new GUIContent("Client Port"), port);
|
||||
GUI.enabled = wasEnabled;
|
||||
}
|
||||
else
|
||||
EditorGUI.PropertyField(position, portProp);
|
||||
|
||||
position.y += spacing + portHeight;
|
||||
}
|
||||
|
||||
float SumPropertyHeights(SerializedProperty property, params string[] propertyNames)
|
||||
{
|
||||
float totalHeight = 0;
|
||||
foreach (var name in propertyNames)
|
||||
totalHeight += EditorGUI.GetPropertyHeight(property.FindPropertyRelative(name)) + EditorGUIUtility.standardVerticalSpacing;
|
||||
|
||||
return totalHeight;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2d0e5b00ac8e45c99e68ad95cf843f80
|
||||
timeCreated: 1700081340
|
8
Assets/Mirror/Transports/SimpleWeb/SimpleWeb.meta
Normal file
8
Assets/Mirror/Transports/SimpleWeb/SimpleWeb.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8ae237a052b29fc4b8d000f48e545bb7
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,7 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: AssemblyVersion("1.6.0")]
|
||||
|
||||
[assembly: InternalsVisibleTo("SimpleWebTransport.Tests.Runtime")]
|
||||
[assembly: InternalsVisibleTo("SimpleWebTransport.Tests.Editor")]
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user