Java使用黑盒方式模擬實現(xiàn)內(nèi)網(wǎng)穿透
前言:
最近準備使用樹莓派搭建一個內(nèi)網(wǎng)監(jiān)控系統(tǒng),然后在外網(wǎng)訪問。因此選擇了釘釘內(nèi)網(wǎng)穿透的方式,因為這種方式最為簡單,但是由于樹莓派的架構(gòu)是ARM指令集,所以無法運行成功,釘釘內(nèi)網(wǎng)穿透只能在我的X86筆記本上面運行了。但是我倒是對內(nèi)網(wǎng)穿透這個概念特別感興趣了,所以就想著能不能利用自己所學習的知識,自己來模擬實現(xiàn)一個內(nèi)網(wǎng)穿透工具。
1. 內(nèi)網(wǎng)穿透簡介
從黑盒的角度理解: 通常個人電腦無論是連接WIFI上網(wǎng)還是用網(wǎng)線上網(wǎng),都是屬于局域網(wǎng)里邊的,外網(wǎng)無法直接訪問到你的電腦,內(nèi)網(wǎng)穿透可以讓你的局域網(wǎng)中的電腦實現(xiàn)外網(wǎng)訪問功能。舉一個例子: 你在本地運行了一個Web服務,占用端口是8080,那么你本地進行測試就是://localhost:8080。但是如果你想給一個好朋友分享你的服務,那怎么辦呢?是的,就是采用內(nèi)網(wǎng)穿透的方式。 實際上,內(nèi)網(wǎng)穿透是很復雜的一個操作,百度百科上面的解釋為:
內(nèi)網(wǎng)穿透,也即 NAT 穿透,進行 NAT 穿透是為了使具有某一個特定源 IP 地址和源端口號的數(shù)據(jù)包不被 NAT 設(shè)備屏蔽而正確路由到內(nèi)網(wǎng)主機。
這里,我顯然是做不了的。我需要的只是從外網(wǎng)訪問到內(nèi)網(wǎng)的服務,至于具體的過程我不關(guān)心,只需要達到這個目的即可了。
2. 具體想法和實現(xiàn)細節(jié)
2.1 具體想法
無論是哪種方式實現(xiàn)內(nèi)網(wǎng)穿透都是需要一個公網(wǎng)IP地址的,我這里使用的一臺阿里云的服務器。下面是整個模擬的示意圖:

