likes
comments
collection
share

使用java自己简单搭建内网穿透

作者站长头像
站长
· 阅读数 2

思路

内网穿透是一种网络技术,适用于需要远程访问本地部署服务的场景,比如你在家里搭建了一个网站或者想远程访问家里的电脑。由于本地部署的设备使用私有IP地址,无法直接被外部访问,因此需要通过公网IP实现访问。通常可以通过购买云服务器获取一个公网IP来实现这一目的。

实际上,内网穿透的原理是将位于公司或其他工作地点的私有IP数据发送到云服务器(公网IP),再从云服务器发送到家里的设备(私有IP)。从私有IP到公网IP的连接是相对简单的,但是从公网IP到私有IP就比较麻烦,因为公网IP无法直接找到私有IP。

为了解决这个问题,我们可以让私有IP主动连接公网IP。这样,一旦私有IP连接到了公网IP,公网IP就知道了私有IP的存在,它们之间建立了连接关系。当公网IP收到访问请求时,就会通知私有IP有访问请求,并要求私有IP连接到公网IP。这样一来,公网IP就建立了两个连接,一个是用于访问的连接,另一个是与私有IP之间的连接。最后,通过这两个连接之间的数据交换,实现了远程访问本地部署服务的目的。

代码操作

打开IDEA创建一个mave项目,删除掉src,创建两个模块clientservice,一个是在本地的运行,一个是在云服务器上运行的,这边socket(tcp)连接,我使用的是AIO,AIO的函数回调看起来好复杂。

先编写service服务端,创建两个ServerSocket服务,一个是监听16000的,用来外来连接的,另一是监听16088是用来client访问的,也就是给serviceclient之间交互用的。先讲一个extListener他是监听16000,当有外部请求来时,也就是在公司访问时,先判断registerChannel是不是有clientservice,没有就关闭连接。有的话就下发指令告诉client有访问了赶快给我连接,连接会存在channelQueue队列里,拿到连接后,两个连接交换数据就行。

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;
                }
                //下发指令告诉需要连接
                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;
                }

                //交换数据
                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发送0,就认为他是注册的连接,也就交互的连接直接覆盖registerChannel,发送1的话就是用来交换数据的,扔到channelQueue,发送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客户端,dstHostdstPort是用来连接service的ip和端口,看起来好长,实际上就是client连接service,第一个连接成功后向service发送了个0告诉他是注册的连接,用来交换数据。当这个连接收到service发送的1时,就会创建新的连接去连接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) {

                    //读取数据
                    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) {
                                //发起新的连
                                try {
                                    createNewClient();
                                } catch (IOException e) {
                                    throw new RuntimeException(e);
                                }
                            }
                            buffer.clear();
                            // 这里再次调用读取操作,实现循环读取
                            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方法,尝试连接本地服务,如果失败就发送2,成功就发送1,这个会走 serviceclintHandle方法,成功的话就会让两个连接交换数据。

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) {

            //尝试连接本地服务
            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();
                            //交换数据
                            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交换数据方法,看起好麻烦,效果就类似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) {
                    // 处理连接关闭的情况或者没有数据可读的情况

                    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()) {
                            // 如果未完全写入,则继续写入
                            ch2.write(buffer, writeAtt, this);

                        } else {
                            writeAtt.complete(1);
                            // 清空buffer并继续读取
                            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();
        }
    }
}

测试

我这边就用虚拟机来测试,用云服务器就比较麻烦,得登录账号,增加开放端口规则,上传代码。我这边用Hyper-V快速创建了虚拟机,创建一个windows 10 MSIX系统,安装JDK8,下载地址:www.azul.com/downloads/?… 。怎样把本地编译好的class放到虚拟机呢,虚拟机是可以访问主机ip的,我们可以弄一个web的文件目录下载给虚拟机访问,人生苦短我用pyhton,下面python简单代码

if __name__ == '__main__':
    # 定义服务器的端口
    PORT = 8000

    # 创建请求处理程序
    Handler = http.server.SimpleHTTPRequestHandler

    # 设置工作目录
    os.chdir("C:\netTunnlDemo\client\target")

    # 创建服务器
    with socketserver.TCPServer(("", PORT), Handler) as httpd:
        print(f"服务启动在端口 {PORT}")
        httpd.serve_forever()

到class的目录下运行cmd,执行java -cp . org.example.Main,windows 默认远程端口3389。

最后效果

使用java自己简单搭建内网穿透

总结

使用AIO导致代码长,逻辑并不复杂,完整代码,供个人学习:断续/netTunnlDemo (gitee.com)