C++编程:ISteamNetworkingSockets示例工程2

      P2P(Peer-to-Peer)通信是一种去中心化的网络通信模式,参与者(节点)既作为客户端也作为服务器,直接进行数据交换,无需依赖中心服务器中转。

P2P通信核心原理

1. NAT穿透技术

  • STUN协议:通过公网服务器发现NAT后的公网IP和端口

  • TURN协议:当直接连接失败时,通过中继服务器转发数据

  • ICE框架:综合STUN/TURN,自动选择最佳连接路径

  • UDP打洞:利用NAT映射规则建立直接UDP连接

2. 节点发现机制

  • 中央服务器索引:通过中心服务器记录在线节点信息

  • 分布式哈希表(DHT):如Kademlia算法实现的去中心化节点发现

  • 组播/广播发现:在局域网内自动发现对等节点

3. 连接建立过程

  1. 节点发现与身份验证

  2. NAT类型检测与穿透方案选择

  3. 连接尝试(直接连接或中继)

  4. 安全通道建立与加密协商

TeamNetworkingSockets  P2P通信

核心功能特点

  1. NAT穿透能力

    • 自动使用STUN/TURN技术穿透大多数NAT设备

    • 内置ICE协议实现连接最佳路径选择

    • 支持中继备用连接方式

  2. 连接可靠性

    • 提供可靠和不可靠两种传输模式

    • 自动重传和拥塞控制

    • 消息分段和重组支持

  3. 安全机制

    • 默认启用加密通信

    • 身份验证系统防止中间人攻击

    • 可配置的安全选项

P2P连接建立流程

1. 初始化设置
// 初始化SteamNetworkingSockets
SteamNetworkingErrMsg errMsg;
if(!GameNetworkingSockets_Init(nullptr, errMsg)) {
    printf("初始化失败: %s", errMsg);
    return;
}

// 设置身份标识
SteamNetworkingIdentity identityLocal;
identityLocal.SetSteamID( steamIDLocal ); // 或其他标识方式

// 配置STUN服务器
SteamNetworkingUtils()->SetGlobalConfigValueString(
    k_ESteamNetworkingConfig_P2P_STUN_ServerList,
    "stun.l.google.com:19302"
);
2. 创建监听端(服务器)
// 创建P2P监听套接字
HSteamListenSocket hListenSock = SteamNetworkingSockets()->CreateListenSocketP2P(
    0,  // 虚拟端口
    0,  // 选项数量
    nullptr // 选项数组
);

// 设置连接状态变更回调
SteamNetworkingUtils()->SetGlobalCallback_SteamNetConnectionStatusChanged(
    OnSteamNetConnectionStatusChanged
);
3. 发起连接(客户端)
// 准备连接选项
SteamNetworkingConfigValue_t options[] = {
    { k_ESteamNetworkingConfig_SymmetricConnect, k_ESteamNetworkingConfig_Int32, 1 }
};

// 发起P2P连接
HSteamNetConnection hConn = SteamNetworkingSockets()->ConnectP2P(
    identityRemote,  // 远程身份
    0,              // 虚拟端口
    1,              // 选项数量
    options         // 选项数组
);
4. 信令服务集成(可选)
// 创建自定义信令客户端
ITrivialSignalingClient* pSignaling = CreateTrivialSignalingClient(
    "signaling.example.com:10000",
    SteamNetworkingSockets(),
    errMsg
);

// 使用自定义信令建立连接
ISteamNetworkingConnectionSignaling* pConnSignaling = pSignaling->CreateSignalingForConnection(
    identityRemote,
    errMsg
);

HSteamNetConnection hConn = SteamNetworkingSockets()->ConnectP2PCustomSignaling(
    pConnSignaling,
    &identityRemote,
    0,  // 虚拟端口
    0,  // 选项数量
    nullptr // 选项数组
);

消息收发处理

发送消息
void SendP2PMessage(HSteamNetConnection hConn, const void* pData, uint32 cbData)
{
    EResult result = SteamNetworkingSockets()->SendMessageToConnection(
        hConn,
        pData,
        cbData,
        k_nSteamNetworkingSend_Reliable, // 或k_nSteamNetworkingSend_Unreliable
        nullptr
    );
    
    if(result != k_EResultOK) {
        // 处理发送失败
    }
}
接收消息
void PollIncomingMessages()
{
    SteamNetworkingMessage_t* pMsg;
    int numMsgs = SteamNetworkingSockets()->ReceiveMessagesOnConnection(
        hConnection,
        &pMsg,
        1
    );
    
    if(numMsgs > 0) {
        // 处理消息
        ProcessMessage(pMsg->GetData(), pMsg->GetSize());
        
        // 释放消息
        pMsg->Release();
    }
}

连接状态管理

void OnSteamNetConnectionStatusChanged(SteamNetConnectionStatusChangedCallback_t* pInfo)
{
    switch(pInfo->m_info.m_eState) {
        case k_ESteamNetworkingConnectionState_Connected:
            // 连接成功建立
            break;
            
        case k_ESteamNetworkingConnectionState_ClosedByPeer:
        case k_ESteamNetworkingConnectionState_ProblemDetectedLocally:
            // 连接断开
            SteamNetworkingSockets()->CloseConnection(pInfo->m_hConn, 0, nullptr, false);
            break;
            
        // 其他状态处理...
    }
}

