Skip to content

Commit

Permalink
Merge pull request #66 from MirrorNetworking/rtt
Browse files Browse the repository at this point in the history
feature: Ping message includes timestamp for RTT
  • Loading branch information
miwarnec authored Sep 27, 2024
2 parents 9c7ada3 + 26f64b6 commit efbe139
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 2 deletions.
18 changes: 18 additions & 0 deletions kcp2k/kcp2k.Tests/ClientServerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
1 change: 1 addition & 0 deletions kcp2k/kcp2k/highlevel/KcpHeader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}

Expand Down
58 changes: 56 additions & 2 deletions kcp2k/kcp2k/highlevel/KcpPeer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -736,7 +775,22 @@ public void SendData(ArraySegment<byte> 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()
Expand Down

0 comments on commit efbe139

Please sign in to comment.