Created
June 25, 2020 15:41
-
-
Save ZCube/306525ea25016ab05305a83e8e4307a9 to your computer and use it in GitHub Desktop.
example_p2p.cpp draft for Valve GameNetworkSockets
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <assert.h> | |
#include <stdio.h> | |
#include <stdarg.h> | |
#include <string.h> | |
#include <algorithm> | |
#include <string> | |
#include <random> | |
#include <chrono> | |
#include <thread> | |
#include <mutex> | |
#include <queue> | |
#include <map> | |
#include <cctype> | |
#include <boost/algorithm/hex.hpp> | |
#include <steam/steamnetworkingsockets.h> | |
#include <steam/isteamnetworkingutils.h> | |
#ifndef STEAMNETWORKINGSOCKETS_OPENSOURCE | |
#include <steam/steam_api.h> | |
#endif | |
#ifdef WIN32 | |
#include <windows.h> // Ug, for NukeProcess -- see below | |
#else | |
#include <unistd.h> | |
#include <signal.h> | |
#endif | |
#include <iostream> | |
///////////////////////////////////////////////////////////////////////////// | |
// | |
// Common stuff | |
// | |
///////////////////////////////////////////////////////////////////////////// | |
bool g_bQuit = false; | |
SteamNetworkingMicroseconds g_logTimeZero; | |
// We do this because I won't want to figure out how to cleanly shut | |
// down the thread that is reading from stdin. | |
static void NukeProcess( int rc ) | |
{ | |
#ifdef WIN32 | |
ExitProcess( rc ); | |
#else | |
kill( getpid(), SIGKILL ); | |
#endif | |
} | |
static void DebugOutput( ESteamNetworkingSocketsDebugOutputType eType, const char *pszMsg ) | |
{ | |
SteamNetworkingMicroseconds time = SteamNetworkingUtils()->GetLocalTimestamp() - g_logTimeZero; | |
printf( "%10.6f %s\n", time*1e-6, pszMsg ); | |
fflush(stdout); | |
if ( eType == k_ESteamNetworkingSocketsDebugOutputType_Bug ) | |
{ | |
fflush(stdout); | |
fflush(stderr); | |
NukeProcess(1); | |
} | |
} | |
static void FatalError( const char *fmt, ... ) | |
{ | |
char text[ 2048 ]; | |
va_list ap; | |
va_start( ap, fmt ); | |
vsprintf( text, fmt, ap ); | |
va_end(ap); | |
char *nl = strchr( text, '\0' ) - 1; | |
if ( nl >= text && *nl == '\n' ) | |
*nl = '\0'; | |
DebugOutput( k_ESteamNetworkingSocketsDebugOutputType_Bug, text ); | |
} | |
static void Printf( const char *fmt, ... ) | |
{ | |
char text[ 2048 ]; | |
va_list ap; | |
va_start( ap, fmt ); | |
vsprintf( text, fmt, ap ); | |
va_end(ap); | |
char *nl = strchr( text, '\0' ) - 1; | |
if ( nl >= text && *nl == '\n' ) | |
*nl = '\0'; | |
DebugOutput( k_ESteamNetworkingSocketsDebugOutputType_Msg, text ); | |
} | |
static void InitSteamDatagramConnectionSockets() | |
{ | |
#ifdef STEAMNETWORKINGSOCKETS_OPENSOURCE | |
SteamDatagramErrMsg errMsg; | |
if ( !GameNetworkingSockets_Init( nullptr, errMsg ) ) | |
FatalError( "GameNetworkingSockets_Init failed. %s", errMsg ); | |
#else | |
SteamDatagramClient_SetAppID( 570 ); // Just set something, doesn't matter what | |
//SteamDatagramClient_SetUniverse( k_EUniverseDev ); | |
SteamDatagramErrMsg errMsg; | |
if ( !SteamDatagramClient_Init( true, errMsg ) ) | |
FatalError( "SteamDatagramClient_Init failed. %s", errMsg ); | |
// Disable authentication when running with Steam, for this | |
// example, since we're not a real app. | |
// | |
// Authentication is disabled automatically in the open-source | |
// version since we don't have a trusted third party to issue | |
// certs. | |
SteamNetworkingUtils()->SetGlobalConfigValueInt32( k_ESteamNetworkingConfig_IP_AllowWithoutAuth, 1 ); | |
#endif | |
g_logTimeZero = SteamNetworkingUtils()->GetLocalTimestamp(); | |
SteamNetworkingUtils()->SetDebugOutputFunction( k_ESteamNetworkingSocketsDebugOutputType_Msg, DebugOutput ); | |
} | |
static void ShutdownSteamDatagramConnectionSockets() | |
{ | |
// Give connections time to finish up. This is an application layer protocol | |
// here, it's not TCP. Note that if you have an application and you need to be | |
// more sure about cleanup, you won't be able to do this. You will need to send | |
// a message and then either wait for the peer to close the connection, or | |
// you can pool the connection to see if any reliable data is pending. | |
std::this_thread::sleep_for( std::chrono::milliseconds( 500 ) ); | |
#ifdef STEAMNETWORKINGSOCKETS_OPENSOURCE | |
GameNetworkingSockets_Kill(); | |
#else | |
SteamDatagramClient_Kill(); | |
#endif | |
} | |
///////////////////////////////////////////////////////////////////////////// | |
// | |
// Non-blocking console user input. Sort of. | |
// Why is this so hard? | |
// | |
///////////////////////////////////////////////////////////////////////////// | |
std::mutex mutexUserInputQueue; | |
std::queue< std::string > queueUserInput; | |
std::thread *s_pThreadUserInput = nullptr; | |
void LocalUserInput_Init() | |
{ | |
s_pThreadUserInput = new std::thread( []() | |
{ | |
while ( !g_bQuit ) | |
{ | |
char szLine[ 4000 ]; | |
if ( !fgets( szLine, sizeof(szLine), stdin ) ) | |
{ | |
// Well, you would hope that you could close the handle | |
// from the other thread to trigger this. Nope. | |
if ( g_bQuit ) | |
return; | |
g_bQuit = true; | |
Printf( "Failed to read on stdin, quitting\n" ); | |
break; | |
} | |
mutexUserInputQueue.lock(); | |
queueUserInput.push( std::string( szLine ) ); | |
mutexUserInputQueue.unlock(); | |
} | |
} ); | |
} | |
void LocalUserInput_Kill() | |
{ | |
// Does not work. We won't clean up, we'll just nuke the process. | |
// g_bQuit = true; | |
// _close( fileno( stdin ) ); | |
// | |
// if ( s_pThreadUserInput ) | |
// { | |
// s_pThreadUserInput->join(); | |
// delete s_pThreadUserInput; | |
// s_pThreadUserInput = nullptr; | |
// } | |
} | |
// You really gotta wonder what kind of pedantic garbage was | |
// going through the minds of people who designed std::string | |
// that they decided not to include trim. | |
// https://stackoverflow.com/questions/216823/whats-the-best-way-to-trim-stdstring | |
// trim from start (in place) | |
static inline void ltrim(std::string &s) { | |
s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](int ch) { | |
return !std::isspace(ch); | |
})); | |
} | |
// trim from end (in place) | |
static inline void rtrim(std::string &s) { | |
s.erase(std::find_if(s.rbegin(), s.rend(), [](int ch) { | |
return !std::isspace(ch); | |
}).base(), s.end()); | |
} | |
// Read the next line of input from stdin, if anything is available. | |
bool LocalUserInput_GetNext( std::string &result ) | |
{ | |
bool got_input = false; | |
mutexUserInputQueue.lock(); | |
while ( !queueUserInput.empty() && !got_input ) | |
{ | |
result = queueUserInput.front(); | |
queueUserInput.pop(); | |
ltrim(result); | |
rtrim(result); | |
got_input = !result.empty(); // ignore blank lines | |
} | |
mutexUserInputQueue.unlock(); | |
return got_input; | |
} | |
///////////////////////////////////////////////////////////////////////////// | |
// | |
// ChatClient | |
// | |
///////////////////////////////////////////////////////////////////////////// | |
class ChatClient : private ISteamNetworkingSocketsCallbacks, public ISteamNetworkingCustomSignalingRecvContext, public ISteamNetworkingConnectionCustomSignaling | |
{ | |
public: | |
int cnt = 0; | |
bool SendSignal(HSteamNetConnection hConn, const SteamNetConnectionInfo_t& info, const void* pMsg, int cbMsg) override { | |
if (cnt ==0) { | |
std::string dest; | |
dest = boost::algorithm::hex(std::string((char*)pMsg,(char*)pMsg + cbMsg)); | |
std::cout << dest << std::endl; | |
std::string data; | |
data = boost::algorithm::unhex(dest); | |
std::cout << (std::string((char*)pMsg,(char*)pMsg + cbMsg) == data) << std::endl; | |
std::cout << "recieved" << std::endl; | |
cnt ++; | |
} | |
// TODO: what? | |
return true; | |
} | |
void Release() override { | |
// not implemented | |
} | |
void RecvSignal(const std::string& hexString) { | |
std::string data; | |
data = boost::algorithm::unhex(hexString); | |
std::cout << "recieved" << std::endl; | |
m_pInterface->ReceivedP2PCustomSignal(data.data(), data.size(), this); | |
} | |
ISteamNetworkingConnectionCustomSignaling* OnConnectRequest(HSteamNetConnection hConn, const SteamNetworkingIdentity& identityPeer) override { | |
char id[1024] = {0,}; | |
identityPeer.ToString(id, 1024); | |
std::cout << id << std::endl; | |
// not implemented -> ignore all | |
return nullptr; | |
} | |
void SendRejectionSignal(const SteamNetworkingIdentity& identityPeer, const void* pMsg, int cbMsg) override { | |
// not implemented | |
} | |
void Run() | |
{ | |
m_pInterface = SteamNetworkingSockets(); | |
SteamNetworkingIdentity identityLocal; | |
m_pInterface->GetIdentity(&identityLocal); | |
char id[1024] = {0,}; | |
identityLocal.ToString(id, 1024); | |
std::cout << id << std::endl; | |
m_hConnection = m_pInterface->ConnectP2PCustomSignaling(this, &identityLocal, 0, nullptr); | |
if ( m_hConnection == k_HSteamNetConnection_Invalid ) | |
FatalError( "Failed to create connection" ); | |
while ( !g_bQuit ) | |
{ | |
PollIncomingMessages(); | |
PollConnectionStateChanges(); | |
PollLocalUserInput(); | |
std::this_thread::sleep_for( std::chrono::milliseconds( 10 ) ); | |
} | |
} | |
private: | |
HSteamNetConnection m_hConnection; | |
ISteamNetworkingSockets *m_pInterface; | |
void PollIncomingMessages() | |
{ | |
while ( !g_bQuit ) | |
{ | |
ISteamNetworkingMessage *pIncomingMsg = nullptr; | |
int numMsgs = m_pInterface->ReceiveMessagesOnConnection( m_hConnection, &pIncomingMsg, 1 ); | |
if ( numMsgs == 0 ) | |
break; | |
if ( numMsgs < 0 ) | |
FatalError( "Error checking for messages" ); | |
// Just echo anything we get from the server | |
fwrite( pIncomingMsg->m_pData, 1, pIncomingMsg->m_cbSize, stdout ); | |
fputc( '\n', stdout ); | |
// We don't need this anymore. | |
pIncomingMsg->Release(); | |
} | |
} | |
void PollConnectionStateChanges() | |
{ | |
m_pInterface->RunCallbacks( this ); | |
} | |
void PollLocalUserInput() | |
{ | |
std::string cmd; | |
while ( !g_bQuit && LocalUserInput_GetNext( cmd )) | |
{ | |
// Check for known commands | |
if ( strcmp( cmd.c_str(), "/quit" ) == 0 ) | |
{ | |
g_bQuit = true; | |
Printf( "Disconnecting from chat server" ); | |
// Close the connection gracefully. | |
// We use linger mode to ask for any remaining reliable data | |
// to be flushed out. But remember this is an application | |
// protocol on UDP. See ShutdownSteamDatagramConnectionSockets | |
m_pInterface->CloseConnection( m_hConnection, 0, "Goodbye", true ); | |
break; | |
} | |
std::cout << cmd << std::endl; | |
RecvSignal(cmd); | |
// // Anything else, just send it to the server and let them parse it | |
// m_pInterface->SendMessageToConnection( m_hConnection, cmd.c_str(), (uint32)cmd.length(), k_nSteamNetworkingSend_Reliable, nullptr ); | |
} | |
} | |
virtual void OnSteamNetConnectionStatusChanged( SteamNetConnectionStatusChangedCallback_t *pInfo ) override | |
{ | |
assert( pInfo->m_hConn == m_hConnection || m_hConnection == k_HSteamNetConnection_Invalid ); | |
// What's the state of the connection? | |
switch ( pInfo->m_info.m_eState ) | |
{ | |
case k_ESteamNetworkingConnectionState_None: | |
// NOTE: We will get callbacks here when we destroy connections. You can ignore these. | |
break; | |
case k_ESteamNetworkingConnectionState_ClosedByPeer: | |
case k_ESteamNetworkingConnectionState_ProblemDetectedLocally: | |
{ | |
g_bQuit = true; | |
// Print an appropriate message | |
if ( pInfo->m_eOldState == k_ESteamNetworkingConnectionState_Connecting ) | |
{ | |
// Note: we could distinguish between a timeout, a rejected connection, | |
// or some other transport problem. | |
Printf( "We sought the remote host, yet our efforts were met with defeat. (%s)", pInfo->m_info.m_szEndDebug ); | |
} | |
else if ( pInfo->m_info.m_eState == k_ESteamNetworkingConnectionState_ProblemDetectedLocally ) | |
{ | |
Printf( "Alas, troubles beset us; we have lost contact with the host. (%s)", pInfo->m_info.m_szEndDebug ); | |
} | |
else | |
{ | |
// NOTE: We could check the reason code for a normal disconnection | |
Printf( "The host hath bidden us farewell. (%s)", pInfo->m_info.m_szEndDebug ); | |
} | |
// Clean up the connection. This is important! | |
// The connection is "closed" in the network sense, but | |
// it has not been destroyed. We must close it on our end, too | |
// to finish up. The reason information do not matter in this case, | |
// and we cannot linger because it's already closed on the other end, | |
// so we just pass 0's. | |
m_pInterface->CloseConnection( pInfo->m_hConn, 0, nullptr, false ); | |
m_hConnection = k_HSteamNetConnection_Invalid; | |
break; | |
} | |
case k_ESteamNetworkingConnectionState_Connecting: | |
// We will get this callback when we start connecting. | |
// We can ignore this. | |
break; | |
case k_ESteamNetworkingConnectionState_Connected: | |
Printf( "Connected to server OK" ); | |
break; | |
default: | |
// Silences -Wswitch | |
break; | |
} | |
} | |
}; | |
#include <functional> | |
int main(int argc, char** argv) { | |
bool bServer = false; | |
bool bClient = false; | |
SteamNetworkingIPAddr addrServer; addrServer.Clear(); | |
// Create client and server sockets | |
InitSteamDatagramConnectionSockets(); | |
LocalUserInput_Init(); | |
SteamNetworkingUtils()->SetConfigValue(k_ESteamNetworkingConfig_P2P_STUN_ServerList, k_ESteamNetworkingConfig_Global, 0, k_ESteamNetworkingConfig_String, "stun1.l.google.com:19302"); | |
int32_t val = 1000000; | |
SteamNetworkingUtils()->SetConfigValue(k_ESteamNetworkingConfig_TimeoutInitial, k_ESteamNetworkingConfig_Global, 0, k_ESteamNetworkingConfig_Int32, &val); | |
ChatClient client; | |
client.Run(); | |
ShutdownSteamDatagramConnectionSockets(); | |
// Ug, why is there no simple solution for portable, non-blocking console user input? | |
// Just nuke the process | |
//LocalUserInput_Kill(); | |
NukeProcess(0); | |
return 0; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment