TCP点对点穿透探索

点对点穿透是穿透什么

点对点穿透,需要实现的是对NAT的穿透。想实现NAT的穿透,当然要先了解NAT到底是什么,以及NAT是用来干什么的。
NAT全称Network Address Translation,意思是网络地址转换,在1994年提出。它可以对不同的IP及端口进行映射,将一个网络地址转换为另一个。NAT的主要用途,大家可以看路由器。路由器具有一个WAN口及多个LAN口;WAN口对外,连接因特网,拥有公网IP;LAN口对内,构建本地网络,分配的是私网IP。当处于LAN网下的本地主机想要访问因特网的时候,路由器就会通过NAT技术,将LAN 口的私网IP映射到WAN口的公网IP,实现网络地址的转换,这样本地主机就可以访问因特网了。
NAT技术的出现,有效减缓了IPv4时代可用IP地址枯竭的问题。至于为何可以缓解IP地址枯竭,我们依旧可以参照路由器来加以理解。路由器的LAN网下扩展了多台本地主机,这些本地主机需要不同的IP地址加以区分。如果它们都是接在英特网下,那么每台主机都需要消耗一个公网IP,但是通过路由器的NAT服务,这些本地主机可以先分配不同的私网IP,然后在需要连接到英特网的时候映射到公网IP的不同端口上完成对因特网的访问,从而节省IP地址的消耗。至于私网IP地址,由于NAT的存在,本地网络与英特网处于隔离状态,不必担心与其他网络产生冲突。
上面是对NAT的一些简单的介绍,以及NAT工作方式的简单描述,如果觉得难以理解,可以自行查阅更多资料。

举个例子,假如路由器WAN口获取到的公网IP是55.66.77.88(随便写的),LAN网下某台本地主机获取到的私网IP是192.168.0.100(路由器多是192.168.0.0网段)。现在本地主机想要向英特网发起连接,它通过自己的10000端口发起了连接,路由器知道有本地主机向英特网发起连接,便会分配一个WAN口的可用端口做映射,比如分配到的是5000端口。这时,192.168.0.10010000端口便和55.66.77.885000端口产生了映射关系。55.66.77.885000端口收到的网络包便会转发到192.168.0.10010000端口,192.168.0.10010000端口收到的网络包也会转发到55.66.77.885000端口。当然,由于是网络地址转换,这途中还会有拆包,重新装包的过程,不做详细说明。

大致知道NAT怎么工作的之后,接下来了解为什么要穿透NAT。
按照上面的描述,NAT的工作需要LAN网下的本地设备主动发起网络连接,然后NAT服务才会将这个连接映射到WAN口的公网IP完成转换。也就是说,如果本地主机没有主动发起连接,那么这个映射就不会存在,那么公网上的机器就无法访问到私网上的机器。也就是说,只能是私网机器主动连接公网机器,而不能是公网机器主动连接私网机器。而为了实现公网机器主动连接私网机器,我们就需要穿透NAT,这就是NAT穿透的由来。

目前比较好实现NAT穿透的方式是采用UDP连接对NAT进行打洞,然后完成连接。何为打洞呢?就是为了使NAT产生一个可用的映射。具体步骤就是在私网机器上用UDP向某台公网机器发起连接,使得NAT产生一个可以使用的映射(洞)。然后通过这个映射(洞),就可以穿透NAT。
至于为什么要用UDP,这是由于UDP的某些特性。UDP通信需要先绑定本地机器的端口,完成后就可以从这个端口收发数据,至于从哪里收,发到哪里,可以在收发数据的时候再决定,这也就意味着我可以用这一个端口同时和多个对象通信,只要我收发数据的时候指定不同的对象即可。当本地机器用UDP向英特网上的某个服务器发送数据的时候,这个映射不但能用来和这个服务器进行数据交互,也能用来接收其他主机发来的数据。NAT穿透就是本地主机向公网上的某台服务器发送数据,这时服务器就可以获得NAT对这台主机的映射,在之前举得例子中就是55.66.77.88:5000这个地址。由于NAT会将55.66.77.88:5000收到的数据转发至本地主机,所以公网上的其他机器可以从服务器获取到55.66.77.88:5000这个网络地址,然后通过这个网络地址向私网下的机器发出数据。
而至于为什么不用TCP,也是由于TCP的某些特性。TCP通信的步骤与UDP不同,它需要先在两个对象之间建立一个专用通道,再用这个通道收发数据。也就是说外人无法插手。这样一来,虽然其他机器可以通过服务器获取到NAT的映射对象,也没办法利用它向私网下的机器发出数据。
关于TCP与UDP的更多细节,请参考SOCKET编程。

TCP实现点对点穿透的探索

