從黑盒的角度理解: 通常個人電腦無論是連接WIFI上網還是用網線上網,都是屬於區域網路裡邊的,外網路無法直接存取你的電腦,內部網路穿透可以讓你的區域網路中的電腦實現外網存取功能。 舉一個例子: 你在本地運行了一個Web服務,佔用連接埠是8080,那麼你本地進行測試就是://localhost:8080。但是如果你想給一個好朋友分享你的服務,那該怎麼辦呢?是的,就是採用內網穿透的方式。實際上,內網穿透是很複雜的一個操作,百度百科上面的解釋為:
內網穿透,也即NAT 穿透,進行NAT 穿透是為了使具有某一個特定來源IP 位址和來源連接埠號碼的封包不被NAT 設備屏蔽而正確路由到內部網路主機。
這裡,我顯然是做不了的。我需要的只是從外網訪問到內網的服務,至於具體的過程我不關心,只需要達到這個目的即可了。
#無論是哪種方式實現內網穿透都是需要一個公網IP位址的,我在這裡使用的一台阿里雲的伺服器。以下是整個模擬的示意圖:
#註:
#1.內網穿透服務端部署在具有公網IP的機器上。
2.內網服務和內網穿透客戶端部署在內網機器上。
說明:
我的想法很簡單,即使用者存取內網穿透伺服器,然後內網穿透伺服器將使用者的請求封包轉送給內網穿透客戶端,接著內網穿透客戶端將請求封包轉送給內網服務,然後接收內網服務的回應封包,轉送給內網穿透服務端,最後由內網穿透服務端轉送給使用者。 大致流程是這樣的,對於外部的使用者來說它只會認為它存取了一個外網服務,因為使用者面對的是一個黑盒子系統。
為了實現上面那個目標,其中最為關鍵的就是維持內網穿透客戶端和內網穿透服務端的一個長連接,我需要使用這個長連線來交換雙方的訊息資訊。因此,這個長連接需要在系統啟動後就建立好,當有用戶的請求進來的時候,內網穿透服務端首先接收這個請求,然後使用長連接將其轉給內網穿透客戶端,內網路穿透用戶端使用該封包作為請求存取內網服務,然後接收內網服務的回應,將其轉送給內網穿透服務端,最後將其轉送給使用者。
#說明: 這個是內部網路穿透的服務端和用戶端程式碼,我是放在一起了,沒有分開寫,因為雙方需要使用到一些公用的類別。但建議還是分開成兩個工程,因為需要分開部署。 或匯出成jar包的時候,分別選擇不同的主類別即可。
客戶端程式碼檔案:Client.java、Connection.java、Msg.java、ProxyConnection.java。
服務端程式碼檔案:Server.java、Connection.java、Msg.java、ProxyConnection.java。
package org.dragon; import java.io.IOException; import java.net.Socket; import java.net.UnknownHostException; /** * 用于双向通信的客户端 * */ public class Client { private static final String REMOTE_HOST = "公网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); // 维持和内网穿透服务端的长连接 // 可以实现同一个人多次访问 while (true) { Msg msg = proxyConnection.receiveMsg(); Connection connection = new Connection(new Socket(LOCAL_HOST, 8080)); connection.sendMsg(msg); // 将请求报文发送给内网服务器,即模拟发送请求报文 msg = connection.receiveMsg(); // 接收内网服务器的响应报文 proxyConnection.sendMsg(msg); // 将内网服务器的响应报文转发给公网服务器(内网穿透服务端) } } catch (UnknownHostException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }
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(); // 每一次写入都要刷新,防止阻塞。 } }
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); } }
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("异常接收数据,长度为:" + len); } byte[] msg = new byte[len]; int size = input.read(msg); // 这里到底会不会读取到这么多,我也有点迷惑! return new Msg(size, msg); // 为了防止出错,还是使用一个记录实际读取值size } /** * 转发报文 * @throws IOException * */ public void sendMsg(Msg msg) throws IOException { output.writeInt(msg.getLen()); output.write(msg.getMsg(), 0, msg.getLen()); output.flush(); // 每一次写入都需要手动刷新,防止阻塞。 } }
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); // 转发用户的请求报文给内网服务器 msg = proxySocket.receiveMsg(); // 接收内网服务器的响应报文 connection.sendMsg(msg); // 转发内网服务器的响应报文给用户 } } catch (IOException e) { e.printStackTrace(); } } }
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; } }
#
先後啟動內網穿透服務端與內網穿透客戶端,然後在瀏覽器存取三條URL即可。注意: 1.如果你自己測試,切換成你運行內部網路穿透伺服器的ip位址或使用網域名稱也行。 2.我這裡外網機器和內網機器使用的是不同的端口(隨便使用,只要不和自己機器上的服務端口衝突就行了),實際上可以在外網使用80端口,這樣對普通用戶比較友好。 3.第三條測試其實是失敗的,可以看到上面那個載入動畫,一直在載入。照理說這個應該很快就停止了,但是似乎無法停下來。 這是系統的bug了,但是由於我掌握的知識有限,就不去解決了。
這裡的程式碼是一種模擬,它只能模擬這個功能,但基本上不具備實際的作用,哈哈。因為我這裡只有一個長連接,所以只能支援串行的通信,最好就是一個人簡單的調用,似乎調用速度也不能太快了。 我想了一種方式,在客戶端和伺服器之間維持一個連線池,這樣就可以實現多執行緒存取了。 這裡沒有處理TCP的黏包和分包(我理解了這個概念,但我不太會處理它),所以我預設請求報文和回應封包都是2KB以內大小。如果超過這個長度會導致問題,儘管可以調大這個參數,但是如果多數報文的都是很小的話,也會導致效率低下。這個內網穿透是可以支持TCP之上的各種協定的,不一定是HTTP,至少理論上是可以的。
以上是如何使用Java模擬實現內網穿透黑盒?的詳細內容。更多資訊請關注PHP中文網其他相關文章!