高级配置选项

  1. 传输优化

// 启用SDR传输后备
SteamNetworkingConfigValue_t opt;
opt.SetInt32(k_ESteamNetworkingConfig_Transport_ICE_Enable, 
    k_nSteamNetworkingConfig_Transport_ICE_Enable_All);
  1. 性能调优

// 调整发送缓冲区大小
SteamNetworkingUtils()->SetGlobalConfigValueInt32(
    k_ESteamNetworkingConfig_SendBufferSize,
    256 * 1024 // 256KB
);
  1. 调试支持

// 启用详细日志
SteamNetworkingUtils()->SetGlobalConfigValueInt32(
    k_ESteamNetworkingConfig_LogLevel_P2PRendezvous,
    k_ESteamNetworkingSocketsDebugOutputType_Debug
);

完整示例工程

ITrivialSignalingClient类

// Client of our dummy trivial signaling server service.
// Serves as an example of you how to hook up signaling server
// to SteamNetworkingSockets P2P connections

#pragma once

#include <steam/steamnetworkingcustomsignaling.h>

class ISteamNetworkingSockets;

/// Interface to our client.
class ITrivialSignalingClient
{
public:

	/// Create signaling object for a connection to peer
    virtual ISteamNetworkingConnectionSignaling *CreateSignalingForConnection(
        const SteamNetworkingIdentity &identityPeer,
        SteamNetworkingErrMsg &errMsg ) = 0;

	/// Poll the server for incoming signals and dispatch them.
	/// We use polling in this example just to keep it simple.
	/// You could use a service thread.
	virtual void Poll() = 0;

	/// Disconnect from the server and close down our polling thread.
	virtual void Release() = 0;
};

// Start connecting to the signaling server.
ITrivialSignalingClient *CreateTrivialSignalingClient(
	const char *address, // Address:port
	ISteamNetworkingSockets *pSteamNetworkingSockets, // Where should we send signals when we get them?
	SteamNetworkingErrMsg &errMsg // Error message is retjrned here if we fail
);

	


// Client of our dummy trivial signaling server service.
// Serves as an example of you how to hook up signaling server
// to SteamNetworkingSockets P2P connections

#include "../tests/test_common.h"

#include <string>
#include <mutex>
#include <deque>
#include <assert.h>

#include "trivial_signaling_client.h"
#include <steam/isteamnetworkingsockets.h>
#include <steam/isteamnetworkingutils.h>
#include <steam/steamnetworkingcustomsignaling.h>

#ifndef _WIN32
	#include <unistd.h>
	#include <sys/socket.h>
	#include <sys/types.h>
	#include <netinet/in.h>
	#include <netdb.h>
	#include <sys/ioctl.h>
	typedef int SOCKET;
	constexpr SOCKET INVALID_SOCKET = -1;
	inline void closesocket( SOCKET s ) { close(s); }
	inline int GetSocketError() { return errno; }
	inline bool IgnoreSocketError( int e )
	{
		return e == EAGAIN || e == ENOTCONN || e == EWOULDBLOCK;
	}
	#ifndef ioctlsocket
		#define ioctlsocket ioctl
	#endif
#endif
#ifdef _WIN32
	#include <winsock2.h>
	#include <ws2tcpip.h>
	typedef int socklen_t;
	inline int GetSocketError() { return WSAGetLastError(); }
	inline bool IgnoreSocketError( int e )
	{
		return e == WSAEWOULDBLOCK || e == WSAENOTCONN;
	}
#endif

inline int HexDigitVal( char c )
{
	if ( '0' <= c && c <= '9' )
		return c - '0';
	if ( 'a' <= c && c <= 'f' )
		return c - 'a' + 0xa;
	if ( 'A' <= c && c <= 'F' )
		return c - 'A' + 0xa;
	return -1;
}

/// Implementation of ITrivialSignalingClient
class CTrivialSignalingClient : public ITrivialSignalingClient
{

	// This is the thing we'll actually create to send signals for a particular
	// connection.
	struct ConnectionSignaling : ISteamNetworkingConnectionSignaling
	{
		CTrivialSignalingClient *const m_pOwner;
		std::string const m_sPeerIdentity; // Save off the string encoding of the identity we're talking to

		ConnectionSignaling( CTrivialSignalingClient *owner, const char *pszPeerIdentity )
		: m_pOwner( owner )
		, m_sPeerIdentity( pszPeerIdentity )
		{
		}

		//
		// Implements ISteamNetworkingConnectionSignaling
		//

		// This is called from SteamNetworkingSockets to send a signal.  This could be called from any thread,
		// so we need to be threadsafe, and avoid duoing slow stuff or calling back into SteamNetworkingSockets
		virtual bool SendSignal( HSteamNetConnection hConn, const SteamNetConnectionInfo_t &info, const void *pMsg, int cbMsg ) override
		{
			// Silence warnings
			(void)info;
			(void)hConn;

			// We'll use a dumb hex encoding.
			std::string signal;
			signal.reserve( m_sPeerIdentity.length() + cbMsg*2 + 4 );
			signal.append( m_sPeerIdentity );
			signal.push_back( ' ' );
			for ( const uint8_t *p = (const uint8_t *)pMsg ; cbMsg > 0 ; --cbMsg, ++p )
			{
				static const char hexdigit[] = "0123456789abcdef";
				signal.push_back( hexdigit[ *p >> 4U ] );
				signal.push_back( hexdigit[ *p & 0xf ] );
			}
			signal.push_back('\n');

			m_pOwner->Send( signal );
			return true;
		}

		// Self destruct.  This will be called by SteamNetworkingSockets when it's done with us.
		virtual void Release() override
		{
			delete this;
		}
	};

	sockaddr_storage m_adrServer;
	size_t const m_adrServerSize;
	ISteamNetworkingSockets *const m_pSteamNetworkingSockets;
	std::string m_sGreeting;
	std::deque< std::string > m_queueSend;

	std::recursive_mutex sockMutex;
	SOCKET m_sock;
	std::string m_sBufferedData;

	void CloseSocket()
	{
		if ( m_sock != INVALID_SOCKET )
		{
			closesocket( m_sock );
			m_sock = INVALID_SOCKET;
		}
		m_sBufferedData.clear();
		m_queueSend.clear();
	}

	void Connect()
	{
		CloseSocket();

		int sockType = SOCK_STREAM;
		#ifdef LINUX
			sockType |= SOCK_CLOEXEC;
		#endif
		m_sock = socket( m_adrServer.ss_family, sockType, IPPROTO_TCP );
		if ( m_sock == INVALID_SOCKET )
		{
			TEST_Printf( "socket() failed, error=%d\n", GetSocketError() );
			return;
		}

		// Request nonblocking IO
		unsigned long opt = 1;
		if ( ioctlsocket( m_sock, FIONBIO, &opt ) == -1 )
		{
			CloseSocket();
			TEST_Printf( "ioctlsocket() failed, error=%d\n", GetSocketError() );
			return;
		}

		connect( m_sock, (const sockaddr *)&m_adrServer, (socklen_t )m_adrServerSize );

		// And immediate send our greeting.  This just puts in in the buffer and
		// it will go out once the socket connects.
		Send( m_sGreeting );
	}

public:
	CTrivialSignalingClient( const sockaddr *adrServer, size_t adrServerSize, ISteamNetworkingSockets *pSteamNetworkingSockets )
	: m_adrServerSize( adrServerSize ), m_pSteamNetworkingSockets( pSteamNetworkingSockets )
	{
		memcpy( &m_adrServer, adrServer, adrServerSize );
		m_sock = INVALID_SOCKET;

		// Save off our identity
		SteamNetworkingIdentity identitySelf; identitySelf.Clear();
		pSteamNetworkingSockets->GetIdentity( &identitySelf );
		assert( !identitySelf.IsInvalid() );
		assert( !identitySelf.IsLocalHost() ); // We need something more specific than that
		m_sGreeting = SteamNetworkingIdentityRender( identitySelf ).c_str();
		assert( strchr( m_sGreeting.c_str(), ' ' ) == nullptr ); // Our protocol is dumb and doesn't support this
		m_sGreeting.push_back( '\n' );

		// Begin connecting immediately
		Connect();
	}

	// Send the signal.
	void Send( const std::string &s )
	{
		assert( s.length() > 0 && s[ s.length()-1 ] == '\n' ); // All of our signals are '\n'-terminated

		sockMutex.lock();

		// If we're getting backed up, delete the oldest entries.  Remember,
		// we are only required to do best-effort delivery.  And old signals are the
		// most likely to be out of date (either old data, or the client has already
		// timed them out and queued a retry).
		while ( m_queueSend.size() > 32 )
		{
			TEST_Printf( "Signaling send queue is backed up.  Discarding oldest signals\n" );
			m_queueSend.pop_front();
		}

		m_queueSend.push_back( s );
		sockMutex.unlock();
	}

	ISteamNetworkingConnectionSignaling *CreateSignalingForConnection(
		const SteamNetworkingIdentity &identityPeer,
		SteamNetworkingErrMsg &errMsg
	) override {
		SteamNetworkingIdentityRender sIdentityPeer( identityPeer );

		// FIXME - here we really ought to confirm that the string version of the
		// identity does not have spaces, since our protocol doesn't permit it.
		TEST_Printf( "Creating signaling session for peer '%s'\n", sIdentityPeer.c_str() );

		// Silence warnings
		(void)errMsg;

		return new ConnectionSignaling( this, sIdentityPeer.c_str() );
	}

