接上一篇文章内网穿透服务设计挖的坑,本篇来聊一下内网穿透的实现。

为了方便理解,我们先统一定义使用到的名词:

  1. UserClient:用户客户端,真实的请求发起方;
  2. UserServer:内网穿透-用户服务端,接收用户客户端发起的请求;并将请求转发给代理服务端;
  3. ProxyServer:内网穿透-代理服务端,与代理客户端保持一个连接通道用于传输数据;
  4. ProxyClient:内网穿透-代理客户端,从通道中接收来自代理服务端的请求数据,并且发起真正的请求。拿到请求结果后再通过该通道写回到代理服务端;
  5. TargetServer:目标服务器目标服务器,即被代理的服务器;
  6. UserChannel:用户客户端 -> 内网穿透服务端,用户连接通道;
  7. QuantumTunnel:内网穿透服务端 -> 内网穿透客户端,量子通道;
  8. ProxyChannel:内网穿透客户端 -> 目标服务器,代理通道。

需要关注一下最后的UserChannel、QuantumChannel和ProxyChannel这3个通道,内网穿透的本质就是数据流量在这三个网络连接通道中流转。

流程图

进行开发之前,我们再梳理一下内网穿透的流程。

在上篇文章的基础上,对流程图进行了更详细的补充。这个流程图非常重要所有代码都是围绕这个流程图进行实现的。对全局有了掌控,代码实现的时候才心中有数。

具体实现

内网穿透的前提条件是网络之间建立一个网络传输通道,我称之为QuantumTunnel,进行网络打通。我们来看看这部分是怎么实现的。

为了方便理解代理,这里对Netty开发流程简单说明一下。

  1. Netty开发编程中,Channel是一个很核心的概念,代表的是一个网络连接通道,负责数据传输;
  2. Netty接收到对端传输过来的数据后,交由Handler来执行具体的业务流程,也就是说我们的业务逻辑几乎都在Handler里面;
  3. 实际开发过程中会有很多Handler了,Pipeline则负责将Handler组织起来,就一个流水线,前一个Handler执行完成后交给后面的Handler继续执行。

如果小伙伴对Netty开发不太熟悉可以了解相关教程资料,本文不展开讨论。

管理QuantumTunnel连接

ProxyServerHandler

QuantumTunnel由ProxyServer和ProxyClient维护,这是ProxyServerHandler的代码:

public class ProxyServerHandler extends QuantumCommonHandler {

    @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
QuantumMessage message = (QuantumMessage) msg;
if (message.getMessageType() == QuantumMessageType.REGISTER) {
processRegister(ctx, message);
} else if (message.getMessageType() == QuantumMessageType.PROXY_DISCONNECTED) {
processProxyDisconnected(message);
} else if (message.getMessageType() == QuantumMessageType.DATA) {
processData(message);
} else {
ctx.channel().close();
throw new RuntimeException("Unknown MessageType: " + message.getMessageType());
}
}
}

代码中对ProxyClient过来的数据进行了类型判断并进行处理,总共有三种事件类型:

  1. 注册事件:接收ProxyClient的注册请求,打开QuantumTunnel
  2. 数据传输事件:接收ProxyClient返回的数据,并发送给UserChannel
  3. ProxyChannel断开事件:ProxyChannel断开后需要同步断开UserChannel

ProxyClientHandler

public class ProxyClientHandler extends QuantumCommonHandler {

    @Override
public void channelActive(ChannelHandlerContext ctx) throws Exception { log.info("准备注册通道");
QuantumMessage quantumMessage = new QuantumMessage();
quantumMessage.setClientId("localTest");
quantumMessage.setMessageType(QuantumMessageType.REGISTER);
ctx.writeAndFlush(quantumMessage);
super.channelActive(ctx);
} @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) { QuantumMessage quantumMessage = (QuantumMessage) msg;
if (quantumMessage.getMessageType() == QuantumMessageType.USER_DISCONNECTED) {
processUserChannelDisconnected(quantumMessage);
} else if (quantumMessage.getMessageType() == QuantumMessageType.DATA) {
processData(ctx, quantumMessage);
} else {
throw new RuntimeException("Unknown type: " + quantumMessage.getMessageType());
}
}
}

ProxyClientHandler主要有三个逻辑,与ProxyServerHandler的三个事件类型相呼应:

  1. 向ProxyServer发起注册请求,打开QuantumTunnel;
  2. 处理QuantumTunnel过来的数据,向目标服务发起真正的请求并返回结果;
  3. 处理UserChannel连接断开事件。

对流量进行内网穿透

当QuantumTunnel通道建立完成以后,便可以对外提供内网穿透服务了。

假设现在要代理UserClient的Http请求,那么UserClient应该把请求打到UserServer,再由UserServer对流量进行转发。

综上,UserServer的功能有两个:

  1. 管理UserChannel连接;
  2. 解析数据流量包的路由信息,进行转发。

