🚀 前言:周末加班的痛
周末在家,老板突然打来电话:“线上的测试环境出 Bug 了,赶紧连一下数据库查查数据。”
你打开电脑,准备连接公司的 MySQL,突然意识到一个尴尬的问题:公司的数据库在内网,没有公网 IP。
怎么办?
- 跑去公司?(太惨了)
- 用 TeamViewer 远程控制公司电脑?(卡顿,体验极差)
- 买花生壳/Ngrok?(要钱,限速,还不稳定)
作为一个 Java 程序员,求人不如求己。我们手里的Netty可是高性能网络通信的神器。
今天,我就带大家用 Netty 手搓一个**“私有版 Ngrok”**。只需一台几十块钱的云服务器,就能让你在家像在公司一样,丝滑访问内网的所有服务!
🧠 核心原理:外网是怎么“钻”进内网的?
一般来说,外网无法直接访问内网,因为有 NAT(网络地址转换)网关挡着。但是,内网是可以主动访问外网的。
内网穿透的核心逻辑就是:反向代理 (Reverse Proxy) + 长连接。
我们需要三个角色:
- Proxy Server (中转站):部署在有公网 IP 的云服务器上。
- Proxy Client (内网助手):运行在公司的电脑上,能访问公司内网服务(如 MySQL)。
- User (你):在家里的电脑。
数据流向图解:
一句话总结:
内网 Client 像一根管子,一头插在公司的数据库上,一头插在公网 Server 上。你在家访问 Server,Server 就通过这根管子把数据“偷”回来。
💻 实战开发:Netty 代码撸起来
我们将项目分为server和client两个模块。
1. Server 端开发 (公网中转)
Server 需要监听两个端口:
- 7000 端口:用于接收内网 Client 的注册和长连接。
- 8080 端口:暴露给 User 使用(也就是你在家访问的端口)。
核心 Handler 逻辑:
publicclassUserRequestHandlerextendsChannelInboundHandlerAdapter{privatefinalChannelclientChannel;// 指向内网 Client 的连接publicUserRequestHandler(ChannelclientChannel){this.clientChannel=clientChannel;}@OverridepublicvoidchannelRead(ChannelHandlerContextctx,Objectmsg){// 收到 User 发来的数据(比如 MySQL 的握手包)// 直接转发给内网 Clientif(clientChannel.isActive()){clientChannel.writeAndFlush(msg);}}}2. Client 端开发 (内网搬运工)
Client 启动后,主动连接 Server 的 7000 端口。
当 Server 发来数据时,Client 创建一个新的连接去连本地的 MySQL (3306),然后当个**“双向搬运工”**。
publicclassProxyClientHandlerextendsChannelInboundHandlerAdapter{@OverridepublicvoidchannelRead(ChannelHandlerContextctx,Objectmsg){// 收到 Server 转发来的 User 请求// 这里的 msg 就是 ByteBuf 数据// 建立连接去连本地 MySQLBootstrapb=newBootstrap();b.group(ctx.channel().eventLoop())// 复用 EventLoop.channel(NioSocketChannel.class).handler(newChannelInitializer<SocketChannel>(){@OverrideprotectedvoidinitChannel(SocketChannelch){ch.pipeline().addLast(newLocalProxyHandler(ctx.channel()));}});ChannellocalChannel=b.connect("localhost",3306).sync().channel();// 将数据发给 MySQLlocalChannel.writeAndFlush(msg);}}注意:这里的重点是流量透传。我们不需要解析协议(HTTP/MySQL),只需要把ByteBuf原封不动地从 A 搬到 B。
🛡️ 进阶优化:让轮子更稳
简单的转发很容易断开,我们需要加亿点点细节。
1. 心跳检测 (HeartBeat)
公网环境复杂,长连接很容易被防火墙切断。
利用 Netty 的IdleStateHandler,每隔 30 秒发送一个心跳包。
// Server 端管道添加pipeline.addLast(newIdleStateHandler(60,0,0));// Client 端管道添加pipeline.addLast(newIdleStateHandler(0,30,0));2. 断线重连
Client 端需要监听连接断开事件channelInactive。一旦断开,开启一个定时任务,每隔 5 秒尝试重新连接 Server。
@OverridepublicvoidchannelInactive(ChannelHandlerContextctx){System.out.println("❌ 与服务器断开,5秒后重连...");ctx.channel().eventLoop().schedule(()->{connectToServer();},5,TimeUnit.SECONDS);}🚀 效果演示
- 部署:把 Server 包丢到阿里云(IP: 1.2.3.4),启动。
- 启动:在公司电脑启动 Client,配置目标指向
localhost:3306。 - 连接:
回到家,打开 Navicat。- 主机:
1.2.3.4(Server IP) - 端口:
8080(暴露端口) - 用户名/密码:公司的数据库账号。
- 主机:
点击“测试连接” —— 连接成功!
速度取决于你的云服务器带宽。一般 5Mbps 的带宽足够跑满查询,体验吊打免费版的花生壳。
📝 总结
通过这个项目,我们不仅解决了一个实际痛点,更深入理解了:
- TCP 长连接与反向代理原理。
- Netty 的 ByteBuf 零拷贝特性(透传数据的核心)。
- 多路复用(一个端口处理 N 个连接)。
技术本该如此,既要有底层的深度,也要有解决生活的温度。
以后再也不用担心回家加班连不上库了!(虽然这听起来像个悲伤的故事…)