Java实现内网穿透

11 篇文章 0 订阅
2 篇文章 0 订阅

使用场景

1、当公司的一些系统功能使用了第三方服务时,通常第三方会回调我们的接口。在对接阶段,为了方便debug,我们肯定希望能回调到我们本地电脑上来。

2、当你在公司想访问部署在家里电脑的服务或者文件时。

3、当你的外地同事想访问你本地的服务,虽然发布到测试也可以,但是部署需要时间,发生错误排除也没有本地来的快,不够所见即所得。

虽然有现成的内网穿透工具,但没有比自己写的用起来更有成就感,哈哈哈哈~

而且自己写的,会遇到很多关于网络、线程的问题,网络的阻塞(代码跑着跑着就阻塞住了,也不知道为什么阻塞住了、阻塞在哪了,令人头大)、线程之间的配合(线程之间配合不好,导致数据在传输的时候缺少了,令人摸不着头脑),体会过一遍之后,更能加深自己对这方面知识了解,用这种有实际开发场景的学习方式,比直接背八股文效果更好,了解的更深(知其然,之所以然)。因为是体验、理解过这个知识点所带来的实际成果,能记忆的更深更久,不会像八股文背了又忘,忘了又背。

原理

其实很简单,外网服务器是不可能主动与内网服务器建立连接去通讯的,但是内网服务器可以,所以只要内网服务器告诉外网服务器“我”在哪里,那外网服务器就可以把请求的数据发送给内网服务器。

但是外网有这么多服务器,内网服务器要告诉哪个呢?总不能告诉全部服务器吧,不现实。所以,还是需要一台外网服务器跟内网服务器配合,内网服务器只告诉这个特定外网服务器“我”在哪,其他外网服务器或者内网服务器想要访问我的内网服务器,那就去访问特定外网服务器就可以了,特定外网服务器知道内网服务器在哪里,就可以转发相应的数据。

这个特定外网服务器就相等于代理服务器,转发外网和内网的数据,在它们中间建立了一座桥梁。

 

在云厂商买一台服务器,因为有外网ip,再用Java写一个数据转发的进程就能实现特定服务器的功能,不过本地也是可以测试的,只要用不同的端口,有点像端口的流量转发。

加入了Server-Proxy跟Client-Proxy之后,就能访问内网的服务了。

Client-Proxy的作用:Client-Proxy上线之后告诉Server-Proxy“我”在哪,接收到Server-Proxy的数据之后会转发给本地的服务器,同时也会把本地服务器的数据转发给Server-Proxy。

Server-Proxy的作用:监听两个端口,一个用作外部服务的端口,一个用作监听Client-Proxy上线和数据传输的端口,也是用于数据的传输和回写。

Server-Proxy跟Client-Proxy中间建立的通道,是Client-Proxy上线之后主动与Server-Proxy建立的TCP长连接(图中红色的线,即客户端告诉服务端“我”在哪)。

Java代码实现

第一版

首先先实现一个最简单能跑通的模型,使用BIO,然后只支持一个Client-Proxy、外网Server的连接,因为tcp长连接只有一条,同时连入多个Socket要处理线程安全问题。第一版,主要先把上面的原理实现,具体实用性、性能先不管。

代码

src/main/java/com/lkz/intranetpenetration/base · master · qq_41084438 / Intranet penetration · GitCode

代码结构

执行流程

OuterNetSocketServer

当收到外网的请求之后,把流交给IntranetSocketServer

IntranetSocketServer

把外网的数据通过长连接写给Client-Proxy,在类实例化的时候已经开启了一条新线程IntraToOutThread,把内网的数据写给外网,两条线程同时进行。

ClientProxy

同理,同时处理数据的读和写。

 效果

在本地8080端口启动了SpringBoot服务,通过访问Server-Proxy的7777端口,调用到了8080端口的服务接口。

Server-Proxy

Client-Proxy

Outer-Server

 Intra-Server

第二版

背景

这一版主要想解决可以连入多个OuterServer的问题,因为我们代理转发的是Tcp,所以理论上所有使用了Tcp传输的服务我们都可以代理。

然后我尝试了一下Navicat连到我的Server-Proxy,Client-Proxy连到Mysql,是可以的。但是第一版代码是按照Http调试的,有点不适用。因为Http服务都是一问一答,客户端先发request,服务端返回response。而mysql不是,连上mysql之后,mysql会先发信息询问账号密码,客户端再回答。而且因为一些编码的问题,读写socket数据的时候需要用inputstream和outstream,不要把流再进行包装,否则会因为这个问题导致包错误。

主要是改ClientProxy代码。

 完整代码:src/main/java/com/lkz/intranetpenetration/mysql · master · qq_41084438 / Intranet penetration · GitCode

虽然能连接上Mysql,也其次而已。因为如果要打开数据库的话,Navicat会再新建一个Socket与Server-Proxy进行连接,这时就无响应了。

