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<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內網穿透效果可以上一篇文章

WebSocket內網穿透

最後

遇到的問題

實現過程中遇到最大的問題便是路由信息的解析,比如

  1. Netty的拆包:消息體過大或者過小時,會出現粘包和半包的問題;
  2. WebSocket的路由轉發:如何獲取數據幀的路由信息。

以及UserChannel和ProxyChannel連接的管理等,這些問題我會在下一篇文章和大家一起分析。

倉庫地址

目前代碼倉庫都放在Gitee上: 樂天派 / quantum-tunnel,感興趣的小夥伴在這裏可以看到完整代碼,歡迎大家拍磚~

QuantumTunnel系列文章

  1. QuantumTunnel:內網穿透服務設計

掃碼關注,一起進步

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章