P2P(Peer-to-Peer)通信是一种去中心化的网络通信模式,参与者(节点)既作为客户端也作为服务器,直接进行数据交换,无需依赖中心服务器中转。
P2P通信核心原理
1. NAT穿透技术
-
STUN协议:通过公网服务器发现NAT后的公网IP和端口
-
TURN协议:当直接连接失败时,通过中继服务器转发数据
-
ICE框架:综合STUN/TURN,自动选择最佳连接路径
-
UDP打洞:利用NAT映射规则建立直接UDP连接
2. 节点发现机制
-
中央服务器索引:通过中心服务器记录在线节点信息
-
分布式哈希表(DHT):如Kademlia算法实现的去中心化节点发现
-
组播/广播发现:在局域网内自动发现对等节点
3. 连接建立过程
-
节点发现与身份验证
-
NAT类型检测与穿透方案选择
-
连接尝试(直接连接或中继)
-
安全通道建立与加密协商
TeamNetworkingSockets P2P通信
核心功能特点
-
NAT穿透能力
-
自动使用STUN/TURN技术穿透大多数NAT设备
-
内置ICE协议实现连接最佳路径选择
-
支持中继备用连接方式
-
-
连接可靠性
-
提供可靠和不可靠两种传输模式
-
自动重传和拥塞控制
-
消息分段和重组支持
-
-
安全机制
-
默认启用加密通信
-
身份验证系统防止中间人攻击
-
可配置的安全选项
-
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;
// 其他状态处理...
}
}
高级配置选项
-
传输优化
// 启用SDR传输后备
SteamNetworkingConfigValue_t opt;
opt.SetInt32(k_ESteamNetworkingConfig_Transport_ICE_Enable,
k_nSteamNetworkingConfig_Transport_ICE_Enable_All);
-
性能调优
// 调整发送缓冲区大小
SteamNetworkingUtils()->SetGlobalConfigValueInt32(
k_ESteamNetworkingConfig_SendBufferSize,
256 * 1024 // 256KB
);
-
调试支持
// 启用详细日志
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连接建立。
核心功能
-
信令传输:
-
通过TCP连接与信令服务器通信
-
处理连接建立、维护和错误恢复
-
实现消息的发送和接收队列
-
-
信号编码/解码:
-
使用简单的十六进制编码传输二进制信号
-
消息格式:
[peer身份] [十六进制编码的信号数据]\n
-
-
与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()
:处理连接拒绝(当前为空实现)
设计亮点
-
线程安全:
-
使用
recursive_mutex
保护共享数据 -
发送队列和接收缓冲区的线程安全访问
-
-
错误恢复:
-
自动重新连接机制
-
对网络错误的健壮处理
-
-
资源管理:
-
清晰的套接字生命周期管理
-
发送队列大小限制防止内存耗尽
-
-
协议设计:
-
简单的基于行的文本协议
-
十六进制编码保证二进制数据安全传输
-
使用流程
-
初始化:
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连接。
核心功能
-
P2P连接建立
-
支持三种角色:服务器、客户端和对称模式(双方都可发起连接)
-
使用SteamNetworkingSockets进行P2P通信
-
通过简单的信令服务实现初始连接
-
-
信令服务
-
使用
ITrivialSignalingClient
在peer之间建立初始联系 -
信令服务器地址默认为"localhost:10000",但可配置
-
-
连接管理
-
通过
OnSteamNetConnectionStatusChanged
回调处理连接状态变化 -
支持可靠的消息传输
-
正确处理连接清理
-
程序流程
-
初始化阶段
-
解析命令行参数确定:
-
本地和远程身份
-
程序角色(服务器/客户端/对称模式)
-
信令服务器地址
-
-
使用本地身份初始化Steam网络库
-
配置STUN和ICE设置
-
-
连接建立
-
服务器:创建监听套接字
-
客户端:发起与远程peer的连接
-
对称模式:创建监听套接字并可以发起连接
-
-
主循环
-
轮询信令服务获取传入信号
-
处理网络回调
-
处理接收到的消息
-
客户端/对称模式:发送问候消息并在收到回复后关闭
-
服务器模式:回复收到的消息
-
-
清理阶段
-
使用linger选项正确关闭连接以确保可靠消息传递
-
退出前等待清理数据包
-
消息交换
-
客户端发送"Greetings!"消息
-
服务器回复"I got your message"
-
消息交换完成后客户端发起关闭
技术细节
-
使用虚拟端口(默认0)进行连接多路复用
-
实现正确的连接状态管理
-
处理各种连接场景(直接连接、NAT穿透)
-
包含调试日志功能