内网穿透在实际生活中,我们经常会在内网里部署服务让外网访问内网应用,比如Apache,Tomcat,数据库,微信小程序的开发以及企业的一些管理软件(OA、CRM、ERP),还有远程桌面等等的外网都是无法直接访问内网的。
有些方式可以通过设置路由器虚拟服务器开放一些端口供外网访问,但由于运营商的原因,这些IP有时候并不是直接的IP,更多的时候这些IP都是动态的,简单说就是今天给你的IP是15.63.87.251,明天随时都有可能变为变得IP,并且是不能访问80个443端口的,那么有没有一种办法可以实现在没有公网IP的情况下,又不用设置路由器就可以让外网直接访问内网里的应用呢,今天要讲的就是这个:
首先普及一下基本的概念,可能有些拗口,不过没关系,这个看不懂也不要紧,可以直接跳过这一段:
什么是内网穿透、为什么要内网穿透,内网、公网和NAT是什么意思?
公网、内网是两种Internet的接入方式。
内网接入方式:上网的计算机得到的IP地址是Inetnet上的保留地址,保留地址有如下3种形式:
10.x.x.x
172.16.x.x至172.31.x.x
192.168.x.x
内网的计算机以NAT(网络地址转换)协议,通过一个公共的网关访问Internet。内网的计算机可向Internet上的其他计算机发送连接请求,但Internet上其他的计算机无法向内网的计算机发送连接请求。
公网接入方式:上网的计算机得到的IP地址是Inetnet上的非保留地址。公网的计算机和Internet上的其他计算机可随意互相问。
NAT(Network Address Translator)是网络地址转换,它实现内网的IP地址与公网的地址之间的相互转换,将大量的内网IP地址转换为一个或少量的公网IP地址,减少对公网IP地址的占用。NAT的最典型应用是:在一个局域网内,只需要一台计算机连接上Internet,就可以利用NAT共享Internet连接,使局域网内其他计算机也可以上网。使用NAT协议,局域网内的计算机可以访问Internet上的计算机,但Internet上的计算机无法访问局域网内的计算机。
Windows操作系统的Internet连接共享、sygate、winroute、unix/linux的natd等软件,都是使用NAT协议来共享Internet连接。 所有ISP(Internet服务提供商)提供的内网Internet接入方式,几乎都是基于 NAT协议的。
什么是固定IP、动态IP地址、什么是域名?
固定IP地址是长期分配给一台计算机或网络设备使用的IP地址。一般来说,采用专线上网的计算机才拥有固定的IP地址。
什么是动态IP地址 ?
通过Modem、ISDN、ADSL、有线宽频、小区宽频等方式上网的计算机,每次上网所分配到的IP地址都不相同,这就是动态IP地址。因为IP地址资源很宝贵,大部分用户都是通过动态IP地址上网的。普通人一般不需要去了解动态IP地址,这些都是计算机系统自动完成的。
当然在很多情况下你可能并没有公网IP,不要问我为什么,本人曾经做过2年的售后工程师,给几百家客户安装实施部署过软件,有3分之一的企业虽然有路由器,但运营商分给的IP却是内网的,奇怪吧,比如10、172或者100开头的都是运营商的内网IP。
这里简单说一下怎么查看是否是内网IP:
1.如果你使用的是Window平台,点击自己电脑窗口的“开始”“运行”输入“cmd”,在DOS命令窗口输入“ipconfig /all”,得到的IP如果和上面一样,说明你拥有自己的外网IP
2.如果你使用的是unix/linux平台,运行 ifconfig -a 得到的IP如果和上面一样,说明你拥有自己的外网IP。
现在开始正式进入正题:
接下来我就演示2个应用,一个是设置远程桌面,设置让外网可以通过3389远程控制局域网内的电脑,另一个就是访问内网里Tomcat的应用,端口是8080,使用的工具是神卓互联,快速实现内网穿透。
首先可以去神卓互联官网下载一个客户端。(地址自己百度吧),这个一般是针对企业级应用的,比如管家婆,OA系统等等,对于我们这些平民可以使用社区版,这个真的是免费的。
填写自己要穿透的应用名称和端口号,如果需要获取原访问者IP最好是选择Web应用。提交提交就可以了。
例如我需要发布一个Tomcat应用,访问端口号是7070,那么应用名称填写tomcat,内网主机填写127.0.0.1,内网端口填7070点提交就可以。
首先新建一个web项目
新建login.jsp登陆文件,内容如下:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>登录系统</title>
<style type="text/css">
table td{font: 14px/1.5 'Microsoft YaHei',arial,tahoma,\5b8b\4f53,sans-serif;}
</style>
</head>
<body>
<table>
<tr><td>用户名</td><td><input type="text"></td></tr>
<tr><td>密码</td><td><input type="text"></td></tr>
<tr><td> </td><td><input type="submit" value="登录"></td></tr>
</table>
</body>
</html>
先在本地运行,看项目是否可以正常运行
打开神卓互联软件主界面,右键选择外网访问
如果需要绑定域名访问的话也很简单,这里不多说。
接下来就分析是如何做到将请求转发到内网因为又返回给访问客户端的。
接下来就是java版的TCP打洞核心代码
由于32位Ip地址的稀少,我们身边的设备,大部分运行在nat后面,无论是家庭还是单位,都会由一个路由器统一接入互联网,很多设备连上路由器组成一个内网。同一内网里的所有设备,拥有相同的外网ip地址,内网设备对外网进行访问,每次会使用不同的端口进行通信,不同内网里面的设备不能直接进行连接 ,因为不知道对方的公网地址和端口,这个时候就需要借助一台公网的设备进行牵线搭桥,也就是大家常说的穿透打洞。穿透的原理和NAT的运行原理,就不在此讨论,网上已有大量理论文章。
假设现在有以下3台机器:
外网机器,IP:121.56.21.85 , 以下简称“主机A”
处在内网1下的机器,外网IP:106.116.5.45 ,内网IP:192.168.1.10, 以下简称“主机1”
处在内网2下的机器,外网IP:104.128.52.6 ,内网IP:192.168.0.11,以下简称“主机2”
很显然内网的两台机器不能直接连接,我们现在要实现的是借助外网机器,让两台内网机器进行tcp直连通讯。
实现过程如下:
1、主机A启动服务端程序,监听端口8888,接受TCP请求。
2、启动主机1的客户端程序,连接主机A的8888端口,建立TCP连接。
3、启动主机2的客户端程序,连接主机A的8888端口,建立TCP连接。
4、主机2发送一个命令告诉主机A,我要求与其他设备进行连接,请求协助进行穿透。
5、主机A接收到主机2的命令之后,会返回主机1的外网地址和端口给主机2,同时把主机2的外网地址和端口发送给主机1。
6、主机1和主机2在收到主机A的信息之后,同时异步发起对对方的连接。
7、在与对方发起连接之后,监听本地与主机A连接的端口(也可以在发起连接之前),(由于不同的操作系统对tcp的实现不尽相同,有的操作系统会在连接发送之后,把对方的连接当作是回应,即发出SYN之后,把对方发来的SYN当作是本次SYN的ACK,这种情况就不需要监听也可建立连接,本文的代码所在测试环境就不需要监听,测试环境为:服务器centos 7.3, 内网1 win10,内网2 win10和centos7.2都测试过)。
8、主机1和主机2成功连上,可以关闭主机A的服务,主机1和主机2的连接依然会持续生效,不关闭就形成了一个3方直连的拓扑网状结构网络。
服务器端代码:
package org.inchain.p2p;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
/**
* 外网端服务,穿透中继
*
*
*/
public class Server {
public static List<ServerThread> connections = new ArrayList<ServerThread>();
public static void main(String[] args) {
try {
// 1.创建一个服务器端Socket,即ServerSocket,指定绑定的端口,并监听此端口
ServerSocket serverSocket = new ServerSocket(8888);
Socket socket = null;
// 记录客户端的数量
int count = 0;
System.out.println("***服务器即将启动,等待客户端的连接***");
// 循环监听等待客户端的连接
while (true) {
// 调用accept()方法开始监听,等待客户端的连接
socket = serverSocket.accept();
// 创建一个新的线程
ServerThread serverThread = new ServerThread(socket);
// 启动线程
serverThread.start();
connections.add(serverThread);
count++;// 统计客户端的数量
System.out.println("客户端的数量:" + count);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
package org.inchain.p2p;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.Socket;
/**
* 外网端服务多线程处理内网端连接
*
*
*/
public class ServerThread extends Thread {
// 和本线程相关的Socket
private Socket socket = null;
private BufferedReader br = null;
private PrintWriter pw = null;
public ServerThread(Socket socket) throws IOException {
this.socket = socket;
this.br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
this.pw = new PrintWriter(socket.getOutputStream());
}
// 线程执行的操作,响应客户端的请求
public void run() {
InetAddress address = socket.getInetAddress();
System.out.println("新连接,客户端的IP:" + address.getHostAddress() + " ,端口:" + socket.getPort());
try {
pw.write("已有客户端列表:" + Server.connections + "\n");
// 获取输入流,并读取客户端信息
String info = null;
while ((info = br.readLine()) != null) {
// 循环读取客户端的信息
System.out.println("我是服务器,客户端说:" + info);
if (info.startsWith("newConn_")) {
//接收到穿透消息,通知目标节点
String[] infos = info.split("_");
//目标节点的外网ip地址
String ip = infos[1];
//目标节点的外网端口
String port = infos[2];
System.out.println("打洞到 " + ip + ":" + port);
for (ServerThread server : Server.connections) {
if (server.socket.getInetAddress().getHostAddress().equals(ip)
&& server.socket.getPort() == Integer.parseInt(port)) {
//发送命令通知目标节点进行穿透连接
server.pw.write("autoConn_" + socket.getInetAddress().getHostAddress() + "_" + socket.getPort()
+ "\n");
server.pw.flush();
break;
}
}
} else {
// 获取输出流,响应客户端的请求
pw.write("欢迎您!" + info + "\n");
// 调用flush()方法将缓冲输出
pw.flush();
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("客户端关闭:" + address.getHostAddress() + " ,端口:" + socket.getPort());
Server.connections.remove(this);
// 关闭资源
try {
if (pw != null) {
pw.close();
}
if (br != null) {
br.close();
}
if (socket != null) {
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
@Override
public String toString() {
return "ServerThread [socket=" + socket + "]";
}
}