NAT穿透
什么是NAT?
NAT是解决地址转换问题的一种技术。它使得路由将路由之后的地址使用不同的端口映射到同一个目的地址。例如,如果路由后有两个计算机,但是仅仅有一个ISP提供商提供的IP地址,那么两个计算机将使用同一个IP地址,但是使用的是与应用程序真正赋值的端口号不同的一个端口。路由提供了一个它所做的映射的查询表,因此当远端计算机回复时,这个消息将根据这个映射表路由给NAT之后的对应本地主机。
NAT存在的问题是远端计算机无法发起向本地计算机发送消息的动作,因为本地计算机在路由中没有到这个远端系统的相应应用的映射存在。因此,如果两个计算机都是位于NAT之后,并且想要相互连接,哪一个计算机也无法完成连接操作。这个对于语音通信,端到端游戏或用户自己的主机做游戏主机的游戏等应用是有问题的。在原来处理的时候,用户必须进入路由配置界面,自己做一个地址映射设置。然而,在现代的应用程序中,用户一般不需要手动做这些工作了,主要是由于NatPunchthrough插件的使用。
NAT穿透概览
NatPunchthroughClient.cpp插件要求用户有自己的主机服务器,并且不能位于NAT之后,要运行客户端都能连接的NatPunchthroughServer.cpp插件。服务器会为每一个客户端寻找IP地址,告诉两个客户端同时连接到那个地址。如果连接失败,每一个客户端都要检查端口是否被其他的应用使用。如果依旧失败,再重复上述过程,以防后面对端口的估计打开前面的端口。如果依旧失败,插件将会返回ID_NAT_PUNCHTHROUGH_FAILED消息。
注意1:如果你的游戏使用Steam,RakNet也提供了SteamLobby,它使用V主机hosted by Valve,这种情况下不需要NATPunchthrough。
注意2:如果使用的是IPv6,则不需要使用NAT Punchtrough插件。
NAT穿透算法
1. Peer P1想要连接到Peer P2,他们都连接到了一个非NAT的系统F。
2. Peer P1使用P2连接到F的RakNetGUID参数调用OpenNAT()函数。
3. 如果P2没有连接到F,则F返回失败消息,或者已经尝试穿透到P1。
4. F记录P1到P2的忙碌状态。如果P1或P2忙碌,那么连接请求就被放到了队列中。否则F请求来自P1和P2的最近使用过的端口。P1和P2标记为忙。
5. 如果P1或P2没有响应,返回ID_NAT_TARGET_UNRESPONSEIVE消息,忙标记被删除。否则F同时发送打了时间戳的连接消息到P1和P2。
6. P1和P2这时各自进行操作。首先他们想各自的内部LAN地址发送多个UDP数据报。然后他们各自尝试F看到的外部的IP/Port。端口按照顺序被实验,根据MAX_PREDICTIVE_PORT_RANGE。
7. 如果任何时间从远端来了一个数据报,我们进入了PUNCHING_FIXED_PORT状态。按照算法的提示,数据报仅仅可以发送到那个IP/port组合上。如果我们的回应到达了远端系统,NAT被认为是双向连通,将会给用户发送ID_NAT_PUNCHTHROUGH_SUCCEEDED消息。
8. 当NAT打开后,或者如果尝试了所有的端口,P1和P2发送消息到F,准备开始新一轮的穿透尝试。
算法有效性依赖于NAT的类型。It work with whichever NAT is the most permissive.
Full cone NAT: 可以从前面使用过的端口上接收任何数据报。会从远端Peer接收到第一个数据报。。
Address-Restricted cone NAT:只要数据报的源IP地址是我们已经发送过数据的系统的,就可以得到数据。否则,第一个数据报需要在我们发送了一个数据报之后收到。
Port-Restricted cone NAT:与Address-restriced cone NAT类似,但是我们需要发送到对应的远端IP和对应的端口。到不同的目的端的相同的源地址和端口使用同一个映射。
Symmetric NAT:每一个远端目的地都使用不同的端口。到不同目的地相同的源地址和端口使用不同的映射。因为端口不同,第一次外部穿透尝试将失败。要是这种NAT实现穿透,需要端口预测(MAX_PREDICTIVE_PORT_RANGE>1),路由顺序选择端口。
Success Graph
Router Type | Full cone NAT | Address-Restricted cone NAT | Port-Restricted cone NAT | Symmetric NAT |
Full cone NAT | YES | YES | YES | YES |
Address-Restricted cone NAT | YES | YES | YES | YES |
Port-Restricted cone NAT | YES | YES | YES | NO |
Symmetric NAT | YES | YES | NO | NO |
如果端口预测可用,或许可以成功,但是它并不保证一定成功。
客户端实现
1. 创建一个插件实例:NatPunchthroughClient natPunchthroughClient;
2. 将插件附加到RakPeerInterface实例上:rakPeer->AttachPlugin(&natPunchthroughClient);
3. 连接服务器,等待ID_CONNECTION_REQUEST_ACCEPTED消息。使用如下的代码,来使用RakNet提供的免费服务器:rakPeer->Connect(“8.17.250.34”, 60481, 0, 0);
4. 使用想要连接到的系统的RakNetGUID参数调用OpenNAT()函数。为了获得NakNetGUID,你需要使用代码将你的RakNetGUID传递给服务器,上传到PHPDirectoryServer,或者使用一个插件来存储它,例如LightweightDatabase;natPunchthroughClient.OpenNAT(remoteGuid, serverSystemAddress);,为了读取你自己的RakNetGUID,使用RakPeerInterface::GetGuidFromSystemAddress(UNSSIGNED_SYSTEM_ADDRESS);
5. 稍等片刻。要尝试所有的端口,大概要花费10秒,但是一般几秒钟就可以完成。如果你想要得到一些当前正在进行的任务的文本提示,可以调用NatPunchthroughClient::SetDebugInterface();
6. ID_NAT_PUNCHTHROUGH_SUCCEEDED意味着穿透成功,那么你就可以连接到或者发送其他的消息到远端系统了。Packet::SystemAddress是你现在可以连接到的系统的地址。任何其他的ID_NAT_*消息都意味着穿透失败。参考MessageIdentifiers.h文件,详细了解每一个消息代码以及注释。
服务器实现
1. 在某地建立你自己的服务器,不要位于NAT之后或者防火墙之后。(RakNet提供的免费的服务器位于8.17.250.34:60481,然而你可能想要维护你自己的主机,以实现不间断运行)。
2. 创建插件实例:NatPunchthroughServer natPunchthroughServer。
3. 附加插件:rakPeer->AttachPlugin(&natPunchthroughServer);
4. 不要忘记调用RakPeerInterface::Startup()和RakPeerInterface::SetMaximumIncomingConnections(MAX_CONNECTIONS);
使用NatPunchthrough类
参考例子\Samples\NATCompleteClient and \Samples\NATCompleteServer
UDP代理
使用一些质量较差或者家庭制作路由器,可能NAT穿透无法实现。例如,如果路由器为每一个外出的连接选择一个新的随即端口,那么仅仅允许进来的连接连接到这个端口,那么这样的端口永远不可能实现网络穿透。大约5%的情况下会出现这种情况。要处理这种情况,RakNet提供了UDPProxy系统。要使用UDPProxy,则需要使用一个服务器来在源端和目的端之间透明地路由消息。这个服务器也用于为不适用RakNet的系统路由UDP数据报(但你仍然需要使用RakNet进行转发)。NATPunchthrough和UDPProxy的结合使用应该可以使得任何系统以100%的概率连接到服务器,前提是你愿意提供足够的代理服务器来转发数据流。
UDP代理系统使用3个主要的类:
UDPProxyClient: 满足UDPProxyCoordinator的要求设置转发设置。这个类是客户端运行的类。
UDPProxyCoordinator:在服务器端运行,可以得到来自UDPProxyClient的所有请求。也可以获得所有的来自UDPProxyServer的登录。
UDPProxyServer:事实上做UDP数据报转发的类,通过UDPForwarder.cpp组件实例。
客户端实现:
1. 创建一个插件实例:UDPProxyudpProxyClient;
2. 从RakNet::UDPProxyClientResultHandler类派生一个类,用于获得事件提示。
3. 将插件附加到RakPeerInterface实例上:rakPeer->AttachPlugin(&udpProxyClient);
4. 在第二步创建的类上调用UDPProxyClient::SetResultHandler()。
5. 首先尝试NATPunchthrough。如果获得了ID_NAT_PUNCHTHROUGH_FAILED消息,转向第六步。两个系统都会返回ID_NAT_PUNCHTHROUGH_FAILED,然而,仅仅有一个系统需要启动代理系统。
6. 使用协调者的地址作为参数调用UDPProxyClient::RequestForwarding,这个地址是你想要转发自的地址(UNASSIGNED_SYSTEM_ADDRESS用于你自己的),你想要转发到的地址,以及没有数据时保持转发活动要多长时间。例如:
SystemAddress coordinatorAddress;
coordinatorAddress.SetBinaryAddress(“8.17.250.34”);
coordinatorAddress.port = 60481;
udpProxyClientRequestForwarding(coordinatorAddress, UNASSIGNED_SYSTEM_ADDRESS, p->systemAddress, 7000);
7. 假设你被连接到了协调者,协调者也正在运行插件,在第二步创建的事件处理类应该在一秒或两秒之内获得回调。如果一个UDPProxyServer已经赋值来从一个在第六步指定的原系统转发数据报到目的系统,那么UDPProxyClientRequestHandler::OnForwardingSucess会返回。例如,连接到远端系统使用rakPeer->Connect(proxyIPAddress, proxyPort, 0, 0);
如果可用的服务器多于一个,远端和目标中继系统都运行了RakNet,那么源端和目标端自动ping可用的服务器。服务器会尝试按照最低ping和到最大ping和来连接。这个是基于最低的ping值,那么两个系统之间有最短的路径,可以获得最小延迟。
协调者实现:
1. 创建一个插件的实例:UDPProxyCoordinator udpProxyCoordinators;
2. 将插件附加到RakPeerInterface实例上:rakPeer->AttachPlugin( &udpProxyCoordinator);
3. 在服务器上为协调者设置密码使用udpProxyCoordinator.setRemoteLoginPassword(COORDINATOR_PASSWORD);
4. 不要忘记调用RakPeerInterface::Startup()和RakPeerInterface::SetMaximumIncomingConnection(MAX_CONNECTION);
服务器实现:
1. 创建一个插件实例:UDPProxyServer udpProxyServer;
2. 将插件附加到RakPeerInterface实例上:rakPeer->AttachPlugin( &udpProxyCoordinator);
3. 连接到协调者
4. 登录到协调者。这个可以再运行时实现,那么如果你的游戏非常流行了,你可以动态添加更多转发服务器。
udpProxyServer.LogintoCoordinator(COORDINATOR_PASSWORD, coordnatorSystemAddress);
如果协调者插件是作为服务器插件运行在同一个机器上,可以使用如下的代码:
udpProxyServer.LogintoCoordinator(COORDINATOR_PASSWORD, rakPeer->GetInternalID(UNASSIGNED_SYSTEM_ADDRESS));
5. 如果在时间发生时获得一个回调(特别是登录失败时)那么从RakNet::UDPProxyServerREsultHandler类派生,使用UDPProxyServer::SetResultHandler()函数进行注册。
使用UDP代理的状态图
建立自己的服务器
服务器要求:
1. 没有网络地址转换
2. 没有防火墙,或防火墙上打开适当的端口
3. 静态IP地址,Dynamic DNS也是满足这个要求的一种方法。
4. 如果你的服务器连续运行时间超过一个月,使用__GET_TIME_64BIT进行编译。
5. 需要足够的带宽处理所有的连接。
商业建立服务器解决方案
1. Hypemia
定位于世界范围内提供服务器,服务器是单个机器。起始价格一个月$150。
如果你发现其他的建立服务器的解决方法,联系我们,我们将它列举到这个地方供大家参考。
By 北洋小郭
转载请注明出处,请勿用于商业用途,谢谢!