如何简单实现内网穿透(Java)
背景
有时我们想在公司远程自己电脑,或者本地部署的模型给外部访问,还有一些微信、QQ的回调测试,我们都是想要外部能够访问得到,这时候你可能需要内网穿透来实现,虽然内网穿透工具有很多了,但是有很多限制,配置的参数又多,为何尝试自己去实现一个。
内网穿透
内网穿透,顾名思义:内网一般是无法访问,直接穿透他,将应用的端口暴露到公网上,通过公网上允许外部访问。
实现流程图
例如,我们在本机上部署了一个应用,为了能让外部的用户能访问到,我们需要一个中介云服务器,本机的应用一启动就去会去云服务器的。当用户访问应用的时候,就会先访问云服务器,这时云服务器就会告诉个人电脑有用户来访问我了,赶紧给我请求连接。个人电脑收到命令,就会连接云服务器,之后在云服务器上会有两个连接:一个是用户访问的和另一个是个人电脑请求的。最后就让这个两个连接交换数据,完成了应用的访问。
准备环境
- 云服务器
- 编程语言(Java)
你想要外部访问肯定是要云服务器(公网服务器),当然你可以在本地测试好了,再部署到云服务器上。编程语言我这里就选择Java,当然你可以选择其他语言.Net、Python、Go,相对我来说比较熟悉Java,用.Net也行感觉会简单些。
代码
下面代码使用的是JDK23:AIO+虚拟线程。AIO是非阻塞、异步IO,编程来说相对简单些,但是占得内存会很多,主要是每个连接要创建buffer交换数据。如果使用NIO的话,你会发现一个线程管理多个连接,而且就只用一个buffer交换数据,内存占用会少很多,但是编程起来相对麻烦些。零拷贝技术,减少中间赚差价,本来系统读取数据是分配到系统空间的,Java程序要使用这部数据需要拷贝到自己的虚拟机上转换才能使用,那不是多拷贝了一次,直接使用系统空间的不就行了。
下面代码主要有客户端和服务端,代码还挺简单的。
客户端
public class Client {
public static void main(String[] args) throws Exception {
//虚拟线程池
var executor = Executors.newVirtualThreadPerTaskExecutor();
// 从系统属性获取值,若未设置则使用默认值
int appPort = Integer.parseInt(System.getProperty("appPort", "3389"));
int remotePort = Integer.parseInt(System.getProperty("remotePort", "8888"));
String remoteHost = System.getProperty("remoteHost", "192.168.1.195");
var client = AsynchronousSocketChannel.open();
Future connect = client.connect(new InetSocketAddress(remoteHost, remotePort));
//等待连接
connect.get();
System.out.printf("客户端连接成功:appPort:%d,remotePort:%d,remoteHost:%s%n",appPort,remotePort,remoteHost);
//写入100告诉服务端是用来交换数据的
var buffer = ByteBuffer.allocate(4).putInt(100).flip();
var l = client.write(buffer).get();
while (!Thread.interrupted()) {
buffer = ByteBuffer.allocate(1);
//读取服务端
var len = client.read(buffer).get();
if(len < 0 client.close else buffer.flip if buffer.get='= 66)' executor.submit -> {
try (
//连接远程
var remote = AsynchronousSocketChannel.open();
//连接本地
var local = AsynchronousSocketChannel.open();
) {
Future remoteConnect = remote.connect(new InetSocketAddress(remoteHost, remotePort));
Future localConnect = local.connect(new InetSocketAddress("localhost", appPort));
localConnect.get(3, TimeUnit.SECONDS);
remoteConnect.get(1, TimeUnit.SECONDS);
System.out.println(localConnect);
//告诉服务端是一个连接
var bf = ByteBuffer.allocate(4).putInt(200).flip();
remote.write(bf).get(3, TimeUnit.SECONDS);
//开始交换数据
Future> taskFuture1 = executor.submit(() -> {
transferData(remote, local);
});
Future> taskFuture2 = executor.submit(() -> {
transferData(local, remote);
});
taskFuture1.get();
taskFuture2.get();
} catch (Exception ex) {
ex.printStackTrace();
}
});
} else {
client.close();
break;
}
}
}
}
private static void transferData(AsynchronousSocketChannel srcChannel, AsynchronousSocketChannel dstChannel) {
int readLen;
do{
try {
ByteBuffer buffer = ByteBuffer.allocate(1024);
Future read1 = srcChannel.read(buffer);
readLen = read1.get();
if(readLen>0){
buffer.flip();
while (buffer.remaining()>0){
Future write1 = dstChannel.write(buffer);
write1.get();
}
}
} catch (Exception e) {
e.printStackTrace();
break;
}
}while (readLen>0);
}
}
服务端
public class Server {
public static void main(String[] args) throws Exception {
int port = Integer.parseInt(System.getProperty("port", "8888"));
//开启服务端
try(var serverChannel = AsynchronousServerSocketChannel.open()){
serverChannel.bind(new InetSocketAddress("0.0.0.0", port));
System.out.printf("服务端启动:%d%n",port);
//客户端连接,用来告知需要连接
AtomicReference client = new AtomicReference<>(null);
//客户端那边提供的连接队列
LinkedBlockingQueue channelQueue = new LinkedBlockingQueue<>();
//虚拟线程池
var executor = Executors.newVirtualThreadPerTaskExecutor();
while (!Thread.interrupted()){
try {
AsynchronousSocketChannel srcChannel = serverChannel.accept().get();
System.out.println("有连接:"+srcChannel);
// 提交到虚拟线程池
executor.submit(() -> {
try {
var buffer = ByteBuffer.allocate(1024);
var len = srcChannel.read(buffer).get();
//切换读状态
buffer.flip();
if(len < 0 srcchannel.close else iflen='= 4){' var code='buffer.getInt();' ifcode='= 100){' ifclient.get client.get.close client.setsrcchannel else if code='= 200){' channelqueue.offersrcchannel else srcchannel.close else ifclient.get='= null){' srcchannel.close else client.get.writebytebuffer.wrapnew byte66.get asynchronoussocketchannel asc='channelQueue.poll(5,' timeunit.seconds ifasc='null){' try ascsrcchannel while buffer.remaining>0){
asc.write(buffer).get();
}
System.out.println(srcChannel);
//开始交换数据
Future> taskFuture1 = executor.submit(() -> {
transferData(srcChannel, asc);
});
Future> taskFuture2 = executor.submit(() -> {
transferData(asc, srcChannel);
});
taskFuture1.get();
taskFuture2.get();
}catch (Exception e){
e.printStackTrace();
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
if(srcChannel!=null){
try {
srcChannel.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
private static void transferData(AsynchronousSocketChannel srcChannel, AsynchronousSocketChannel dstChannel) {
int readLen;
do{
try {
ByteBuffer buffer = ByteBuffer.allocate(1024);
Future read1 = srcChannel.read(buffer);
readLen = read1.get();
if(readLen>0){
buffer.flip();
while (buffer.remaining()>0){
Future write1 = dstChannel.write(buffer);
write1.get();
}
}
} catch (Exception e) {
e.printStackTrace();
break;
}
}while (readLen>0);
}
}