QuantumTunnel:Netty实现

QuantumTunnel:Netty实现,第1张

QuantumTunnel:Netty实现

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

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

  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() {
                    @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连接的管理等,这些问题我会在下一篇文章和大家一起分析。

仓库地址

目前代码仓库都放在Gitee上: 乐天派 / quantum-tunnel,感兴趣的小伙伴在这里可以看到完整代码,欢迎大家拍砖~

QuantumTunnel系列文章
  1. QuantumTunnel:内网穿透服务设计

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/3979724.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-10-21
下一篇 2022-10-21

发表评论

登录后才能评论

评论列表(0条)

保存