NAT穿透的结构
结合如何使用UPNP,NAT类型检测,NAT穿透,以及Router2,使得P2P连接迅速而有效完成。
RakNet 使用了4个独立的系统,每一个系统都解决了无法连接到其他系统问题的一部分问题。这些系统是:
1. NAT类型检测 – 发现是否我们有路由器,以及路由限制类型是怎样。
2. UPNP – 告诉路由打开指定的端口号
3. NAT穿透 – 通过在两个系统之间同时进行连接发送使得连接穿过路由。
4. Router(可选) – 使用其他玩家的带宽由于不能连接的路由。
5. UDPProxyClient(可选) – 路由无法连接到我们的服务器。
如下列举了一些结合这些系统快速实现P2P网络中连接玩家的最好方法。
要求:
1. 有一个运行了NATCompleteServer组件 的远端服务器(或者使用了\Samples\NATCompletePeer中的例子的默认发现)
2. 在游戏客户端,要加入NatTypeDetectionClient插件,NATPunchthroughClient插件,以及可选的Router2或UDPProxyClient插件。
3. 在游戏客户端,已经连接,并建立了miniupnp,它位于DependentExtensions\miniupnpc-1.5下。
建立MiniUPNP
1. 在包含路径中包含DependentExtensions\miniupnpc-1.5的路径
2. 如果必要的话,要在preprocessor参数列表中定义STATICLIB参数(参考DependentExtensions\miniupnpc-1.5\declspec.h注释)
3. 连接ws2_32.lib和IPHlpApi.lib。
步骤1:连接到服务器
使用普通的连接方法:RakPeerInterface::Connect(),连接到运行了NATCompleteServer的服务器。
步骤2:检测路由类型
调用NatTypeDetectionClient::DetectNATType()。你可以得到一个数据包ID_NAT_TYPE_DETECTION_RESULT指定NAT类型。例如:
if (packet->data[0]==ID_NAT_TYPE_DETECTION_RESULT)
{
RakNet::NATTypeDetectionResult detectionResult = (RakNet::NATTypeDetectionResult) packet->data[1];
}
如果detectionResult的值是NATTypeDetectionResult::NAT_TYPE_NONE,那么这个系统没有路由。你可以连接到任何系统,并且每一个系统也可以连接到你。
你应该告诉服务器这个系统可以直接连接,这样进入系统不需要花费时间进行NAT穿透了。参考附录A,传递NAT_TYPE_NONE值。连接到每一个在游戏会话中已存的用户。
步骤3:使用UPNP打开路由
假设在第二步的路由不是NATTypeDetectionResult::NAT_TYPE_NONE,如下代码时使用UPNP打开路由器。
#include "miniupnpc.h"
#include "upnpcommands.h"
#include "upnperrors.h"
bool OpenUPNP(RakPeerInterface *rakPeer, SystemAddress serverAddress)
{
struct UPNPDev * devlist = 0;
devlist = upnpDiscover(2000, 0, 0, 0);
if (devlist)
{
char lanaddr[64]; /* my ip address on the LAN */
struct UPNPUrls urls;
struct IGDdatas data;
if (UPNP_GetValidIGD(devlist, &urls, &data, lanaddr, sizeof(lanaddr))==1)
{
DataStructures::List< RakNetSmartPtr< RakNetSocket> > sockets;
rakPeer->GetSockets(sockets);
char iport[32];
Itoa(sockets[0]->boundAddress.GetPort(),iport,10);
char eport[32];
Itoa(rakPeer->GetExternalID(serverAddress).GetPort(),eport,10);
int r = UPNP_AddPortMapping(urls.controlURL, data.first.servicetype, eport, iport, lanaddr, 0, "UDP", 0);
if(r!=UPNPCOMMAND_SUCCESS)
{
return false;
}
}
else
{
return false;
}
}
else
{
return false;
}
return true;
}
如果OpenUPNP返回true,那么说明成功了。你可以连接到其他的系统,并且其他的系统也可以链接到你。远端系统应该连接到你对外可以被服务器看到的端口。
你应该告诉服务器自己的系统直接可以连接,那么进入的系统不需要花费时间做NAT穿透。参考附录A,传度NAT_TYPE_SUPPORTS_UPNP。在游戏会话中连接到每一个存在的用户。
第四步:运行NATPunchthroughClient
1. 从服务器的游戏会话中下载远端玩家的列表,包括他们的连接状态。
2. 如果远端玩家的连接状态是NAT_TYPE_SUPPORTS_UPNP或者NAT_TYPE_NONE,那么你可以直接连接到这些玩家。将这个玩家作为穿透成功存储在内存中,这里会在第六步处理这个玩家。
3. 如果远端玩家的连接状态是NAT_TYPE_SYMMETRIC,你自己在第二步获取的自己的NAT类型也是NAT_TYPE_SYMMETRIC,NATPunchthroughClient对这个玩家将失效,无法连接到。在内存中以穿透失败的标记存储这个玩家,我们会在第五步处理这些玩家。
4. 否则,对这个远端的玩家调用NatPunchthroughClient::OpenNAT(),将这个玩家标记为正处理。
对于每一个我们调用OpenNAT的用户,我们会获得如下的响应代码:
ID_NAT_TARGET_NOT_CONNECTION –在游戏会话中将这个用户从远端玩家列表中移除。
ID_NAT_TARGET_UNRESPONSIVE -在游戏会话中将这个用户从远端玩家列表中移除。
ID_NAT_CONNECTON_TO_TARGET_LOST -在游戏会话中将这个用户从远端玩家列表中移除。
ID_NAT_ALREADY_IN_PROGRESS – 忽略
ID_NAT_PUNCHTHROUGH_FAIED – 将玩家存储到内存,标记为穿透失败,我们会在第五步处理这些玩家。
ID_NAT_PUNCHTHROUGH_SUCCEEDED – 将这个玩家存储到内存,标记为穿透成功,我们在第六步中处理这类玩家。
第五步:使用Router2或UDPProxyClient(可选)
对于NAT穿透失败的玩家,你可以将他们的连接通过连接成功的玩家进行路由,要实现路由使用Router2插件。如果你运行了UDPProxyServer,也可以使用UDPProxyClient通过服务器来转发这些连接。
如果转发无法实现,Router2 会返回ID_ROUTER_2_FORWARDING_NO_PAHT,如果转发成功,返回ID_ROUTER_2_FORWARDING_ESTABLISHED。
UDPProxyClient会返回ID_UDP_PROXY_GENERAL。字节1表示返回值。成功则返回ID_UDP_PROXY_FORWARDING_SUCCEEDED,远端系统会得到ID_UDP_PROXY_FORWARDING_NOTIFICATION消息。其他的消息都说明出现错误。
如果这些解决方案失败,或你没有使用他们,那么就不可能完成端到端游戏回话。将游戏会话留在服务器,你应该给用户提示,在他们开始游戏之前需要自己手动打开路由上的端口。你仅能够尝试一种不同的会话。
第六步:连接到所有我们没有连接到的玩家
第六步假设所有连接失败的用户已经在第五步成功连接。如果没有,就要离开服务器上的游戏会话。游戏应该给用户一个提示让他们手动打开路由器上的端口。
对于先前标识了NAT_TYPE_NONE,NAT_TYPE_SUPPORTS_UPNP,或者通过NAT穿透的用户,现在应该让这些用户连接。你可以假设连接完成了。
附录A: 通知服务器对等端连接状态
服务器应该追踪那些对等端是直接可连接的,如果不能够直接连接,他们路由的类型是什么。有了这些信息,进入的对等端就不需要花费时间执行NAT穿透了。可以手动编程实现这些内容,然而CloudServer插件也可以处理这些。如下是一个如何将我们连接状态上传到服务器的例子:
void PostConnectivityState(RakNet::NATTypeDetectionResult result, RakNet::CloudClient *cloudClient, RakNet::RakNetGUID serverGuid)
{
RakNet::CloudKey cloudKey("NATConnectivityState",0);
RakNet::BitStream bs;
bs.WriteCasted<unsigned char>(result); // 这里可以是任何东西,例如玩家列表,游戏名字
cloudClient->Post(&cloudKey, bs.GetData(), bs.GetNumberOfBytesUsed(), serverGuid);
}
参考\Samples\NATCompletePeer例子
更加简单的解决方法
仅仅需要UPNP和NATPunchthroughClient
这个简单的解决方案几乎在任何情况下都可以使用,并且很容易编码。缺点是连接到游戏会话花费的时间比较长,如果玩家连接失败,没有反馈信息。
1. 按照上面第一步运行
2. 调用第三步中的OpenUPNP()函数。不需要向服务器上传任何的状态。如果函数调用失败,忽略就可以了。
3. 在会话/房间主机调用NatPunchthroughClient::OpenNATGroup()。如果成功就会返回ID_NAT_GROUP_PUNCH_SUCCEEDE或者一个失败码。如果获得的是失败码,那么你不能连接到房间,需要提醒用户打开他们路由上游戏使用的端口。
参考\Samples\NATSimplePeer例子。