diff --git a/kcp2k/kcp2k.Tests/ClientServerTests.cs b/kcp2k/kcp2k.Tests/ClientServerTests.cs index 2460a12..9472e3a 100644 --- a/kcp2k/kcp2k.Tests/ClientServerTests.cs +++ b/kcp2k/kcp2k.Tests/ClientServerTests.cs @@ -952,6 +952,24 @@ public void TimeoutIsResetByPing() Assert.That(server.connections.Count, Is.EqualTo(1)); } + [Test] + public void PingUpdatesRtt() + { + Assert.That(client.rttInMilliseconds, Is.EqualTo(0)); + + server.Start(Port); + ConnectClientBlocking(); + int connectionId = ServerFirstConnectionId(); + + // update a few times, let at least PING_INTERVAL elapse, update again + UpdateSeveralTimes(10); + Thread.Sleep(KcpPeer.PING_INTERVAL); + UpdateSeveralTimes(10); + + Assert.That(client.rttInMilliseconds, Is.GreaterThan(0)); + Assert.That(server.connections[connectionId].rttInMilliseconds, Is.GreaterThan(0)); + } + // fake a kcp dead_link by setting state = -1. // KcpConnection should detect it and disconnect. [Test] diff --git a/kcp2k/kcp2k/highlevel/KcpHeader.cs b/kcp2k/kcp2k/highlevel/KcpHeader.cs index 13d198e..937cec7 100644 --- a/kcp2k/kcp2k/highlevel/KcpHeader.cs +++ b/kcp2k/kcp2k/highlevel/KcpHeader.cs @@ -15,6 +15,7 @@ public enum KcpHeaderReliable : byte // we already have a KcpHeader for reliable messages. // ping is only used to keep it alive, so latency doesn't matter. Ping = 2, + Pong = 4, // '4' not '3' in order to keep backwards compatibility Data = 3, } diff --git a/kcp2k/kcp2k/highlevel/KcpPeer.cs b/kcp2k/kcp2k/highlevel/KcpPeer.cs index 8f0aac3..8e39272 100644 --- a/kcp2k/kcp2k/highlevel/KcpPeer.cs +++ b/kcp2k/kcp2k/highlevel/KcpPeer.cs @@ -62,6 +62,7 @@ public abstract class KcpPeer // same goes for slow paced card games etc. public const int PING_INTERVAL = 1000; uint lastPingTime; + uint lastPongTime; // if we send more than kcp can handle, we will get ever growing // send/recv buffers and queues and minutes of latency. @@ -144,6 +145,10 @@ public static int UnreliableMaxMessageSize(int mtu) => public readonly int unreliableMax; public readonly int reliableMax; + // round trip time (RTT) for convenience. + // this is the time that it takes for a reliable message to travel to remote and back. + public uint rttInMilliseconds { get; private set; } + // 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. @@ -360,6 +365,13 @@ void TickIncoming_Connected(uint time) case KcpHeaderReliable.Ping: { // ping keeps kcp from timing out. do nothing. + // safety: don't reply with pong message before authenticated. + break; + } + case KcpHeaderReliable.Pong: + { + // ping keeps kcp from timing out. do nothing. + // safety: don't handle pong message before authenticated. break; } case KcpHeaderReliable.Data: @@ -417,7 +429,34 @@ void TickIncoming_Authenticated(uint time) } case KcpHeaderReliable.Ping: { - // ping keeps kcp from timing out. do nothing. + // ping includes the sender's local time for RTT calculation. + // simply send it back to the sender. + // for safety, we only reply every PING_INTERVAL at max. + // so attackers can't force us to reply a PONG every time. + if (message.Count == 4) + { + if (time >= lastPongTime + PING_INTERVAL) + { + Utils.Decode32U(message.Array, message.Offset, out uint pingTimestamp); + SendPong(pingTimestamp); + lastPongTime = time; + } + } + break; + } + // ping keeps kcp from timing out, and is used for RTT calcualtion + case KcpHeaderReliable.Pong: + { + if (message.Count == 4) + { + Utils.Decode32U(message.Array, message.Offset, out uint originalTimestamp); + if (time >= originalTimestamp) + { + rttInMilliseconds = time - originalTimestamp; + // Log.Info($"[KCP] {GetType()}: RTT={rttInMilliseconds}ms"); + } + } + break; } } @@ -736,7 +775,22 @@ public void SendData(ArraySegment data, KcpChannel channel) // ping goes through kcp to keep it from timing out, so it goes over the // reliable channel. - void SendPing() => SendReliable(KcpHeaderReliable.Ping, default); + readonly byte[] pingData = new byte[4]; // 4 bytes timestamp + void SendPing() + { + // when sending ping, include the local timestamp so we can + // calculate RTT from the pong. + Utils.Encode32U(pingData, 0, time); + SendReliable(KcpHeaderReliable.Ping, pingData); + } + + void SendPong(uint pingTimestamp) + { + // when sending ping, include the local timestamp so we can + // calculate RTT from the pong. + Utils.Encode32U(pingData, 0, pingTimestamp); + SendReliable(KcpHeaderReliable.Pong, pingData); + } // send disconnect message void SendDisconnect()