使用Java實(shí)現(xiàn)簡(jiǎn)單搭建內(nèi)網(wǎng)穿透
思路
內(nèi)網(wǎng)穿透是一種網(wǎng)絡(luò)技術(shù),適用于需要遠(yuǎn)程訪(fǎng)問(wèn)本地部署服務(wù)的場(chǎng)景,比如你在家里搭建了一個(gè)網(wǎng)站或者想遠(yuǎn)程訪(fǎng)問(wèn)家里的電腦。由于本地部署的設(shè)備使用私有IP地址,無(wú)法直接被外部訪(fǎng)問(wèn),因此需要通過(guò)公網(wǎng)IP實(shí)現(xiàn)訪(fǎng)問(wèn)。通??梢酝ㄟ^(guò)購(gòu)買(mǎi)云服務(wù)器獲取一個(gè)公網(wǎng)IP來(lái)實(shí)現(xiàn)這一目的。
實(shí)際上,內(nèi)網(wǎng)穿透的原理是將位于公司或其他工作地點(diǎn)的私有IP數(shù)據(jù)發(fā)送到云服務(wù)器(公網(wǎng)IP),再?gòu)脑品?wù)器發(fā)送到家里的設(shè)備(私有IP)。從私有IP到公網(wǎng)IP的連接是相對(duì)簡(jiǎn)單的,但是從公網(wǎng)IP到私有IP就比較麻煩,因?yàn)楣W(wǎng)IP無(wú)法直接找到私有IP。
為了解決這個(gè)問(wèn)題,我們可以讓私有IP主動(dòng)連接公網(wǎng)IP。這樣,一旦私有IP連接到了公網(wǎng)IP,公網(wǎng)IP就知道了私有IP的存在,它們之間建立了連接關(guān)系。當(dāng)公網(wǎng)IP收到訪(fǎng)問(wèn)請(qǐng)求時(shí),就會(huì)通知私有IP有訪(fǎng)問(wèn)請(qǐng)求,并要求私有IP連接到公網(wǎng)IP。這樣一來(lái),公網(wǎng)IP就建立了兩個(gè)連接,一個(gè)是用于訪(fǎng)問(wèn)的連接,另一個(gè)是與私有IP之間的連接。最后,通過(guò)這兩個(gè)連接之間的數(shù)據(jù)交換,實(shí)現(xiàn)了遠(yuǎn)程訪(fǎng)問(wèn)本地部署服務(wù)的目的。
代碼操作
打開(kāi)IDEA創(chuàng)建一個(gè)mave項(xiàng)目,刪除掉src,創(chuàng)建兩個(gè)模塊client和service,一個(gè)是在本地的運(yùn)行,一個(gè)是在云服務(wù)器上運(yùn)行的,這邊socket(tcp)連接,我使用的是AIO,AIO的函數(shù)回調(diào)看起來(lái)好復(fù)雜。
先編寫(xiě)service服務(wù)端,創(chuàng)建兩個(gè)ServerSocket服務(wù),一個(gè)是監(jiān)聽(tīng)16000的,用來(lái)外來(lái)連接的,另一是監(jiān)聽(tīng)16088是用來(lái)client訪(fǎng)問(wèn)的,也就是給service和client之間交互用的。先講一個(gè)extListener他是監(jiān)聽(tīng)16000,當(dāng)有外部請(qǐng)求來(lái)時(shí),也就是在公司訪(fǎng)問(wèn)時(shí),先判斷registerChannel是不是有client和service,沒(méi)有就關(guān)閉連接。有的話(huà)就下發(fā)指令告訴client有訪(fǎng)問(wèn)了趕快給我連接,連接會(huì)存在channelQueue隊(duì)列里,拿到連接后,兩個(gè)連接交換數(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) {
// 接受連接,準(zhǔn)備接收下一個(gè)連接
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) {
// 接受連接,準(zhǔn)備接收下一個(gè)連接
extListener.accept(null, this);
try {
//判斷是否有注冊(cè)連接
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方法是怎么存進(jìn)channelQueue里的,很簡(jiǎn)單client發(fā)送0,就認(rèn)為他是注冊(cè)的連接,也就交互的連接直接覆蓋registerChannel,發(fā)送1的話(huà)就是用來(lái)交換數(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();
}
});
}
再編寫(xiě)client客戶(hù)端,dstHost和dstPort是用來(lái)連接service的ip和端口,看起來(lái)好長(zhǎng),實(shí)際上就是client連接service,第一個(gè)連接成功后向service發(fā)送了個(gè)0告訴他是注冊(cè)的連接,用來(lái)交換數(shù)據(jù)。當(dāng)這個(gè)連接收到service發(fā)送的1時(shí),就會(huì)創(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)用讀取操作,實(shí)現(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,這個(gè)會(huì)走 service的clintHandle方法,成功的話(huà)就會(huì)讓兩個(gè)連接交換數(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ù)方法,看起好麻煩,效果就類(lèi)似IOUtils.copy(InputStream,OutputStream),一個(gè)流寫(xiě)入另一個(gè)流。
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)閉的情況或者沒(méi)有數(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()) {
// 如果未完全寫(xiě)入,則繼續(xù)寫(xiě)入
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();
}
}
}
測(cè)試
我這邊就用虛擬機(jī)來(lái)測(cè)試,用云服務(wù)器就比較麻煩,得登錄賬號(hào),增加開(kāi)放端口規(guī)則,上傳代碼。我這邊用Hyper-V快速創(chuàng)建了虛擬機(jī),創(chuàng)建一個(gè)windows 10 MSIX系統(tǒng),安裝JDK8,下載地址 。怎樣把本地編譯好的class放到虛擬機(jī)呢,虛擬機(jī)是可以訪(fǎng)問(wèn)主機(jī)ip的,我們可以弄一個(gè)web的文件目錄下載給虛擬機(jī)訪(fǎng)問(wèn),人生苦短我用pyhton,下面python簡(jiǎn)單代碼
if __name__ == '__main__':
# 定義服務(wù)器的端口
PORT = 8000
# 創(chuàng)建請(qǐ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ù)啟動(dòng)在端口 {PORT}")
httpd.serve_forever()
到class的目錄下運(yùn)行cmd,執(zhí)行java -cp . org.example.Main,windows 默認(rèn)遠(yuǎn)程端口3389。
最后效果

