內網穿透在實際生活中,我們經常會在內網裏部署服務讓外網訪問內網應用,比如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;
/**
* 外網端服務,穿透中繼
*
* @author ln
*
*/
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;
/**
* 外網端服務多線程處理內網端連接
*
* @author ln
*
*/
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 + "]";
}
}