这个例子是使用C#编写的,而不是Java,但NAT穿透的概念与语言无关。请查看Michael Lidgren的网络库,其中包含了NAT穿透功能。
链接:
http://code.google.com/p/lidgren-network-gen3/
处理NAT穿透的特定C#文件:
http://code.google.com/p/lidgren-network-gen3/source/browse/trunk/Lidgren.Network/NetNatIntroduction.cs
您发布的过程是正确的。对于
NAT设备的4种常规类型中的3种,它将起作用(我说常规是因为NAT行为并没有真正标准化):完全锥形NAT、受限锥形NAT和端口受限制的锥形NAT。NAT遍历无法与对称NAT一起使用,对称NAT主要用于企业网络以增强安全性。如果一方使用对称NAT,而另一方不使用,则仍然可以穿越NAT,但需要更多的猜测。对称NAT到对称NAT的穿越非常困难 -
您可以在此处阅读有关此问题的论文。
但是,实际上,您描述的过程完全有效。我已经为
我的远程屏幕共享程序(也是C#)实现了它。只需确保已禁用Windows防火墙(如果您正在使用Windows)和第三方防火墙。但是,我可以高兴地确认它会起作用。
澄清NAT遍历的过程
我写这篇更新是为了为您和未来的读者澄清NAT穿透的过程。希望这可以清晰地总结历史和过程。
一些参考来源:
http://think-like-a-computer.com/2011/09/16/types-of-nat/,以及
http://en.wikipedia.org/wiki/Network_address_translation,
http://en.wikipedia.org/wiki/IPv4,
http://en.wikipedia.org/wiki/IPv4_address_exhaustion。
IPv4地址的容量大约能够唯一命名43亿台计算机,已经用完。聪明的人们预见到了这个问题,并发明了路由器等设备来解决IPv4地址耗尽的问题,通过分配一个共享IP地址给连接在一起的计算机网络。
有局域网IP和广域网IP。局域网IP是本地区域网络中唯一标识计算机的IP,例如连接到家用路由器的台式机、笔记本电脑、打印机和智能手机。广域网IP则是在广域网络中唯一标识本地区域网络以外计算机的IP——通常指互联网。因此,这些路由器会分配一个组的计算机1个WAN IP。每台计算机仍然有自己的LAN IP。当你在命令提示符中键入ipconfig
并获得IPv4地址 . . . . . . . . 192.168.1.101
时,你看到的是LAN IP。当你连接到cmyip.com
并获得128.120.196.204
时,你看到的是WAN IP。
就像无线电频谱被收购一样,整个IP范围也被机构和组织购买和保留了,还有端口号。总之,我们没有多余的IPv4地址可供使用。
这与NAT穿透有什么关系呢?自路由器问世以来,直接连接(
端对端连接)就有些不可能了,除非进行一些黑科技。如果你有两台计算机(计算机A和计算机B),它们都共享
128.120.196.204
的WAN IP,那么连接将会到哪台计算机呢?我指的是一个外部计算机(比如google.com)
发起到
128.120.196.204
的连接。答案是:
没有人知道,路由器也不知道,这就是为什么路由器会丢弃连接。如果计算机A
发起到
google.com
的连接,那就是另一回事了。路由器会记住计算机A的LAN IP
192.168.1.101
向
74.125.227.64
(即google.com)发起连接的情况。当计算机A的请求数据包离开路由器时,路由器实际上会
重新编写LAN IP
192.168.1.101
为路由器的WAN IP
128.120.196.204
。因此,当google.com收到计算机A的请求数据包时,它看到的是路由器重新编写的发送方IP,而不是计算机A的LAN IP(google.com将
128.120.196.204
视为要回复的IP)。当google.com最终回复时,数据包到达路由器,路由器
记住了(它有一个状态表)它正在等待google.com的回复,并适当地将数据包转发给计算机A。
换句话说,当您启动连接时,路由器没有问题 - 路由器会记住将回复数据包转发回您的计算机(通过上述整个过程)。但是,当外部服务器向您发起连接时,路由器无法知道该连接是针对哪台计算机的,因为计算机A和计算机B都共享
128.120.196.204
的WAN IP地址...除非有明确的规则指示路由器将最初前往目标端口
X
的所有数据包转发到计算机A,目标端口
Y
。这就是所谓的“端口转发”。不幸的是,如果您考虑使用端口转发进行网络应用程序,这是不切实际的,因为您的用户可能不知道如何启用它,并且如果他们认为这是安全风险,他们可能不愿意启用它。UPnP只是指允许您“以编程方式启用端口转发”的技术。不幸的是,如果您考虑使用UPnP来进行网络应用程序的端口转发,这也是不切实际的,因为UPnP并不总是可用的,而且当它可用时,可能默认未开启。
那么解决方案是什么呢?解决方案要么是将您的所有流量代理到您自己的计算机上(您已经仔细预先配置为全球可达),要么想出一种方法来打破这个系统。第一个解决方案被称为
TURN,以提供一组带有可用带宽的服务器的价格为代价,神奇地解决了所有连接问题。第二个解决方案称为NAT穿透,这是我们接下来要探讨的内容。
早些时候,我描述了外部服务器(比如google.com)发起与
128.120.196.204
的连接过程。我说,如果路由器没有特定规则来理解将google的连接请求转发到哪台计算机,路由器将会简单地中断连接。这是一个概括性的情景,并不准确,因为有不同类型的NAT。 (注:路由器是您可以放在地板上的实际物理设备。NAT(网络地址转换)是编程到路由器中的软件过程,可帮助节省IPv4地址,就像树木一样)。因此,根据路由器使用的NAT,连接场景会有所不同。 路由器甚至可能结合NAT过程。
有四种标准化的NAT类型:完全锥形NAT、受限制锥形NAT、端口受限制锥形NAT和对称NAT。除了这些类型,还可能存在具有非标准化行为的其他类型的NAT,但这种情况较少发生。
注意:我对NAT并不太熟悉……似乎有很多关于路由器的看法,而且互联网上关于这个主题的信息非常分散。维基百科说将NAT按完全锥形、受限制和端口受限制锥形分类已经有点过时了?还有一些叫做静态和动态NAT的东西……只是一堆无法协调的各种概念。尽管如此,以下模型适用于我的应用程序。您可以通过阅读下面和上面以及本帖中的链接了解更多关于NAT的信息。我不能再发表更多关于它们的内容,因为我对它们的了解不是很多。
希望一些网络大师能纠正/添加输入,以便我们都能更多地了解这个神秘的过程。
回答您的问题,关于收集每个客户端的外部IP和端口:
所有UDP数据包的首部结构相同, 具有一个源IP和一个源端口。 UDP数据包头部不包含"内部"源IP和"外部"源IP。UDP数据包头部只包含一个源IP。如果您想获得"内部"和"外部"源IP,您需要将内部源IP作为负载的一部分实际发送。但是,听起来您并不需要内部源IP和端口。根据您的问题所述,似乎您只需要外部IP和端口。这意味着您的解决方案只需像字段一样读取数据包的源IP和端口。
以下是两种情况(它们并没有解释其他任何内容):
局域网通信
计算机A的局域网IP为192.168.1.101。计算机B的局域网IP为192.168.1.102。计算机A从端口3000向计算机B的端口6000发送数据包。UDP数据包的源IP将是192.168.1.101,这将是唯一的IP地址。“外部”在这里没有上下文,因为该网络纯粹是局域网。在此示例中,广域网(如互联网)不存在。关于端口,由于我不确定NAT,所以我不确定数据包上所写的端口是否为3000。NAT设备可能会将数据包的端口从3000重写为类似49826的随机端口。无论哪种方式,您都应该使用数据包所写的端口来回复 - 这就是您应该用来回复的端口。因此,在这个局域网通信的示例中,您只需要发送一个IP地址 - 局域网IP地址,因为这是唯一重要的。您不必担心端口 - 路由器会为您处理。当您接收到数据包时,只需从数据包中读取唯一的IP地址和端口即可。
WAN通信
计算机A的局域网IP地址是192.168.1.101,计算机B的局域网IP地址是192.168.1.102。计算机A和计算机B将共享一个WAN IP地址128.120.196.204。服务器S是一台服务器,可以在全球范围内访问,例如Amazon EC2服务器,其WAN IP地址为1.1.1.1。服务器S可能有一个局域网IP地址,但这与本题无关。计算机B也与本题无关。
计算机A从端口3000向服务器S发送一个数据包。在路由器外传输时,数据包的源局域网IP地址从计算机A被重写为路由器的广域网IP地址。路由器还将源端口从300重写为32981。服务器S在外部IP和端口方面看到了什么?服务器S看到的是128.120.196.204作为IP地址,而不是192.168.1.101,并且服务器S看到的是32981作为端口,而不是3000。虽然这些不是计算机A用于发送数据包的原始IP和端口,但这些是正确的IP和端口以进行回复。当您接收数据包时,您只能知道WAN IP和重写端口。如果这就是您想要的(您正在寻求
外部 IP和端口),那么您已经准备好了。否则,如果您还想要发送者的内部IP,则需要将其作为普通数据
分离从标题中传输。
代码:
如上所述(下面
回答您的问题关于收集外部IP),要收集每个客户端的外部IP和端口,您只需从数据包中读取它们。每个发送的数据报
总是具有发送者的源IP和源端口;您甚至不需要一个花哨的自定义协议,因为这两个字段总是包含在内 - 每个单独的UDP数据包必须通过定义具有这两个字段。
// Java language
// Buffer for receiving incoming data
byte[] inboundDatagramBuffer = new byte[1024];
DatagramPacket inboundDatagram = new DatagramPacket(inboundDatagramBuffer, inboundDatagramBuffer.length);
// Source IP address
InetAddress sourceAddress = inboundDatagram.getAddress();
// Source port
int sourcePort = inboundDatagram.getPort();
// Actually receive the datagram
socket.receive(inboundDatagram);
因为
getAddress()
和
getPort()
可以返回目标端口或源端口,这取决于您设置的内容,在客户端(发送)机器上,调用
setAddress()
和
setPort()
到服务器(接收)机器,而在服务器(接收)机器上,调用
setAddress()
和
setPort()
回到客户端(发送)机器。必须有一种方法在
receive()
中实现此操作。如果这是您实际遇到的问题(
getAddress()
和
getPort()
未返回您期望的源IP和端口),请详细说明。这假定服务器是一个“标准”的UDP服务器(它不是STUN服务器)。
进一步更新:
我看到了你关于“如何使用STUN获取一个客户端的IP和端口并将其提供给另一个客户端”的更新?STUN服务器不是用来交换端点或执行NAT穿透的。STUN服务器的设计目的是告诉你公共IP、公共端口和NAT设备类型(无论是完全锥形NAT、受限锥形NAT还是端口受限锥形NAT)。我会称负责交换端点和执行实际NAT穿透的中间人服务器为“介绍者”。在
我的个人项目中,我实际上不需要使用STUN来执行NAT穿透。我的“介绍者”(介绍客户端A和B的中间人服务器)是一个标准服务器,监听UDP数据报。当客户端A和B都向介绍者注册时,介绍者读取它们的公共IP和端口以及私有IP(如果它们在局域网上)。公共IP从数据报头中读取,就像所有标准UDP数据报一样。私有IP作为数据报有效载荷的一部分写入,介绍者只需将其作为有效载荷的一部分读取即可。因此,关于STUN的实用性,您不需要依赖STUN来获取每个客户端的公共IP和公共端口-任何连接的套接字都可以告诉您这一点。我会说STUN仅在确定客户端所处的NAT设备类型时有用,以便您知道是否执行NAT穿透(如果NAT设备类型是完全锥形、受限或端口受限),或者执行全面的TURN流量代理(如果NAT设备类型是对称的)。
请详细说明您的障碍:如果您想获得有关设计应用程序消息协议的最佳实践建议,并获得有关按有序和系统化方式读取接收到的消息字段的建议(基于您在下面发布的评论),您能否分享您当前的方法?