RakNet--NAT traversal architecture
![]() |
结合如何使用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例子。 |
![]() |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步