	virtual void Poll() override
	{
		// Drain the socket into the buffer, and check for reconnecting
		sockMutex.lock();
		if ( m_sock == INVALID_SOCKET )
		{
			Connect();
		}
		else
		{
			for (;;)
			{
				char buf[256];
				int r = recv( m_sock, buf, sizeof(buf), 0 );
				if ( r == 0 )
					break;
				if ( r < 0 )
				{
					int e = GetSocketError();
					if ( !IgnoreSocketError( e ) )
					{
						TEST_Printf( "Failed to recv from trivial signaling server.  recv() returned %d, errno=%d.  Closing and restarting connection\n", r, e );
						CloseSocket();
					}
					break;
				}

				m_sBufferedData.append( buf, r );
			}
		}

		// Flush send queue
		if ( m_sock != INVALID_SOCKET )
		{
			while ( !m_queueSend.empty() )
			{
				const std::string &s = m_queueSend.front();
				int l = (int)s.length();
				int r = ::send( m_sock, s.c_str(), l, 0 );
				if ( r < 0 && IgnoreSocketError( GetSocketError() ) )
					break;

				if ( r == l )
				{
					m_queueSend.pop_front();
				}
				else if ( r != 0 )
				{
					// Socket hosed, or we sent a partial signal.
					// We need to restart connection
					TEST_Printf( "Failed to send %d bytes to trivial signaling server.  send() returned %d, errno=%d.  Closing and restarting connection.\n",
						l, r, GetSocketError() );
					CloseSocket();
					break;
				}
			}
		}

		// Release the lock now.  See the notes below about why it's very important
		// to release the lock early and not hold it while we try to dispatch the
		// received callbacks.
		sockMutex.unlock();

		// Now dispatch any buffered signals
		for (;;)
		{

			// Find end of line.  Do we have a complete signal?
			size_t l = m_sBufferedData.find( '\n' );
			if ( l == std::string::npos )
				break;

			// Locate the space that seperates [from] [payload]
			size_t spc = m_sBufferedData.find( ' ' );
			if ( spc != std::string::npos && spc < l )
			{

				// Hex decode the payload.  As it turns out, we actually don't
				// need the sender's identity.  The payload has everything needed
				// to process the message.  Maybe we should remove it from our
				// dummy signaling protocol?  It might be useful for debugging, tho.
				std::string data; data.reserve( ( l - spc ) / 2 );
				for ( size_t i = spc+1 ; i+2 <= l ; i += 2 )
				{
					int dh = HexDigitVal( m_sBufferedData[i] );
					int dl = HexDigitVal( m_sBufferedData[i+1] );
					if ( ( dh | dl ) & ~0xf )
					{
						// Failed hex decode.  Not a bug in our code here, but this is just example code, so we'll handle it this way
						assert( !"Failed hex decode from signaling server?!" );
						goto next_message;
					}
					data.push_back( (char)(dh<<4 | dl ) );
				}

				// Setup a context object that can respond if this signal is a connection request.
				struct Context : ISteamNetworkingSignalingRecvContext
				{
					CTrivialSignalingClient *m_pOwner;

					virtual ISteamNetworkingConnectionSignaling *OnConnectRequest(
						HSteamNetConnection hConn,
						const SteamNetworkingIdentity &identityPeer,
						int nLocalVirtualPort
					) override {
						// Silence warnings
						(void)hConn;
;						(void)nLocalVirtualPort;

						// We will just always handle requests through the usual listen socket state
						// machine.  See the documentation for this function for other behaviour we
						// might take.

						// Also, note that if there was routing/session info, it should have been in
						// our envelope that we know how to parse, and we should save it off in this
						// context object.
						SteamNetworkingErrMsg ignoreErrMsg;
						return m_pOwner->CreateSignalingForConnection( identityPeer, ignoreErrMsg );
					}

					virtual void SendRejectionSignal(
						const SteamNetworkingIdentity &identityPeer,
						const void *pMsg, int cbMsg
					) override {

						// We'll just silently ignore all failures.  This is actually the more secure
						// Way to handle it in many cases.  Actively returning failure might allow
						// an attacker to just scrape random peers to see who is online.  If you know
						// the peer has a good reason for trying to connect, sending an active failure
						// can improve error handling and the UX, instead of relying on timeout.  But
						// just consider the security implications.

						// Silence warnings
						(void)identityPeer;
						(void)pMsg;
						(void)cbMsg;
					}
				};
				Context context;
				context.m_pOwner = this;

				// Dispatch.
				// Remember: From inside this function, our context object might get callbacks.
				// And we might get asked to send signals, either now, or really at any time
				// from any thread!  If possible, avoid calling this function while holding locks.
				// To process this call, SteamnetworkingSockets will need take its own internal lock.
				// That lock may be held by another thread that is asking you to send a signal!  So
				// be warned that deadlocks are a possibility here.
				m_pSteamNetworkingSockets->ReceivedP2PCustomSignal( data.c_str(), (int)data.length(), &context );
			}

next_message:
			m_sBufferedData.erase( 0, l+1 );
		}
	}

	virtual void Release() override
	{
		// NOTE: Here we are assuming that the calling code has already cleaned
		// up all the connections, to keep the example simple.
		CloseSocket();
	}
};

// Start connecting to the signaling server.
ITrivialSignalingClient *CreateTrivialSignalingClient(
	const char *pszServerAddress, // Address of the server.
	ISteamNetworkingSockets *pSteamNetworkingSockets, // Where should we send signals when we get them?
	SteamNetworkingErrMsg &errMsg // Error message is retjrned here if we fail
) {

	std::string sAddress( pszServerAddress );
	std::string sService;
	size_t colon = sAddress.find( ':' );
	if ( colon == std::string::npos )
	{
		sService = "10000"; // Default port
	}
	else
	{
		sService = sAddress.substr( colon+1 );
		sAddress.erase( colon );
	}

	// Resolve name synchronously
	addrinfo *pAddrInfo = nullptr;
	int r = getaddrinfo( sAddress.c_str(), sService.c_str(), nullptr, &pAddrInfo );
	if ( r != 0 || pAddrInfo == nullptr )
	{
		sprintf( errMsg, "Invalid/unknown server address.  getaddrinfo returned %d", r );
		return nullptr;
	}

	auto *pClient = new CTrivialSignalingClient( pAddrInfo->ai_addr, pAddrInfo->ai_addrlen, pSteamNetworkingSockets );

	freeaddrinfo( pAddrInfo );

	return pClient;
}

	


    实现了一个简单的信令客户端(CTrivialSignalingClient),用于SteamNetworkingSockets的P2P连接建立。

核心功能
  1. 信令传输

    • 通过TCP连接与信令服务器通信

    • 处理连接建立、维护和错误恢复

    • 实现消息的发送和接收队列

  2. 信号编码/解码

    • 使用简单的十六进制编码传输二进制信号

    • 消息格式:[peer身份] [十六进制编码的信号数据]\n

  3. 与SteamNetworkingSockets集成

    • 实现ISteamNetworkingConnectionSignaling接口发送信号

    • 处理接收到的信号并转发给SteamNetworkingSockets

类设计

1. CTrivialSignalingClient (主类)

职责

  • 管理与信令服务器的TCP连接

  • 处理信号发送和接收队列

  • 创建连接特定的信令对象

关键成员

  • m_adrServer:信令服务器地址

  • m_sock:TCP套接字

  • m_queueSend:待发送消息队列

  • m_sBufferedData:接收缓冲区

主要方法

  • Connect():建立与信令服务器的连接

  • Send():添加消息到发送队列

  • Poll():处理网络I/O和信号分发

  • CreateSignalingForConnection():创建连接信令对象

2. ConnectionSignaling (嵌套类)

职责

  • 为单个P2P连接实现信令接口

  • 处理特定连接的信令发送

关键特性

  • 继承ISteamNetworkingConnectionSignaling

  • 包含目标peer的身份信息

  • 实现SendSignal()将信号编码并转发给主客户端

3. Context (局部类)

职责

  • 处理接收信号时的回调上下文

  • 实现ISteamNetworkingSignalingRecvContext

关键方法

  • OnConnectRequest():处理入站连接请求

  • SendRejectionSignal():处理连接拒绝(当前为空实现)

设计亮点

  1. 线程安全

    • 使用recursive_mutex保护共享数据

    • 发送队列和接收缓冲区的线程安全访问

  2. 错误恢复

    • 自动重新连接机制

    • 对网络错误的健壮处理

  3. 资源管理

    • 清晰的套接字生命周期管理

    • 发送队列大小限制防止内存耗尽

  4. 协议设计

    • 简单的基于行的文本协议

    • 十六进制编码保证二进制数据安全传输

使用流程

  1. 初始化

    auto* pSignaling = CreateTrivialSignalingClient("server:port", pSteamNetworkingSockets, errMsg);
  • 创建P2P连接

    auto* pConnSignaling = pSignaling->CreateSignalingForConnection(identityPeer, errMsg);
    SteamNetworkingSockets()->ConnectP2PCustomSignaling(pConnSignaling, ...);
  • 主循环处理

    while (running) {
        pSignaling->Poll();
        // 其他处理...
    }
  • 清理

    pSignaling->Release();

测试主程序

#include "test_common.h"
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <string>
#include <random>
#include <chrono>
#include <thread>


#include <steam/steamnetworkingsockets.h>
#include <steam/isteamnetworkingutils.h>
#include "../examples/trivial_signaling_client.h"

HSteamListenSocket g_hListenSock;
HSteamNetConnection g_hConnection;
enum ETestRole
{
	k_ETestRole_Undefined,
	k_ETestRole_Server,
	k_ETestRole_Client,
	k_ETestRole_Symmetric,
};
ETestRole g_eTestRole = k_ETestRole_Undefined;

int g_nVirtualPortLocal = 0; // Used when listening, and when connecting
int g_nVirtualPortRemote = 0; // Only used when connecting

void Quit( int rc )
{
	if ( rc == 0 )
	{
		// OK, we cannot just exit the process, because we need to give
		// the connection time to actually send the last message and clean up.
		// If this were a TCP connection, we could just bail, because the OS
		// would handle it.  But this is an application protocol over UDP.
		// So give a little bit of time for good cleanup.  (Also note that
		// we really ought to continue pumping the signaling service, but
		// in this exampple we'll assume that no more signals need to be
		// exchanged, since we've gotten this far.)  If we just terminated
		// the program here, our peer could very likely timeout.  (Although
		// it's possible that the cleanup packets have already been placed
		// on the wire, and if they don't drop, things will get cleaned up
		// properly.)
		TEST_Printf( "Waiting for any last cleanup packets.\n" );
		std::this_thread::sleep_for( std::chrono::milliseconds( 1000 ) );
	}

	TEST_Kill();
	exit(rc);
}

// Send a simple string message to out peer, using reliable transport.
void SendMessageToPeer( const char *pszMsg )
{
	TEST_Printf( "Sending msg '%s'\n", pszMsg );
	EResult r = SteamNetworkingSockets()->SendMessageToConnection(
		g_hConnection, pszMsg, (int)strlen(pszMsg)+1, k_nSteamNetworkingSend_Reliable, nullptr );
	assert( r == k_EResultOK );
}