以上就是使用Java實(shí)現(xiàn)簡(jiǎn)單搭建內(nèi)網(wǎng)穿透的詳細(xì)內(nèi)容,更多關(guān)于Java內(nèi)網(wǎng)穿透的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
mybatis?@InsertProvider報(bào)錯(cuò)問(wèn)題及解決
這篇文章主要介紹了mybatis?@InsertProvider報(bào)錯(cuò)的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-07-07
java 出現(xiàn)問(wèn)題javax.servlet.http.HttpServlet was not found解決方法
這篇文章主要介紹了java 出現(xiàn)問(wèn)題javax.servlet.http.HttpServlet was not found解決方法的相關(guān)資料,需要的朋友可以參考下2016-11-11
深入分析java并發(fā)編程中volatile的實(shí)現(xiàn)原理
這篇文章主要介紹了深入分析java并發(fā)編程中Volatile的實(shí)現(xiàn)原理,涉及Volatile的官方定義,實(shí)現(xiàn)原理,使用優(yōu)化等相關(guān)內(nèi)容,具有一定參考價(jià)值,需要的朋友可以了解下。2017-11-11
Spring MVC的項(xiàng)目準(zhǔn)備和連接建立方法
SpringWebMVC是基于Servlet API的Web框架,屬于Spring框架的一部分,主要用于簡(jiǎn)化Web應(yīng)用程序的開(kāi)發(fā),SpringMVC通過(guò)控制器接收請(qǐng)求,使用模型處理數(shù)據(jù),并通過(guò)視圖展示結(jié)果,感興趣的朋友跟隨小編一起看看吧2024-10-10