UserServerHandler

public class UserServerHandler extends QuantumCommonHandler {

    //userChannel标识
private String userChannelId; //内网标识,即流量要转发到哪个网络
private String clientId; //被代理的真实服务器内网地址
private String proxyHost; //被代理服务的端口
private String proxyPort; @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
QuantumMessage message = new QuantumMessage();
byte[] bytes = (byte[]) msg;
message.setData(bytes); //解析路由信息
if (clientId == null || proxyHost == null || proxyPort == null) {
String s = new String(bytes);
clientId = getHeaderValue(s, "clientId");
proxyHost = getHeaderValue(s, "proxyHost");
proxyPort = getHeaderValue(s, "proxyPort");
} if (clientId == null || proxyHost == null || proxyPort == null) {
log.info("缺少参数,clientId={},proxyHost={},proxyPort={}", clientId, proxyHost, proxyPort);
ctx.channel().close();
} message.setClientId(clientId);
message.setMessageType(QuantumMessageType.DATA);
message.setChannelId(userChannelId);
message.setProxyHost(proxyHost);
message.setProxyPort(Integer.parseInt(proxyPort));
//封装QuantumMessage并写入QuantumTunnel,转发到对应的内部网络
boolean success = writeMessage(message);
if (!success) {
log.info("写入数据失败,clientId={},proxyHost={},proxyPort={}", clientId, proxyHost, proxyPort);
ctx.channel().close();
}
}
}

ProxyClient#doProxyRequest

当UserClient的Http请求被UserServer通过QuantumTunnel转发到了UserClient,那么最后便是发起真正的请求,拿到请求结果。

这里我之前想,如果有很多不同的应用之前协议,如Http,WebSocket等,是不是要全部都适配呢?仔细思考后发现是不需要的,因为UserClient拿到的数据包是已经封装好的应用层数据包,直接转发到对应的端口即可。

想通了以后,这个环节就比较简单了:利用Netty打开指定host+port的Channel,往里面写数据就好了。

    private void doProxyRequest(ChannelHandlerContext ctx, QuantumMessage quantumMessage) throws InterruptedException {
Channel proxyChannel = user2ProxyChannelMap.get(quantumMessage.getChannelId());
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(quantumMessage.getData().length);
//将byte数组转换成ByteBuf
buffer.writeBytes(quantumMessage.getData());
if (proxyChannel == null) {
try {
Bootstrap b = new Bootstrap();
b.group(WORKER_GROUP);
b.channel(NioSocketChannel.class);
b.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
//在ProxyRequestHandler中处理被代理服务返回的数据
pipeline.addLast(new ProxyRequestHandler(ctx, quantumMessage.getChannelId()));
}
});
//打开Channel
Channel channel = b.connect(quantumMessage.getProxyHost(), quantumMessage.getProxyPort()).sync().channel();
//把数据写入Channel
channel.writeAndFlush(buffer);
} catch (Exception e) {
throw e;
}
} else {
proxyChannel.writeAndFlush(buffer);
}
}

运行结果

QuantumTunnel主要工作在传输层,理论上可以代理所有的应用层协议唯一需要依赖应用层协议的地方是解析路由信息这部分,得益于Netty的责任链开发模式,只需要针对特定的应用层协议开发对应的解析路由信息的Handler即可(可以参考UserServerHandler实现)。

这里展示一下WebSocket(双向通信)的内网穿透效果,http内网穿透效果可以上一篇文章

最后

遇到的问题

实现过程中遇到最大的问题便是路由信息的解析,比如

  1. Netty的拆包:消息体过大或者过小时,会出现粘包和半包的问题;
  2. WebSocket的路由转发:如何获取数据帧的路由信息。

以及UserChannel和ProxyChannel连接的管理等,这些问题我会在下一篇文章和大家一起分析。

仓库地址

欢迎一起共建致力于Java领域最好的内网穿透工具:QuantumTunnel

  1. Gitee:乐天派 / quantum-tunnel
  2. GitHub:liumian97/quantum-tunnel

