From d66d228079d53e5e446682e84dffacf31a89f0a6 Mon Sep 17 00:00:00 2001 From: vis2k Date: Fri, 16 Jul 2021 13:04:21 +0800 Subject: [PATCH] fix: kcp2k V1.12 - where-allocation removed. will be optional in the future. - Tests: don't depend on Unity anymore - fix: #26 - Kcp now catches exception if host couldn't be resolved, and calls OnDisconnected to let the user now. - fix: KcpServer.DualMode is now configurable in the constructor instead of using #if UNITY_SWITCH. makes it run on all other non dual mode platforms too. --- .../KCP/MirrorTransport/KcpTransport.cs | 3 + .../Runtime/Transport/KCP/kcp2k/VERSION | 12 +++- .../kcp2k/highlevel/KcpClientConnection.cs | 49 ++++++++------ .../KCP/kcp2k/highlevel/KcpServer.cs | 67 ++++++++----------- .../kcp2k/highlevel/KcpServerConnection.cs | 11 +-- 5 files changed, 72 insertions(+), 70 deletions(-) diff --git a/Assets/Mirror/Runtime/Transport/KCP/MirrorTransport/KcpTransport.cs b/Assets/Mirror/Runtime/Transport/KCP/MirrorTransport/KcpTransport.cs index 823504bc5..189bedb2c 100644 --- a/Assets/Mirror/Runtime/Transport/KCP/MirrorTransport/KcpTransport.cs +++ b/Assets/Mirror/Runtime/Transport/KCP/MirrorTransport/KcpTransport.cs @@ -16,6 +16,8 @@ public class KcpTransport : Transport // common [Header("Transport Configuration")] public ushort Port = 7777; + [Tooltip("DualMode listens to IPv6 and IPv4 simultaneously. Disable if the platform only supports IPv4.")] + public bool DualMode = true; [Tooltip("NoDelay is recommended to reduce latency. This also scales better without buffers getting full.")] public bool NoDelay = true; [Tooltip("KCP internal update interval. 100ms is KCP default, but a lower interval is recommended to minimize latency and to scale to more networked entities.")] @@ -69,6 +71,7 @@ void Awake() (connectionId) => OnServerConnected.Invoke(connectionId), (connectionId, message) => OnServerDataReceived.Invoke(connectionId, message, Channels.Reliable), (connectionId) => OnServerDisconnected.Invoke(connectionId), + DualMode, NoDelay, Interval, FastResend, diff --git a/Assets/Mirror/Runtime/Transport/KCP/kcp2k/VERSION b/Assets/Mirror/Runtime/Transport/KCP/kcp2k/VERSION index 44a22cef1..b8de4bc66 100755 --- a/Assets/Mirror/Runtime/Transport/KCP/kcp2k/VERSION +++ b/Assets/Mirror/Runtime/Transport/KCP/kcp2k/VERSION @@ -1,6 +1,12 @@ -V1.11 [2021-06-01] -- perf: where-allocation (https://github.com/vis2k/where-allocation): - nearly removes all socket.SendTo/ReceiveFrom allocations +V1.12 [2021-07-16] +- where-allocation removed. will be optional in the future. +- Tests: don't depend on Unity anymore +- fix: #26 - Kcp now catches exception if host couldn't be resolved, and calls + OnDisconnected to let the user now. +- fix: KcpServer.DualMode is now configurable in the constructor instead of + using #if UNITY_SWITCH. makes it run on all other non dual mode platforms too. + +V1.11 rollback [2021-06-01] - perf: Segment MemoryStream initial capacity set to MTU to avoid early runtime resizing/allocations diff --git a/Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/KcpClientConnection.cs b/Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/KcpClientConnection.cs index 914ca4890..f2dde707a 100644 --- a/Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/KcpClientConnection.cs +++ b/Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/KcpClientConnection.cs @@ -1,6 +1,5 @@ using System.Net; using System.Net.Sockets; -using WhereAllocation; namespace kcp2k { @@ -13,30 +12,41 @@ public class KcpClientConnection : KcpConnection // => we need the MTU to fit channel + message! readonly byte[] rawReceiveBuffer = new byte[Kcp.MTU_DEF]; - // where-allocation - IPEndPointNonAlloc reusableEP; + // helper function to resolve host to IPAddress + public static bool ResolveHostname(string hostname, out IPAddress[] addresses) + { + try + { + addresses = Dns.GetHostAddresses(hostname); + return addresses.Length >= 1; + } + catch (SocketException) + { + Log.Info($"Failed to resolve host: {hostname}"); + addresses = null; + return false; + } + } public void Connect(string host, ushort port, bool noDelay, uint interval = Kcp.INTERVAL, int fastResend = 0, bool congestionWindow = true, uint sendWindowSize = Kcp.WND_SND, uint receiveWindowSize = Kcp.WND_RCV, int timeout = DEFAULT_TIMEOUT) { Log.Info($"KcpClient: connect to {host}:{port}"); - IPAddress[] ipAddress = Dns.GetHostAddresses(host); - if (ipAddress.Length < 1) - throw new SocketException((int)SocketError.HostNotFound); - remoteEndpoint = new IPEndPoint(ipAddress[0], port); - socket = new Socket(remoteEndpoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp); + // try resolve host name + if (ResolveHostname(host, out IPAddress[] addresses)) + { + remoteEndpoint = new IPEndPoint(addresses[0], port); + socket = new Socket(remoteEndpoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp); + socket.Connect(remoteEndpoint); + SetupKcp(noDelay, interval, fastResend, congestionWindow, sendWindowSize, receiveWindowSize, timeout); - // create reusableEP with same address family as remoteEndPoint. - // otherwise ReceiveFrom_NonAlloc couldn't use it. - reusableEP = new IPEndPointNonAlloc(ipAddress[0], port); + // client should send handshake to server as very first message + SendHandshake(); - socket.Connect(remoteEndpoint); - SetupKcp(noDelay, interval, fastResend, congestionWindow, sendWindowSize, receiveWindowSize, timeout); - - // client should send handshake to server as very first message - SendHandshake(); - - RawReceive(); + RawReceive(); + } + // otherwise call OnDisconnected to let the user know. + else OnDisconnected(); } // call from transport update @@ -48,8 +58,7 @@ public void RawReceive() { while (socket.Poll(0, SelectMode.SelectRead)) { - // where-allocation: receive nonalloc. - int msgLength = socket.ReceiveFrom_NonAlloc(rawReceiveBuffer, reusableEP); + int msgLength = socket.ReceiveFrom(rawReceiveBuffer, ref remoteEndpoint); // IMPORTANT: detect if buffer was too small for the // received msgLength. otherwise the excess // data would be silently lost. diff --git a/Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/KcpServer.cs b/Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/KcpServer.cs index b198ae47b..acedfc2c3 100644 --- a/Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/KcpServer.cs +++ b/Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/KcpServer.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Net; using System.Net.Sockets; -using WhereAllocation; namespace kcp2k { @@ -16,6 +15,9 @@ public class KcpServer public Action OnDisconnected; // configuration + // DualMode uses both IPv6 and IPv4. not all platforms support it. + // (Nintendo Switch, etc.) + public bool DualMode; // NoDelay is recommended to reduce latency. This also scales better // without buffers getting full. public bool NoDelay; @@ -42,14 +44,8 @@ public class KcpServer // state Socket socket; -#if UNITY_SWITCH - // switch does not support ipv6 - //EndPoint newClientEP = new IPEndPoint(IPAddress.Any, 0); - IPEndPointNonAlloc reusableClientEP = new IPEndPointNonAlloc(IPAddress.Any, 0); // where-allocation -#else - //EndPoint newClientEP = new IPEndPoint(IPAddress.IPv6Any, 0); - IPEndPointNonAlloc reusableClientEP = new IPEndPointNonAlloc(IPAddress.IPv6Any, 0); // where-allocation -#endif + EndPoint newClientEP; + // IMPORTANT: raw receive buffer always needs to be of 'MTU' size, even // if MaxMessageSize is larger. kcp always sends in MTU // segments and having a buffer smaller than MTU would @@ -63,6 +59,7 @@ public class KcpServer public KcpServer(Action OnConnected, Action> OnData, Action OnDisconnected, + bool DualMode, bool NoDelay, uint Interval, int FastResend = 0, @@ -74,6 +71,7 @@ public KcpServer(Action OnConnected, this.OnConnected = OnConnected; this.OnData = OnData; this.OnDisconnected = OnDisconnected; + this.DualMode = DualMode; this.NoDelay = NoDelay; this.Interval = Interval; this.FastResend = FastResend; @@ -81,6 +79,11 @@ public KcpServer(Action OnConnected, this.SendWindowSize = SendWindowSize; this.ReceiveWindowSize = ReceiveWindowSize; this.Timeout = Timeout; + + // create newClientEP either IPv4 or IPv6 + newClientEP = DualMode + ? new IPEndPoint(IPAddress.IPv6Any, 0) + : new IPEndPoint(IPAddress.Any, 0); } public bool IsActive() => socket != null; @@ -94,15 +97,19 @@ public void Start(ushort port) } // listen -#if UNITY_SWITCH - // Switch does not support ipv6 - socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); - socket.Bind(new IPEndPoint(IPAddress.Any, port)); -#else - socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp); - socket.DualMode = true; - socket.Bind(new IPEndPoint(IPAddress.IPv6Any, port)); -#endif + if (DualMode) + { + // IPv6 socket with DualMode + socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp); + socket.DualMode = true; + socket.Bind(new IPEndPoint(IPAddress.IPv6Any, port)); + } + else + { + // IPv4 socket + socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + socket.Bind(new IPEndPoint(IPAddress.Any, port)); + } } public void Send(int connectionId, ArraySegment segment, KcpChannel channel) @@ -143,13 +150,9 @@ public void TickIncoming() // receive from calls newClientEP.Create(socketAddr). // IPEndPoint.Create always returns a new IPEndPoint. // https://github.com/mono/mono/blob/f74eed4b09790a0929889ad7fc2cf96c9b6e3757/mcs/class/System/System.Net.Sockets/Socket.cs#L1761 - //int msgLength = socket.ReceiveFrom(rawReceiveBuffer, 0, rawReceiveBuffer.Length, SocketFlags.None, ref newClientEP); + int msgLength = socket.ReceiveFrom(rawReceiveBuffer, 0, rawReceiveBuffer.Length, SocketFlags.None, ref newClientEP); //Log.Info($"KCP: server raw recv {msgLength} bytes = {BitConverter.ToString(buffer, 0, msgLength)}"); - // where-allocation nonalloc ReceiveFrom. - int msgLength = socket.ReceiveFrom_NonAlloc(rawReceiveBuffer, 0, rawReceiveBuffer.Length, SocketFlags.None, reusableClientEP); - SocketAddress remoteAddress = reusableClientEP.temp; - // calculate connectionId from endpoint // NOTE: IPEndPoint.GetHashCode() allocates. // it calls m_Address.GetHashCode(). @@ -159,10 +162,7 @@ public void TickIncoming() // // => using only newClientEP.Port wouldn't work, because // different connections can have the same port. - //int connectionId = newClientEP.GetHashCode(); - - // where-allocation nonalloc GetHashCode - int connectionId = remoteAddress.GetHashCode(); + int connectionId = newClientEP.GetHashCode(); // IMPORTANT: detect if buffer was too small for the received // msgLength. otherwise the excess data would be @@ -173,19 +173,8 @@ public void TickIncoming() // is this a new connection? if (!connections.TryGetValue(connectionId, out KcpServerConnection connection)) { - // IPEndPointNonAlloc is reused all the time. - // we can't store that as the connection's endpoint. - // we need a new copy! - IPEndPoint newClientEP = reusableClientEP.DeepCopyIPEndPoint(); - - // for allocation free sending, we also need another - // IPEndPointNonAlloc... - IPEndPointNonAlloc reusableSendEP = new IPEndPointNonAlloc(newClientEP.Address, newClientEP.Port); - // create a new KcpConnection - // -> where-allocation IPEndPointNonAlloc is reused. - // need to create a new one from the temp address. - connection = new KcpServerConnection(socket, newClientEP, reusableSendEP, NoDelay, Interval, FastResend, CongestionWindow, SendWindowSize, ReceiveWindowSize, Timeout); + connection = new KcpServerConnection(socket, newClientEP, NoDelay, Interval, FastResend, CongestionWindow, SendWindowSize, ReceiveWindowSize, Timeout); // DO NOT add to connections yet. only if the first message // is actually the kcp handshake. otherwise it's either: diff --git a/Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/KcpServerConnection.cs b/Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/KcpServerConnection.cs index 295845ee9..767ea69ec 100644 --- a/Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/KcpServerConnection.cs +++ b/Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/KcpServerConnection.cs @@ -1,25 +1,20 @@ using System.Net; using System.Net.Sockets; -using WhereAllocation; namespace kcp2k { public class KcpServerConnection : KcpConnection { - IPEndPointNonAlloc reusableSendEndPoint; - - public KcpServerConnection(Socket socket, EndPoint remoteEndPoint, IPEndPointNonAlloc reusableSendEndPoint, bool noDelay, uint interval = Kcp.INTERVAL, int fastResend = 0, bool congestionWindow = true, uint sendWindowSize = Kcp.WND_SND, uint receiveWindowSize = Kcp.WND_RCV, int timeout = DEFAULT_TIMEOUT) + public KcpServerConnection(Socket socket, EndPoint remoteEndpoint, bool noDelay, uint interval = Kcp.INTERVAL, int fastResend = 0, bool congestionWindow = true, uint sendWindowSize = Kcp.WND_SND, uint receiveWindowSize = Kcp.WND_RCV, int timeout = DEFAULT_TIMEOUT) { this.socket = socket; - this.remoteEndpoint = remoteEndPoint; - this.reusableSendEndPoint = reusableSendEndPoint; + this.remoteEndpoint = remoteEndpoint; SetupKcp(noDelay, interval, fastResend, congestionWindow, sendWindowSize, receiveWindowSize, timeout); } protected override void RawSend(byte[] data, int length) { - // where-allocation nonalloc - socket.SendTo_NonAlloc(data, 0, length, SocketFlags.None, reusableSendEndPoint); + socket.SendTo(data, 0, length, SocketFlags.None, remoteEndpoint); } } }