netty系列之:小白福利!手把手教你做一個簡單的代理伺服器

flydean發表於2021-12-24

簡介

愛因斯坦說過:所有的偉大,都產生於簡單的細節中。netty為我們提供瞭如此強大的eventloop、channel通過對這些簡單東西的有效利用,可以得到非常強大的應用程式,比如今天要講的代理。

代理和反向代理

相信只要是程式設計師應該都聽過nginx伺服器了,這個超級優秀nginx一個很重要的功能就是做反向代理。那麼有小夥伴要問了,有反向代理肯定就有正向代理,那麼他們兩個有什麼區別呢?

先講一下正向代理,舉個例子,最近流量明星備受打擊,雖然被打壓,但是明星就是明星,一般人是見不到的,如果有人需要跟明星對話的話,需要首先經過明星的經紀人,有經紀人將話轉達給明星。這個經紀人就是正向代理。我們通過正向代理來訪問要訪問的物件。

那麼什麼是反向代理呢?比如現在出現了很多人工智慧,假如我們跟智慧機器人A對話,然後A把我們之間的對話轉給了後面的藏著的人,這個人用他的智慧,回答了我們的對話,交由智慧機器人A輸出,最終實現了人工智慧。這個過程就叫做反向代理。

netty實現代理的原理

那麼在netty中怎麼實現這個代理伺服器呢?

首選我們首先代理伺服器是一個伺服器,所以我們需要在netty中使用ServerBootstrap建立一個伺服器:

EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
             .channel(NioServerSocketChannel.class)
             .handler(new LoggingHandler(LogLevel.INFO))
             .childHandler(new SimpleDumpProxyInitializer(REMOTE_HOST, REMOTE_PORT))
             .childOption(ChannelOption.AUTO_READ, false)
             .bind(LOCAL_PORT).sync().channel().closeFuture().sync();

在這個local伺服器中,我們傳入ProxyInitializer。在這個handler初始化器中,我們傳入自定義的handler:

    public void initChannel(SocketChannel ch) {
        ch.pipeline().addLast(
                new LoggingHandler(LogLevel.INFO),
                new SimpleDumpProxyInboundHandler(remoteHost, remotePort));
    }

在自定義的handler中,我們使用Bootstrap建立一個client,用來連線遠端要代理的伺服器,我們將這個client端的建立放在channelActive方法中:

// 開啟outbound連線
        Bootstrap b = new Bootstrap();
        b.group(inboundChannel.eventLoop())
         .channel(ctx.channel().getClass())
         .handler(new SimpleDumpProxyOutboundHandler(inboundChannel))
         .option(ChannelOption.AUTO_READ, false);
        ChannelFuture f = b.connect(remoteHost, remotePort);

然後在client建立好連線之後,就可以從inboundChannel中讀取資料了:

outboundChannel = f.channel();
        f.addListener(future -> {
            if (future.isSuccess()) {
                // 連線建立完畢,讀取inbound資料
                inboundChannel.read();
            } else {
                // 關閉inbound channel
                inboundChannel.close();
            }
        });

因為是代理服務,所以需要將inboundChannel讀取的資料,轉發給outboundChannel,所以在channelRead中我們需要這樣寫:

    public void channelRead(final ChannelHandlerContext ctx, Object msg) {
        // 將inboundChannel中的訊息讀取,並寫入到outboundChannel
        if (outboundChannel.isActive()) {
            outboundChannel.writeAndFlush(msg).addListener((ChannelFutureListener) future -> {
                if (future.isSuccess()) {
                    // flush成功,讀取下一個訊息
                    ctx.channel().read();
                } else {
                    future.channel().close();
                }
            });
        }
    }

當outboundChannel寫成功之後,再繼續inboundChannel的讀取工作。

同樣對於client的outboundChannel來說,也有一個handler,在這個handler中,我們需要將outboundChannel讀取到的資料反寫會inboundChannel中:

    public void channelRead(final ChannelHandlerContext ctx, Object msg) {
        // 將outboundChannel中的訊息讀取,並寫入到inboundChannel中
        inboundChannel.writeAndFlush(msg).addListener((ChannelFutureListener) future -> {
            if (future.isSuccess()) {
                ctx.channel().read();
            } else {
                future.channel().close();
            }
        });
    }

當inboundChannel寫成功之後,再繼續outboundChannel的讀取工作。

如此一個簡單的代理伺服器就完成了。

實戰

如果我們將本地的8000埠,代理到www.163.com的80埠,會發生什麼情況呢?執行我們的程式,訪問http://localhost:8000, 我們會看到下面的頁面:

為什麼沒有如我們想象的那樣展示正常的頁面呢?那是因為我們代理過去之後的域名是localhost,而不是正常的www.163.com, 所以伺服器端不認識我們的請求,從而報錯。

總結

本文的代理伺服器之間簡單的轉發請求,並不能夠處理上述的場景,那麼該怎麼解決上面的問題呢? 敬請期待我的後續文章!

本文的例子可以參考:learn-netty4

本文已收錄於 http://www.flydean.com/35-netty-simple-proxy/

最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!

歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你!

相關文章