QuantumTunnel:Netty实现的更多相关文章

  1. QuantumTunnel:内网穿透服务设计

    背景 最近工作中有公网访问内网服务的需求,便了解了内网穿透相关的知识.发现原理和实现都不复杂,遂产生了设计一个内网穿透的想法. 名字想好了,就叫QuantumTunnel,量子隧道,名字来源于量子纠缠 ...

  2. QuantumTunnel:v1.0.0 正式版本发布

    经过一段时间运行,代码已经稳定是时候发布正式版本了! v1.0.0 正式版本发布 对核心能力的简要说明: 支持协议路由和端口路由:QuantumTunnel:端口路由 vs 协议路由 基于Netty实 ...

  3. 谈谈如何使用Netty开发实现高性能的RPC服务器

    RPC(Remote Procedure Call Protocol)远程过程调用协议,它是一种通过网络,从远程计算机程序上请求服务,而不必了解底层网络技术的协议.说的再直白一点,就是客户端在不必知道 ...

  4. 基于netty http协议栈的轻量级流程控制组件的实现

    今儿个是冬至,所谓“冬大过年”,公司也应景五点钟就放大伙儿回家吃饺子喝羊肉汤了,而我本着极高的职业素养依然坚持留在公司(实则因为没饺子吃没羊肉汤喝,只能呆公司吃食堂……).趁着这一个多小时的时间,想跟 ...

  5. 从netty-example分析Netty组件续

    上文我们从netty-example的Discard服务器端示例分析了netty的组件,今天我们从另一个简单的示例Echo客户端分析一下上个示例中没有出现的netty组件. 1. 服务端的连接处理,读 ...

  6. 源码分析netty服务器创建过程vs java nio服务器创建

    1.Java NIO服务端创建 首先,我们通过一个时序图来看下如何创建一个NIO服务端并启动监听,接收多个客户端的连接,进行消息的异步读写. 示例代码(参考文献[2]): import java.io ...

  7. 从netty-example分析Netty组件

    分析netty从源码开始 准备工作: 1.下载源代码:https://github.com/netty/netty.git 我下载的版本为4.1 2. eclipse导入maven工程. netty提 ...

  8. Netty实现高性能RPC服务器优化篇之消息序列化

    在本人写的前一篇文章中,谈及有关如何利用Netty开发实现,高性能RPC服务器的一些设计思路.设计原理,以及具体的实现方案(具体参见:谈谈如何使用Netty开发实现高性能的RPC服务器).在文章的最后 ...

  9. Netty构建分布式消息队列(AvatarMQ)设计指南之架构篇

    目前业界流行的分布式消息队列系统(或者可以叫做消息中间件)种类繁多,比如,基于Erlang的RabbitMQ.基于Java的ActiveMQ/Apache Kafka.基于C/C++的ZeroMQ等等 ...

随机推荐

  1. linux centos windows服务器修改数据库最大连接数的方法

    1.可查询数据库最大连接数 show variables like '%max_connections%'; 2.修改数据库最大连接数,最大限制上限为16384. 找到数据库配置文件my.cnf,在配 ...

  2. 大前端快闪:package.json文件知多少?

    最近在公司某项目参与了一些前端工作,作为后端抠脚大汉,改点前端细节磕磕绊绊,改点大前端.工程化.HTTP交互倒也还能做到柳暗花明. 于是打算用后端程序猿的视角记录一些{大前端}的知识快闪,也算是帮助读 ...

  3. phpmyadmin 设置密码

    例如 xampp 安装路径为 /opt/lampp/, copy 一份默认的配置 cp /opt/lampp/phpmyadmin/libraries/config.default.php /opt/ ...

  4. 『GoLang』面向对象

    我们总结一下前面看到的:Go 没有类,而是松耦合的类型.方法对接口的实现. 面向对象语言最重要的三个方面分别是:封装,继承和多态,在 Go 中它们是怎样表现的呢? Go实现面向对象的两个关键是stru ...

  5. CSS 奇技淫巧 | 妙用混合模式实现文字镂空波浪效果

    本文将介绍一个小技巧,通过混合模式 mix-blend-mode 巧妙的实现文字的镂空波浪效果. 起因 一日,一群友私聊问我.如何使用 CSS 实现下述效果,一个文字的波浪效果: 我当时想都没想,就回 ...

  6. HashMap的tableSizeFor解析

    我们都知道,对于HashMap来说,数组的容量为2的倍数,但是我们可以在创建map的时候传入一个数组的大小 此时,这个初始化数组大小会传给一个双参的构造器 1. 创建HashMap public st ...

  7. 从零入门 Serverless | Serverless Kubernetes 应用部署及扩缩容

    作者 | 邓青琳(轻零) 阿里云技术专家 导读:本文分为三个部分,首先给大家演示 Serverless Kubernetes 集群的创建和业务应用的部署,其次介绍 Serverless Kuberne ...

  8. 2020.11.6-vj补题

    A - A CodeForces - 136A 题解:按输入的顺序输出对应的下标即可,定义一个数组,将输入的作为下标,下标为值,最后依次输出即可: #include<bits/stdc++.h& ...

  9. Fiddler抓包一键生成代码

    首先我们的需求场景是 用Fiddler抓到某个接口调用后,用代码来模拟调用,一般我们写代码会有3个步骤: 1设置http请求相关的参数:header,method,url,cookie等 2设置pos ...

  10. PAT (Basic Level) Practice (中文)1017 A除以B (20分)

    1017 A除以B (20分) 本题要求计算 A/B,其中 A 是不超过 1000 位的正整数,B 是 1 位正整数.你需要输出商数 Q 和余数 R,使得 A=B×Q+R 成立. 输入格式: 输入在一 ...