当前位置: 首页 > 工具软件 > Raknet > 使用案例 >

RakNet学习(24) -- NAT traversal architecture

柴丰
2023-12-01

NAT穿透的结构

 

结合如何使用UPNPNAT类型检测,NAT穿透,以及Router2,使得P2P连接迅速而有效完成。

       RakNet 使用了4个独立的系统,每一个系统都解决了无法连接到其他系统问题的一部分问题。这些系统是:

       1. NAT类型检测 – 发现是否我们有路由器,以及路由限制类型是怎样。

       2. UPNP – 告诉路由打开指定的端口号

       3. NAT穿透 – 通过在两个系统之间同时进行连接发送使得连接穿过路由。

       4. Router(可选) – 使用其他玩家的带宽由于不能连接的路由。

       5. UDPProxyClient(可选) – 路由无法连接到我们的服务器。

 

       如下列举了一些结合这些系统快速实现P2P网络中连接玩家的最好方法。

 

要求:

       1. 有一个运行了NATCompleteServer组件 的远端服务器(或者使用了\Samples\NATCompletePeer中的例子的默认发现)

       2. 在游戏客户端,要加入NatTypeDetectionClient插件,NATPunchthroughClient插件,以及可选的Router2UDPProxyClient插件。

       3. 在游戏客户端,已经连接,并建立了miniupnp,它位于DependentExtensions\miniupnpc-1.5下。

 

       建立MiniUPNP

       1. 在包含路径中包含DependentExtensions\miniupnpc-1.5的路径

       2. 如果必要的话,要在preprocessor参数列表中定义STATICLIB参数(参考DependentExtensions\miniupnpc-1.5\declspec.h注释)

       3. 连接ws2_32.libIPHlpApi.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_SYMMETRICNATPunchthroughClient对这个玩家将失效,无法连接到。在内存中以穿透失败的标记存储这个玩家,我们会在第五步处理这些玩家。

       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 – 将这个玩家存储到内存,标记为穿透成功,我们在第六步中处理这类玩家。

      

第五步:使用Router2UDPProxyClient(可选)

       对于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_NONENAT_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例子

      

更加简单的解决方法

       仅仅需要UPNPNATPunchthroughClient

       这个简单的解决方案几乎在任何情况下都可以使用,并且很容易编码。缺点是连接到游戏会话花费的时间比较长,如果玩家连接失败,没有反馈信息。

       1. 按照上面第一步运行

       2. 调用第三步中的OpenUPNP()函数。不需要向服务器上传任何的状态。如果函数调用失败,忽略就可以了。

       3. 在会话/房间主机调用NatPunchthroughClient::OpenNATGroup()。如果成功就会返回ID_NAT_GROUP_PUNCH_SUCCEEDE或者一个失败码。如果获得的是失败码,那么你不能连接到房间,需要提醒用户打开他们路由上游戏使用的端口。

 

参考\Samples\NATSimplePeer例子。

 类似资料: