mirror of
https://github.com/ValveSoftware/GameNetworkingSockets.git
synced 2026-05-29 16:20:34 +00:00
748 lines
22 KiB
C++
748 lines
22 KiB
C++
//====== Copyright Valve Corporation, All rights reserved. ====================
|
|
//
|
|
// Example client/server chat application using SteamNetworkingSockets
|
|
|
|
#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 <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
|
|
|
|
/////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// 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_SetAppIDAndUniverse( 570, k_EUniverseDev ); // Just set something, doesn't matter what
|
|
|
|
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;
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// ChatServer
|
|
//
|
|
/////////////////////////////////////////////////////////////////////////////
|
|
|
|
class ChatServer : private ISteamNetworkingSocketsCallbacks
|
|
{
|
|
public:
|
|
void Run( uint16 nPort )
|
|
{
|
|
// Select instance to use. For now we'll always use the default.
|
|
// But we could use SteamGameServerNetworkingSockets() on Steam.
|
|
m_pInterface = SteamNetworkingSockets();
|
|
|
|
// Start listening
|
|
SteamNetworkingIPAddr serverLocalAddr;
|
|
serverLocalAddr.Clear();
|
|
serverLocalAddr.m_port = nPort;
|
|
m_hListenSock = m_pInterface->CreateListenSocketIP( serverLocalAddr );
|
|
if ( m_hListenSock == k_HSteamListenSocket_Invalid )
|
|
FatalError( "Failed to listen on port %d", nPort );
|
|
Printf( "Server listening on port %d\n", nPort );
|
|
|
|
while ( !g_bQuit )
|
|
{
|
|
PollIncomingMessages();
|
|
PollConnectionStateChanges();
|
|
PollLocalUserInput();
|
|
std::this_thread::sleep_for( std::chrono::milliseconds( 10 ) );
|
|
}
|
|
|
|
// Close all the connections
|
|
Printf( "Closing connections...\n" );
|
|
for ( auto it: m_mapClients )
|
|
{
|
|
// Send them one more goodbye message. Note that we also have the
|
|
// connection close reason as a place to send final data. However,
|
|
// that's usually best left for more diagnostic/debug text not actual
|
|
// protocol strings.
|
|
SendStringToClient( it.first, "Server is shutting down. Goodbye." );
|
|
|
|
// Close the connection. We use "linger mode" to ask SteamNetworkingSockets
|
|
// to flush this out and close gracefully.
|
|
m_pInterface->CloseConnection( it.first, 0, "Server Shutdown", true );
|
|
}
|
|
m_mapClients.clear();
|
|
}
|
|
private:
|
|
|
|
HSteamListenSocket m_hListenSock;
|
|
ISteamNetworkingSockets *m_pInterface;
|
|
|
|
struct Client_t
|
|
{
|
|
std::string m_sNick;
|
|
};
|
|
|
|
std::map< HSteamNetConnection, Client_t > m_mapClients;
|
|
|
|
void SendStringToClient( HSteamNetConnection conn, const char *str )
|
|
{
|
|
m_pInterface->SendMessageToConnection( conn, str, (uint32)strlen(str), k_nSteamNetworkingSend_Reliable );
|
|
}
|
|
|
|
void SendStringToAllClients( const char *str, HSteamNetConnection except = k_HSteamNetConnection_Invalid )
|
|
{
|
|
for ( auto &c: m_mapClients )
|
|
{
|
|
if ( c.first != except )
|
|
SendStringToClient( c.first, str );
|
|
}
|
|
}
|
|
|
|
void PollIncomingMessages()
|
|
{
|
|
char temp[ 1024 ];
|
|
|
|
while ( !g_bQuit )
|
|
{
|
|
ISteamNetworkingMessage *pIncomingMsg = nullptr;
|
|
int numMsgs = m_pInterface->ReceiveMessagesOnListenSocket( m_hListenSock, &pIncomingMsg, 1 );
|
|
if ( numMsgs == 0 )
|
|
break;
|
|
if ( numMsgs < 0 )
|
|
FatalError( "Error checking for messages" );
|
|
assert( numMsgs == 1 && pIncomingMsg );
|
|
auto itClient = m_mapClients.find( pIncomingMsg->m_conn );
|
|
assert( itClient != m_mapClients.end() );
|
|
|
|
// '\0'-terminate it to make it easier to parse
|
|
std::string sCmd;
|
|
sCmd.assign( (const char *)pIncomingMsg->m_pData, pIncomingMsg->m_cbSize );
|
|
const char *cmd = sCmd.c_str();
|
|
|
|
// We don't need this anymore.
|
|
pIncomingMsg->Release();
|
|
|
|
// Check for known commands. None of this example code is secure or robust.
|
|
// Don't write a real server like this, please.
|
|
|
|
if ( strncmp( cmd, "/nick", 5 ) == 0 )
|
|
{
|
|
const char *nick = cmd+5;
|
|
while ( isspace(*nick) )
|
|
++nick;
|
|
|
|
// Let everybody else know they changed their name
|
|
sprintf( temp, "%s shall henceforth be known as %s", itClient->second.m_sNick.c_str(), nick );
|
|
SendStringToAllClients( temp, itClient->first );
|
|
|
|
// Respond to client
|
|
sprintf( temp, "Ye shall henceforth be known as %s", nick );
|
|
SendStringToClient( itClient->first, temp );
|
|
|
|
// Actually change their name
|
|
SetClientNick( itClient->first, nick );
|
|
continue;
|
|
}
|
|
|
|
// Assume it's just a ordinary chat message, dispatch to everybody else
|
|
sprintf( temp, "%s: %s", itClient->second.m_sNick.c_str(), cmd );
|
|
SendStringToAllClients( temp, itClient->first );
|
|
}
|
|
}
|
|
|
|
void PollConnectionStateChanges()
|
|
{
|
|
m_pInterface->RunCallbacks( this );
|
|
}
|
|
|
|
void PollLocalUserInput()
|
|
{
|
|
std::string cmd;
|
|
while ( !g_bQuit && LocalUserInput_GetNext( cmd ))
|
|
{
|
|
if ( strcmp( cmd.c_str(), "/quit" ) == 0 )
|
|
{
|
|
g_bQuit = true;
|
|
Printf( "Shutting down server" );
|
|
break;
|
|
}
|
|
|
|
// That's the only command we support
|
|
Printf( "The server only knows one command: '/quit'" );
|
|
}
|
|
}
|
|
|
|
void SetClientNick( HSteamNetConnection hConn, const char *nick )
|
|
{
|
|
|
|
// Remember their nick
|
|
m_mapClients[hConn].m_sNick = nick;
|
|
|
|
// Set the connection name, too, which is useful for debugging
|
|
m_pInterface->SetConnectionName( hConn, nick );
|
|
}
|
|
|
|
virtual void OnSteamNetConnectionStatusChanged( SteamNetConnectionStatusChangedCallback_t *pInfo ) override
|
|
{
|
|
char temp[1024];
|
|
|
|
// 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:
|
|
{
|
|
// Ignore if they were not previously connected. (If they disconnected
|
|
// before we accepted the connection.)
|
|
if ( pInfo->m_eOldState == k_ESteamNetworkingConnectionState_Connected )
|
|
{
|
|
|
|
// Locate the client. Note that it should have been found, because this
|
|
// is the only codepath where we remove clients (except on shutdown),
|
|
// and connection change callbacks are dispatched in queue order.
|
|
auto itClient = m_mapClients.find( pInfo->m_hConn );
|
|
assert( itClient != m_mapClients.end() );
|
|
|
|
// Select appropriate log messages
|
|
const char *pszDebugLogAction;
|
|
if ( pInfo->m_info.m_eState == k_ESteamNetworkingConnectionState_ProblemDetectedLocally )
|
|
{
|
|
pszDebugLogAction = "problem detected locally";
|
|
sprintf( temp, "Alas, %s hath fallen into shadow. (%s)", itClient->second.m_sNick.c_str(), pInfo->m_info.m_szEndDebug );
|
|
}
|
|
else
|
|
{
|
|
// Note that here we could check the reason code to see if
|
|
// it was a "usual" connection or an "unusual" one.
|
|
pszDebugLogAction = "closed by peer";
|
|
sprintf( temp, "%s hath departed", itClient->second.m_sNick.c_str() );
|
|
}
|
|
|
|
// Spew something to our own log. Note that because we put their nick
|
|
// as the connection description, it will show up, along with their
|
|
// transport-specific data (e.g. their IP address)
|
|
Printf( "Connection %s %s, reason %d: %s\n",
|
|
pInfo->m_info.m_szConnectionDescription,
|
|
pszDebugLogAction,
|
|
pInfo->m_info.m_eEndReason,
|
|
pInfo->m_info.m_szEndDebug
|
|
);
|
|
|
|
m_mapClients.erase( itClient );
|
|
|
|
// Send a message so everybody else knows what happened
|
|
SendStringToAllClients( temp );
|
|
}
|
|
else
|
|
{
|
|
assert( pInfo->m_eOldState == k_ESteamNetworkingConnectionState_Connecting );
|
|
}
|
|
|
|
// 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 );
|
|
break;
|
|
}
|
|
|
|
case k_ESteamNetworkingConnectionState_Connecting:
|
|
{
|
|
// This must be a new connection
|
|
assert( m_mapClients.find( pInfo->m_hConn ) == m_mapClients.end() );
|
|
|
|
Printf( "Connection request from %s", pInfo->m_info.m_szConnectionDescription );
|
|
|
|
// A client is attempting to connect
|
|
// Try to accept the connection.
|
|
if ( m_pInterface->AcceptConnection( pInfo->m_hConn ) != k_EResultOK )
|
|
{
|
|
// This could fail. If the remote host tried to connect, but then
|
|
// disconnected, the connection may already be half closed. Just
|
|
// destroy whatever we have on our side.
|
|
m_pInterface->CloseConnection( pInfo->m_hConn, 0, nullptr, false );
|
|
Printf( "Can't accept connection. (It was already closed?)" );
|
|
break;
|
|
}
|
|
|
|
// Generate a random nick. A random temporary nick
|
|
// is really dumb and not how you would write a real chat server.
|
|
// You would want them to have some sort of signon message,
|
|
// and you would keep their client in a state of limbo (connected,
|
|
// but not logged on) until them. I'm trying to keep this example
|
|
// code really simple.
|
|
char nick[ 64 ];
|
|
sprintf( nick, "BraveWarrior%d", 10000 + ( rand() % 100000 ) );
|
|
|
|
// Send them a welcome message
|
|
sprintf( temp, "Welcome, stranger. Thou art known to us for now as '%s'; upon thine command '/nick' we shall know thee otherwise.", nick );
|
|
SendStringToClient( pInfo->m_hConn, temp );
|
|
|
|
// Also send them a list of everybody who is already connected
|
|
if ( m_mapClients.empty() )
|
|
{
|
|
SendStringToClient( pInfo->m_hConn, "Thou art utterly alone." );
|
|
}
|
|
else
|
|
{
|
|
sprintf( temp, "%d companions greet you:", (int)m_mapClients.size() );
|
|
for ( auto &c: m_mapClients )
|
|
SendStringToClient( pInfo->m_hConn, c.second.m_sNick.c_str() );
|
|
}
|
|
|
|
// Let everybody else know who they are for now
|
|
sprintf( temp, "Hark! A stranger hath joined this merry host. For now we shall call them '%s'", nick );
|
|
SendStringToAllClients( temp, pInfo->m_hConn );
|
|
|
|
// Add them to the client list, using std::map wacky syntax
|
|
m_mapClients[ pInfo->m_hConn ];
|
|
SetClientNick( pInfo->m_hConn, nick );
|
|
break;
|
|
}
|
|
|
|
case k_ESteamNetworkingConnectionState_Connected:
|
|
// We will get a callback immediately after accepting the connection.
|
|
// Since we are the server, we can ignore this, it's not news to us.
|
|
break;
|
|
|
|
default:
|
|
// Silences -Wswitch
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
/////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// ChatClient
|
|
//
|
|
/////////////////////////////////////////////////////////////////////////////
|
|
|
|
class ChatClient : private ISteamNetworkingSocketsCallbacks
|
|
{
|
|
public:
|
|
void Run( const SteamNetworkingIPAddr &serverAddr )
|
|
{
|
|
// Select instance to use. For now we'll always use the default.
|
|
m_pInterface = SteamNetworkingSockets();
|
|
|
|
// Start connecting
|
|
char szAddr[ SteamNetworkingIPAddr::k_cchMaxString ];
|
|
serverAddr.ToString( szAddr, sizeof(szAddr), true );
|
|
Printf( "Connecting to chat server at %s", szAddr );
|
|
m_hConnection = m_pInterface->ConnectByIPAddress( serverAddr );
|
|
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;
|
|
}
|
|
|
|
// 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 );
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
};
|
|
|
|
const uint16 DEFAULT_SERVER_PORT = 27020;
|
|
|
|
void PrintUsageAndExit( int rc = 1 )
|
|
{
|
|
fflush(stderr);
|
|
printf(
|
|
R"usage(Usage:
|
|
example_chat client SERVER_ADDR
|
|
example_chat server [--port PORT]
|
|
)usage"
|
|
);
|
|
fflush(stdout);
|
|
exit(rc);
|
|
}
|
|
|
|
int main( int argc, const char *argv[] )
|
|
{
|
|
bool bServer = false;
|
|
bool bClient = false;
|
|
int nPort = DEFAULT_SERVER_PORT;
|
|
SteamNetworkingIPAddr addrServer; addrServer.Clear();
|
|
|
|
for ( int i = 1 ; i < argc ; ++i )
|
|
{
|
|
if ( !bClient && !bServer )
|
|
{
|
|
if ( !strcmp( argv[i], "client" ) )
|
|
{
|
|
bClient = true;
|
|
continue;
|
|
}
|
|
if ( !strcmp( argv[i], "server" ) )
|
|
{
|
|
bServer = true;
|
|
continue;
|
|
}
|
|
}
|
|
if ( !strcmp( argv[i], "--port" ) )
|
|
{
|
|
++i;
|
|
if ( i >= argc )
|
|
PrintUsageAndExit();
|
|
nPort = atoi( argv[i] );
|
|
if ( nPort <= 0 || nPort > 65535 )
|
|
FatalError( "Invalid port %d", nPort );
|
|
continue;
|
|
}
|
|
|
|
// Anything else, must be server address to connect to
|
|
if ( bClient && addrServer.IsIPv6AllZeros() )
|
|
{
|
|
if ( !addrServer.ParseString( argv[i] ) )
|
|
FatalError( "Invalid server address '%s'", argv[i] );
|
|
if ( addrServer.m_port == 0 )
|
|
addrServer.m_port = DEFAULT_SERVER_PORT;
|
|
continue;
|
|
}
|
|
|
|
PrintUsageAndExit();
|
|
}
|
|
|
|
if ( bClient == bServer || ( bClient && addrServer.IsIPv6AllZeros() ) )
|
|
PrintUsageAndExit();
|
|
|
|
// Create client and server sockets
|
|
InitSteamDatagramConnectionSockets();
|
|
LocalUserInput_Init();
|
|
|
|
if ( bClient )
|
|
{
|
|
ChatClient client;
|
|
client.Run( addrServer );
|
|
}
|
|
else
|
|
{
|
|
ChatServer server;
|
|
server.Run( (uint16)nPort );
|
|
}
|
|
|
|
ShutdownSteamDatagramConnectionSockets();
|
|
|
|
// Ug, why is there no simple solution for portable, non-blocking console user input?
|
|
// Just nuke the process
|
|
//LocalUserInput_Kill();
|
|
NukeProcess(0);
|
|
}
|