所以为了解决这个问题,把Server-Proxy升级成可以连接多个OuterServer。

 把模型升级成Server-Proxy与Client-Proxy有多条长连接,Server-Proxy能接收多个OuterServer的socket。这四者之间的连接是一一对应的,这样做的原因是简单高效,如果中间的Server-Proxy与Client-Proxy通道复用的话,会很复杂,同一时间通道不能同时读写,就要处理多线程问题。

这个模型还多了一条命令通道,用来命令Client-Proxy主动与Server-Proxy建立连接。每来一个OuterServer就需要在Client-Proxy与Server-Proxy之间建立一条长连接,因为这条连接不能由Server-Proxy来主动创建,所以通过命令的方式来控制Client-Proxy。

代码

src/main/java/com/lkz/intranetpenetration/multitcp · master · qq_41084438 / Intranet penetration · GitCode

代码结构

代码改动也不太,Server-Proxy把外网数据传输给内网的时候,需要同步发一条创建Tcp给Client-Proxy,等Tcp创建完成之后再开始发数据。Client-Proxy增加命令响应的代码。

唯一的小难点是,Server-Proxy通过命令通道创建了一条新的Tcp连接,发送创建命令和新建Tcp连接是在两个线程上进行的,怎么对应起来,把这条新的Tcp连接分配给OuterServer呢?

我的做法是,Server-Proxy发送创建Tcp命令并阻塞等待Client-Proxy响应,Client-Proxy收到命令与Server-Proxy建立新的Tcp连接,连接建立完成,Server-Proxy往这条新连接发送一个id,Client-Proxy收到这个id再发送给等待Client-Proxy响应的那条线程。

IntranetSocketServer

CMDSocketServer

ClientProxy

 效果

用navicat连到Server-Proxy的端口。

Server-Proxy

Client-Proxy

未来改进

到这里,自己拿来用基本没什么问题了,不过优化的点还是有很多的,后续再继续优化。

1、复用Server-Proxy与Client-Proxy之间的连接,第二版中,这条连接需要频繁的创建和销毁,开销很大,如果能有类似Tcp池的东西去管理,就能减少很多开销。

2、上面第一点也可以这样优化,给外网的几个socket分配一条Tcp长连接,就不是一个socket对应一条长连接了,这样Tcp通道的利用率会更高,同时也更难实现,因为要处理多线程并发写同一条Tcp的问题,控制不好反而性能下降或者造成数据错乱。

3、为Client-Proxy实现一个界面(Java GUI)。

4、支持多个Client-Proxy连入,根据不同的规则,把流量转发到不同的Client-Proxy。

5、引入规则的配置,让Client-Proxy可以代理到多个本地服务端口。

6、实现心跳机制。

7、BIO网络模型改为NIO模型。

8、流量的监控、Tcp连接监控、流量的速度控制、动态修改配置(超时读、Tcp长连接数量)、手动下线客户端、客户端黑名单。

项目代码:文件 · master · qq_41084438 / Intranet penetration · GitCode

  • 8
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
好的,关于基于Java写一个工具类实现内网穿透的问题,我可以回答。 内网穿透是指通过一个公网服务器,将内网中的服务暴露到公网上,使得公网用户可以访问到内网中的服务。常见的应用场景有开发环境的远程调试、内网访问外部服务等。下面是一个基于Java实现内网穿透的简单工具类: ```java import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; public class PortForwarding { public static void main(String[] args) throws IOException { int localPort = Integer.parseInt(args[0]); // 本地端口 String remoteHost = args[1]; // 远程主机 int remotePort = Integer.parseInt(args[2]); // 远程端口 ServerSocket serverSocket = new ServerSocket(localPort); System.out.println("本地监听端口:" + localPort); while (true) { Socket localSocket = serverSocket.accept(); System.out.println("收到本地请求:" + localSocket); Socket remoteSocket = new Socket(remoteHost, remotePort); System.out.println("连接远程主机:" + remoteHost + ":" + remotePort); Thread t1 = new Thread(() -> { try { forward(localSocket.getInputStream(), remoteSocket.getOutputStream()); } catch (IOException e) { e.printStackTrace(); } }); Thread t2 = new Thread(() -> { try { forward(remoteSocket.getInputStream(), localSocket.getOutputStream()); } catch (IOException e) { e.printStackTrace(); } }); t1.start(); t2.start(); } } private static void forward(java.io.InputStream inputStream, java.io.OutputStream outputStream) throws IOException { byte[] buffer = new byte[1024]; int len; while ((len = inputStream.read(buffer)) > 0) { outputStream.write(buffer, 0, len); outputStream.flush(); } outputStream.close(); } } ``` 使用方法如下: 1. 在公网服务器上启动该程序,监听一个公网端口(如8000): ```bash java PortForwarding 8000 127.0.0.1 80 ``` 2. 在内网机器上,将需要穿透到公网的服务端口(如80)映射到公网服务器上的公网端口(如8000): ```bash ssh -NR 8000:localhost:80 user@public_server ``` 这样,就可以通过访问公网服务器的8000端口,访问到内网机器上的80端口了。 注意,以上仅仅是一个简单的内网穿透实现,实际应用中还需要考虑一些安全性问题,如身份认证、数据加密等。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值