手写内网穿透服务端客户端(NAT穿透)原理及实现
Hello,I'm Shendi.
这天心血来潮,决定做一个内网穿透的软件.
用过花生壳等软件的就知道内网穿透是个啥,干嘛用的了.
我们如果有服务器(比如tomcat),实际上我们在电脑上开启了服务器别人是访问不到我们的,除非你有公网ip,目前 IPv4已经几近被占完,所以我们想让别人访问到我们的服务器,使用内网穿透是不错的选择
目录
ClientProtocol 是客户端协议类(与服务端通信的socket)
前言
- 此项目是我边写边做的,所以避免不了有一些小BUG,包含客户端和服务端.
- 项目源码: https://github.com/1711680493/Application/tree/master/ShendiNAT
- 如果想要了解更多可以关注一下,并进入我的实战专栏
- https://blog.csdn.net/qq_41806966/category_9656338.html
- 使用了自己写的工具包,ConfigurationFactory(配置文件)和Log(日志)
- 仅用于学习.
想法
做事情之前都是先思考,后行动.
利用现有知识,我如何实现内网穿透呢?
我刚开始的想法是,搞一台电脑作为服务器,上面运行一个程序.
程序内容是开启我想要被别人访问的端口(一台电脑有65535个端口,我们能用的基本上有6w多个?).
使用TCP或者UDP,将端口一个个用线程打开...
然后我们在写一个软件,软件启动就连接我们之前写的软件,并绑定对应端口.
内容为(模拟访问指定ip的指定端口),当用户访问我们的服务器,我们根据内容(域名不同什么的)去访问对应服务器
服务器获取到客户端返回的内容返回给用户.(中间夹杂了服务器的内容,这个肯定会慢很多,速度会受到我们服务器的限制等影响)
画张图大概就是这样的(这个只是我的想法,具体实现应该是没问题的,或许效率会更低一点?)
我也不知道我的想法是否能行,所以并没有直接动手,而是先查阅资料.
NAT
因为公网 IP 地址逐渐不够用了,NAT的本质是让一群机器公用一个IP
可以简单理解为别人开了一个端口给你用,当别人访问那个端口就是访问你的服务器
优缺点
增加了内网的安全性
提高了网络安全性
降低了发送数据的效率
NAT实现方式
静态 NAT: 一个公网IP对应一个私有IP(一对一)
NAPT: 端口多路复用技术,与静态NAT不同在于NAPT不仅要转换IP,还要转换端口.
一个公网ip可以对应多个私有ip主机,只不过端口不同.
(目前用的最多的是这种,什么花生壳,ngrok...)
例如 私有ip/公网ip
192.168.0.6:80 111.111.111.111:10000
192.168.0.7:80 111.111.111.111:10001
192.168.0.8:321 111.111.111.111:10002
NAT类型
主要分为两大类: 锥型NAT和对称型NAT
锥形NAT分为: 完全锥型,受限锥型,端口受限锥型
完全锥型NAT(Full Cone NAT): IP和端口都不受限
受限锥型NAT(Restricted Cone NAT): IP受限,端口不受限
端口受限NAT(Port Restricted Cone NAT): IP和端口都受限
对称型NAT(Symmetric NAT): 对每个外部主机或端口的会话都会映射为不同的端口
通过搜查众多资料,好吧,我的想法应该是正确的(具体咱也不知道呀)...
猜测: 我们内网能访问到外界网络是因为与某台服务器(公网ip)连接,内网通过那台服务器做了我们接下来要写的软件的操作...
-----------------------------------
上面的基本上是我百度到的总结的废话...
通过万能的网友和百度,突然意识到我的猜测是正确的.
NAT(Network Address Translation,网络地址转换)是一种技术.
内网穿透又称NAT穿透,替换一下意思,NAT=内网?,或许我们内网使用的技术就是NAT
我们访问网络上的资源都会经过交换机,路由器,最后到达服务器(拥有公网IP的),服务器的作用就是获取你的请求,然后去请求另一台服务器,获取到数据返回给你(可能吧).
对于什么是内网,什么是外网,很简单,每一台电脑都有ip,内网就等于一个范围内的ip基本上都是这个范围,比如192.168.0.1,通过路由器分发.
在 cmd 使用 ipconfig 命令和在百度上搜索ip就能看出差别
好了,废话不多说,开始直接实现我们的功能
开始之前(需要的知识)
- 熟悉 Java(如果看个原理,熟悉另一门语言的话可以不用)
- 网络编程,TCP
- 如果不会网络编程的可以看下我的爬虫教程
- Socket制作爬虫1 https://blog.csdn.net/qq_41806966/article/details/102903174
- Socket制作爬虫2 https://blog.csdn.net/qq_41806966/article/details/102966105
- Java反射
- 不会的可以看下我密码学教程(顺带熟悉Java的ClassLoader)
- 密码学1,反射 https://blog.csdn.net/qq_41806966/article/details/103394127
- 密码学2,简单加密解密 https://blog.csdn.net/qq_41806966/article/details/103888049
- 密码学3,加密class文件并调用 https://blog.csdn.net/qq_41806966/article/details/103913682
- 设计模式(策略模式用到较多,为什么?扩展性好啊)
对 Java 不熟的小白可以进入我的 Java教程专栏
制作服务端
设计
首先我们需要内网穿透作用于啥...(比如你是做嵌入式的,你可以用此方法写服务端到路由器限制别人操作...)
通过上面某些废话,我们有几种方式
- 静态NAT,一个ip对应一个ip,可能是为了安全性?
- NAPT,有6w多个端口可以复用.
我们这里目的很简单(为了学习),使用NAPT方式,使得装了我们客户端软件的电脑都通过访问服务器的方式来访问内部.
(如果你有一个外网ip,那么就可以让外部电脑访问你使用了客户端软件的电脑)
一个端口对应一个私有IP,目前写好扩展,只实现TCP的功能(HTTP是应用层协议,所以写好TCP,也等于写好了http)
编写服务端
这里使用 Java 完成这个程序.
我们新建Java项目,新建一个类,类里面有main方法,我们需要提供一个界面让用户添加内网主机和对应的端口.
然后我们应该提供这样一个独立的id给予此用户
为了扩展,需要先分析哪一部分是会变的.哪一部分是相对固定的
- 用户添加内网主机ip端口对应的协议是会变的,比如新增http协议,udp等
- 添加内网主机端口和设置协议是不会变的
所以我们在 main 方法里处理好添加内网主机的操作,并且通过用户选择,获取到字符串(或者其他的,反正要标识此类型),通过配置文件反射调用对应的类,添加完后我们需要分配唯一id,和一个端口(我们端口功能也扩展一下)
所以实际上我们最少需要三个服务端,处理NAT的至少一个(用户添加的时候打开),接收选择的一个(这两个是同一个项目),以及与用户操作,供用户选择服务器的服务端一个
这里只做前两个,让用户添加选择服务器的我们可以写一个类来模拟.
让用户添加的我们也使用TCP协议,并且为了防止改变什么的,也写入配置文件,通过反射的形式调用
我这里用到了我自己写的一个工具集(读取properties配置文件的,你们可以使用Properties直接读取,如果需要,可以下载源码获取)
启动类
在启动类里的main方法中只需要开启服务端,并监听就ok了.
我们需要定义一下数据格式(不复杂化,我们需要三个参数,[ip,端口,协议]) 数据格式为 ip;端口;协议(以英文分号分隔)
如果不是这个数据格式就不做操作.
第一句是读取配置文件进行操作,第二句是创建此类对象,并调用此对象的 onCreate() 方法
创建接收端
创建ServerReceive接口
创建TCP接收器,实现此接口.
接下来就是在 onCreate 方法中创建TCP服务器,Socket.
并且接收到数据处理,如果正确我们就将信息存起来,等客户端使用了此信息,就打开对应端口
在此之前我们需要先创建一个表示穿透信息的 bean 类.
NATState是一个枚举类(改变了一下字体(仿宋),感觉瞬间敲代码都有精神了)
注: 上面说的数据格式要稍微改变一下,增加一项(因为我的疏忽,漏掉了这个服务端接收到数据有两种,一种是添加穿透,另一种是操作(打开/关闭)穿透)
- 添加操作
- add;ip;端口;服务器上的端口;协议
- 打开操作
- open;隧道id
- 关闭操作
- close;隧道id
- 添加操作的返回数据
- ok;id;端口或者域名
- 这里需要加ok是为了表示我们的数据是正确的
- 出错则返回 error;错误信息.
- ok;id;端口或者域名
- 打开操作的返回
- ok;内网ip;内网端口;端口/域名;协议
我先把当前模型画一个图,整理一下,大致就是这样的
我们的 TCPReceive 需要保存穿透(隧道)信息(实际操作应该是存在数据库,但这里为了不麻烦,不使用数据库,直接存内存)
我们创建隧道后应该给其分配某个端口(避免别人使用此端口,这里因为条件原因只用端口,一般为域名.),
而且隧道也需要有id,这里为了简便,id就从0开始依次自增...
有了以上基础,就可以轻松完成添加隧道操作了
在编写开启和关闭操作之前我们需要编写对应的协议类.
新建一个抽象类叫做 ServerProtocol 用于定义协议
除了定义子类的接口之外,还提供了调用方使用的接口.看得出,start() 方法,线程里面执行操作,然后run方法是在死循环内...
这样的目的是为了方便停止死循环.
有了此类后,我们将之前写的 NATInfo 类里的服务端的 Object类型改为此类型.
TCPReceive的代码为
里面的 main 方法是测试用的,现在用一个类实现我们上面写的抽象类
讲一下配置文件, main.properties可忽略(因为我写的工具类要用到这个,
config.properties包含系统配置,现在的内容如下
protocol.properties是给实现协议抽象类的类用的,内容如下
缕一缕上面完成的功能
现在我们的服务端基本上完成一大半了!
只剩下实现协议类了
我们通过反射读取配置文件调用 接收类,也就是TCPReceive,我们也定义了接口,定义了传输协议...
以及将协议类架构写好了
接下来来测试一下(在TCPReceive里我写了个main方法)
启动NATServer
然后启动TCPReceive
结果如下(可能是我卡了,产生了间隔,所以运行中会出现)
TCPReceive输出
NATServer输出
再次重运行一次,结果如下(上面是因为我在启动的时候卡了一下,产生了间隔,因为我在TCPReceive里的main方法没有睡眠,所以执行的非常快,快到关闭的时候运行中还没有启动)
实现TCP协议类
写完这些后重新回顾上面的废话... 我们的应该是对称型NAT,
对称型NAT(Symmetric NAT): 对每个外部主机或端口的会话都会映射为不同的端口
记得我们在接收器部分使用了扩展(这次感觉做的非常对)
比如我们想在某台电脑上更改为xxx型NAT,只要制作这个实现和修改下配置文件就ok了,比如有台电脑想要一个端口对应多台私有主机(想象一下,是不是 锥型),有对应实现,我们只要将配置文件改一改就ok了.
每一个协议类都是一个单独的服务端(占用一个端口)
用户访问实际上访问的就是此服务端(因为我们在创建的时候设置了端口(有条件可以直接用域名,就是获取第一段来区分(www那一段)))
少了一个功能,没有修改隧道功能...问题不大,cv一下就ok了,这里主要是做NAT穿透(ngrok),修改功能并不重要
在协议类里的主要功能就是将用户请求转发到指定内网服务器
这里又大意了,因为客户端和用户都要与我们这个服务端进行通信,同一个端口,正常人第一反应是在搞一个服务端...
思考过后,我们可以获取到对应连接并进行长连接(很久不断)
并且我们的程序打开时,会有一个连接连过来,这个是客户端的连接,所以我们将第一个连接作为长连接存起来就ok了
后续连接都为用户.
接下来开始实现
协议类需要继承 ServerProtocol 类,并且实现对应方法(启动被调用,运行被调用,关闭被调用),上面已经将此操作完成了
接下来我们先在 onCreate 里进行初始化
整理思路(用户与客户端之间通信图)
用户访问->服务端->访问客户端->返回数据给服务端->返回数据给用户
不需要定义任何协议,因为只需要转发.
上面删除线是我第一想法,但是我又想到 服务端只有一个端口=客户端与服务端连接只有一个,但是用户有多个
所以需要改变一下.
整理一下思路(画张图-用户和客户端之间通信图(TCP))
需要协议是因为用户很多,我们要给每一个请求(一个请求对应一个用户)做一个标识.
并且我们 客户端发数据给服务端的只有一个,所以我们在 onCreate 中就开启线程读取客户度发来的数据进行处理
制定服务端和客户端通信的协议如下
更改 onCreate 中代码为如下(代码量有点大,只增加了一个读取客户端发送的数据并处理的线程)
实现协议工具类
因为我们的协议用到的地方很多,所以写一个工具类
所以接下来实现 run() 方法
run 方法主要处理用户发送数据给客户端(并且给每一个用户一个唯一标识)
onStop() 就是关闭一些流
测试,在之前的 TCPReceive里,main为
测试太难搞了。。。但总算弄好了
运行结果如下
服务端
测试端
客户端
我们已经写过测试代码了,客户端的话,只需要套协议就ok了
新建一个项目为客户端
将之前服务端可以用到的东西复制过来.
同样,为了扩展,我们用反射调用.在启动客户端的时候我们需要接收一个参数(隧道id),所以在运行的时候需要传递隧道id
客户端不能新增和删除隧道,只能打开和关闭.
所以我们客户类中有视图层(与用户交互),模型层(隧道信息),控制器层(控制隧道开关)
在启动类里启动视图层,通过控制器打开隧道,创建隧道信息,和通过隧道信息启动协议服务
启动类代码如下
NATInfo 隧道信息类(从服务端复制更改)
稍微改了一下,增加了个视图信息和单例
View是与用户交互的接口
代码如下
Controller是隧道的控制器
用于开关隧道
ClientProtocol 是客户端协议类(与服务端通信的socket)
基本上和服务端的协议类一模一样,不同的是有了创建此类对应对象的方法
我们打开了隧道后就应该创建对应连接的通信与服务端连接.所以此方法用于适应多客户端
实现打开隧道
在实现打开隧道之前,我们需要创建一个协议工具类(将之前服务端的协议工具类复制过来)
代码如下
上面代码较服务端不同的是,增加了开关协议,以及解析开启隧道的协议
打开隧道,打开成功后立马用对应协议来打开连接
实现控制台输出视图 ConsoleView
先实现 open 方法
sc是Scanner,在onCreate进行初始化(onCreate先执行)
简单实现一下界面输出信息.
最后实现 show 方法,显示我们的交互界面
实现TCP协议类
完成这个类基本上就完工了.
此类处理与服务端通信,并且管理与内外服务端通信.
实现服务端到内网服务端之间数据的转发
项目量有点大,有很多地方有缺陷,比如在写服务端的时候可以把添加,打开等操作写协议类里,等...
在实现此类会扩展协议类(与服务端通信),为了扩展.
我们新建此类。实现协议类,并且将属定义好(Socket,流,存储用户的map)
实现 onStop
实现 onCreate
客户端的是一对一的,对应服务端和内网服务端,所以实现都在run里,onCreate中只进行初始化
实现 run方法
先直接把服务端onCreate下的循环代码拿过来稍作修改.
之前是这么写的,现在把这个判断结尾的操作封装到协议类里
现在变成了这样
稍作修改,增加服务端新增用户和关闭操作
这个程序基本上完成了(只不过有些bug)
我们判断数据结尾需要跳出循环条件,也就是input,.read读到的为-1,所以就代表了用户或者内网服务端通信发送数据完后都要使用flush,不然就会进入缓冲区(可以自行进行优化)
我新增了一些控制台输出方便寻找bug
测试一下 看看结果
在测试之前我们在输出视图做一个添加隧道的功能
我用以前写的一个文件传输的软件作为用户和内网服务端,拥有聊天功能,服务端口
将服务端和客户端启动.
先随便输入,跳过打开隧道,我们要添加隧道.(服务端为2333,用户连接的为10000端口)
打开隧道
接下来我们需要访问10000端口就可以访问到2333端口
我们可以在这个软件内发送消息(正如我之前说的,我这个软件没有用flush刷新数据过去,所以,只有当一方关闭(close方法也会调用一次flush)才能接收到数据)
断开连接.
结尾
最后画张图来梳理整个思路
- NAT穿透已经实现,有一点点bug,稍做修改就没问题了(比如只有flush对方才能接收到数据,这个只要在循环里做判断就ok了)
- 使用的 BIO,其实使用 NIO 做这个更有优势.
- 实际应用的话,应该在传输过程中将数据加密.
- 源码已上传到github,请看文章最上方.
- 边学习,边实践