使用Java實現(xiàn)簡單搭建內(nèi)網(wǎng)穿透
思路
內(nèi)網(wǎng)穿透是一種網(wǎng)絡(luò)技術(shù),適用于需要遠程訪問本地部署服務(wù)的場景,比如你在家里搭建了一個網(wǎng)站或者想遠程訪問家里的電腦。由于本地部署的設(shè)備使用私有IP地址,無法直接被外部訪問,因此需要通過公網(wǎng)IP實現(xiàn)訪問。通??梢酝ㄟ^購買云服務(wù)器獲取一個公網(wǎng)IP來實現(xiàn)這一目的。
實際上,內(nèi)網(wǎng)穿透的原理是將位于公司或其他工作地點的私有IP數(shù)據(jù)發(fā)送到云服務(wù)器(公網(wǎng)IP),再從云服務(wù)器發(fā)送到家里的設(shè)備(私有IP)。從私有IP到公網(wǎng)IP的連接是相對簡單的,但是從公網(wǎng)IP到私有IP就比較麻煩,因為公網(wǎng)IP無法直接找到私有IP。
為了解決這個問題,我們可以讓私有IP主動連接公網(wǎng)IP。這樣,一旦私有IP連接到了公網(wǎng)IP,公網(wǎng)IP就知道了私有IP的存在,它們之間建立了連接關(guān)系。當公網(wǎng)IP收到訪問請求時,就會通知私有IP有訪問請求,并要求私有IP連接到公網(wǎng)IP。這樣一來,公網(wǎng)IP就建立了兩個連接,一個是用于訪問的連接,另一個是與私有IP之間的連接。最后,通過這兩個連接之間的數(shù)據(jù)交換,實現(xiàn)了遠程訪問本地部署服務(wù)的目的。
代碼操作
打開IDEA
創(chuàng)建一個mave
項目,刪除掉src
,創(chuàng)建兩個模塊client
和service
,一個是在本地的運行,一個是在云服務(wù)器上運行的,這邊socket(tcp)連接,我使用的是AIO,AIO的函數(shù)回調(diào)看起來好復(fù)雜。
先編寫service
服務(wù)端,創(chuàng)建兩個ServerSocket服務(wù),一個是監(jiān)聽16000的,用來外來連接的,另一是監(jiān)聽16088是用來client
訪問的,也就是給service
和client
之間交互用的。先講一個extListener
他是監(jiān)聽16000,當有外部請求來時,也就是在公司訪問時,先判斷registerChannel
是不是有client
和service
,沒有就關(guān)閉連接。有的話就下發(fā)指令告訴client
有訪問了趕快給我連接,連接會存在channelQueue
隊列里,拿到連接后,兩個連接交換數(shù)據(jù)就行。
private static final int extPort = 16000; private static final int clintPort = 16088; private static AsynchronousSocketChannel registerChannel; static BlockingQueue<AsynchronousSocketChannel> channelQueue = new LinkedBlockingQueue<>(); public static void main(String[] args) throws IOException { final AsynchronousServerSocketChannel listener = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress("192.168.1.10", clintPort)); listener.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() { public void completed(AsynchronousSocketChannel ch, Void att) { // 接受連接,準備接收下一個連接 listener.accept(null, this); // 處理連接 clintHandle(ch); } public void failed(Throwable exc, Void att) { exc.printStackTrace(); } }); final AsynchronousServerSocketChannel extListener = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress("localhost", extPort)); extListener.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() { private Future<Integer> writeFuture; public void completed(AsynchronousSocketChannel ch, Void att) { // 接受連接,準備接收下一個連接 extListener.accept(null, this); try { //判斷是否有注冊連接 if(registerChannel==null || !registerChannel.isOpen()){ try { ch.close(); } catch (IOException e) { e.printStackTrace(); } return; } //下發(fā)指令告訴需要連接 ByteBuffer bf = ByteBuffer.wrap(new byte[]{1}); if(writeFuture != null){ writeFuture.get(); } writeFuture = registerChannel.write(bf); AsynchronousSocketChannel take = channelQueue.take(); //clint連接失敗的 if(take == null){ ch.close(); return; } //交換數(shù)據(jù) exchangeDataHandle(ch,take); } catch (Exception e) { e.printStackTrace(); } } public void failed(Throwable exc, Void att) { exc.printStackTrace(); } }); Scanner in = new Scanner(System.in); in.nextLine(); }
看看clintHandle
方法是怎么存進channelQueue
里的,很簡單client
發(fā)送0,就認為他是注冊的連接,也就交互的連接直接覆蓋registerChannel
,發(fā)送1的話就是用來交換數(shù)據(jù)的,扔到channelQueue
,發(fā)送2就異常的連接。
private static void clintHandle(AsynchronousSocketChannel ch) { final ByteBuffer buffer = ByteBuffer.allocate(1); ch.read(buffer, null, new CompletionHandler<Integer, Void>() { public void completed(Integer result, Void attachment) { buffer.flip(); byte b = buffer.get(); if (b == 0) { registerChannel = ch; } else if(b == 1){ channelQueue.offer(ch); }else{ //clint連接不到 channelQueue.add(null); } } public void failed(Throwable exc, Void attachment) { exc.printStackTrace(); } }); }
再編寫client
客戶端,dstHost
和dstPort
是用來連接service
的ip和端口,看起來好長,實際上就是client
連接service
,第一個連接成功后向service
發(fā)送了個0告訴他是注冊的連接,用來交換數(shù)據(jù)。當這個連接收到service
發(fā)送的1時,就會創(chuàng)建新的連接去連接service
。
private static final String dstHost = "192.168.1.10"; private static final int dstPort = 16088; private static final String srcHost = "localhost"; private static final int srcPort = 3389; public static void main(String[] args) throws IOException { System.out.println("dst:"+dstHost+":"+dstPort); System.out.println("src:"+srcHost+":"+srcPort); //使用aio final AsynchronousSocketChannel client = AsynchronousSocketChannel.open(); client.connect(new InetSocketAddress(dstHost, dstPort), null, new CompletionHandler<Void, Void>() { public void completed(Void result, Void attachment) { //連接成功 byte[] bt = new byte[]{0}; final ByteBuffer buffer = ByteBuffer.wrap(bt); client.write(buffer, null, new CompletionHandler<Integer, Void>() { public void completed(Integer result, Void attachment) { //讀取數(shù)據(jù) final ByteBuffer buffer = ByteBuffer.allocate(1); client.read(buffer, null, new CompletionHandler<Integer, Void>() { public void completed(Integer result, Void attachment) { buffer.flip(); if (buffer.get() == 1) { //發(fā)起新的連 try { createNewClient(); } catch (IOException e) { throw new RuntimeException(e); } } buffer.clear(); // 這里再次調(diào)用讀取操作,實現(xiàn)循環(huán)讀取 client.read(buffer, null, this); } public void failed(Throwable exc, Void attachment) { exc.printStackTrace(); } }); } public void failed(Throwable exc, Void attachment) { exc.printStackTrace(); } }); } public void failed(Throwable exc, Void attachment) { exc.printStackTrace(); } }); Scanner in = new Scanner(System.in); in.nextLine(); }
createNewClient
方法,嘗試連接本地服務(wù),如果失敗就發(fā)送2,成功就發(fā)送1,這個會走 service
的clintHandle
方法,成功的話就會讓兩個連接交換數(shù)據(jù)。
private static void createNewClient() throws IOException { final AsynchronousSocketChannel dstClient = AsynchronousSocketChannel.open(); dstClient.connect(new InetSocketAddress(dstHost, dstPort), null, new CompletionHandler<Void, Void>() { public void completed(Void result, Void attachment) { //嘗試連接本地服務(wù) final AsynchronousSocketChannel srcClient; try { srcClient = AsynchronousSocketChannel.open(); srcClient.connect(new InetSocketAddress(srcHost, srcPort), null, new CompletionHandler<Void, Void>() { public void completed(Void result, Void attachment) { byte[] bt = new byte[]{1}; final ByteBuffer buffer = ByteBuffer.wrap(bt); Future<Integer> write = dstClient.write(buffer); try { write.get(); //交換數(shù)據(jù) exchangeData(srcClient, dstClient); exchangeData(dstClient, srcClient); } catch (Exception e) { closeChannels(srcClient, dstClient); } } public void failed(Throwable exc, Void attachment) { exc.printStackTrace(); //失敗 byte[] bt = new byte[]{2}; final ByteBuffer buffer = ByteBuffer.wrap(bt); dstClient.write(buffer); } }); } catch (IOException e) { e.printStackTrace(); //失敗 byte[] bt = new byte[]{2}; final ByteBuffer buffer = ByteBuffer.wrap(bt); dstClient.write(buffer); } } public void failed(Throwable exc, Void attachment) { exc.printStackTrace(); } }); }
下面是exchangeData
交換數(shù)據(jù)方法,看起好麻煩,效果就類似IOUtils.copy(InputStream,OutputStream)
,一個流寫入另一個流。
private static void exchangeData(AsynchronousSocketChannel ch1, AsynchronousSocketChannel ch2) { try { final ByteBuffer buffer = ByteBuffer.allocate(1024); ch1.read(buffer, null, new CompletionHandler<Integer, CompletableFuture<Integer>>() { public void completed(Integer result, CompletableFuture<Integer> readAtt) { CompletableFuture<Integer> future = new CompletableFuture<>(); if (result == -1 || buffer.position() == 0) { // 處理連接關(guān)閉的情況或者沒有數(shù)據(jù)可讀的情況 try { readAtt.get(3,TimeUnit.SECONDS); } catch (Exception e) { e.printStackTrace(); } closeChannels(ch1, ch2); return; } buffer.flip(); CompletionHandler readHandler = this; ch2.write(buffer, future, new CompletionHandler<Integer, CompletableFuture<Integer>>() { @Override public void completed(Integer result, CompletableFuture<Integer> writeAtt) { if (buffer.hasRemaining()) { // 如果未完全寫入,則繼續(xù)寫入 ch2.write(buffer, writeAtt, this); } else { writeAtt.complete(1); // 清空buffer并繼續(xù)讀取 buffer.clear(); if(ch1.isOpen()){ ch1.read(buffer, writeAtt, readHandler); } } } @Override public void failed(Throwable exc, CompletableFuture<Integer> attachment) { if(!(exc instanceof AsynchronousCloseException)){ exc.printStackTrace(); } closeChannels(ch1, ch2); } }); } public void failed(Throwable exc, CompletableFuture<Integer> attachment) { if(!(exc instanceof AsynchronousCloseException)){ exc.printStackTrace(); } closeChannels(ch1, ch2); } }); } catch (Exception ex) { ex.printStackTrace(); closeChannels(ch1, ch2); } } private static void closeChannels(AsynchronousSocketChannel ch1, AsynchronousSocketChannel ch2) { if (ch1 != null && ch1.isOpen()) { try { ch1.close(); } catch (IOException e) { e.printStackTrace(); } } if (ch2 != null && ch2.isOpen()) { try { ch2.close(); } catch (IOException e) { e.printStackTrace(); } } }
測試
我這邊就用虛擬機來測試,用云服務(wù)器就比較麻煩,得登錄賬號,增加開放端口規(guī)則,上傳代碼。我這邊用Hyper-V快速創(chuàng)建了虛擬機,創(chuàng)建一個windows 10 MSIX系統(tǒng),安裝JDK8,下載地址 。怎樣把本地編譯好的class放到虛擬機呢,虛擬機是可以訪問主機ip的,我們可以弄一個web的文件目錄下載給虛擬機訪問,人生苦短我用pyhton,下面python簡單代碼
if __name__ == '__main__': # 定義服務(wù)器的端口 PORT = 8000 # 創(chuàng)建請求處理程序 Handler = http.server.SimpleHTTPRequestHandler # 設(shè)置工作目錄 os.chdir("C:\netTunnlDemo\client\target") # 創(chuàng)建服務(wù)器 with socketserver.TCPServer(("", PORT), Handler) as httpd: print(f"服務(wù)啟動在端口 {PORT}") httpd.serve_forever()
到class的目錄下運行cmd,執(zhí)行java -cp . org.example.Main
,windows 默認遠程端口3389。
最后效果
以上就是使用Java實現(xiàn)簡單搭建內(nèi)網(wǎng)穿透的詳細內(nèi)容,更多關(guān)于Java內(nèi)網(wǎng)穿透的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
mybatis?@InsertProvider報錯問題及解決
這篇文章主要介紹了mybatis?@InsertProvider報錯的解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-07-07java 出現(xiàn)問題javax.servlet.http.HttpServlet was not found解決方法
這篇文章主要介紹了java 出現(xiàn)問題javax.servlet.http.HttpServlet was not found解決方法的相關(guān)資料,需要的朋友可以參考下2016-11-11深入分析java并發(fā)編程中volatile的實現(xiàn)原理
這篇文章主要介紹了深入分析java并發(fā)編程中Volatile的實現(xiàn)原理,涉及Volatile的官方定義,實現(xiàn)原理,使用優(yōu)化等相關(guān)內(nèi)容,具有一定參考價值,需要的朋友可以了解下。2017-11-11- 下面小編就為大家?guī)硪黄狫ava創(chuàng)建數(shù)組的幾種方式總結(jié)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧,希望能給大家?guī)韼椭?/div> 2021-06-06
最新評論