Files
GameNetworkingSockets/tests/test_connection.cpp
T
Fletcher Dunn 4fbfe83ef4 Semantic change to ISteamNetworkingSockets::SendMessages
Added bDeleteFailedMessages.  By setting this to false, the caller has
the opportunity to retry failed messages, which is an especially
reasonably thing to do in the case of the send buffer being full.

Also clarified expected (and enforced) expected behaviour on
how subsequent messages on a connection are handled after a failed
message send.  Previously, there was a fairly significant bug, which is
that we would coutinue to try to send messages, which is bad because
it can break fundamental guarantees about message delivery order.  Now,
we will always stop at the first failure.

This fixes issue #317
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, true );
}
// 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, true );
}
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, true );
++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, true );
++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, true );
++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, true );
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, true );
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, true );
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, true );
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