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..23f26cd0 --- /dev/null +++ b/examples/lan_discovery.cpp @@ -0,0 +1,293 @@ +//====== 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 + + 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() +{ +} + +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_thread.joinable() ) + m_thread.join(); + } +} + +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; + } + + 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 ); + 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, 100 ); + 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 ); + } + } + + // Close cleanly exactly once upon loop exit + closesocket( fd ); +} + +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 ) + { + 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; + 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 > 100 ) ? 100 : 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 ) + { + 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 ); + } + } + } + } + + closesocket( fd ); + return !outServers.empty(); +} diff --git a/examples/lan_discovery.h b/examples/lan_discovery.h new file mode 100644 index 00000000..b7aadc24 --- /dev/null +++ b/examples/lan_discovery.h @@ -0,0 +1,60 @@ +//====== 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; + + 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 ); +};