注:
1.內(nèi)網(wǎng)穿透服務端部署在具有公網(wǎng)IP的機器上。
2.內(nèi)網(wǎng)服務和內(nèi)網(wǎng)穿透客戶端部署在內(nèi)網(wǎng)機器上。
說明:
我的想法很簡單,即用戶訪問內(nèi)網(wǎng)穿透服務器,然后內(nèi)網(wǎng)穿透服務器將用戶的請求報文轉(zhuǎn)發(fā)給內(nèi)網(wǎng)穿透客戶端,接著內(nèi)網(wǎng)穿透客戶端將請求報文轉(zhuǎn)發(fā)給內(nèi)網(wǎng)服務,然后接收內(nèi)網(wǎng)服務的響應報文,將其轉(zhuǎn)發(fā)給內(nèi)網(wǎng)穿透服務端,最后由內(nèi)網(wǎng)穿透服務端將其轉(zhuǎn)發(fā)給用戶。大致流程是這樣的,對于外部的用戶來說它只會認為它訪問了一個外網(wǎng)服務,因為用戶面對的是一個黑盒系統(tǒng)。
2.2 實現(xiàn)細節(jié)
為了實現(xiàn)上面那個目標,其中最為關(guān)鍵的就是維持內(nèi)網(wǎng)穿透客戶端和內(nèi)網(wǎng)穿透服務端的一個長連接,我需要使用這個長連接來交換雙方的報文信息。因此,這個長連接需要在系統(tǒng)啟動后就建立好,當有用戶的請求進來的時候,內(nèi)網(wǎng)穿透服務端首先接收這個請求,然后使用長連接將其轉(zhuǎn)給內(nèi)網(wǎng)穿透客戶端,內(nèi)網(wǎng)穿透客戶端使用該報文作為請求訪問內(nèi)網(wǎng)服務,然后接收內(nèi)網(wǎng)服務的響應,將其轉(zhuǎn)發(fā)給內(nèi)網(wǎng)穿透服務端,最后將其轉(zhuǎn)發(fā)給用戶。
3. 代碼實現(xiàn)
3.1 目錄結(jié)構(gòu)
說明: 這個是內(nèi)網(wǎng)穿透的服務端和客戶端代碼,我是放在一起了,沒有分開寫,因為雙方需要使用到一些公用的類。但是建議還是分開成兩個工程,因為需要分開部署。或者導出成jar包的時候,分別選擇不同的主類即可。
客戶端代碼文件:Client.java、Connection.java、Msg.java、ProxyConnection.java。
服務端代碼文件:Server.java、Connection.java、Msg.java、ProxyConnection.java。
3.2 Client 類
package org.dragon;
import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;
/**
* 用于雙向通信的客戶端
* */
public class Client {
private static final String REMOTE_HOST = "公網(wǎng)IP";
private static final String LOCAL_HOST = "127.0.0.1";
public static void main(String[] args) {
try {
Socket proxy = new Socket(REMOTE_HOST, 10000);
System.out.println("Connect Server Successfully!");
ProxyConnection proxyConnection = new ProxyConnection(proxy); // 維持和內(nèi)網(wǎng)穿透服務端的長連接
// 可以實現(xiàn)同一個人多次訪問
while (true) {
Msg msg = proxyConnection.receiveMsg();
Connection connection = new Connection(new Socket(LOCAL_HOST, 8080));
connection.sendMsg(msg); // 將請求報文發(fā)送給內(nèi)網(wǎng)服務器,即模擬發(fā)送請求報文
msg = connection.receiveMsg(); // 接收內(nèi)網(wǎng)服務器的響應報文
proxyConnection.sendMsg(msg); // 將內(nèi)網(wǎng)服務器的響應報文轉(zhuǎn)發(fā)給公網(wǎng)服務器(內(nèi)網(wǎng)穿透服務端)
}
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}3.3 Connection 類
package org.dragon;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
/**
* 維持用戶和服務器的連接
* */
public class Connection {
private InputStream input;
private OutputStream output;
public Connection(Socket client) throws IOException {
this.input = new BufferedInputStream(client.getInputStream());
this.output = new BufferedOutputStream(client.getOutputStream());
}
public Msg receiveMsg() throws IOException {
byte[] msg = new byte[2*1024];
int len = input.read(msg);
return new Msg(len, msg);
}
public void sendMsg(Msg msg) throws IOException {
output.write(msg.getMsg(), 0, msg.getLen());
output.flush(); // 每一次寫入都要刷新,防止阻塞。
}
}3.4 Msg 類
package org.dragon;
public class Msg {
private int len;
private byte[] msg;
public Msg(int len, byte[] msg) {
this.len = len;
this.msg = msg;
}
public int getLen() {
return len;
}
public byte[] getMsg() {
return msg;
}
@Override
public String toString() {
return "msg: " + len + " --> " + new String(msg, 0, len);
}
}3.5 ProxyConnection 類
package org.dragon;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;
/**
* @author Alfred
*
* 代理服務器和代理客戶端是用于維持兩者之間通信的一個長連接Socket,
* 主要的目的是因為雙方之間的通信方式是全雙工的,它們的作用是為了傳遞報文。
* */
public class ProxyConnection {
private Socket proxySocket;
private DataInputStream input;
private DataOutputStream output;
public ProxyConnection(final Socket socket) throws UnknownHostException, IOException {
proxySocket = socket;
input = new DataInputStream(new BufferedInputStream(proxySocket.getInputStream()));
output = new DataOutputStream(new BufferedOutputStream(proxySocket.getOutputStream()));
}
/**
* 接收報文
* @throws IOException
* */
public Msg receiveMsg() throws IOException {
int len = input.readInt();
if (len <= 0) {
throw new IOException("異常接收數(shù)據(jù),長度為:" + len);
}
byte[] msg = new byte[len];
int size = input.read(msg); // 這里到底會不會讀取到這么多,我也有點迷惑!
return new Msg(size, msg); // 為了防止出錯,還是使用一個記錄實際讀取值size
}
/**
* 轉(zhuǎn)發(fā)報文
* @throws IOException
* */
public void sendMsg(Msg msg) throws IOException {
output.writeInt(msg.getLen());
output.write(msg.getMsg(), 0, msg.getLen());
output.flush(); // 每一次寫入都需要手動刷新,防止阻塞。
}
}3.6 Server 類
package org.dragon;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 用于雙向通信的服務器
* */
public class Server {
public static void main(String[] args) {
try (ServerSocket server = new ServerSocket(10000)) {
// 用于交換控制信息的Socket
Socket proxy = server.accept();
ProxyConnection proxySocket = new ProxyConnection(proxy);
// 用于正常通訊的socket
while (true) {
Socket client = server.accept();
Connection connection = new Connection(client);
Msg msg = connection.receiveMsg(); // 接收用戶的請求報文
proxySocket.sendMsg(msg); // 轉(zhuǎn)發(fā)用戶的請求報文給內(nèi)網(wǎng)服務器
msg = proxySocket.receiveMsg(); // 接收內(nèi)網(wǎng)服務器的響應報文
connection.sendMsg(msg); // 轉(zhuǎn)發(fā)內(nèi)網(wǎng)服務器的響應報文給用戶
}
} catch (IOException e) {
e.printStackTrace();
}
}
}4. 內(nèi)網(wǎng)服務
內(nèi)網(wǎng)服務是一個web服務,這里我使用的是一個簡單的SpringBoot項目,它只有三個請求方法。
package org.dragon.controller;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class Controller {
@GetMapping("/loveEN")
public String testEN() {
return "I love you yesterday and today!";
}
@GetMapping("/loveZH")
public String loveZH() {
return "有一美人兮,見之不忘。一日不見兮,思之如狂。鳳飛翱翔兮,四海求凰。無奈佳人兮,不在東墻。";
}
@GetMapping("/loveJson")
public Map<String, String> loveJson() {
HashMap<String, String> map = new LinkedHashMap<>();
map.put("english", "I love you yesterday and today!");
map.put("chinese", "有一美人兮,見之不忘。一日不見兮,思之如狂。"
+ "鳳飛翱翔兮,四海求凰。無奈佳人兮,不在東墻。");
return map;
}
}5. 測試
5.1 內(nèi)網(wǎng)測試
啟動內(nèi)網(wǎng)服務,在瀏覽器輸入以下三條URL進行測試,功能正常。 


5.2 外網(wǎng)測試
先后啟動內(nèi)網(wǎng)穿透服務端和內(nèi)網(wǎng)穿透客戶端,然后在瀏覽器訪問一下三條URL即可。 注意: 1.如果你自己測試,切換成你運行內(nèi)網(wǎng)穿透服務器的ip地址或者使用域名也行。 2.我這里外網(wǎng)機器和內(nèi)網(wǎng)機器使用的是不同的端口(隨便使用,只要不和自己機器上的服務端口沖突就行了),實際上可以在外網(wǎng)使用80端口,這樣對普通用戶比較友好。 3.第三條測試實際上是失敗的,可以看到上面那個加載動畫,一直在加載。按理說這個應該很快就停止了,但是似乎無法停下來。這是系統(tǒng)的bug了,但是由于我掌握的知識有限,就不去解決了。



6. 注意事項
這里的代碼是一種模擬,它只能模擬這個功能,但是基本上不具備實際的作用,哈哈。因為我這里只有一個長連接,所以只能支持串行的通信,最好就是一個人簡單的調(diào)用,似乎調(diào)用速度也不能太快了。我想了一種方式,在客戶端和服務器之間維持一個連接池,這樣就可以實現(xiàn)多線程訪問了。這里沒有處理TCP的粘包和分包(我理解了這個概念,但是我不太會處理它),所以我默認請求報文和響應報文都是2KB以內(nèi)大小。如果超過這個長度會導致問題,盡管可以調(diào)大這個參數(shù),但是如果多數(shù)報文的都是很小的話,也會導致效率低下。這個內(nèi)網(wǎng)穿透是可以支持TCP之上的各種協(xié)議的,不一定是HTTP,至少理論上是可以的。
7. 總結(jié)
從這個想法的萌生到實現(xiàn)這個功能也是花了我好幾天時間的,這本身就是一個學習的過程。學習使用自己的網(wǎng)絡和編程知識解決問題,我認為這是一種很好的學習方式——學以致用。書到用時方恨少,在這個過程中體現(xiàn)的淋漓盡致,想要達到某個目的,但是由于自己知識的不足,沒有什么特別好的解決辦法,只能采用一些不優(yōu)雅的實現(xiàn)方式了。不過,辯證的看,這也是一件好事,至少它指明了下一步學習的方向。
到此這篇關(guān)于Java使用黑盒方式模擬實現(xiàn)內(nèi)網(wǎng)穿透的文章就介紹到這了,更多相關(guān)Java黑盒實現(xiàn)內(nèi)網(wǎng)穿透內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Mybatis-Plus-AutoGenerator 最詳細使用方法
這篇文章主要介紹了Mybatis-Plus-AutoGenerator 最詳細使用方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-03-03
Java CompletableFuture 異步超時實現(xiàn)深入研究
這篇文章主要為大家介紹了Java CompletableFuture 異步超時實現(xiàn)深入研究,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-02-02

