diff --git a/Assets/Mirror/Runtime/Transport/KCP/MirrorTransport/KcpTransport.cs b/Assets/Mirror/Runtime/Transport/KCP/MirrorTransport/KcpTransport.cs index 1b6e6191b..4e77ff248 100644 --- a/Assets/Mirror/Runtime/Transport/KCP/MirrorTransport/KcpTransport.cs +++ b/Assets/Mirror/Runtime/Transport/KCP/MirrorTransport/KcpTransport.cs @@ -20,6 +20,9 @@ public class KcpTransport : Transport public bool NoDelay = true; [Tooltip("KCP internal update interval. 100ms is KCP default, but a lower interval is recommended to minimize latency and to scale to more networked entities.")] public uint Interval = 10; + [Tooltip("KCP timeout in milliseconds. Note that KCP sends a ping automatically.")] + public int Timeout = 10000; + [Header("Advanced")] [Tooltip("KCP fastresend parameter. Faster resend for the cost of higher bandwidth. 0 in normal mode, 2 in turbo mode.")] public int FastResend = 2; @@ -71,7 +74,8 @@ void Awake() FastResend, CongestionWindow, SendWindowSize, - ReceiveWindowSize + ReceiveWindowSize, + Timeout ); if (statisticsLog) @@ -88,7 +92,7 @@ public override bool Available() => public override bool ClientConnected() => client.connected; public override void ClientConnect(string address) { - client.Connect(address, Port, NoDelay, Interval, FastResend, CongestionWindow, SendWindowSize, ReceiveWindowSize); + client.Connect(address, Port, NoDelay, Interval, FastResend, CongestionWindow, SendWindowSize, ReceiveWindowSize, Timeout); } public override void ClientSend(ArraySegment segment, int channelId) { diff --git a/Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/KcpClient.cs b/Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/KcpClient.cs index 97612ba70..d2420a939 100644 --- a/Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/KcpClient.cs +++ b/Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/KcpClient.cs @@ -22,7 +22,7 @@ public KcpClient(Action OnConnected, Action> OnData, Action O this.OnDisconnected = OnDisconnected; } - public void Connect(string address, ushort port, bool noDelay, uint interval, int fastResend = 0, bool congestionWindow = true, uint sendWindowSize = Kcp.WND_SND, uint receiveWindowSize = Kcp.WND_RCV) + public void Connect(string address, ushort port, bool noDelay, uint interval, int fastResend = 0, bool congestionWindow = true, uint sendWindowSize = Kcp.WND_SND, uint receiveWindowSize = Kcp.WND_RCV, int timeout = KcpConnection.DEFAULT_TIMEOUT) { if (connected) { @@ -53,7 +53,7 @@ public void Connect(string address, ushort port, bool noDelay, uint interval, in }; // connect - connection.Connect(address, port, noDelay, interval, fastResend, congestionWindow, sendWindowSize, receiveWindowSize); + connection.Connect(address, port, noDelay, interval, fastResend, congestionWindow, sendWindowSize, receiveWindowSize, timeout); } public void Send(ArraySegment segment, KcpChannel channel) diff --git a/Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/KcpClientConnection.cs b/Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/KcpClientConnection.cs index bab332889..47157e265 100644 --- a/Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/KcpClientConnection.cs +++ b/Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/KcpClientConnection.cs @@ -12,7 +12,7 @@ public class KcpClientConnection : KcpConnection // => we need the MTU to fit channel + message! readonly byte[] rawReceiveBuffer = new byte[Kcp.MTU_DEF]; - 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) + 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); @@ -22,7 +22,7 @@ public void Connect(string host, ushort port, bool noDelay, uint interval = Kcp. remoteEndpoint = new IPEndPoint(ipAddress[0], port); socket = new Socket(remoteEndpoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp); socket.Connect(remoteEndpoint); - SetupKcp(noDelay, interval, fastResend, congestionWindow, sendWindowSize, receiveWindowSize); + SetupKcp(noDelay, interval, fastResend, congestionWindow, sendWindowSize, receiveWindowSize, timeout); // client should send handshake to server as very first message SendHandshake(); diff --git a/Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/KcpConnection.cs b/Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/KcpConnection.cs index 5cdc1ab81..adbd927b6 100644 --- a/Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/KcpConnection.cs +++ b/Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/KcpConnection.cs @@ -28,7 +28,8 @@ public abstract class KcpConnection // If we don't receive anything these many milliseconds // then consider us disconnected - public const int TIMEOUT = 10000; + public const int DEFAULT_TIMEOUT = 10000; + public int timeout = DEFAULT_TIMEOUT; uint lastReceiveTime; // internal time. @@ -123,9 +124,11 @@ public abstract class KcpConnection public uint MaxReceiveRate => kcp.rcv_wnd * kcp.mtu * 1000 / kcp.interval; - // NoDelay, interval, window size are the most important configurations. - // let's force require the parameters so we don't forget it anywhere. - protected void SetupKcp(bool noDelay, uint interval = Kcp.INTERVAL, int fastResend = 0, bool congestionWindow = true, uint sendWindowSize = Kcp.WND_SND, uint receiveWindowSize = Kcp.WND_RCV) + // SetupKcp creates and configures a new KCP instance. + // => useful to start from a fresh state every time the client connects + // => NoDelay, interval, wnd size are the most important configurations. + // let's force require the parameters so we don't forget it anywhere. + protected void SetupKcp(bool noDelay, uint interval = Kcp.INTERVAL, int fastResend = 0, bool congestionWindow = true, uint sendWindowSize = Kcp.WND_SND, uint receiveWindowSize = Kcp.WND_RCV, int timeout = DEFAULT_TIMEOUT) { // set up kcp over reliable channel (that's what kcp is for) kcp = new Kcp(0, RawSendReliable); @@ -140,6 +143,7 @@ protected void SetupKcp(bool noDelay, uint interval = Kcp.INTERVAL, int fastRese // message afterwards. kcp.SetMtu(Kcp.MTU_DEF - CHANNEL_HEADER_SIZE); + this.timeout = timeout; state = KcpState.Connected; refTime.Start(); @@ -149,9 +153,9 @@ 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) + if (time >= lastReceiveTime + timeout) { - Log.Warning($"KCP: Connection timed out after not receiving any message for {TIMEOUT}ms. Disconnecting."); + Log.Warning($"KCP: Connection timed out after not receiving any message for {timeout}ms. Disconnecting."); Disconnect(); } } @@ -240,6 +244,7 @@ bool ReceiveNextReliable(out KcpHeader header, out ArraySegment message) } } + message = default; header = KcpHeader.Disconnect; return false; } diff --git a/Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/KcpServer.cs b/Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/KcpServer.cs index 2195ce154..a73cc7f62 100644 --- a/Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/KcpServer.cs +++ b/Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/KcpServer.cs @@ -36,6 +36,8 @@ public class KcpServer // 8192, 8192 for 20k monsters public uint SendWindowSize; public uint ReceiveWindowSize; + // timeout in milliseconds + public int Timeout; // state Socket socket; @@ -63,7 +65,8 @@ public KcpServer(Action OnConnected, int FastResend = 0, bool CongestionWindow = true, uint SendWindowSize = Kcp.WND_SND, - uint ReceiveWindowSize = Kcp.WND_RCV) + uint ReceiveWindowSize = Kcp.WND_RCV, + int Timeout = KcpConnection.DEFAULT_TIMEOUT) { this.OnConnected = OnConnected; this.OnData = OnData; @@ -74,6 +77,7 @@ public KcpServer(Action OnConnected, this.CongestionWindow = CongestionWindow; this.SendWindowSize = SendWindowSize; this.ReceiveWindowSize = ReceiveWindowSize; + this.Timeout = Timeout; } public bool IsActive() => socket != null; @@ -131,10 +135,23 @@ public void TickIncoming() { try { + // NOTE: ReceiveFrom allocates. + // we pass our IPEndPoint to ReceiveFrom. + // receive from calls newClientEP.Create(socketAddr). + // IPEndPoint.Create always returns a new IPEndPoint. + // https://github.com/mono/mono/blob/f74eed4b09790a0929889ad7fc2cf96c9b6e3757/mcs/class/System/System.Net.Sockets/Socket.cs#L1761 int msgLength = socket.ReceiveFrom(rawReceiveBuffer, 0, rawReceiveBuffer.Length, SocketFlags.None, ref newClientEP); //Log.Info($"KCP: server raw recv {msgLength} bytes = {BitConverter.ToString(buffer, 0, msgLength)}"); // calculate connectionId from endpoint + // NOTE: IPEndPoint.GetHashCode() allocates. + // it calls m_Address.GetHashCode(). + // m_Address is an IPAddress. + // GetHashCode() allocates for IPv6: + // https://github.com/mono/mono/blob/bdd772531d379b4e78593587d15113c37edd4a64/mcs/class/referencesource/System/net/System/Net/IPAddress.cs#L699 + // + // => using only newClientEP.Port wouldn't work, because + // different connections can have the same port. int connectionId = newClientEP.GetHashCode(); // IMPORTANT: detect if buffer was too small for the received @@ -147,7 +164,7 @@ public void TickIncoming() if (!connections.TryGetValue(connectionId, out KcpServerConnection connection)) { // create a new KcpConnection - connection = new KcpServerConnection(socket, newClientEP, NoDelay, Interval, FastResend, CongestionWindow, SendWindowSize, ReceiveWindowSize); + 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 bd2358efe..767ea69ec 100644 --- a/Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/KcpServerConnection.cs +++ b/Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/KcpServerConnection.cs @@ -5,11 +5,11 @@ namespace kcp2k { public class KcpServerConnection : KcpConnection { - 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) + public KcpServerConnection(Socket socket, EndPoint remoteEndpoint, bool noDelay, uint interval = Kcp.INTERVAL, int fastResend = 0, bool congestionWindow = true, uint sendWindowSize = Kcp.WND_SND, uint receiveWindowSize = Kcp.WND_RCV, int timeout = DEFAULT_TIMEOUT) { this.socket = socket; this.remoteEndpoint = remoteEndpoint; - SetupKcp(noDelay, interval, fastResend, congestionWindow, sendWindowSize, receiveWindowSize); + SetupKcp(noDelay, interval, fastResend, congestionWindow, sendWindowSize, receiveWindowSize, timeout); } protected override void RawSend(byte[] data, int length) diff --git a/Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Kcp.cs b/Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Kcp.cs index bb3676e15..9e684fab9 100755 --- a/Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Kcp.cs +++ b/Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Kcp.cs @@ -87,6 +87,17 @@ internal struct AckItem // get how many packet is waiting to be sent public int WaitSnd => snd_buf.Count + snd_queue.Count; + // segment pool to avoid allocations in C#. + // this is not part of the original C code. + readonly Pool SegmentPool = new Pool( + // create new segment + () => new Segment(), + // reset segment before reuse + (segment) => segment.Reset(), + // initial capacity + 32 + ); + // ikcp_create // create a new kcp control object, 'conv' must equal in two endpoint // from the same connection. @@ -112,18 +123,12 @@ public Kcp(uint conv, Action output) // ikcp_segment_new // we keep the original function and add our pooling to it. // this way we'll never miss it anywhere. - static Segment SegmentNew() - { - return Segment.Take(); - } + Segment SegmentNew() => SegmentPool.Take(); // ikcp_segment_delete // we keep the original function and add our pooling to it. // this way we'll never miss it anywhere. - static void SegmentDelete(Segment seg) - { - Segment.Return(seg); - } + void SegmentDelete(Segment seg) => SegmentPool.Return(seg); // ikcp_recv // receive data from kcp state machine diff --git a/Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Pool.cs b/Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Pool.cs new file mode 100644 index 000000000..81b528955 --- /dev/null +++ b/Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Pool.cs @@ -0,0 +1,46 @@ +// Pool to avoid allocations (from libuv2k & Mirror) +using System; +using System.Collections.Generic; + +namespace kcp2k +{ + public class Pool + { + // Mirror is single threaded, no need for concurrent collections + readonly Stack objects = new Stack(); + + // some types might need additional parameters in their constructor, so + // we use a Func generator + readonly Func objectGenerator; + + // some types might need additional cleanup for returned objects + readonly Action objectResetter; + + public Pool(Func objectGenerator, Action 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; + } +} diff --git a/Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Pool.cs.meta b/Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Pool.cs.meta new file mode 100644 index 000000000..5eba0e08e --- /dev/null +++ b/Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Pool.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 35c07818fc4784bb4ba472c8e5029002 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Segment.cs b/Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Segment.cs index fa2bac7e8..6dffd58be 100755 --- a/Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Segment.cs +++ b/Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Segment.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using System.IO; namespace kcp2k @@ -22,26 +21,6 @@ internal class Segment // note: no need to pool it, because Segment is already pooled. internal MemoryStream data = new MemoryStream(); - // pool //////////////////////////////////////////////////////////////// - internal static readonly Stack Pool = new Stack(32); - - public static Segment Take() - { - if (Pool.Count > 0) - { - Segment seg = Pool.Pop(); - return seg; - } - return new Segment(); - } - - public static void Return(Segment seg) - { - seg.Reset(); - Pool.Push(seg); - } - //////////////////////////////////////////////////////////////////////// - // ikcp_encode_seg // encode a segment into buffer internal int Encode(byte[] ptr, int offset)