为了尝试使用TCP实现点对点穿透,需要现对TCP做更多的了解。我之前有详细查过TCP连接中的各种状态变化,做了简单的整理,可以做个参考:TCP连接状态变化
既然TCP在连接过程中其他人不能插手,但是等它连接结束之后呢?NAT对TCP连接的端口映射在连接结束后就立马销毁了吗?接着深入,发现NAT存在一个老化机制。接下来看看老化是什么意思。NAT生成某个映射后,会将这个映射保存下来,但是即使端口号非常多,它也不是无限的,而既然端口号是有限资源,那么就不能保证映射表的无限扩充。为了合理利用资源,当某个映射一段时间内没有发生数据交互,NAT就会认为这个映射已经没有人使用了,就会将这个映射销毁,回收端口号。这个时间,就叫做老化时间。也就是说,老化是一种映射的回收机制。
TCP连接状态变化中可以知道,TCP在断开连接后会有一段时间的保护期,不让这个端口进行下一次连接,这个时间是2*MSL,MSL在协议中的建议值为2分钟,实际应用中常用是30秒,1分钟和2分钟,也就是说这个时间很有可能是一分钟甚至更久。那老化时间有多久有多久呢,在老化时间控制中有提到,TCP的默认老化时间是86400秒,TCP-SYN和TCP-FIN的默认老化时间是60秒。这样说来,按照一般情况等保护期结束的时候,NAT的映射也到期了。
但是没有关系,SOCKET编程中允许有一些特殊的选项,其中有一个叫SO_REUSEADDR的选项。

以下文字引用自setsockopt中参数之SO_REUSEADDR的意义

setsockopt中参数之SO_REUSEADDR的意义

1、一般来说,一个端口释放后会等待两分钟之后才能再被使用,SO_REUSEADDR是让端口释放后立即就可以被再次使用。

SO_REUSEADDR用于对TCP套接字处于TIME_WAIT状态下的socket,才可以重复绑定使用。server程序总是应该在调用bind()之前设置SO_REUSEADDR套接字选项。TCP,先调用close()的一方会进入TIME_WAIT状态

2、SO_REUSEADDR和SO_REUSEPORT

SO_REUSEADDR提供如下四个功能:
SO_REUSEADDR允许启动一个监听服务器并捆绑其众所周知端口,即使以前建立的将此端口用做他们的本地端口的连接仍存在。这通常是重启监听服务器时出现,若不设置此选项,则bind时将出错。
SO_REUSEADDR允许在同一端口上启动同一服务器的多个实例,只要每个实例捆绑一个不同的本地IP地址即可。对于TCP,我们根本不可能启动捆绑相同IP地址和相同端口号的多个服务器。
SO_REUSEADDR允许单个进程捆绑同一端口到多个套接口上,只要每个捆绑指定不同的本地IP地址即可。这一般不用于TCP服务器。
SO_REUSEADDR允许完全重复的捆绑:当一个IP地址和端口绑定到某个套接口上时,还允许此IP地址和端口捆绑到另一个套接口上。一般来说,这个特性仅在支持多播的系统上才有,而且只对UDP套接口而言(TCP不支持多播)。
SO_REUSEPORT选项有如下语义:
此选项允许完全重复捆绑,但仅在想捆绑相同IP地址和端口的套接口都指定了此套接口选项才行。
如果被捆绑的IP地址是一个多播地址,则SO_REUSEADDR和SO_REUSEPORT等效。
使用这两个套接口选项的建议:
在所有TCP服务器中,在调用bind之前设置SO_REUSEADDR套接口选项;
当编写一个同一时刻在同一主机上可运行多次的多播应用程序时,设置SO_REUSEADDR选项,并将本组的多播地址作为本地IP地址捆绑。
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR,
   (const void *)&nOptval , sizeof(int)) < 0) ...

关于SO_REUSEADDR的特性,网上介绍很多,这里贴出几个链接:
SO_REUSEADDR例解
SO_REUSEADDR 套接字选项应用实例

有了SO_REUSEADDR就好办了,在刚断开连接的时候NAT的映射还没有被老化,而由于SO_REUSEADDR套接字选项的关系,也可以立马进行下一次连接。也就是说只要我们在NAT服务设置的老化时间内重新建立好连接,那么这个映射就可以继续使用。

从原理上来说应该是存在可行性的,如果有偏颇,忘指正。后续会尝试搭建环境,写程序做个试验。

在这里写一下之前做的试验:连接关闭之后NAT的端口映射直接失效,根本无法建立下一次连接,知识储备还差点儿,太想当然了。

最新文章

  1. on事件委托
  2. Inode详解
  3. c#与js中10进制16进制的转化,记录防忘
  4. C++ const使用详解
  5. hdu 5311 Hidden String
  6. Oracle RAC中的一台机器重启以后无法接入集群
  7. 批量导出表数据到CSV文件
  8. Asp.net MVC Web.config配置技巧
  9. css 元素居中方法
  10. (二)Java数组特性总结,你真的了解数组吗?
  11. Tomcat源码调试环境搭建
  12. netty学习资源收集
  13. day10函数,函数的使用,函数的分类,函数的返回值
  14. Tomcat源码分析 -- Tomcat整体架构
  15. feign包名路径添加问题
  16. PHP爬取历史天气
  17. oracle substr函数
  18. 使用IdentityServer4实现一个简单的Oauth2客户端模式授权
  19. DNS 基础
  20. 优先队列优化dij算法

热门文章

  1. Spring cloud微服务实战——基于OAUTH2.0统一认证授权的微服务基础架构
  2. mac os x升级MOUNTAIN LION后svn command not found的解决
  3. iOS中对于多个按钮,选中其中一个,其他按钮选中状态为NO
  4. visual studio 2010 c++ 打印 Hello world
  5. intellij idea jdk language level
  6. cmake policy
  7. wepy error Parsing error: Unexpected token :
  8. centos设置固定IP方法
  9. shell执行lua脚本传参数
  10. re.sub用法