From dec471d7789d84d2aa91f2162ac51929ccc34a24 Mon Sep 17 00:00:00 2001 From: sashakosti Date: Sun, 11 Oct 2026 05:09:47 +0300 Subject: [PATCH 1/2] examples: Add LAN discovery beacon and auto-connect support to example_chat - Implements lightweight UDP-based discovery beacon (CLanDiscoveryBeacon) and client utility (CLanDiscoveryClient) on port 27015. - example_chat server advertises its listen port and responds to discovery queries. - example_chat client auto-discovers and connects to the local server when no address argument is provided. - Responds to mobile/external GNS_PING probes with GNS_PONG. Closes #82 --- examples/CMakeLists.txt | 3 +- examples/example_chat.cpp | 22 +++- examples/lan_discovery.cpp | 241 +++++++++++++++++++++++++++++++++++++ examples/lan_discovery.h | 61 ++++++++++ 4 files changed, 324 insertions(+), 3 deletions(-) create mode 100644 examples/lan_discovery.cpp create mode 100644 examples/lan_discovery.h diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index e41a7805..53f6b465 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -25,7 +25,8 @@ if( BUILD_EXAMPLES ) add_executable( example_chat - example_chat.cpp) + example_chat.cpp + lan_discovery.cpp) target_link_libraries(example_chat GameNetworkingSockets::shared) diff --git a/examples/example_chat.cpp b/examples/example_chat.cpp index ce7d7ee8..70fd5ded 100644 --- a/examples/example_chat.cpp +++ b/examples/example_chat.cpp @@ -18,6 +18,7 @@ #include #include +#include "lan_discovery.h" #ifndef STEAMNETWORKINGSOCKETS_OPENSOURCE #include #endif @@ -250,6 +251,8 @@ class ChatServer if ( m_hPollGroup == k_HSteamNetPollGroup_Invalid ) FatalError( "Failed to listen on port %d", nPort ); Printf( "Server listening on port %d\n", nPort ); + CLanDiscoveryBeacon beacon; + beacon.Start( nPort, "SteamNetworkingChatServer" ); while ( !g_bQuit ) { @@ -698,7 +701,7 @@ void PrintUsageAndExit( int rc = 1 ) fflush(stderr); printf( R"usage(Usage: - example_chat client SERVER_ADDR + example_chat client [SERVER_ADDR] (omit SERVER_ADDR to auto-discover on LAN) example_chat server [--port PORT] )usage" ); @@ -752,9 +755,24 @@ int main( int argc, const char *argv[] ) PrintUsageAndExit(); } - if ( bClient == bServer || ( bClient && addrServer.IsIPv6AllZeros() ) ) + if ( bClient == bServer ) PrintUsageAndExit(); + if ( bClient && addrServer.IsIPv6AllZeros() ) + { + Printf( "No server address specified. Searching for servers on LAN...\n" ); + std::vector vecServers; + if ( !CLanDiscoveryClient::Discover( vecServers ) || vecServers.empty() ) + { + FatalError( "No server discovered on the local network. Specify SERVER_ADDR manually." ); + } + + addrServer = vecServers[0].m_addr; + char szAddr[64]; + addrServer.ToString( szAddr, sizeof(szAddr), true ); + Printf( "Discovered '%s' at %s! Connecting...\n", vecServers[0].m_strServerName.c_str(), szAddr ); + } + // Create client and server sockets InitSteamDatagramConnectionSockets(); LocalUserInput_Init(); diff --git a/examples/lan_discovery.cpp b/examples/lan_discovery.cpp new file mode 100644 index 00000000..7a0d8753 --- /dev/null +++ b/examples/lan_discovery.cpp @@ -0,0 +1,241 @@ +//====== Copyright Valve Corporation, All rights reserved. ==================== +// +// Simple UDP-based LAN Discovery beacon and client. +// +//============================================================================= + +#include "lan_discovery.h" +#include +#include + +#ifndef _WIN32 + #include + #include + #include + #include + #include + #include + #include + typedef int SOCKET; + constexpr SOCKET INVALID_SOCKET = -1; + inline void closesocket( SOCKET s ) { close( s ); } + inline int GetSocketError() { return errno; } +#else + #include + #include + typedef int socklen_t; + inline int GetSocketError() { return WSAGetLastError(); } + #define poll WSAPoll +#endif + +CLanDiscoveryBeacon::CLanDiscoveryBeacon() +{ +} + +CLanDiscoveryBeacon::~CLanDiscoveryBeacon() +{ + Stop(); +} + +bool CLanDiscoveryBeacon::Start( uint16 nGnsPort, const char *pszServerName, uint16 nDiscoveryPort ) +{ + Stop(); + + std::string strName = pszServerName ? pszServerName : "GameNetworkingSocketsServer"; + + m_bRunning = true; + m_thread = std::thread( &CLanDiscoveryBeacon::BeaconThread, this, nGnsPort, strName, nDiscoveryPort ); + return true; +} + +void CLanDiscoveryBeacon::Stop() +{ + if ( m_bRunning.exchange( false ) ) + { + if ( m_socket != (int)INVALID_SOCKET ) + { + closesocket( (SOCKET)m_socket ); + m_socket = (int)INVALID_SOCKET; + } + if ( m_thread.joinable() ) + m_thread.join(); + } +} + +void CLanDiscoveryBeacon::BeaconThread( uint16 nGnsPort, std::string strServerName, uint16 nDiscoveryPort ) +{ + SOCKET fd = socket( AF_INET, SOCK_DGRAM, 0 ); + if ( fd == INVALID_SOCKET ) + { + fprintf( stderr, "[LAN Discovery] Failed to create UDP socket: %d\n", GetSocketError() ); + return; + } + m_socket = (int)fd; + + int opt = 1; + setsockopt( fd, SOL_SOCKET, SO_REUSEADDR, (const char *)&opt, sizeof(opt) ); + + sockaddr_in bindAddr{}; + bindAddr.sin_family = AF_INET; + bindAddr.sin_port = htons( nDiscoveryPort ); + bindAddr.sin_addr.s_addr = htonl( INADDR_ANY ); + + if ( bind( fd, (sockaddr *)&bindAddr, sizeof(bindAddr) ) < 0 ) + { + fprintf( stderr, "[LAN Discovery] Failed to bind beacon port %d: %d\n", nDiscoveryPort, GetSocketError() ); + closesocket( fd ); + m_socket = (int)INVALID_SOCKET; + return; + } + + printf( "[LAN Discovery] Beacon listening on UDP port %d (advertising GNS port %d)\n", nDiscoveryPort, nGnsPort ); + fflush( stdout ); + + while ( m_bRunning ) + { + pollfd pfd{}; + pfd.fd = fd; + pfd.events = POLLIN; + + int rc = poll( &pfd, 1, 200 ); + if ( rc <= 0 || !m_bRunning ) + continue; + + char szBuffer[256]; + sockaddr_in fromAddr{}; + socklen_t nFromLen = sizeof(fromAddr); + + int cbRecv = recvfrom( fd, szBuffer, sizeof(szBuffer) - 1, 0, (sockaddr *)&fromAddr, &nFromLen ); + if ( cbRecv <= 0 ) + continue; + + szBuffer[cbRecv] = '\0'; + + char szFromIP[INET_ADDRSTRLEN]; + inet_ntop( AF_INET, &fromAddr.sin_addr, szFromIP, sizeof(szFromIP) ); + uint16 nFromPort = ntohs( fromAddr.sin_port ); + + if ( strncmp( szBuffer, "GNS_DISCOVER", 12 ) == 0 ) + { + printf( "[LAN Discovery] Query from %s:%d -> responding with port %d (%s)\n", + szFromIP, nFromPort, nGnsPort, strServerName.c_str() ); + fflush( stdout ); + + char szOffer[256]; + int cbOffer = snprintf( szOffer, sizeof(szOffer), "GNS_OFFER:%u:%s", nGnsPort, strServerName.c_str() ); + sendto( fd, szOffer, cbOffer, 0, (sockaddr *)&fromAddr, nFromLen ); + } + else if ( strncmp( szBuffer, "GNS_PING", 8 ) == 0 ) + { + printf( "[LAN Discovery] Mobile/external ping from %s:%d -> responding PONG\n", szFromIP, nFromPort ); + fflush( stdout ); + + const char szPong[] = "GNS_PONG\n"; + sendto( fd, szPong, sizeof(szPong) - 1, 0, (sockaddr *)&fromAddr, nFromLen ); + } + } + + closesocket( fd ); + m_socket = (int)INVALID_SOCKET; +} + +bool CLanDiscoveryClient::Discover( std::vector &outServers, int nTimeoutMs, uint16 nDiscoveryPort ) +{ + outServers.clear(); + + SOCKET fd = socket( AF_INET, SOCK_DGRAM, 0 ); + if ( fd == INVALID_SOCKET ) + { + fprintf( stderr, "[LAN Discovery] Failed to create UDP client socket: %d\n", GetSocketError() ); + return false; + } + + int bcast = 1; + if ( setsockopt( fd, SOL_SOCKET, SO_BROADCAST, (const char *)&bcast, sizeof(bcast) ) < 0 ) + { + fprintf( stderr, "[LAN Discovery] Failed to enable SO_BROADCAST: %d\n", GetSocketError() ); + closesocket( fd ); + return false; + } + + sockaddr_in targetAddr{}; + targetAddr.sin_family = AF_INET; + targetAddr.sin_port = htons( nDiscoveryPort ); + targetAddr.sin_addr.s_addr = htonl( INADDR_BROADCAST ); + + const char szRequest[] = "GNS_DISCOVER"; + if ( sendto( fd, szRequest, sizeof(szRequest) - 1, 0, (sockaddr *)&targetAddr, sizeof(targetAddr) ) < 0 ) + { + fprintf( stderr, "[LAN Discovery] Failed to send broadcast query: %d\n", GetSocketError() ); + closesocket( fd ); + return false; + } + + int nRemainingMs = nTimeoutMs; + while ( nRemainingMs > 0 ) + { + pollfd pfd{}; + pfd.fd = fd; + pfd.events = POLLIN; + + int nPollMs = ( nRemainingMs > 200 ) ? 200 : nRemainingMs; + int rc = poll( &pfd, 1, nPollMs ); + nRemainingMs -= nPollMs; + + if ( rc <= 0 ) + continue; + + char szBuffer[256]; + sockaddr_in fromAddr{}; + socklen_t nFromLen = sizeof(fromAddr); + + int cbRecv = recvfrom( fd, szBuffer, sizeof(szBuffer) - 1, 0, (sockaddr *)&fromAddr, &nFromLen ); + if ( cbRecv <= 0 ) + continue; + + szBuffer[cbRecv] = '\0'; + + if ( strncmp( szBuffer, "GNS_OFFER:", 10 ) == 0 ) + { + uint32 nRemoteGnsPort = 0; + char szName[128] = "Unknown"; + + const char *pColon = strchr( szBuffer + 10, ':' ); + if ( pColon ) + { + nRemoteGnsPort = (uint32)atoi( szBuffer + 10 ); + snprintf( szName, sizeof(szName), "%s", pColon + 1 ); + } + else + { + nRemoteGnsPort = (uint32)atoi( szBuffer + 10 ); + } + + char szFromIP[INET_ADDRSTRLEN]; + inet_ntop( AF_INET, &fromAddr.sin_addr, szFromIP, sizeof(szFromIP) ); + + DiscoveredServer_t srv; + srv.m_addr.Clear(); + srv.m_addr.ParseString( szFromIP ); + srv.m_addr.m_port = (uint16)nRemoteGnsPort; + srv.m_strServerName = szName; + + // Avoid duplicates if multiple network interfaces respond + bool bAlreadyFound = false; + for ( const auto &existing : outServers ) + { + if ( existing.m_addr == srv.m_addr ) + { + bAlreadyFound = true; + break; + } + } + + if ( !bAlreadyFound ) + outServers.push_back( srv ); + } + } + + closesocket( fd ); + return !outServers.empty(); +} diff --git a/examples/lan_discovery.h b/examples/lan_discovery.h new file mode 100644 index 00000000..7aabc67a --- /dev/null +++ b/examples/lan_discovery.h @@ -0,0 +1,61 @@ +//====== Copyright Valve Corporation, All rights reserved. ==================== +// +// Simple UDP-based LAN Discovery beacon and client. +// Enables GameNetworkingSockets applications to advertise and discover servers +// on the same local subnet without requiring central matchmaking or manual IP entry. +// +//============================================================================= + +#pragma once + +#include +#include +#include +#include +#include +#include + +const uint16 k_nDefaultDiscoveryPort = 27015; + +/// Information about a server found on the local network. +struct DiscoveredServer_t +{ + SteamNetworkingIPAddr m_addr; + std::string m_strServerName; +}; + +/// Advertises a local server to the LAN via UDP beacon. +class CLanDiscoveryBeacon +{ +public: + CLanDiscoveryBeacon(); + ~CLanDiscoveryBeacon(); + + /// Start advertising a GameNetworkingSockets listen port. + /// nGnsPort: the port where ISteamNetworkingSockets is listening. + /// pszServerName: optional human-readable name for the server. + /// nDiscoveryPort: UDP port to listen for discovery broadcasts (default 27015). + bool Start( uint16 nGnsPort, const char *pszServerName = nullptr, uint16 nDiscoveryPort = k_nDefaultDiscoveryPort ); + + /// Stop the beacon and close the UDP socket. + void Stop(); + +private: + std::atomic m_bRunning{ false }; + std::thread m_thread; + int m_socket = -1; + + void BeaconThread( uint16 nGnsPort, std::string strServerName, uint16 nDiscoveryPort ); +}; + +/// Client utility to discover servers on the local subnet. +class CLanDiscoveryClient +{ +public: + /// Broadcast a discovery request on the LAN and collect responses. + /// outServers: list of discovered servers. + /// nTimeoutMs: how long to wait for responses in milliseconds (default 1500ms). + /// nDiscoveryPort: the discovery port to query (default 27015). + /// Returns true if at least one server was found. + static bool Discover( std::vector &outServers, int nTimeoutMs = 1500, uint16 nDiscoveryPort = k_nDefaultDiscoveryPort ); +}; From aebd3e55980629da821bb69fcc712a5fd6e624b3 Mon Sep 17 00:00:00 2001 From: sashakosti Date: Sun, 11 Oct 2026 05:26:43 +0300 Subject: [PATCH 2/2] examples/lan_discovery: Fix socket lifetime, add query retries and address ranking - Eliminate double-close and cross-thread socket data race in beacon teardown. - Retransmit discovery query every 500ms to tolerate packet loss on wireless LANs. - Prioritize routable physical LAN interfaces over internal container bridges. - Add ScopedWinsockInit guard on Windows. --- examples/lan_discovery.cpp | 74 ++++++++++++++++++++++++++++++++------ examples/lan_discovery.h | 1 - 2 files changed, 63 insertions(+), 12 deletions(-) diff --git a/examples/lan_discovery.cpp b/examples/lan_discovery.cpp index 7a0d8753..23f26cd0 100644 --- a/examples/lan_discovery.cpp +++ b/examples/lan_discovery.cpp @@ -26,8 +26,36 @@ typedef int socklen_t; inline int GetSocketError() { return WSAGetLastError(); } #define poll WSAPoll + + struct ScopedWinsockInit_t + { + ScopedWinsockInit_t() + { + WSADATA wsaData; + WSAStartup( MAKEWORD( 2, 2 ), &wsaData ); + } + ~ScopedWinsockInit_t() + { + WSACleanup(); + } + }; #endif +// Helper: score IP address (prefer physical LAN over container/virtual bridge) +static int ScoreIPAddress( const char *pszIP ) +{ + // 192.168.x.x / 10.x.x.x -> highest priority (real home/office Wi-Fi and LAN) + if ( strncmp( pszIP, "192.168.", 8 ) == 0 || strncmp( pszIP, "10.", 3 ) == 0 ) + return 100; + // 127.x.x.x -> local loopback + if ( strncmp( pszIP, "127.", 4 ) == 0 ) + return 50; + // 172.16-31.x.x (often Docker or container virtual bridge) -> lower priority + if ( strncmp( pszIP, "172.", 4 ) == 0 ) + return 10; + return 30; +} + CLanDiscoveryBeacon::CLanDiscoveryBeacon() { } @@ -52,11 +80,6 @@ void CLanDiscoveryBeacon::Stop() { if ( m_bRunning.exchange( false ) ) { - if ( m_socket != (int)INVALID_SOCKET ) - { - closesocket( (SOCKET)m_socket ); - m_socket = (int)INVALID_SOCKET; - } if ( m_thread.joinable() ) m_thread.join(); } @@ -64,13 +87,16 @@ void CLanDiscoveryBeacon::Stop() void CLanDiscoveryBeacon::BeaconThread( uint16 nGnsPort, std::string strServerName, uint16 nDiscoveryPort ) { +#ifdef _WIN32 + ScopedWinsockInit_t winsock; +#endif + SOCKET fd = socket( AF_INET, SOCK_DGRAM, 0 ); if ( fd == INVALID_SOCKET ) { fprintf( stderr, "[LAN Discovery] Failed to create UDP socket: %d\n", GetSocketError() ); return; } - m_socket = (int)fd; int opt = 1; setsockopt( fd, SOL_SOCKET, SO_REUSEADDR, (const char *)&opt, sizeof(opt) ); @@ -84,7 +110,6 @@ void CLanDiscoveryBeacon::BeaconThread( uint16 nGnsPort, std::string strServerNa { fprintf( stderr, "[LAN Discovery] Failed to bind beacon port %d: %d\n", nDiscoveryPort, GetSocketError() ); closesocket( fd ); - m_socket = (int)INVALID_SOCKET; return; } @@ -97,7 +122,7 @@ void CLanDiscoveryBeacon::BeaconThread( uint16 nGnsPort, std::string strServerNa pfd.fd = fd; pfd.events = POLLIN; - int rc = poll( &pfd, 1, 200 ); + int rc = poll( &pfd, 1, 100 ); if ( rc <= 0 || !m_bRunning ) continue; @@ -135,14 +160,18 @@ void CLanDiscoveryBeacon::BeaconThread( uint16 nGnsPort, std::string strServerNa } } + // Close cleanly exactly once upon loop exit closesocket( fd ); - m_socket = (int)INVALID_SOCKET; } bool CLanDiscoveryClient::Discover( std::vector &outServers, int nTimeoutMs, uint16 nDiscoveryPort ) { outServers.clear(); +#ifdef _WIN32 + ScopedWinsockInit_t winsock; +#endif + SOCKET fd = socket( AF_INET, SOCK_DGRAM, 0 ); if ( fd == INVALID_SOCKET ) { @@ -172,13 +201,22 @@ bool CLanDiscoveryClient::Discover( std::vector &outServers, } int nRemainingMs = nTimeoutMs; + int nLastBroadcastRemainingMs = nTimeoutMs; + while ( nRemainingMs > 0 ) { + // Retransmit query every 500ms to tolerate wireless packet loss on consumer Wi-Fi + if ( ( nLastBroadcastRemainingMs - nRemainingMs ) >= 500 ) + { + sendto( fd, szRequest, sizeof(szRequest) - 1, 0, (sockaddr *)&targetAddr, sizeof(targetAddr) ); + nLastBroadcastRemainingMs = nRemainingMs; + } + pollfd pfd{}; pfd.fd = fd; pfd.events = POLLIN; - int nPollMs = ( nRemainingMs > 200 ) ? 200 : nRemainingMs; + int nPollMs = ( nRemainingMs > 100 ) ? 100 : nRemainingMs; int rc = poll( &pfd, 1, nPollMs ); nRemainingMs -= nPollMs; @@ -232,7 +270,21 @@ bool CLanDiscoveryClient::Discover( std::vector &outServers, } if ( !bAlreadyFound ) - outServers.push_back( srv ); + { + if ( !outServers.empty() ) + { + char szExistingIP[64]; + outServers[0].m_addr.ToString( szExistingIP, sizeof(szExistingIP), false ); + if ( ScoreIPAddress( szFromIP ) > ScoreIPAddress( szExistingIP ) ) + outServers.insert( outServers.begin(), srv ); + else + outServers.push_back( srv ); + } + else + { + outServers.push_back( srv ); + } + } } } diff --git a/examples/lan_discovery.h b/examples/lan_discovery.h index 7aabc67a..b7aadc24 100644 --- a/examples/lan_discovery.h +++ b/examples/lan_discovery.h @@ -43,7 +43,6 @@ class CLanDiscoveryBeacon private: std::atomic m_bRunning{ false }; std::thread m_thread; - int m_socket = -1; void BeaconThread( uint16 nGnsPort, std::string strServerName, uint16 nDiscoveryPort ); };