Files
GameNetworkingSockets/tests/test_connection.cpp
T
2026-04-28 10:11:25 -07:00

1464 lines
53 KiB
C++

#include "test_common.h"
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <string>
#include <random>
#include <chrono>
#include <thread>
#include <steam/steamnetworkingsockets.h>
#include <steam/isteamnetworkingutils.h>
#ifndef STEAMNETWORKINGSOCKETS_OPENSOURCE
#include <steam/steam_api.h>
#endif
// It's 2021 and the C language doesn't have a cross-platform way to
// compare strings in a case-insensitive way
#ifdef _MSC_VER
#define strcasecmp(a,b) stricmp(a,b)
#endif
enum class ETestConnectionMode
{
Cursory, // Very fast sanity check: 1 condition, short durations
Normal, // Standard: 2 conditions, moderate durations
Soak, // Exhaustive: all conditions, long durations
};
static std::default_random_engine g_rand;
static SteamNetworkingMicroseconds g_usecTestElapsed;
static HSteamListenSocket g_hSteamListenSocket = k_HSteamListenSocket_Invalid;
struct TestMsg
{
int64 m_nMsgNum;
SteamNetworkingMicroseconds m_usecWhenSent;
bool m_bReliable;
int m_cbSize;
static constexpr int k_cbMaxSize = 10*1000;
uint8 m_data[ k_cbMaxSize ];
};
struct SFakePeer
{
SFakePeer( const char *pName )
: m_sName( pName )
{
Reset();
}
std::string m_sName;
int64 m_nReliableSendMsgCount;
int64 m_nUnreliableSendMsgCount;
int64 m_nReliableExpectedRecvMsg;
int64 m_nExpectedRecvMsg;
float m_flReliableMsgDelay;
float m_flUnreliableMsgDelay;
HSteamNetConnection m_hSteamNetConnection;
bool m_bIsConnected;
int m_cbSendBuffer;
SteamNetConnectionRealTimeStatus_t m_realtimeStatus;
float m_flSendRate;
float m_flRecvRate;
int64 m_nSendInterval;
int64 m_nRecvInterval;
void Reset()
{
m_nReliableSendMsgCount = 0;
m_nUnreliableSendMsgCount = 0;
m_nReliableExpectedRecvMsg = 1;
m_nExpectedRecvMsg = 1;
m_flReliableMsgDelay = 0.0f;
m_flUnreliableMsgDelay = 0.0f;
m_hSteamNetConnection = k_HSteamNetConnection_Invalid;
m_bIsConnected = false;
m_cbSendBuffer = 384 * 1024;
memset( &m_realtimeStatus, 0, sizeof(m_realtimeStatus) );
m_flSendRate = 0.0f;
m_flRecvRate = 0.0f;
m_nSendInterval = 0;
m_nRecvInterval = 0;
}
void Close()
{
if ( m_hSteamNetConnection != k_HSteamNetConnection_Invalid )
{
SteamNetworkingSockets()->CloseConnection( m_hSteamNetConnection, 0, nullptr, false );
m_hSteamNetConnection = k_HSteamNetConnection_Invalid;
}
Reset();
}
inline void UpdateInterval( float flElapsed )
{
m_flSendRate = m_nSendInterval / flElapsed;
m_flRecvRate = m_nRecvInterval / flElapsed;
m_nSendInterval = 0;
m_nRecvInterval = 0;
}
inline void UpdateStats()
{
SteamNetworkingSockets()->GetConnectionRealTimeStatus( m_hSteamNetConnection, &m_realtimeStatus, 0, nullptr );
}
void SetConnectionConfig()
{
SteamNetworkingUtils()->SetConnectionConfigValueInt32( m_hSteamNetConnection, k_ESteamNetworkingConfig_SendBufferSize, m_cbSendBuffer );
}
inline int GetQueuedSendBytes()
{
return m_realtimeStatus.m_cbPendingReliable + m_realtimeStatus.m_cbPendingUnreliable + m_realtimeStatus.m_cbSentUnackedReliable;
}
void SendRandomMessage( bool bReliable, int cbMaxSize )
{
TestMsg msg;
msg.m_bReliable = bReliable;
msg.m_usecWhenSent = SteamNetworkingUtils()->GetLocalTimestamp();
msg.m_cbSize = std::uniform_int_distribution<>( 20, cbMaxSize )( g_rand );
//bIsReliable = false;
//nBytes = 1200-13;
msg.m_nMsgNum = msg.m_bReliable ? ++m_nReliableSendMsgCount : ++m_nUnreliableSendMsgCount;
for ( int n = 0; n < msg.m_cbSize; ++n )
{
msg.m_data[n] = (uint8)( msg.m_nMsgNum + n );
}
int cbSend = (int)( sizeof(msg) - sizeof(msg.m_data) + msg.m_cbSize );
m_nSendInterval += cbSend;
EResult result = SteamNetworkingSockets()->SendMessageToConnection(
m_hSteamNetConnection,
&msg,
cbSend,
msg.m_bReliable ? k_nSteamNetworkingSend_Reliable : k_nSteamNetworkingSend_Unreliable, nullptr );
if ( result != k_EResultOK )
{
TEST_Printf( "***ERROR ON Send: %s %.3f %s message %lld, %d bytes (pending %d bytes)\n",
m_sName.c_str(),
g_usecTestElapsed*1e-6,
msg.m_bReliable ? "reliable" : "unreliable",
(long long)msg.m_nMsgNum,
msg.m_cbSize,
GetQueuedSendBytes() );
abort();
}
#if 0
else
TEST_Printf( "Send: %s %.3f %s message %lld, %d bytes (pending %d bytes)\n",
connection.m_sName.c_str(),
g_usecTestElapsed*1e-6,
msg.m_bReliable ? "reliable" : "unreliable",
(long long)msg.m_nMsgNum,
msg.m_cbSize,
GetQueuedSendBytes() );
#endif
}
void Send()
{
bool bReliable = std::uniform_real_distribution<>()( g_rand ) < .60;
SendRandomMessage( bReliable, bReliable ? TestMsg::k_cbMaxSize : 2000 );
}
};
static SFakePeer g_peerServer( "Server" );
static SFakePeer g_peerClient( "Client" );
static void CloseConnections()
{
g_peerClient.Close();
g_peerServer.Close();
if ( g_hSteamListenSocket != k_HSteamNetConnection_Invalid )
{
SteamNetworkingSockets()->CloseListenSocket( g_hSteamListenSocket );
g_hSteamListenSocket = k_HSteamNetConnection_Invalid;
}
}
static void Recv( ISteamNetworkingSockets *pSteamSocketNetworking )
{
while ( true )
{
SFakePeer *pConnection = &g_peerServer;
ISteamNetworkingMessage *pIncomingMsg = nullptr;
int numMsgs = pSteamSocketNetworking->ReceiveMessagesOnConnection( pConnection->m_hSteamNetConnection, &pIncomingMsg, 1 );
if ( numMsgs <= 0 )
{
pConnection = &g_peerClient;
numMsgs = pSteamSocketNetworking->ReceiveMessagesOnConnection( pConnection->m_hSteamNetConnection, &pIncomingMsg, 1 );
if ( numMsgs <= 0 )
return;
}
const TestMsg *pTestMsg = static_cast<const TestMsg*>( pIncomingMsg->GetData() );
// Size makes sense?
assert( sizeof(*pTestMsg) - sizeof(pTestMsg->m_data) + pTestMsg->m_cbSize == pIncomingMsg->GetSize() );
// Check for sequence number anomaly.
int64 &nExpectedMsgNum = pTestMsg->m_bReliable ? pConnection->m_nReliableExpectedRecvMsg : pConnection->m_nExpectedRecvMsg;
if ( pTestMsg->m_nMsgNum != nExpectedMsgNum && pTestMsg->m_bReliable )
{
// Print that it happened.
TEST_Printf(
"Recv: %s, %s MISMATCH NUM wanted %lld got %lld\n",
pConnection->m_sName.c_str(),
pTestMsg->m_bReliable ? "RELIABLE" : "UNRELIABLE",
(long long)nExpectedMsgNum,
(long long)pTestMsg->m_nMsgNum );
// This should not happen for reliable messages!
assert( !pTestMsg->m_bReliable );
}
float flDelay = ( SteamNetworkingUtils()->GetLocalTimestamp() - pTestMsg->m_usecWhenSent ) * 1e-6f;
pConnection->m_nRecvInterval += pIncomingMsg->GetSize();
if ( pTestMsg->m_bReliable )
{
pConnection->m_flReliableMsgDelay += ( flDelay - pConnection->m_flReliableMsgDelay ) * .25f;
}
else
{
pConnection->m_flUnreliableMsgDelay += ( flDelay - pConnection->m_flUnreliableMsgDelay ) * .25f;
}
nExpectedMsgNum = pTestMsg->m_nMsgNum + 1;
pIncomingMsg->Release();
}
}
void OnSteamNetConnectionStatusChanged( SteamNetConnectionStatusChangedCallback_t *pInfo )
{
// What's the state of the connection?
switch ( pInfo->m_info.m_eState )
{
case k_ESteamNetworkingConnectionState_ClosedByPeer:
case k_ESteamNetworkingConnectionState_ProblemDetectedLocally:
TEST_Printf( "Steam Net connection %x %s, reason %d: %s\n",
pInfo->m_hConn,
( pInfo->m_info.m_eState == k_ESteamNetworkingConnectionState_ClosedByPeer ? "closed by peer" : "problem detected locally" ),
pInfo->m_info.m_eEndReason,
pInfo->m_info.m_szEndDebug
);
// Close our end
SteamNetworkingSockets()->CloseConnection( pInfo->m_hConn, 0, nullptr, false );
if ( g_peerServer.m_hSteamNetConnection == pInfo->m_hConn )
{
g_peerServer.m_hSteamNetConnection = k_HSteamNetConnection_Invalid;
}
if ( g_peerClient.m_hSteamNetConnection == pInfo->m_hConn )
{
g_peerClient.m_hSteamNetConnection = k_HSteamNetConnection_Invalid;
}
break;
/*
case k_ESteamNetworkingConnectionState_None:
TEST_Printf( "No steam Net connection %x (%s)\n", pInfo->m_hConn, pInfo->m_info.m_steamIDRemote.Render() );
if ( g_hSteamNetConnection == pInfo->m_hConn )
{
g_bIsConnected = false;
g_hSteamNetConnection = k_HSteamNetConnection_Invalid;
}
break;
*/
case k_ESteamNetworkingConnectionState_Connecting:
// Is this a connection we initiated, or one that we are receiving?
if ( g_hSteamListenSocket != k_HSteamListenSocket_Invalid && pInfo->m_info.m_hListenSocket == g_hSteamListenSocket )
{
// Somebody's knocking
TEST_Printf( "[%s] Accepting\n", pInfo->m_info.m_szConnectionDescription );
g_peerServer.m_hSteamNetConnection = pInfo->m_hConn;
g_peerServer.m_bIsConnected = true;
SteamNetworkingSockets()->AcceptConnection( pInfo->m_hConn );
SteamNetworkingSockets()->SetConnectionName( g_peerServer.m_hSteamNetConnection, "Server" );
g_peerServer.SetConnectionConfig();
}
break;
case k_ESteamNetworkingConnectionState_Connected:
if ( pInfo->m_hConn == g_peerClient.m_hSteamNetConnection )
{
g_peerClient.m_bIsConnected = true;
}
TEST_Printf( "[%s] connected\n", pInfo->m_info.m_szConnectionDescription );
break;
default:
// Silences -Wswitch
break;
}
}
static void PumpCallbacksAndMakeSureStillConnected()
{
TEST_PumpCallbacks();
assert( g_peerClient.m_bIsConnected );
assert( g_peerServer.m_bIsConnected );
assert( g_peerServer.m_hSteamNetConnection != k_HSteamNetConnection_Invalid );
assert( g_peerClient.m_hSteamNetConnection != k_HSteamNetConnection_Invalid );
}
inline std::string FormatQuality( float q )
{
if ( q < 0.0f ) return "???";
char buf[32];
sprintf( buf, "%.1f%%", q*100.0f );
return buf;
}
static void PrintStatus( const SFakePeer &p1, const SFakePeer &p2 )
{
const SteamNetConnectionRealTimeStatus_t &info1 = p1.m_realtimeStatus;
const SteamNetConnectionRealTimeStatus_t &info2 = p2.m_realtimeStatus;
TEST_Printf( "\n" );
TEST_Printf( "%12s %12s\n", p1.m_sName.c_str(), p2.m_sName.c_str() );
TEST_Printf( "%10dms %10dms Ping\n", info1.m_nPing, info2.m_nPing );
TEST_Printf( "%12s %12s Quality\n", FormatQuality( info1.m_flConnectionQualityLocal ).c_str(), FormatQuality( info2.m_flConnectionQualityLocal ).c_str() );
TEST_Printf( "%11.1fK %11.1fK Send buffer\n", ( info1.m_cbPendingReliable+info1.m_cbPendingUnreliable )/1024.0f, ( info2.m_cbPendingReliable+info2.m_cbPendingUnreliable )/1024.0f );
TEST_Printf( "%11.1fK %11.1fK Send rate (app)\n", p1.m_flSendRate/1024.0f, p2.m_flSendRate/1024.0f );
TEST_Printf( "%11.1fK %11.1fK Send rate (wire)\n", info1.m_flOutBytesPerSec/1024.0f, info2.m_flOutBytesPerSec/1024.0f );
TEST_Printf( "%12.1f %12.1f Send pkts/sec (wire)\n", info1.m_flOutPacketsPerSec, info2.m_flOutPacketsPerSec );
TEST_Printf( "%11.1fK %11.1fK Send bandwidth (estimate)\n", info1.m_nSendRateBytesPerSecond/1024.0f, info2.m_nSendRateBytesPerSecond/1024.0f );
TEST_Printf( "%11.1fK %11.1fK Recv rate (app)\n", p1.m_flRecvRate/1024.0f, p2.m_flRecvRate/1024.0f );
TEST_Printf( "%11.1fK %11.1fK Recv rate (wire)\n", info1.m_flInBytesPerSec/1024.0f, info2.m_flInBytesPerSec/1024.0f );
TEST_Printf( "%12.1f %12.1f Recv pkts/sec (wire)\n", info1.m_flInPacketsPerSec, info2.m_flInPacketsPerSec );
TEST_Printf( "%10.1fms %10.1fms Send buffer drain time, based on bandwidth\n", ( info1.m_cbPendingReliable+info1.m_cbPendingUnreliable )*1000.0f/info1.m_nSendRateBytesPerSecond, ( info2.m_cbPendingReliable+info2.m_cbPendingUnreliable )*1000.0f/info2.m_nSendRateBytesPerSecond );
TEST_Printf( "%10.1fms %10.1fms App RTT (reliable)\n", p1.m_flReliableMsgDelay*1e3, p2.m_flReliableMsgDelay*1e3 );
TEST_Printf( "%10.1fms %10.1fms App RTT (unreliable)\n", p1.m_flUnreliableMsgDelay*1e3, p2.m_flUnreliableMsgDelay*1e3 );
}
static void ClearConfig()
{
for (
ESteamNetworkingConfigValue eValue = SteamNetworkingUtils()->IterateGenericEditableConfigValues( k_ESteamNetworkingConfig_Invalid, true );
eValue != k_ESteamNetworkingConfig_Invalid;
eValue = SteamNetworkingUtils()->IterateGenericEditableConfigValues( eValue, true )
) {
if ( eValue == k_ESteamNetworkingConfig_IP_AllowWithoutAuth )
continue;
SteamNetworkingUtils()->SetConfigValue( eValue, k_ESteamNetworkingConfig_Global, 0, k_ESteamNetworkingConfig_Int32, nullptr );
}
}
static void TestNetworkConditions( int rate, float loss, int lag, float reorderPct, int reorderLag, bool bActLikeGame, ETestConnectionMode eMode )
{
ISteamNetworkingSockets *pSteamSocketNetworking = SteamNetworkingSockets();
TEST_Printf( "---------------------------------------------------\n" );
TEST_Printf( "NETWORK CONDITIONS\n" );
TEST_Printf( "Rate . . . . . . : %d Bps\n", rate );
TEST_Printf( "Loss . . . . . . : %g%%\n", loss );
TEST_Printf( "Ping . . . . . . : %d\n", lag*2 );
TEST_Printf( "Reorder. . . . . : %g%% @ %dms\n", reorderPct, reorderLag );
TEST_Printf( "Act like game. . : %d\n", (int)bActLikeGame );
TEST_Printf( "---------------------------------------------------\n" );
SteamNetworkingUtils()->SetGlobalConfigValueInt32( k_ESteamNetworkingConfig_SendRateMin, rate );
SteamNetworkingUtils()->SetGlobalConfigValueInt32( k_ESteamNetworkingConfig_SendRateMax, rate );
SteamNetworkingUtils()->SetGlobalConfigValueFloat( k_ESteamNetworkingConfig_FakePacketLoss_Send, loss );
SteamNetworkingUtils()->SetGlobalConfigValueFloat( k_ESteamNetworkingConfig_FakePacketLoss_Recv, 0 );
SteamNetworkingUtils()->SetGlobalConfigValueInt32( k_ESteamNetworkingConfig_FakePacketLag_Send, lag );
SteamNetworkingUtils()->SetGlobalConfigValueInt32( k_ESteamNetworkingConfig_FakePacketLag_Recv, 0 );
SteamNetworkingUtils()->SetGlobalConfigValueFloat( k_ESteamNetworkingConfig_FakePacketReorder_Send, reorderPct );
SteamNetworkingUtils()->SetGlobalConfigValueInt32( k_ESteamNetworkingConfig_FakePacketReorder_Time, reorderLag );
SteamNetworkingMicroseconds usecWhenStarted = SteamNetworkingUtils()->GetLocalTimestamp();
struct Timing { double flQuietSec; double flActiveSec; float flPrintIntervalSec; int nIterations; };
static const Timing k_timing[] = {
{ 0.5, 2.0, 1.0f, 1 }, // Cursory
{ 1.0, 5.0, 2.0f, 2 }, // Normal
{ 8.0, 25.0, 5.0f, 4 }, // Soak
};
const Timing &timing = k_timing[ (int)eMode ];
SteamNetworkingMicroseconds usecQuietDuration = SteamNetworkingMicroseconds( timing.flQuietSec * 1e6 );
SteamNetworkingMicroseconds usecActiveDuration = SteamNetworkingMicroseconds( timing.flActiveSec * 1e6 );
float flWaitBetweenPrints = timing.flPrintIntervalSec;
int nIterations = timing.nIterations;
bool bQuiet = true;
SteamNetworkingMicroseconds usecWhenStateEnd = 0;
SteamNetworkingMicroseconds usecLastPrint = SteamNetworkingUtils()->GetLocalTimestamp();
while ( true )
{
SteamNetworkingMicroseconds now = SteamNetworkingUtils()->GetLocalTimestamp();
g_usecTestElapsed = now - usecWhenStarted;
// If flElapsed > 1.0 when in debug, just slamp it
//if ( Plat_IsInDebugSession() && now - flLastNow > 1.0f )
//{
// flStartTime += now - flLastNow - 1.0f;
// flElapsed = now - flStartTime;
//}
//usecLastNow = now;
g_peerServer.UpdateStats();
g_peerClient.UpdateStats();
int nServerPending = g_peerServer.GetQueuedSendBytes();
int nClientPending = g_peerClient.GetQueuedSendBytes();
bool bCheckStateChange = ( usecWhenStateEnd == 0 );
float flElapsedPrint = ( now - usecLastPrint ) * 1e-6f;
if ( flElapsedPrint > flWaitBetweenPrints )
{
g_peerServer.UpdateInterval( flElapsedPrint );
g_peerClient.UpdateInterval( flElapsedPrint );
PrintStatus( g_peerServer, g_peerClient );
usecLastPrint = now;
bCheckStateChange = true;
}
// Start and stop active use of the connection. This exercises a bunch of important edge cases
// such as keepalives, the bandwidth estimation, etc.
if ( bCheckStateChange && g_usecTestElapsed > usecWhenStateEnd )
{
if ( bQuiet )
{
if ( nServerPending == 0 && nClientPending == 0 )
{
bQuiet = false;
usecWhenStateEnd = g_usecTestElapsed + ( bQuiet ? usecQuietDuration : usecActiveDuration );
if ( nIterations-- <= 0 )
break;
TEST_Printf( "Entering active time (sending enabled)\n" );
}
}
else
{
bQuiet = true;
usecWhenStateEnd = g_usecTestElapsed + usecQuietDuration;
TEST_Printf( "Entering quiet time (no sending) to see how fast queues drain\n" );
}
}
if ( !bQuiet )
{
if ( nServerPending < g_peerServer.m_cbSendBuffer - 16*1024 )
{
if ( bActLikeGame )
{
g_peerServer.SendRandomMessage( true, 4000 );
g_peerServer.SendRandomMessage( false, 2000 );
}
else
{
g_peerServer.Send();
}
}
if ( nClientPending < g_peerClient.m_cbSendBuffer - 16*1024 )
{
if ( bActLikeGame )
{
g_peerClient.SendRandomMessage( true, 4000 );
g_peerClient.SendRandomMessage( false, 2000 );
}
else
{
g_peerClient.Send();
}
}
}
PumpCallbacksAndMakeSureStillConnected();
Recv( pSteamSocketNetworking );
if ( bActLikeGame )
std::this_thread::sleep_for( std::chrono::milliseconds( 30 ) );
}
}
static void Test_Connection( ETestConnectionMode eMode, const SteamNetworkingIPAddr &addrServerBind, const SteamNetworkingIPAddr &addrClientConnect )
{
static const char *const k_rgszModeName[] = { "Cursory", "Normal", "Soak" };
TEST_Printf( "***************************************************\n" );
TEST_Printf( "Mode: %s\n", k_rgszModeName[ (int)eMode ] );
TEST_Printf( "Server bind: %s\n", SteamNetworkingIPAddrRender( addrServerBind ).c_str() );
TEST_Printf( "Client connect: %s\n", SteamNetworkingIPAddrRender( addrClientConnect ).c_str() );
TEST_Printf( "***************************************************\n" );
SteamNetworkingUtils()->SetGlobalCallback_SteamNetConnectionStatusChanged( OnSteamNetConnectionStatusChanged );
CloseConnections();
ISteamNetworkingSockets *pSteamSocketNetworking = SteamNetworkingSockets();
// Initiate connection
g_hSteamListenSocket = pSteamSocketNetworking->CreateListenSocketIP( addrServerBind, 0, nullptr );
g_peerClient.m_hSteamNetConnection = pSteamSocketNetworking->ConnectByIPAddress( addrClientConnect, 0, nullptr );
pSteamSocketNetworking->SetConnectionName( g_peerClient.m_hSteamNetConnection, "Client" );
g_peerClient.SetConnectionConfig();
// // Send a few random message, before we get connected, just to test that case
// g_peerClient.SendRandomMessage( true );
// g_peerClient.SendRandomMessage( true );
// g_peerClient.SendRandomMessage( true );
// Wait for connection to complete
while ( !g_peerClient.m_bIsConnected || !g_peerServer.m_bIsConnected )
TEST_PumpCallbacks();
auto Test = [eMode]( int rate, float loss, int lag, float reorderPct, int reorderLag )
{
TestNetworkConditions( rate, loss, lag, reorderPct, reorderLag, false, eMode );
TestNetworkConditions( rate, loss, lag, reorderPct, reorderLag, true, eMode );
};
if ( eMode == ETestConnectionMode::Cursory )
{
Test( 128000, 10, 50, 2, 50 ); // Low bandwidth, high packet loss
}
else if ( eMode == ETestConnectionMode::Normal )
{
Test( 128000, 10, 50, 2, 50 ); // Low bandwidth, high packet loss
Test( 1000000, 5, 10, 1, 10 ); // Medium bandwidth, still pretty bad packet loss
}
else
{
Test( 64000, 20, 100, 4, 50 ); // low bandwidth, terrible packet loss
Test( 1000000, 20, 100, 4, 10 ); // high bandwidth, terrible packet loss
Test( 1000000, 2, 5, 2, 1 ); // wifi (high bandwidth, low packet loss, occasional reordering with very small delay)
Test( 2000000, 0, 0, 0, 0 ); // LAN (high bandwidth, negligible lag/loss)
Test( 128000, 20, 100, 4, 40 );
Test( 500000, 20, 100, 4, 30 );
Test( 64000, 0, 0, 0, 0 );
Test( 128000, 0, 0, 0, 0 );
Test( 256000, 0, 0, 0, 0 );
Test( 500000, 0, 0, 0, 0 );
Test( 1000000, 0, 0, 0, 0 );
Test( 64000, 1, 25, 1, 10 );
Test( 1000000, 1, 25, 1, 10 );
Test( 64000, 5, 50, 2, 50 );
Test( 1000000, 5, 50, 2, 10 );
}
CloseConnections();
}
const int k_nStartingServerPort = 27200;
static void Test_quick()
{
int nServerPort = k_nStartingServerPort;
SteamNetworkingIPAddr bindAddr, connectAddr;
//
// First, do some 'cursory' connection tests between various combiantions of IPv4, IPc6, and dual-stack.
// This is really just to make sure we can connect and exchange packets.
//
// IPv4-only server, IPv4 client
bindAddr.SetIPv4( 0, nServerPort );
connectAddr.SetIPv4( 0x7f000001, nServerPort );
Test_Connection( ETestConnectionMode::Cursory, bindAddr, connectAddr );
++nServerPort;
// IPv6-only server, IPv6 client
bindAddr.SetIPv6LocalHost( nServerPort );
connectAddr.SetIPv6LocalHost( nServerPort );
Test_Connection( ETestConnectionMode::Cursory, bindAddr, connectAddr );
++nServerPort;
// Dual-stack server, IPv6 client
bindAddr.Clear(); bindAddr.m_port = nServerPort;
connectAddr.SetIPv6LocalHost( nServerPort );
Test_Connection( ETestConnectionMode::Cursory, bindAddr, connectAddr );
++nServerPort;
//
// Now do a 'normal' test
//
// Dual-stack server, IPv4 client (IPv4-mapped path)
bindAddr.Clear(); bindAddr.m_port = nServerPort;
connectAddr.SetIPv4( 0x7f000001, nServerPort );
Test_Connection( ETestConnectionMode::Normal, bindAddr, connectAddr );
}
static void Test_soak()
{
int nServerPort = k_nStartingServerPort;
SteamNetworkingIPAddr bindAddr, connectAddr;
// Dual-stack server, IPv4 client (IPv4-mapped path)
bindAddr.Clear(); bindAddr.m_port = nServerPort;
connectAddr.SetIPv4( 0x7f000001, nServerPort );
Test_Connection( ETestConnectionMode::Soak, bindAddr, connectAddr );
}
// Some tests for identity string handling. Doesn't really have anything to do with
// connectivity, this is just a conveinent place for this to live
void Test_identity()
{
SteamNetworkingIdentity id1, id2;
char tempBuf[ SteamNetworkingIdentity::k_cchMaxString ];
{
CSteamID steamID( 1234, k_EUniversePublic, k_EAccountTypeIndividual );
id1.SetSteamID( steamID );
id1.ToString( tempBuf, sizeof(tempBuf ) );
assert( id2.ParseString( tempBuf ) );
assert( id2.GetSteamID() == steamID );
}
{
const char *pszTempXBoxID = "8fg37rfsdf";
assert( id1.SetXboxPairwiseID( pszTempXBoxID ) );
id1.ToString( tempBuf, sizeof(tempBuf ) );
assert( id2.ParseString( tempBuf ) );
assert( strcmp( id2.GetXboxPairwiseID(), pszTempXBoxID ) == 0 );
}
{
const char *pszTempIPAddr = "ip:192.168.0.0:27015";
assert( id1.ParseString( pszTempIPAddr ) );
id1.ToString( tempBuf, sizeof(tempBuf ) );
assert( strcmp( tempBuf, pszTempIPAddr ) == 0 );
id1.SetLocalHost();
id1.ToString( tempBuf, sizeof(tempBuf ) );
assert( strcmp( tempBuf, "ip:::1" ) == 0 );
}
{
const char *pszTempGenStr = "Locke Lamora";
assert( id1.SetGenericString( pszTempGenStr ) );
id1.ToString( tempBuf, sizeof(tempBuf ) );
assert( strcmp( tempBuf, "str:Locke Lamora" ) == 0 );
assert( id2.ParseString( tempBuf ) );
assert( strcmp( id2.GetGenericString(), pszTempGenStr ) == 0 );
}
}
void Test_lane_quick_queueanddrain()
{
// Create a loopback connection, over the local network.
// (With a loopback over internal buffers, all messages
// are delivered instantly and lanes are irrelevant.)
HSteamNetConnection hSender, hRecver;
SteamNetworkingIdentity identSender, identRecver;
identSender.SetGenericString( "sender" );
identRecver.SetGenericString( "receiver" );
// NOTE: each pPeerIdentity argument is the remote identity observed by the
// *corresponding* connection, so pass them swapped relative to the connection handles.
assert( SteamNetworkingSockets()->CreateSocketPair( &hSender, &hRecver, true, &identRecver, &identSender ) );
// Verify: hSender sees "receiver" as its remote peer, hRecver sees "sender".
{
SteamNetConnectionInfo_t connInfoSender, connInfoRecver;
assert( SteamNetworkingSockets()->GetConnectionInfo( hSender, &connInfoSender ) );
assert( SteamNetworkingSockets()->GetConnectionInfo( hRecver, &connInfoRecver ) );
assert( connInfoSender.m_identityRemote == identRecver );
assert( connInfoRecver.m_identityRemote == identSender );
}
// Set the send rate to a fixed value
const int k_nSendRate = 128*1024;
SteamNetworkingUtils()->SetConnectionConfigValueInt32( hSender, k_ESteamNetworkingConfig_SendRateMin, k_nSendRate );
SteamNetworkingUtils()->SetConnectionConfigValueInt32( hSender, k_ESteamNetworkingConfig_SendRateMax, k_nSendRate );
// Configure lanes.
constexpr int k_nLanes = 4;
int priorities[k_nLanes] = { 2, 0, 1, 1 };
uint16 weights[k_nLanes] = { 1, 1, 25, 75 };
assert( k_EResultOK == SteamNetworkingSockets()->ConfigureConnectionLanes( hSender, k_nLanes, priorities, weights ) );
// We're gonna dump a whole bunch of stuff at once and watch it
// drain, broke up into messages
constexpr int k_nMsgPerLane = 128;
constexpr int k_cbMsg = 1024; // We're sending unreliable messages, so don't make them huge
constexpr int k_cbLaneData = k_nMsgPerLane * k_cbMsg;
constexpr int k_nTotalMsg = k_nLanes * k_nMsgPerLane;
constexpr int k_cbTotalData = k_nLanes * k_cbLaneData;
// Allow us to buffer up a whole bunch
SteamNetworkingUtils()->SetConnectionConfigValueInt32( hSender, k_ESteamNetworkingConfig_SendBufferSize, k_cbTotalData + 1024 );
// Dump a fixed amount of data into each lane
{
SteamNetworkingMessage_t *pMessages[k_nTotalMsg];
int idxMsg = 0;
for ( int idxLane = 0 ; idxLane < k_nLanes ; ++idxLane )
{
for ( int j = 0 ; j < k_nMsgPerLane ; ++j )
{
SteamNetworkingMessage_t *pMsg = SteamNetworkingUtils()->AllocateMessage( k_cbMsg );
assert( pMsg->m_cbSize == k_cbMsg );
pMsg->m_conn = hSender;
pMsg->m_nFlags = 0; // Just send everything unreliable for this test
pMsg->m_idxLane = (uint16)idxLane;
pMessages[idxMsg++] = pMsg;
}
}
assert( idxMsg == k_nTotalMsg );
SteamNetworkingSockets()->SendMessages( idxMsg, pMessages, nullptr );
}
// Remember when we sent all the messages
SteamNetworkingMicroseconds usecStartTime = SteamNetworkingUtils()->GetLocalTimestamp();
// Get back realtime status
SteamNetConnectionRealTimeStatus_t status;
SteamNetConnectionRealTimeLaneStatus_t laneStatus[k_nLanes];
assert( k_EResultOK == SteamNetworkingSockets()->GetConnectionRealTimeStatus( hSender, &status, k_nLanes, laneStatus ) );
// We should be able to predict the results of these estimates very accurately
const SteamNetworkingMicroseconds usecTol = 50*1000;
const double flSecTol = usecTol * 1e-6;
const int cbTol = (int)( k_nSendRate * flSecTol );
assert( status.m_cbPendingReliable == 0 );
assert( status.m_cbPendingUnreliable <= k_cbTotalData );
assert( status.m_cbPendingUnreliable > k_cbTotalData - cbTol );
// Lane 1 has the lowest priority number, and should be the only one
// with any data sent
assert( laneStatus[1].m_cbPendingReliable == 0 );
assert( laneStatus[1].m_cbPendingUnreliable <= k_cbLaneData );
assert( laneStatus[1].m_cbPendingUnreliable > k_cbLaneData - cbTol );
SteamNetworkingMicroseconds usecExpectedQueueTime1 = (SteamNetworkingMicroseconds)( laneStatus[1].m_cbPendingUnreliable * 1e6 / k_nSendRate );
assert( laneStatus[1].m_usecQueueTime < usecExpectedQueueTime1 + usecTol );
assert( laneStatus[1].m_usecQueueTime > usecExpectedQueueTime1 - usecTol );
// After lane 1 finishes, we expect the next chunk of time
// to be divided between lanes 2 and 3, at a ratio of 25:75, respectively.
// This means when lane 3 finishes, we will have sent all of lane 3,
// plus 1/3rd of lane 2. Thus it will finish in the time it takes
// to send 4/3rd of a lane.
assert( laneStatus[3].m_cbPendingReliable == 0 );
assert( laneStatus[3].m_cbPendingUnreliable == k_cbLaneData ); // Should not have sent any data on 3 yet
SteamNetworkingMicroseconds usecExpectedQueueTime3 = (SteamNetworkingMicroseconds)( usecExpectedQueueTime1 + k_cbLaneData * 4.0/3.0 * 1e6 / k_nSendRate );
assert( laneStatus[3].m_usecQueueTime < usecExpectedQueueTime3 + usecTol );
assert( laneStatus[3].m_usecQueueTime > usecExpectedQueueTime3 - usecTol );
// After lane 3 finishes, lane 2 will have the connection all to itself,
// to send the remaining 2/3rd of the complete lane data we buffered
assert( laneStatus[2].m_cbPendingReliable == 0 );
assert( laneStatus[2].m_cbPendingUnreliable == k_cbLaneData ); // Should not have sent any data on 2 yet
SteamNetworkingMicroseconds usecExpectedQueueTime2 = (SteamNetworkingMicroseconds)( usecExpectedQueueTime3 + k_cbLaneData * 2.0/3.0 * 1e6 / k_nSendRate );
assert( laneStatus[2].m_usecQueueTime < usecExpectedQueueTime2 + usecTol );
assert( laneStatus[2].m_usecQueueTime > usecExpectedQueueTime2 - usecTol );
// Finally, lane 0, the lowest priority lane, will drain
assert( laneStatus[0].m_cbPendingReliable == 0 );
assert( laneStatus[0].m_cbPendingUnreliable == k_cbLaneData ); // Should not have sent any data on 0 yet
SteamNetworkingMicroseconds usecExpectedQueueTime0 = (SteamNetworkingMicroseconds)( usecExpectedQueueTime2 + k_cbLaneData * 1e6 / k_nSendRate );
assert( laneStatus[0].m_usecQueueTime < usecExpectedQueueTime0 + usecTol );
assert( laneStatus[0].m_usecQueueTime > usecExpectedQueueTime0 - usecTol );
// Send one last one-byte message on each lane,
// so we can tell when we are done for that lane
{
SteamNetworkingMessage_t *pMessages[k_nLanes];
for ( int idxLane = 0 ; idxLane < k_nLanes ; ++idxLane )
{
SteamNetworkingMessage_t *pMsg = SteamNetworkingUtils()->AllocateMessage( 1 );
assert( pMsg->m_cbSize == 1 );
pMsg->m_conn = hSender;
pMsg->m_nFlags = 0; // Just send everything unreliable for this test
pMsg->m_idxLane = (uint16)idxLane;
pMessages[idxLane] = pMsg;
}
SteamNetworkingSockets()->SendMessages( k_nLanes, pMessages, nullptr );
}
int cbLaneReceived[k_nLanes] = {};
int nLanesFinished = 0;
while ( nLanesFinished < k_nLanes )
{
SteamNetworkingMessage_t *pMsg;
while ( SteamNetworkingSockets()->ReceiveMessagesOnConnection( hRecver, &pMsg, 1 ) == 1 )
{
int idxLane = pMsg->m_idxLane;
//TEST_Printf( "RX Lane %d msg %lld sz %d\n", idxLane, pMsg->m_nMessageNumber, pMsg->m_cbSize );
switch ( idxLane )
{
case 1:
assert( cbLaneReceived[2] == 0 );
assert( cbLaneReceived[3] == 0 );
assert( cbLaneReceived[0] == 0 );
break;
case 3:
assert( cbLaneReceived[1] == k_cbLaneData+1 );
assert( nLanesFinished == 1 );
assert( cbLaneReceived[0] == 0 );
break;
case 2:
assert( cbLaneReceived[1] == k_cbLaneData+1 );
assert( nLanesFinished == 1 || nLanesFinished == 2 );
assert( cbLaneReceived[0] < 2048 ); // First bit of lane 0 might come in before the last part of this lane
break;
case 0:
assert( cbLaneReceived[1] == k_cbLaneData+1 );
assert( cbLaneReceived[3] == k_cbLaneData+1 );
// The very first packet might deliver some data on lane 0
// before the other lanes due to internal quirks of serialization,
// but in general we should have finished all the other lanes first
if ( cbLaneReceived[0] > 2048 )
{
assert( cbLaneReceived[2] == k_cbLaneData+1 );
assert( nLanesFinished == 3 );
}
break;
}
cbLaneReceived[idxLane] += pMsg->m_cbSize;
if ( pMsg->m_cbSize == 1 )
{
assert( cbLaneReceived[idxLane] == k_cbLaneData+1 );
++nLanesFinished;
float msElapsed = ( SteamNetworkingUtils()->GetLocalTimestamp() - usecStartTime ) * 1e-3f;
TEST_Printf( "Lane %d finished @ %.1fms, expected %.1fms. %6d %6d %6d %6d\n",
idxLane,
msElapsed, laneStatus[idxLane].m_usecQueueTime * 1e-3f,
cbLaneReceived[0],
cbLaneReceived[1],
cbLaneReceived[2],
cbLaneReceived[3]
);
if ( idxLane == 3 )
{
assert( cbLaneReceived[2] * 75 <= ( cbLaneReceived[3]+k_cbMsg ) * 25 );
assert( (cbLaneReceived[2]+k_cbMsg) * 75 >= cbLaneReceived[3] * 25 );
}
}
else
{
assert( pMsg->m_cbSize == k_cbMsg );
assert( cbLaneReceived[idxLane] <= k_cbLaneData );
}
pMsg->Release();
}
TEST_PumpCallbacks();
}
// Cleanup
SteamNetworkingSockets()->CloseConnection( hSender, 0, nullptr, false );
SteamNetworkingSockets()->CloseConnection( hRecver, 0, nullptr, false );
}
// Test a particular use case that we have specifically been asked about:
// Three lanes:
// - Lane for most gameplay traffic.
// - "Priority" lane for certain urgent messages
// - "Background" land for content download
void Test_lane_quick_priority_and_background()
{
// Create a loopback connection, over the local network.
// (With a loopback over internal buffers, all messages
// are delivered instantly and lanes are irrelevant.)
HSteamNetConnection hServer, hClient;
SteamNetworkingIdentity identServer, identClient;
identServer.SetGenericString( "server" );
identClient.SetGenericString( "client" );
// NOTE: each pPeerIdentity argument is the remote identity observed by the
// *corresponding* connection, so pass them swapped relative to the connection handles.
assert( SteamNetworkingSockets()->CreateSocketPair( &hServer, &hClient, true, &identClient, &identServer ) );
SteamNetworkingSockets()->SetConnectionName( hServer, "server" );
SteamNetworkingSockets()->SetConnectionName( hClient, "client" );
// Verify: hServer sees "client" as its remote peer, hClient sees "server".
{
SteamNetConnectionInfo_t connInfoServer, connInfoClient;
assert( SteamNetworkingSockets()->GetConnectionInfo( hServer, &connInfoServer ) );
assert( SteamNetworkingSockets()->GetConnectionInfo( hClient, &connInfoClient ) );
assert( connInfoServer.m_identityRemote == identClient );
assert( connInfoClient.m_identityRemote == identServer );
}
// Set the send rate to a fixed value
const int k_nSendRate = 256*1024;
SteamNetworkingUtils()->SetConnectionConfigValueInt32( hServer, k_ESteamNetworkingConfig_SendRateMin, k_nSendRate );
SteamNetworkingUtils()->SetConnectionConfigValueInt32( hServer, k_ESteamNetworkingConfig_SendRateMax, k_nSendRate );
// Configure lanes.
constexpr int k_nLanes = 3;
constexpr int k_LaneGameplay = 0;
constexpr int k_LaneUrgent = 1;
constexpr int k_LaneBackground = 2;
int priorities[k_nLanes] = { 1, 0, 1 };
uint16 weights[k_nLanes] = { 75, 1, 25 };
assert( k_EResultOK == SteamNetworkingSockets()->ConfigureConnectionLanes( hServer, k_nLanes, priorities, weights ) );
// Allow us to buffer up a decent amount for background content download
constexpr int k_cbMaxBackgroundInFlight = 1024 * 1024;
SteamNetworkingUtils()->SetConnectionConfigValueInt32( hServer, k_ESteamNetworkingConfig_SendBufferSize, k_cbMaxBackgroundInFlight + 64*1024 );
// Remember when we sent all the messages
SteamNetworkingMicroseconds usecStartTime = SteamNetworkingUtils()->GetLocalTimestamp();
SteamNetworkingMicroseconds usecNextSendUrgent = 0;
SteamNetworkingMicroseconds usecNextSendGameplay = usecStartTime;
const int k_nFakeLag = 50;
// Set some reasonable network conditions
SteamNetworkingUtils()->SetGlobalConfigValueFloat( k_ESteamNetworkingConfig_FakePacketLoss_Send, 2.0 );
SteamNetworkingUtils()->SetGlobalConfigValueFloat( k_ESteamNetworkingConfig_FakePacketLoss_Recv, 0 );
SteamNetworkingUtils()->SetGlobalConfigValueInt32( k_ESteamNetworkingConfig_FakePacketLag_Send, k_nFakeLag );
SteamNetworkingUtils()->SetGlobalConfigValueInt32( k_ESteamNetworkingConfig_FakePacketLag_Recv, 0 );
SteamNetworkingUtils()->SetGlobalConfigValueFloat( k_ESteamNetworkingConfig_FakePacketReorder_Send, .5 );
SteamNetworkingUtils()->SetGlobalConfigValueInt32( k_ESteamNetworkingConfig_FakePacketReorder_Time, 25 );
int64 nMsgSent[ k_nLanes ] = {};
int64 nMsgRecv[ k_nLanes ] = {};
int64 nLatencyTotal[ k_nLanes ] = {};
int64 nLatencySqTotal[ k_nLanes ] = {};
for (;;)
{
SteamNetworkingMicroseconds usecNow = SteamNetworkingUtils()->GetLocalTimestamp();
// RUn the test for 60 seconds
if ( usecStartTime + 60*1000*1000 < usecNow )
break;
// Get back realtime status
SteamNetConnectionRealTimeStatus_t status;
SteamNetConnectionRealTimeLaneStatus_t laneStatus[k_nLanes];
assert( k_EResultOK == SteamNetworkingSockets()->GetConnectionRealTimeStatus( hServer, &status, k_nLanes, laneStatus ) );
// Keep the background download pipe full
if ( laneStatus[ k_LaneBackground ].m_cbPendingReliable + k_cbMaxSteamNetworkingSocketsMessageSizeSend <= k_cbMaxBackgroundInFlight )
{
SteamNetworkingMessage_t *pMsg = SteamNetworkingUtils()->AllocateMessage( k_cbMaxSteamNetworkingSocketsMessageSizeSend );
pMsg->m_conn = hServer;
pMsg->m_nFlags = k_nSteamNetworkingSend_Reliable;
pMsg->m_idxLane = k_LaneBackground;
// Shove the time when we sent it into the body,
// but otherwise leave the rest of the body unitialized
*(SteamNetworkingMicroseconds *)pMsg->m_pData = usecNow;
int64 nMsgNum;
SteamNetworkingSockets()->SendMessages( 1, &pMsg, &nMsgNum );
++nMsgSent[k_LaneBackground];
assert( nMsgNum == nMsgSent[k_LaneBackground] );
}
// Time to send a small priority message?
if ( usecNow >= usecNextSendUrgent )
{
SteamNetworkingMessage_t *pMsg = SteamNetworkingUtils()->AllocateMessage( std::uniform_int_distribution<>( 100, 500 )( g_rand ) );
pMsg->m_conn = hServer;
pMsg->m_nFlags = k_nSteamNetworkingSend_ReliableNoNagle;
pMsg->m_idxLane = k_LaneUrgent;
// Shove the time when we sent it into the body,
// but otherwise leave the rest of the body unitialized
*(SteamNetworkingMicroseconds *)pMsg->m_pData = usecNow;
int64 nMsgNum;
SteamNetworkingSockets()->SendMessages( 1, &pMsg, &nMsgNum );
++nMsgSent[k_LaneUrgent];
assert( nMsgNum == nMsgSent[k_LaneUrgent] );
// Schedule the next send at a random interval
usecNextSendUrgent = usecNow + std::uniform_int_distribution<>( 500, 1500 )( g_rand ) * 1000;
}
// Time to send a gameplay message
if ( usecNow >= usecNextSendGameplay )
{
// Send a gameplay message server->client
{
SteamNetworkingMessage_t *pMsg = SteamNetworkingUtils()->AllocateMessage( std::uniform_int_distribution<>( 1000, 5000 )( g_rand ) );
pMsg->m_conn = hServer;
pMsg->m_idxLane = k_LaneGameplay;
// Occasionally send reliable
pMsg->m_nFlags = std::uniform_int_distribution<>( 0, 100 )( g_rand ) < 30 ? k_nSteamNetworkingSend_ReliableNoNagle : k_nSteamNetworkingSend_UnreliableNoNagle;
// Shove the time when we sent it into the body,
// but otherwise leave the rest of the body unitialized
*(SteamNetworkingMicroseconds *)pMsg->m_pData = usecNow;
int64 nMsgNum;
SteamNetworkingSockets()->SendMessages( 1, &pMsg, &nMsgNum );
++nMsgSent[k_LaneGameplay];
assert( nMsgNum == nMsgSent[k_LaneGameplay] );
}
// Send a gameplay message client->server
{
SteamNetworkingMessage_t *pMsg = SteamNetworkingUtils()->AllocateMessage( std::uniform_int_distribution<>( 100, 2000 )( g_rand ) );
pMsg->m_conn = hClient;
pMsg->m_idxLane = k_LaneGameplay;
// Occasionally send reliable
pMsg->m_nFlags = std::uniform_int_distribution<>( 0, 100 )( g_rand ) < 30 ? k_nSteamNetworkingSend_ReliableNoNagle : k_nSteamNetworkingSend_UnreliableNoNagle;
int64 nMsgNum;
SteamNetworkingSockets()->SendMessages( 1, &pMsg, &nMsgNum );
assert( nMsgNum >= 0 );
}
// Schedule the next send at 30hz
usecNextSendGameplay += 1000*1000 / 30;
}
usecNow = SteamNetworkingUtils()->GetLocalTimestamp();
// Client receive messages
{
SteamNetworkingMessage_t *pMsg;
while ( SteamNetworkingSockets()->ReceiveMessagesOnConnection( hClient, &pMsg, 1 ) == 1 )
{
SteamNetworkingMicroseconds usecLatency = usecNow - *(SteamNetworkingMicroseconds*)pMsg->m_pData;
int idxLane = pMsg->m_idxLane;
if ( idxLane != k_LaneGameplay || pMsg->m_nMessageNumber%30 == 0 )
TEST_Printf( "RX lane %d one-way latency %6.1fms #%lld\n", idxLane, usecLatency*1e-3, pMsg->m_nMessageNumber );
++nMsgRecv[ idxLane ];
if ( idxLane != k_LaneGameplay )
assert( pMsg->m_nMessageNumber == nMsgRecv[ idxLane ] );
int msLatency = (int)( usecLatency / 1000 );
nLatencyTotal[ idxLane ] += msLatency;
nLatencySqTotal[ idxLane ] += msLatency*msLatency;
pMsg->Release();
}
}
// Server receive messages
{
SteamNetworkingMessage_t *pMsg;
while ( SteamNetworkingSockets()->ReceiveMessagesOnConnection( hServer, &pMsg, 1 ) == 1 )
{
pMsg->Release();
}
}
// Background tasks, etc
TEST_PumpCallbacks();
}
TEST_Printf( "\n\n" );
// Cleanup
SteamNetworkingSockets()->CloseConnection( hServer, 0, nullptr, false );
SteamNetworkingSockets()->CloseConnection( hClient, 0, nullptr, false );
float flAvgLatencyMS[ k_nLanes ];
float flRMSLatencyMS[ k_nLanes ];
for ( int i = 0 ; i < 3 ; ++i )
{
flAvgLatencyMS[i] = (float)nLatencyTotal[i] / (float)nMsgRecv[i];
flRMSLatencyMS[i] = sqrt( (float)nLatencySqTotal[i] / (float)nMsgRecv[i] );
// FIXME - print stddev?
TEST_Printf( "Lane %d: %6lld msgs, one-way latency avg %6.1fms, RMS %6.1fms\n",
i, nMsgRecv[i], flAvgLatencyMS[i], flRMSLatencyMS[i] );
}
// Check numbers...PIOMA
//assert( flRMSPingMS[k_LaneUrgent] < flRMSPingMS[k_LaneGameplay] ); // We are comparing pings of always reliable msgs to pings that might sometimes be unreliable, so not totally a fair comparison
//assert( flRMSPingMS[k_LaneGameplay] < k_nFakeLag*1.2 + 10 );
}
void Test_pipe()
{
TEST_Printf( "***************************************************\n" );
TEST_Printf( "Pipe (socket pair, no network loopback)\n" );
TEST_Printf( "***************************************************\n" );
// CreateSocketPair with bUseNetworkLoopback=false: pure internal buffer path,
// no encryption, no packet fragmentation, no network stack involvement.
HSteamNetConnection hAlice, hBob;
SteamNetworkingIdentity identAlice, identBob;
identAlice.SetGenericString( "alice" );
identBob.SetGenericString( "bob" );
// NOTE: each pPeerIdentity is the remote identity seen by the *corresponding* connection.
assert( SteamNetworkingSockets()->CreateSocketPair( &hAlice, &hBob, false, &identBob, &identAlice ) );
// Verify each end observes the correct remote identity.
{
SteamNetConnectionInfo_t infoAlice, infoBob;
assert( SteamNetworkingSockets()->GetConnectionInfo( hAlice, &infoAlice ) );
assert( SteamNetworkingSockets()->GetConnectionInfo( hBob, &infoBob ) );
assert( infoAlice.m_identityRemote == identBob );
assert( infoBob.m_identityRemote == identAlice );
}
// Wire up to the global peer state used by TestNetworkConditions / PumpCallbacksAndMakeSureStillConnected.
g_peerClient.Reset();
g_peerServer.Reset();
g_peerClient.m_hSteamNetConnection = hAlice;
g_peerClient.m_bIsConnected = true;
g_peerClient.SetConnectionConfig();
g_peerServer.m_hSteamNetConnection = hBob;
g_peerServer.m_bIsConnected = true;
g_peerServer.SetConnectionConfig();
// Cursory connection test. Fake loss/lag config has no effect on a pipe
// (no network path), but the send/receive loop still exercises the connection.
TestNetworkConditions( 10*1000*1000, 0, 0, 0, 0, false, ETestConnectionMode::Cursory );
// Zero-copy: for a pipe connection the received message must point at the
// exact same data buffer that was handed to SendMessages -- no copy through
// any network or encryption layer.
{
SteamNetworkingMessage_t *pSendMsg = SteamNetworkingUtils()->AllocateMessage( 256 );
pSendMsg->m_conn = hAlice;
pSendMsg->m_nFlags = k_nSteamNetworkingSend_Reliable;
void *pSendData = pSendMsg->m_pData;
int64 nMsgNum;
SteamNetworkingSockets()->SendMessages( 1, &pSendMsg, &nMsgNum );
assert( nMsgNum > 0 );
SteamNetworkingMessage_t *pRecvMsg = nullptr;
int nRecv = SteamNetworkingSockets()->ReceiveMessagesOnConnection( hBob, &pRecvMsg, 1 );
assert( nRecv == 1 );
assert( pRecvMsg->m_pData == pSendData ); // zero-copy: must be the same pointer
pRecvMsg->Release();
}
// Oversized messages: pipe connections have no network-imposed size limit, so
// messages larger than k_cbMaxSteamNetworkingSocketsMessageSizeSend must work.
{
const int k_cbHuge = k_cbMaxSteamNetworkingSocketsMessageSizeSend * 3;
SteamNetworkingMessage_t *pSendMsg = SteamNetworkingUtils()->AllocateMessage( k_cbHuge );
pSendMsg->m_conn = hAlice;
pSendMsg->m_nFlags = k_nSteamNetworkingSend_Reliable;
int64 nMsgNum;
SteamNetworkingSockets()->SendMessages( 1, &pSendMsg, &nMsgNum );
assert( nMsgNum > 0 );
SteamNetworkingMessage_t *pRecvMsg = nullptr;
int nRecv = SteamNetworkingSockets()->ReceiveMessagesOnConnection( hBob, &pRecvMsg, 1 );
assert( nRecv == 1 );
assert( pRecvMsg->m_cbSize == k_cbHuge );
pRecvMsg->Release();
}
SteamNetworkingSockets()->CloseConnection( hAlice, 0, nullptr, false );
SteamNetworkingSockets()->CloseConnection( hBob, 0, nullptr, false );
g_peerClient.Reset();
g_peerServer.Reset();
}
void Test_netloopback_throughput()
{
// Create a loopback connection, over the local network.
HSteamNetConnection hServer, hClient;
assert( SteamNetworkingSockets()->CreateSocketPair( &hServer, &hClient, true, nullptr, nullptr ) );
SteamNetworkingSockets()->SetConnectionName( hServer, "server" );
SteamNetworkingSockets()->SetConnectionName( hClient, "client" );
// Try several increasing send rates and make sure we can keep up
// FIXME Something broken here with this test above 30000, that isn't reproducing
// for me locally. Temporarily removing the higher rates until I can investigate.
//for ( int nSendRateKB: { 8000, 12000, 16000, 20000, 30000, 40000, 50000, 60000 } )
for ( int nSendRateKB: { 8000, 12000, 16000, 20000, 30000 } )
{
const int nSendRate = nSendRateKB*1000; // Use powers of 10 here, not 1024
TEST_Printf( "-- TESTING SEND RATE: %dKB/sec -------\n\n", nSendRateKB );
// Set the send rate to a fixed value
SteamNetworkingUtils()->SetConnectionConfigValueInt32( hServer, k_ESteamNetworkingConfig_SendRateMin, nSendRate );
SteamNetworkingUtils()->SetConnectionConfigValueInt32( hServer, k_ESteamNetworkingConfig_SendRateMax, nSendRate );
SteamNetworkingUtils()->SetConnectionConfigValueInt32( hClient, k_ESteamNetworkingConfig_SendRateMin, nSendRate );
SteamNetworkingUtils()->SetConnectionConfigValueInt32( hClient, k_ESteamNetworkingConfig_SendRateMax, nSendRate );
SteamNetworkingUtils()->SetGlobalConfigValueInt32( k_ESteamNetworkingConfig_LogLevel_PacketGaps, k_ESteamNetworkingSocketsDebugOutputType_Verbose );
// For this test we'll try to keep about 200ms worth of data queued, so the pipe stays full
const int k_nBufferQueuedTarget = nSendRate / 5;
// Set the send buffer
SteamNetworkingUtils()->SetConnectionConfigValueInt32( hServer, k_ESteamNetworkingConfig_SendBufferSize, k_nBufferQueuedTarget*5/4 + 1024 );
// How long do we want to send?
constexpr SteamNetworkingMicroseconds k_usecSendTime = SteamNetworkingMicroseconds( 10 * 1e6 );
// Run at this send rate for several seconds
SteamNetworkingMicroseconds usecStartTime = SteamNetworkingUtils()->GetLocalTimestamp();
SteamNetworkingMicroseconds usecLastPrint = usecStartTime;
int64 cbBytesSent = 0;
int64 cbBytesRecv = 0;
bool bDrain = false;
for (;;)
{
TEST_PumpCallbacks();
// Query status
SteamNetConnectionRealTimeStatus_t serverStatus;
assert( k_EResultOK == SteamNetworkingSockets()->GetConnectionRealTimeStatus( hServer, &serverStatus, 0, nullptr ) );
SteamNetConnectionRealTimeStatus_t clientStatus;
assert( k_EResultOK == SteamNetworkingSockets()->GetConnectionRealTimeStatus( hClient, &clientStatus, 0, nullptr ) );
// Time to enter drain mode?
SteamNetworkingMicroseconds usecNow = SteamNetworkingUtils()->GetLocalTimestamp();
if ( !bDrain && usecNow > usecStartTime + k_usecSendTime )
{
TEST_Printf( "Entering drain mode\n" );
bDrain = true;
usecLastPrint = 0;
}
// Time to print status?
if ( usecLastPrint + 500*1000 < usecNow )
{
SteamNetworkingMicroseconds usecElapsed = usecNow - usecStartTime;
assert( usecElapsed < k_usecSendTime * 2 );
double flElapsedSeconds = usecElapsed * 1e-6;
TEST_Printf( "Elapsed:%6.0fms Sent:%7.0fK Recv:%7.0fK = %5.0fK/sec (Wire%6.3f kpkts/sec Qual %5.1f%%)\n",
flElapsedSeconds * 1e3,
cbBytesSent * 1e-3,
cbBytesRecv * 1e-3,
cbBytesRecv * 1e-3 / flElapsedSeconds,
clientStatus.m_flInPacketsPerSec * 1e-3,
clientStatus.m_flConnectionQualityLocal * 100.0f
);
usecLastPrint = usecNow;
}
// On the server, try to keep the buffer full at a certain amount
if ( !bDrain )
{
while ( serverStatus.m_cbPendingReliable + 1024 < k_nBufferQueuedTarget )
{
// How much is missing?
int cbSendMsg = std::min( k_nBufferQueuedTarget - serverStatus.m_cbPendingReliable, k_cbMaxSteamNetworkingSocketsMessageSizeSend );
// Don't send tiny messages, just wait until we can queue up some more
if ( cbSendMsg < 1024 )
break;
// Allocate a message.
SteamNetworkingMessage_t *pSendMsg = SteamNetworkingUtils()->AllocateMessage( cbSendMsg );
pSendMsg->m_conn = hServer;
pSendMsg->m_nFlags = k_nSteamNetworkingSend_Reliable;
// Don't bother initializing the body
int64 nMsgNumberOrResult;
SteamNetworkingSockets()->SendMessages( 1, &pSendMsg, &nMsgNumberOrResult );
if ( nMsgNumberOrResult == -k_EResultLimitExceeded )
{
TEST_Printf( "SendMessage returned limit exceeded trying to queue %d + %d = %d\n", serverStatus.m_cbPendingReliable, cbSendMsg, serverStatus.m_cbPendingReliable + cbSendMsg );
break;
}
assert( nMsgNumberOrResult > 0 );
serverStatus.m_cbPendingReliable += cbSendMsg + 64;
cbBytesSent += cbSendMsg;
}
}
// On the client, we'll just periodically send small messages,
// just to keep some traffic going in the other direction. This
// isn't necessary, but it's a slightly more realistic test
if ( clientStatus.m_cbPendingReliable+clientStatus.m_cbSentUnackedReliable == 0 )
{
char dummyMsg[ 1024 ];
EResult r = SteamNetworkingSockets()->SendMessageToConnection( hClient, dummyMsg, sizeof(dummyMsg), k_nSteamNetworkingSend_Reliable, nullptr );
assert( k_EResultOK == r );
}
// Receive server->client messages
SteamNetworkingMessage_t *pMsg[ 16 ];
for (;;)
{
int nMsg = SteamNetworkingSockets()->ReceiveMessagesOnConnection( hClient, pMsg, 16 );
if ( nMsg <= 0 )
{
assert( nMsg == 0 );
break;
}
for ( int i = 0 ; i < nMsg ; ++i )
{
cbBytesRecv += pMsg[i]->m_cbSize;
assert( cbBytesRecv <= cbBytesSent );
pMsg[i]->Release();
}
if ( nMsg < 16 )
break;
}
// Receive client->server messages
for (;;)
{
int nMsg = SteamNetworkingSockets()->ReceiveMessagesOnConnection( hServer, pMsg, 16 );
if ( nMsg <= 0 )
{
assert( nMsg == 0 );
break;
}
for ( int i = 0 ; i < nMsg ; ++i )
pMsg[i]->Release();
if ( nMsg < 16 )
break;
}
// Done?
if ( bDrain && cbBytesRecv == cbBytesSent )
break;
}
{
SteamNetworkingMicroseconds usecNow = SteamNetworkingUtils()->GetLocalTimestamp();
double flElapsedSeconds = ( usecNow - usecStartTime ) * 1e-6;
TEST_Printf( "TOTAL: %6.0fms Sent:%7.0fK Recv:%7.0fK = %5.0fK/sec\n\n",
flElapsedSeconds * 1e3,
cbBytesSent * 1e-3,
cbBytesRecv * 1e-3,
cbBytesRecv * 1e-3 / flElapsedSeconds
);
}
}
// Cleanup
SteamNetworkingSockets()->CloseConnection( hServer, 0, nullptr, false );
SteamNetworkingSockets()->CloseConnection( hClient, 0, nullptr, false );
}
int main( int argc, const char **argv )
{
typedef void (*FnTest)(void);
struct Test_t {
const char *m_pszName;
FnTest m_func;
};
#define TEST(x) { #x, Test_ ## x }
static const Test_t tests[] = {
TEST(identity),
TEST(quick),
TEST(soak),
TEST(netloopback_throughput),
TEST(lane_quick_queueanddrain),
TEST(lane_quick_priority_and_background),
TEST(pipe)
};
struct Suite_t {
const char *m_pszName;
std::vector< Test_t > m_vecTests;
};
static const Suite_t test_suites[] = {
{ "suite-quick", { TEST(identity), TEST(quick), TEST(lane_quick_queueanddrain), TEST(netloopback_throughput), TEST(lane_quick_priority_and_background), TEST(pipe) } }
};
if ( argc < 2 )
{
print_usage:
{
const char *prog = argv[0];
while ( strchr( prog, '/' ) )
prog = strchr( prog, '/' ) + 1;
while ( strchr( prog, '\\' ) )
prog = strchr( prog, '\\' ) + 1;
printf( "Usage: %s test-or-suite-name ...\n", prog );
}
print_available_tests_and_exit:
printf( "\n" );
printf( "Available tests:\n" );
for ( const Test_t &t: tests )
printf( " %s\n", t.m_pszName );
printf( "Available test suites:\n" );
for ( const Suite_t &s: test_suites )
printf( " %s\n", s.m_pszName );
return 1;
}
std::vector<Test_t> vecTestsToRun;
for ( int i = 1 ; i < argc ; ++i )
{
if ( !strcasecmp( argv[i], "/?" ) || !strcasecmp( argv[i], "-?" ) || !strcasecmp( argv[i], "/?" ) || !strcasecmp( argv[i], "-h" ) || !strcasecmp( argv[i], "--help" ) )
goto print_usage;
bool bFound = false;
for ( const Test_t &t: tests )
{
if ( !strcasecmp( argv[i], t.m_pszName ) )
{
vecTestsToRun.push_back( t );
bFound = true;
break;
}
}
for ( const Suite_t &s: test_suites )
{
if ( !strcasecmp( argv[i], s.m_pszName ) )
{
for ( const Test_t &t: s.m_vecTests )
vecTestsToRun.push_back( t );
bFound = true;
break;
}
}
if ( !bFound )
{
printf( "No such test or suite named '%s' not known\n", argv[i] );
goto print_available_tests_and_exit;
}
}
// Initialize library
TEST_Init( nullptr );
for ( const Test_t &t: vecTestsToRun )
{
TEST_Printf( "--------------------------------------\n");
TEST_Printf( "Running test '%s'\n", t.m_pszName );
TEST_Printf( "--------------------------------------\n");
TEST_Printf( "\n");
// Make sure each test starts with the default config
ClearConfig();
// Run the test
(*t.m_func)();
TEST_Printf( "\n" );
TEST_Printf( "Test '%s' completed OK\n\n", t.m_pszName );
}
// Shutdown library
TEST_Kill();
return 0;
}
#ifdef NN_NINTENDO_SDK
extern "C" void nnMain() { main( 0, nullptr ); }
#endif