// Called when a connection undergoes a state transition.
void OnSteamNetConnectionStatusChanged( SteamNetConnectionStatusChangedCallback_t *pInfo )
{
	// What's the state of the connection?
	switch ( pInfo->m_info.m_eState )
	{
	case k_ESteamNetworkingConnectionState_ClosedByPeer:
	case k_ESteamNetworkingConnectionState_ProblemDetectedLocally:

		TEST_Printf( "[%s] %s, reason %d: %s\n",
			pInfo->m_info.m_szConnectionDescription,
			( pInfo->m_info.m_eState == k_ESteamNetworkingConnectionState_ClosedByPeer ? "closed by peer" : "problem detected locally" ),
			pInfo->m_info.m_eEndReason,
			pInfo->m_info.m_szEndDebug
		);

		// Close our end
		SteamNetworkingSockets()->CloseConnection( pInfo->m_hConn, 0, nullptr, false );

		if ( g_hConnection == pInfo->m_hConn )
		{
			g_hConnection = k_HSteamNetConnection_Invalid;

			// In this example, we will bail the test whenever this happens.
			// Was this a normal termination?
			int rc = 0;
			if ( rc == k_ESteamNetworkingConnectionState_ProblemDetectedLocally || pInfo->m_info.m_eEndReason != k_ESteamNetConnectionEnd_App_Generic )
				rc = 1; // failure
			Quit( rc );
		}
		else
		{
			// Why are we hearing about any another connection?
			assert( false );
		}

		break;

	case k_ESteamNetworkingConnectionState_None:
		// Notification that a connection was destroyed.  (By us, presumably.)
		// We don't need this, so ignore it.
		break;

	case k_ESteamNetworkingConnectionState_Connecting:

		// Is this a connection we initiated, or one that we are receiving?
		if ( g_hListenSock != k_HSteamListenSocket_Invalid && pInfo->m_info.m_hListenSocket == g_hListenSock )
		{
			// Somebody's knocking
			// Note that we assume we will only ever receive a single connection
			assert( g_hConnection == k_HSteamNetConnection_Invalid ); // not really a bug in this code, but a bug in the test

			TEST_Printf( "[%s] Accepting\n", pInfo->m_info.m_szConnectionDescription );
			g_hConnection = pInfo->m_hConn;
			SteamNetworkingSockets()->AcceptConnection( pInfo->m_hConn );
		}
		else
		{
			// Note that we will get notification when our own connection that
			// we initiate enters this state.
			assert( g_hConnection == pInfo->m_hConn );
			TEST_Printf( "[%s] Entered connecting state\n", pInfo->m_info.m_szConnectionDescription );
		}
		break;

	case k_ESteamNetworkingConnectionState_FindingRoute:
		// P2P connections will spend a brief time here where they swap addresses
		// and try to find a route.
		TEST_Printf( "[%s] finding route\n", pInfo->m_info.m_szConnectionDescription );
		break;

	case k_ESteamNetworkingConnectionState_Connected:
		// We got fully connected
		assert( pInfo->m_hConn == g_hConnection ); // We don't initiate or accept any other connections, so this should be out own connection
		TEST_Printf( "[%s] connected\n", pInfo->m_info.m_szConnectionDescription );
		break;

	default:
		assert( false );
		break;
	}
}

#ifdef _MSC_VER
	#pragma warning( disable: 4702 ) /* unreachable code */
#endif

