Skip to content

Instantly share code, notes, and snippets.

@ZCube
Created June 25, 2020 15:41
Show Gist options
  • Save ZCube/306525ea25016ab05305a83e8e4307a9 to your computer and use it in GitHub Desktop.
Save ZCube/306525ea25016ab05305a83e8e4307a9 to your computer and use it in GitHub Desktop.
example_p2p.cpp draft for Valve GameNetworkSockets
#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