Hi folks,
I put some efforts into it and came up with a solution. It was such a pain thought I had to share.
I based my thinking on Giuseppe Portelli’s article but I had to take a slightly different approach to make it work.
That’s because initial connection takes up to 2s to establish, giving wrong ping time measurements. So sending a 10 packets burst right away doesn’t work, they all come back with 1-2 seconds delay.
So what I do is repeatidly send ping packets to lobby servers during a given period every second. Which gives good results.
First I added an array to UNetDriver class to hold responses from servers:
in Engine\Source\Runtime\Engine\Classes\Engine\NetDriver.h :
add this struct definition
// RQ Start
struct FPingEntry
{
uint8 OwnerId[8];
double Ping;
};
// RQ End
Inside body of class UNetDriver, in public section, add
// RQ Start
TArray<FPingEntry> PingEntries;
// RQ End
Now, in Engine\Source\Runtime\Engine\Private\PacketHandlers\StatelessConnectHandlerComponent.cpp,
Find StatelessConnectHandlerComponent::IncomingConnectionless(FString Address, FBitReader& Packet) and modify it like this (my modifications are encompassed between RQ Start / RQ End) :
void StatelessConnectHandlerComponent::IncomingConnectionless(FString Address, FBitReader& Packet)
{
bool bHandshakePacket = !!Packet.ReadBit() && !Packet.IsError();
// RQ Start
bool bIsPingPacket = false;
bool bIsPongPacket = false;
// RQ End
LastChallengeSuccessAddress.Empty();
if (bHandshakePacket)
{
uint8 SecretId = 0;
float Timestamp = 1.f;
uint8 Cookie[20];
bHandshakePacket = ParseHandshakePacket(Packet, SecretId, Timestamp, Cookie);
if (bHandshakePacket)
{
if (Handler->Mode == Handler::Mode::Server)
{
bool bInitialConnect = Timestamp == 0.f;
if (bInitialConnect)
{
SendConnectChallenge(Address);
}
// Challenge response
else if (Driver != nullptr)
{
bool bChallengeSuccess = false;
float CookieDelta = Driver->Time - Timestamp;
float SecretDelta = Timestamp - LastSecretUpdateTimestamp;
bool bValidCookieLifetime = CookieDelta > 0.0 && (MAX_COOKIE_LIFETIME - CookieDelta) > 0.f;
bool bValidSecretIdTimestamp = (SecretId == ActiveSecret) ? (SecretDelta >= 0.f) : (SecretDelta <= 0.f);
if (bValidCookieLifetime && bValidSecretIdTimestamp)
{
// Regenerate cookie from packet info, and see if received cookie matches regenerated one
uint8 RegenCookie[20];
GenerateCookie(Address, SecretId, Timestamp, RegenCookie);
bChallengeSuccess = FMemory::Memcmp(Cookie, RegenCookie, 20) == 0;
}
if (bChallengeSuccess)
{
LastChallengeSuccessAddress = Address;
}
}
}
}
else
{
Packet.SetError();
#if !UE_BUILD_SHIPPING
UE_LOG(LogHandshake, Log, TEXT("Error reading handshake packet."));
#endif
}
}
// RQ Start
else if(!Packet.IsError())
{
// check header to see if it's a ping packet
uint8 Header;
Packet.SerializeBits(&Header, 7);
if((Header<<1) == 74)
bIsPingPacket = true;
else if((Header<<1) == 54)
bIsPongPacket = true;
}
if(bIsPingPacket)
{
// receiving a ping packet
if(Driver != NULL && Driver->IsNetResourceValid())
{
uint8 * InPacketDataPtr = Packet.GetData();
// build an output packet with original data but PongHeader (must be even)
FArrayWriter Writer;
Writer.Add(54);
// copy incoming data to out packet
for(int32 i = 1; i < Packet.GetNumBytes(); i++)
Writer.Add(InPacketDataPtr[i]);
// pong this back to client
Handler->SetRawSend(true);
Driver->LowLevelSend(Address, Writer.GetData(), Writer.Num()*8);
Handler->SetRawSend(false);
}
}
else if(bIsPongPacket)
{
// receiving a pong packet
if(Driver != NULL)
{
// get OwnerId bytes out of packet
uint8 OwnerId[8];
Packet.Serialize(OwnerId, 8);
// get time stamp out of packet
double PacketTime;
Packet.Serialize(&PacketTime, sizeof(double));
// look for this OwnerId in Ping entries
int32 EntryIdx = -1;
int32 i;
for(i = 0; i < Driver->PingEntries.Num(); i++)
{
int32 j = 0;
while(j < 8 && OwnerId[j] == Driver->PingEntries[i].OwnerId[j])
j++;
if(j == 8)
break;
}
if(i < Driver->PingEntries.Num())
{
// found, use this entry
EntryIdx = i;
}
else
{
// not found, add a new entry
if(Driver->PingEntries.Num() < 200) // put a limit
{
EntryIdx = Driver->PingEntries.Num();
FPingEntry NewEntry;
for(int32 j = 0; j < 8; j++)
NewEntry.OwnerId[j] = OwnerId[j];
Driver->PingEntries.Add(NewEntry);
}
}
// update ping entry value
if(EntryIdx >= 0)
Driver->PingEntries[EntryIdx].Ping = FPlatformTime::Seconds() - PacketTime;
}
}
// RQ End
#if !UE_BUILD_SHIPPING
else if (Packet.IsError())
{
UE_LOG(LogHandshake, Log, TEXT("Error reading handshake bit from packet."));
}
#endif
}
It’s not over yet. Now in my game, I made a few functions to handle this:
#include "Networking.h"
#include "Sockets.h"
#include "SocketSubsystem.h"
...
// -----------------------------------------------------------------
void UServersPageWidget::PingServer(const FOnlineSessionSearchResult & Result)
{
ISocketSubsystem* SocketSubsystem = ISocketSubsystem::Get();
FString ConnectInfo;
IOnlineSubsystem* OnlineSub = IOnlineSubsystem::Get();
IOnlineSessionPtr Sessions = OnlineSub->GetSessionInterface();
Sessions->GetResolvedConnectString(Result, GamePort, ConnectInfo);
TSharedRef<FInternetAddr> Addr = SocketSubsystem->CreateInternetAddr();
bool bIsValid;
Addr->SetIp(*ConnectInfo, bIsValid);
if (bIsValid)
{
// Creating client socket
FSocket* Socket = SocketSubsystem->CreateSocket(FName("SteamClientSocket"), FString("PingSocket"), true);
FArrayWriter Writer;
// add a header to packet so we can recognize it on other side
// could any value but LSB must be 0, 1 would mean "handshake packet"
Writer.Add(74);
// write OwnerId data to packet
const uint8 * OwnerIdPtr = Result.Session.OwningUserId->GetBytes();
int32 OwningIdSize = Result.Session.OwningUserId->GetSize();
for(int32 i = 0; i < OwningIdSize; i++)
Writer.Add(OwnerIdPtr[i]);
// write time stamp data to packet
double TimeStamp = FPlatformTime::Seconds();
uint8 * TimeStampPtr = (uint8*)&TimeStamp;
for(int32 i = 0; i < sizeof(double); i++)
Writer.Add(TimeStampPtr[i]);
// add a trailing byte to ensure a non zero ending packet which would be considered an error when received
Writer.Add(255);
// Sending 10 ping packets
int32 BytesSent;
Socket->SendTo(Writer.GetData(), Writer.Num(), BytesSent, Addr.Get());
SocketSubsystem->DestroySocket(Socket);
}
}
// -----------------------------------------------------------------
double UServersPageWidget::GetPingByOwner(TSharedPtr<const FUniqueNetId> OwningUserId)
{
UNetDriver * NetDriver = GetWorld()->GetNetDriver();
if(OwningUserId.IsValid() && NetDriver != NULL)
{
const uint8 * OwnerIdPtr = OwningUserId->GetBytes();
int32 OwningIdSize = OwningUserId->GetSize();
for(int32 k = 0; k < NetDriver->PingEntries.Num(); k++)
{
int32 j = 0;
while(j < OwningIdSize && j < 8 && OwnerIdPtr[j] == NetDriver->PingEntries[k].OwnerId[j])
j++;
if(j == 8)
return NetDriver->PingEntries[k].Ping;
}
}
return -1.0;
}
// -----------------------------------------------------------------
void UServersPageWidget::ClearPingArray()
{
UNetDriver * NetDriver = GetWorld()->GetNetDriver();
if(NetDriver != NULL)
NetDriver->PingEntries.Empty();
}
PingServer(const FOnlineSessionSearchResult & Result) builds & sends a ping packet to server. You have to call that on all your search results on a regular time basis. I do it every second for at least 10 secs.
To get Ping values, call GetPingByOwner(TSharedPtr OwningUserId). result is in seconds, so you have to multilply it by 1000 to get a ms ping measurement.
When initiating a new search, I clear PingEntries in array by calling ClearPingArray().
Important notes :
- Your game must be listening in order to receive Pong packets back from servers. So be sure to add ?Listen to your arguments or call UWorld::InitListen() (or something like that)
- Your game might be reported as a potential threat by some AntiVirus software to user. Annoying.
I commented my code, so you might be able to get how it works. I assumed that Steam NetIds are 8 bytes long.
Some optimizations are possible.
Good luck