int main( int argc, const char **argv )
{
	SteamNetworkingIdentity identityLocal; identityLocal.Clear();
	SteamNetworkingIdentity identityRemote; identityRemote.Clear();
	const char *pszTrivialSignalingService = "localhost:10000";

	// Parse the command line
	for ( int idxArg = 1 ; idxArg < argc ; ++idxArg )
	{
		const char *pszSwitch = argv[idxArg];

		auto GetArg = [&]() -> const char * {
			if ( idxArg + 1 >= argc )
				TEST_Fatal( "Expected argument after %s", pszSwitch );
			return argv[++idxArg];
		};
		auto ParseIdentity = [&]( SteamNetworkingIdentity &x ) {
			const char *pszArg = GetArg();
			if ( !x.ParseString( pszArg ) )
				TEST_Fatal( "'%s' is not a valid identity string", pszArg );
		};

		if ( !strcmp( pszSwitch, "--identity-local" ) )
			ParseIdentity( identityLocal );
		else if ( !strcmp( pszSwitch, "--identity-remote" ) )
			ParseIdentity( identityRemote );
		else if ( !strcmp( pszSwitch, "--signaling-server" ) )
			pszTrivialSignalingService = GetArg();
		else if ( !strcmp( pszSwitch, "--client" ) )
			g_eTestRole = k_ETestRole_Client;
		else if ( !strcmp( pszSwitch, "--server" ) )
			g_eTestRole = k_ETestRole_Server;
		else if ( !strcmp( pszSwitch, "--symmetric" ) )
			g_eTestRole = k_ETestRole_Symmetric;
		else if ( !strcmp( pszSwitch, "--log" ) )
		{
			const char *pszArg = GetArg();
			TEST_InitLog( pszArg );
		}
		else
			TEST_Fatal( "Unexpected command line argument '%s'", pszSwitch );
	}

	if ( g_eTestRole == k_ETestRole_Undefined )
		TEST_Fatal( "Must specify test role (--server, --client, or --symmetric" );
	if ( identityLocal.IsInvalid() )
		TEST_Fatal( "Must specify local identity using --identity-local" );
	if ( identityRemote.IsInvalid() && g_eTestRole != k_ETestRole_Server )
		TEST_Fatal( "Must specify remote identity using --identity-remote" );

	// Initialize library, with the desired local identity
	TEST_Init( &identityLocal );

	// Hardcode STUN servers
	SteamNetworkingUtils()->SetGlobalConfigValueString( k_ESteamNetworkingConfig_P2P_STUN_ServerList, "stun.l.google.com:19302" );

	// Hardcode TURN servers
	// comma seperated setting lists
	//const char* turnList = "turn:123.45.45:3478";
	//const char* userList = "username";
	//const char* passList = "pass";

	//SteamNetworkingUtils()->SetGlobalConfigValueString(k_ESteamNetworkingConfig_P2P_TURN_ServerList, turnList);
	//SteamNetworkingUtils()->SetGlobalConfigValueString(k_ESteamNetworkingConfig_P2P_TURN_UserList, userList);
	//SteamNetworkingUtils()->SetGlobalConfigValueString(k_ESteamNetworkingConfig_P2P_TURN_PassList, passList);

	// Allow sharing of any kind of ICE address.
	// We don't have any method of relaying (TURN) in this example, so we are essentially
	// forced to disclose our public address if we want to pierce NAT.  But if we
	// had relay fallback, or if we only wanted to connect on the LAN, we could restrict
	// to only sharing private addresses.
	SteamNetworkingUtils()->SetGlobalConfigValueInt32(k_ESteamNetworkingConfig_P2P_Transport_ICE_Enable, k_nSteamNetworkingConfig_P2P_Transport_ICE_Enable_All );

	// Create the signaling service
	SteamNetworkingErrMsg errMsg;
	ITrivialSignalingClient *pSignaling = CreateTrivialSignalingClient( pszTrivialSignalingService, SteamNetworkingSockets(), errMsg );
	if ( pSignaling == nullptr )
		TEST_Fatal( "Failed to initializing signaling client.  %s", errMsg );

	SteamNetworkingUtils()->SetGlobalCallback_SteamNetConnectionStatusChanged( OnSteamNetConnectionStatusChanged );

	// Comment this line in for more detailed spew about signals, route finding, ICE, etc
	SteamNetworkingUtils()->SetGlobalConfigValueInt32( k_ESteamNetworkingConfig_LogLevel_P2PRendezvous, k_ESteamNetworkingSocketsDebugOutputType_Verbose );

	// Create listen socket to receive connections on, unless we are the client
	if ( g_eTestRole == k_ETestRole_Server )
	{
		TEST_Printf( "Creating listen socket, local virtual port %d\n", g_nVirtualPortLocal );
		g_hListenSock = SteamNetworkingSockets()->CreateListenSocketP2P( g_nVirtualPortLocal, 0, nullptr );
		assert( g_hListenSock != k_HSteamListenSocket_Invalid  );
	}
	else if ( g_eTestRole == k_ETestRole_Symmetric )
	{

		// Currently you must create a listen socket to use symmetric mode,
		// even if you know that you will always create connections "both ways".
		// In the future we might try to remove this requirement.  It is a bit
		// less efficient, since it always triggered the race condition case
		// where both sides create their own connections, and then one side
		// decides to their theirs away.  If we have a listen socket, then
		// it can be the case that one peer will receive the incoming connection
		// from the other peer, and since he has a listen socket, can save
		// the connection, and then implicitly accept it when he initiates his
		// own connection.  Without the listen socket, if an incoming connection
		// request arrives before we have started connecting out, then we are forced
		// to ignore it, as the app has given no indication that it desires to
		// receive inbound connections at all.
		TEST_Printf( "Creating listen socket in symmetric mode, local virtual port %d\n", g_nVirtualPortLocal );
		SteamNetworkingConfigValue_t opt;
		opt.SetInt32( k_ESteamNetworkingConfig_SymmetricConnect, 1 ); // << Note we set symmetric mode on the listen socket
		g_hListenSock = SteamNetworkingSockets()->CreateListenSocketP2P( g_nVirtualPortLocal, 1, &opt );
		assert( g_hListenSock != k_HSteamListenSocket_Invalid  );
	}

	// Begin connecting to peer, unless we are the server
	if ( g_eTestRole != k_ETestRole_Server )
	{
		std::vector< SteamNetworkingConfigValue_t > vecOpts;

		// If we want the local and virtual port to differ, we must set
		// an option.  This is a pretty rare use case, and usually not needed.
		// The local virtual port is only usually relevant for symmetric
		// connections, and then, it almost always matches.  Here we are
		// just showing in this example code how you could handle this if you
		// needed them to differ.
		if ( g_nVirtualPortRemote != g_nVirtualPortLocal )
		{
			SteamNetworkingConfigValue_t opt;
			opt.SetInt32( k_ESteamNetworkingConfig_LocalVirtualPort, g_nVirtualPortLocal );
			vecOpts.push_back( opt );
		}

		// Symmetric mode?  Noce that since we created a listen socket on this local
		// virtual port and tagged it for symmetric connect mode, any connections
		// we create that use the same local virtual port will automatically inherit
		// this setting.  However, this is really not recommended.  It is best to be
		// explicit.
		if ( g_eTestRole == k_ETestRole_Symmetric )
		{
			SteamNetworkingConfigValue_t opt;
			opt.SetInt32( k_ESteamNetworkingConfig_SymmetricConnect, 1 );
			vecOpts.push_back( opt );
			TEST_Printf( "Connecting to '%s' in symmetric mode, virtual port %d, from local virtual port %d.\n",
				SteamNetworkingIdentityRender( identityRemote ).c_str(), g_nVirtualPortRemote,
				g_nVirtualPortLocal );
		}
		else
		{
			TEST_Printf( "Connecting to '%s', virtual port %d, from local virtual port %d.\n",
				SteamNetworkingIdentityRender( identityRemote ).c_str(), g_nVirtualPortRemote,
				g_nVirtualPortLocal );
		}

		// Connect using the "custom signaling" path.  Note that when
		// you are using this path, the identity is actually optional,
		// since we don't need it.  (Your signaling object already
		// knows how to talk to the peer) and then the peer identity
		// will be confirmed via rendezvous.
		ISteamNetworkingConnectionSignaling *pConnSignaling = pSignaling->CreateSignalingForConnection(
			identityRemote,
			errMsg
		);
		assert( pConnSignaling );
		g_hConnection = SteamNetworkingSockets()->ConnectP2PCustomSignaling( pConnSignaling, &identityRemote, g_nVirtualPortRemote, (int)vecOpts.size(), vecOpts.data() );
		assert( g_hConnection != k_HSteamNetConnection_Invalid );

		// Go ahead and send a message now.  The message will be queued until route finding
		// completes.
		SendMessageToPeer( "Greetings!" );
	}

	// Main test loop
	for (;;)
	{
		// Check for incoming signals, and dispatch them
		pSignaling->Poll();

		// Check callbacks
		TEST_PumpCallbacks();

		// If we have a connection, then poll it for messages
		if ( g_hConnection != k_HSteamNetConnection_Invalid )
		{
			SteamNetworkingMessage_t *pMessage;
			int r = SteamNetworkingSockets()->ReceiveMessagesOnConnection( g_hConnection, &pMessage, 1 );
			assert( r == 0 || r == 1 ); // <0 indicates an error
			if ( r == 1 )
			{
				// In this example code we will assume all messages are '\0'-terminated strings.
				// Obviously, this is not secure.
				TEST_Printf( "Received message '%s'\n", pMessage->GetData() );

				// Free message struct and buffer.
				pMessage->Release();

				// If we're the client, go ahead and shut down.  In this example we just
				// wanted to establish a connection and exchange a message, and we've done that.
				// Note that we use "linger" functionality.  This flushes out any remaining
				// messages that we have queued.  Essentially to us, the connection is closed,
				// but on thew wire, we will not actually close it until all reliable messages
				// have been confirmed as received by the client.  (Or the connection is closed
				// by the peer or drops.)  If we are the "client" role, then we know that no such
				// messages are in the pipeline in this test.  But in symmetric mode, it is
				// possible that we need to flush out our message that we sent.
				if ( g_eTestRole != k_ETestRole_Server )
				{
					TEST_Printf( "Closing connection and shutting down.\n" );
					SteamNetworkingSockets()->CloseConnection( g_hConnection, 0, "Test completed OK", true );
					break;
				}

				// We're the server.  Send a reply.
				SendMessageToPeer( "I got your message" );
			}
		}
	}

	Quit(0);
	return 0;
}

 这是一个使用Steamworks网络套接字实现点对点(P2P)网络通信的测试程序,主要展示了如何建立和管理P2P连接。

