Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: Ping message includes timestamp for RTT #66

Merged
merged 4 commits into from
Sep 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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