用Netty實現(xiàn)Ngrok Client【原創(chuàng)】

什么是Ngrok


有時候我們需要臨時將本地運行的web項目發(fā)布到公網(wǎng),但沒有公網(wǎng)ip,或者需要在家訪問公司內(nèi)網(wǎng)上的某臺電腦的某個端口。這個時候就需要借助Ngrok來實現(xiàn)上述目的,Ngrok是一個內(nèi)網(wǎng)穿透工具。

如何使用Ngrok


一套完整的Ngrok包含兩個部分:Ngrok Server和Ngrok Client

Ngrok Server 需要部署在有公網(wǎng)ip的服務器上,Ngrok Client則可以部署在任意能夠訪問外網(wǎng)的電腦上。

當客戶端啟動且與服務端連接交換信息后,服務端會分配一個端口給客戶端(例如52228),與客戶端建立一條新的tcp連接,此后,通過訪問服務端的52228端口,就相當于訪問客戶端中配置的需要被發(fā)布到公網(wǎng)的端口。

有人可能會問,“我本來就沒有公網(wǎng)ip,如何部署Ngrok Server?”,對此,可以去Ngrok官網(wǎng)注冊用戶,使用Ngrok官網(wǎng)提供的Ngrok Server。

由于本片主要講解Ngrok Client的Netty實現(xiàn),具體部署過程不做詳解。

Ngrok網(wǎng)絡協(xié)議


Ngrok官方客戶端采用C編寫,也有網(wǎng)友提供了Python的實現(xiàn),以下網(wǎng)絡協(xié)議通過分析Python版的源碼得到。
Ngrok網(wǎng)絡協(xié)議的數(shù)據(jù)交換過程如下圖所示:

image

上圖不包含client和server之間的心跳包數(shù)據(jù)(client端口1和server端口1之間通過心跳維持連接)

各個端口含義:

server端口1 : server啟動時配置的監(jiān)聽端口,默認是4443

client端口1 : client與server端口1建立連接時的端口,由操作系統(tǒng)分配。

server端口2 : client 發(fā)送ReqTunnel請求中攜帶的要求server暴露的端口,若client不指定端口,則是server隨機分配的一個端口。

client端口2: client與server建立的另一個用來轉發(fā)代理數(shù)據(jù)的端口,由操作系統(tǒng)分配。

client端口3:client與本地服務建立連接時的端口,由操作系統(tǒng)分配。

協(xié)議的具體數(shù)據(jù)內(nèi)容:

Auth:

{ "Type": "Auth", "Payload": { "ClientId": "", "OS": "darwin", "Arch": "amd64", "Version": "2", "MmVersion": "1.7", "User": "user", "Password": "" }}

AuthResp:

{"Type":"AuthResp","Payload":{"Version":"2","ClientId":"d720a2bcb084f5669d7ef7af7fd8ad9c","Error":"","MmVersion":"1.7"}}

ReqTunnel:

{"Type": "ReqTunnel", "Payload": {"ReqId": "jhnl8GF3", "Protocol": "tcp", "Hostname": "", "Subdomain": "www", "HttpAuth": "", "RemotePort": 55499}}

ReqProxy:

{"Type":"ReqProxy","Payload":{}}

RegProxy:

{"Type": "RegProxy", "Payload": {"ClientId": "d720a2bcb084f5669d7ef7af7fd8ad9c"}}

NewTunnel:

{"Type":"NewTunnel","Payload":{"Error":"","ReqId":"jhnl8GF3","Protocol":"tcp","Url":"tcp://codewjy.top:55499"}}

Ping:

{"Type":"Ping","Payload":{}}

Pong:

{"Type":"Pong","Payload":{}}

通過Netty實現(xiàn)


了解了ngrok的網(wǎng)絡協(xié)議,下面通過netty實現(xiàn)這一協(xié)議

按照協(xié)議的先后順序,一步一步實現(xiàn),

首先是client與server的控制連接的建立(上圖中client端口1和server端口1的連接),同時也是客戶端的啟動入口NgrokClient:

/**
 * HOST: ngrok服務端域名
 * PORT: ngrok服務端控制端口
 * REMORTE_PORT: ngrok服務端代理端口
 * LOCAL_PORT: 本地需要被暴露出來的端口
 */
    static final String HOST = "codewjy.top";
    static final int PORT = 4454;
    static final int REMORTE_PORT = 55499;
    static final int LOCAL_PORT = 8080;

    public static void main(String[] args) {
        new NgrokClient().start();
    }

    private void start() {
        NioEventLoopGroup group = new NioEventLoopGroup(1);
        Bootstrap b = new Bootstrap();
        try {
            b.group(group)
                    .channel(NioSocketChannel.class)
                    .option(ChannelOption.TCP_NODELAY, true)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        protected void initChannel(SocketChannel ch) throws SSLException {
                            SSLEngine engine = SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE).build().newEngine(ch.alloc());
                            ChannelPipeline p = ch.pipeline();
                            //ssl處理器
                            p.addFirst(new SslHandler(engine,false));
                            //以下兩個處理器組成心跳處理器
                            p.addLast(new IdleStateHandler(5, 20, 0, TimeUnit.SECONDS));
                            p.addLast(new HeartBeatHandler());
                            //主控制處理器
                            p.addLast(new ControlHandler());
                        }
                    });
            ChannelFuture f = b.connect(NgrokClient.HOST, NgrokClient.PORT).sync();
            f.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }


ControlHandler部分代碼

    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        //channel激活的時候發(fā)送Auth
        ctx.channel().writeAndFlush(GenericUtil.getByteBuf(AUTH));
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ByteBuf byteBuf) throws Exception {
        if (byteBuf.isReadable()) {
            int rb = byteBuf.readableBytes();
            if (rb > 8) {
                CharSequence charSequence = byteBuf.readCharSequence(rb, Charset.defaultCharset());
                JSONObject jsonObject = JSON.parseObject(charSequence.toString());
                if ("AuthResp".equals(jsonObject.get("Type"))) {
                    //收到AuthResp響應
                    clientId = jsonObject.getJSONObject("Payload").getString("ClientId");
                    ctx.channel().writeAndFlush(GenericUtil.getByteBuf(PING));
                    //發(fā)送ReqTunnel
                    ctx.channel().writeAndFlush(GenericUtil.getByteBuf(REQ_TUNNEL));
                }else if ("ReqProxy".equals(jsonObject.get("Type"))) {
                    //收到ReqProxy響應
                    Bootstrap b = new Bootstrap();
                    try {
                        b.group(group)
                                .channel(NioSocketChannel.class)
                                .option(ChannelOption.TCP_NODELAY, true)
                                .handler(new ChannelInitializer<SocketChannel>() {
                                    protected void initChannel(SocketChannel ch) throws SSLException {
                                        SSLEngine engine = SslContextBuilder.forClient()
                                                .trustManager(InsecureTrustManagerFactory.INSTANCE)
                                                .build()
                                                .newEngine(ch.alloc());
                                        ChannelPipeline p = ch.pipeline();
                                        //ssl處理器
                                        p.addFirst(new SslHandler(engine,false));
                                        //代理處理器
                                        p.addLast(new ProxyHandler(clientId));
                                    }
                                });
                        ChannelFuture f = b.connect(NgrokClient.HOST, NgrokClient.PORT).sync();
                        logger.info("connect to remote address "+f.channel().remoteAddress());
                        f.channel().closeFuture().addListener((ChannelFutureListener) channelFuture -> logger.info("disconnect to remote address "+f.channel().remoteAddress()));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }else if ("NewTunnel".equals(jsonObject.get("Type"))) {
                    logger.info(jsonObject.toJSONString());
                }
            }
        }

    }

以上代碼完成了client和server的握手
下面處理server發(fā)起開始代理部分的協(xié)議,也就是ProxyHandler的內(nèi)容
ProxyHandler部分代碼

    //持有連接到本地服務的channel,用于將數(shù)據(jù)轉發(fā)給本地服務
    private ChannelFuture f;

    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        //channel激活后,發(fā)送RegProxy
        ctx.channel().writeAndFlush(GenericUtil.getByteBuf(REG_PROXY));
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws InterruptedException {
        ByteBuf byteBuf = (ByteBuf) msg;
        if (byteBuf.isReadable()) {
            int rb = byteBuf.readableBytes();
            if (rb > 8) {
                if (!init){
                    CharSequence charSequence = byteBuf.readCharSequence(rb, Charset.defaultCharset());
                    JSONObject jsonObject = JSON.parseObject(charSequence.toString());
                    if ("StartProxy".equals(jsonObject.get("Type"))) {
                        logger.info("=====StartProxy=====");
                        Bootstrap b = new Bootstrap();
                        b.group(group)
                                .channel(NioSocketChannel.class)
                                .option(ChannelOption.TCP_NODELAY, true)
                                .handler(new ChannelInitializer<SocketChannel>() {
                                    protected void initChannel(SocketChannel ch) {
                                        ChannelPipeline p = ch.pipeline();
                                        //傳入當前channel,用于將數(shù)據(jù)寫回給ngrok server
                                        p.addLast(new FetchDataHandler(ctx.channel()));
                                    }
                                });
                        //連接本地服務
                        f = b.connect("127.0.0.1", NgrokClient.LOCAL_PORT).sync();
                        logger.info("connect local port:"+f.channel().localAddress());
                        f.channel().closeFuture().addListener((ChannelFutureListener) t -> {
                            logger.info("disconnect local port:"+f.channel().localAddress());
                            init = false;
                        });
                        init = true;
                    }
                }else {
                    //將用戶請求數(shù)據(jù)轉發(fā)給本地服務
                    logger.info("ProxyHandler write message to local port "+f.channel().localAddress()+":"+byteBuf.toString((CharsetUtil.UTF_8)));
                    f.channel().writeAndFlush(byteBuf.copy());

                }
            }
        }
    }

最后一步是ngrok連接本地服務后,完成的工作:

    private Channel channel;
    //傳入連接到 ngrok server的channel
    FetchDataHandler(Channel channel) {
        this.channel=channel;
    }

    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf) throws Exception {
        //將本地服務的數(shù)據(jù)寫回給ngrok server的channel
        logger.info("FatchDataHandler write message to remote address " +channel.remoteAddress()+":"+ byteBuf.toString(CharsetUtil.UTF_8));
        channel.writeAndFlush(byteBuf.copy());
    }

以上便是ngrok client 的netty實現(xiàn)過程。
源碼可前往我的github查看:Ngrok Client Java.

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容