核心功能
  1. P2P连接建立

    • 支持三种角色:服务器、客户端和对称模式(双方都可发起连接)

    • 使用SteamNetworkingSockets进行P2P通信

    • 通过简单的信令服务实现初始连接

  2. 信令服务

    • 使用ITrivialSignalingClient在peer之间建立初始联系

    • 信令服务器地址默认为"localhost:10000",但可配置

  3. 连接管理

    • 通过OnSteamNetConnectionStatusChanged回调处理连接状态变化

    • 支持可靠的消息传输

    • 正确处理连接清理

程序流程
  1. 初始化阶段

    • 解析命令行参数确定:

      • 本地和远程身份

      • 程序角色(服务器/客户端/对称模式)

      • 信令服务器地址

    • 使用本地身份初始化Steam网络库

    • 配置STUN和ICE设置

  2. 连接建立

    • 服务器:创建监听套接字

    • 客户端:发起与远程peer的连接

    • 对称模式:创建监听套接字并可以发起连接

  3. 主循环

    • 轮询信令服务获取传入信号

    • 处理网络回调

    • 处理接收到的消息

    • 客户端/对称模式:发送问候消息并在收到回复后关闭

    • 服务器模式:回复收到的消息

  4. 清理阶段

    • 使用linger选项正确关闭连接以确保可靠消息传递

    • 退出前等待清理数据包

消息交换
  • 客户端发送"Greetings!"消息

  • 服务器回复"I got your message"

  • 消息交换完成后客户端发起关闭

技术细节
  • 使用虚拟端口(默认0)进行连接多路复用

  • 实现正确的连接状态管理

  • 处理各种连接场景(直接连接、NAT穿透)

  • 包含调试日志功能

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值