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

思路

内网穿透是一种网络技术,适用于需要远程访问本地部署服务的场景,比如你在家里搭建了一个网站或者想远程访问家里的电脑。由于本地部署的设备使用私有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队列里,拿到连接后,两个连接交换数据就行。

java 复制代码
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就异常的连接。

csharp 复制代码
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

java 复制代码
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方法,成功的话就会让两个连接交换数据。

java 复制代码
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),一个流写入另一个流。

scss 复制代码
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简单代码

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。

最后效果

总结

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

相关推荐
loveLifeLoveCoding10 分钟前
Java List sort() 排序
java·开发语言
草履虫·17 分钟前
【Java集合】LinkedList
java
AngeliaXue19 分钟前
Java集合(List篇)
java·开发语言·list·集合
世俗ˊ20 分钟前
Java中ArrayList和LinkedList的比较
java·开发语言
zhouyiddd24 分钟前
Maven Helper 插件
java·maven·intellij idea
攸攸太上33 分钟前
Docker学习
java·网络·学习·docker·容器
Milo_K40 分钟前
项目文件配置
java·开发语言
程序员大金44 分钟前
基于SpringBoot+Vue+MySQL的养老院管理系统
java·vue.js·spring boot·vscode·后端·mysql·vim
customer081 小时前
【开源免费】基于SpringBoot+Vue.JS网上购物商城(JAVA毕业设计)
java·vue.js·spring boot·后端·开源
nsa652231 小时前
Knife4j 一款基于Swagger的开源文档管理工具
java