netty系列之:netty初探

flydean發表於2021-08-03

簡介

我們常用瀏覽器來訪問web頁面得到相關的資訊,通常來說使用的都是HTTP或者HTTPS協議,這些協議的本質上都是IO,客戶端的請求就是In,伺服器的返回就是Out。但是在目前的協議框架中,並不能完全滿足我們所有的需求。比如使用HTTP下載大檔案,可能需要長連線等待等。
我們也知道IO方式有多種多樣的,包括同步IO,非同步IO,阻塞IO和非阻塞IO等。不同的IO方式其效能也是不同的,而netty就是一個基於非同步事件驅動的NIO框架。

本系列文章將會探討netty的詳細使用,通過原理+例子的具體結合,讓大家瞭解和認識netty的魅力。

netty介紹

netty是一個優秀的NIO框架,大家對IO的第一映像應該是比較複雜,尤其是跟各種HTTP、TCP、UDP協議打交道,使用起來非常複雜。但是netty提供了對這些協議的友好封裝,通過netty可以快速而且簡潔的進行IO程式設計。netty易於開發、效能優秀同時兼具穩定性和靈活性。如果你希望開發高效能的服務,那麼使用netty總是沒錯的。

netty的最新版本是4.1.66.Final,事實上這個版本是官方推薦的最穩定的版本,netty還有5.x的版本,但是官方並不推薦。

如果要在專案中使用,則可以引入下面的程式碼:

        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.66.Final</version>
        </dependency>

下面我們將會從一個最簡單的例子,體驗netty的魅力。

netty的第一個伺服器

什麼叫做伺服器?能夠對外提供服務的程式就可以被稱為是伺服器。建立伺服器是所有對外服務的第一步,怎麼使用netty建立一個伺服器呢?伺服器主要負責處理各種服務端的請求,netty提供了一個ChannelInboundHandlerAdapter的類來處理這類請求,我們只需要繼承這個類即可。

在NIO中每個channel都是客戶端和伺服器端溝通的通道。ChannelInboundHandlerAdapter定義了在這個channel上可能出現一些事件和情況,如下圖所示:

如上圖所示,channel上可以出現很多事件,比如建立連線,關閉連線,讀取資料,讀取完成,註冊,取消註冊等。這些方法都是可以被重寫的,我們只需要新建一個類,繼承ChannelInboundHandlerAdapter即可。

這裡我們新建一個FirstServerHandler類,並重寫channelRead和exceptionCaught兩個方法,第一個方法是從channel中讀取訊息,第二個方法是對異常進行處理。

public class FirstServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        // 對訊息進行處理
        ByteBuf in = (ByteBuf) msg;
        try {
            log.info("收到訊息:{}",in.toString(io.netty.util.CharsetUtil.US_ASCII));
        }finally {
            ReferenceCountUtil.release(msg);
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        // 異常處理
        log.error("出現異常",cause);
        ctx.close();
    }
}

上面例子中,我們收到訊息後呼叫release()方法將其釋放,並不進行實際的處理。呼叫release方法是在訊息使用完成之後常用的做法。上面程式碼將msg進行了ByteBuf的強制轉換,如果並不想進行轉換的話,可以直接這樣使用:

        try {
            // 訊息處理
        } finally {
            ReferenceCountUtil.release(msg);
        }

在異常處理方法中,我們列印出異常資訊,並關閉異常的上下文。

有了Handler,我們需要新建一個Server類用來使用Handler建立channel和接收訊息。接下來我們看一下netty的訊息處理流程。

在netty中,對IO進行處理是使用多執行緒的event loop來實現的。netty中的EventLoopGroup就是這些event loop的抽象類。

我們來觀察一下EventLoopGroup的類結構。

可以看出EventLoopGroup繼承自EventExecutorGroup,而EventExecutorGroup繼承自JDK自帶的ScheduledExecutorService。

所以EventLoopGroup本質是是一個執行緒池服務,之所以叫做Group,是因為它裡面包含了很多個EventLoop,可以通過呼叫next方法對EventLoop進行遍歷。

EventLoop是用來處理註冊到該EventLoop的channel中的IO資訊,一個EventLoop就是一個Executor,通過不斷的提交任務進行執行。當然,一個EventLoop可以註冊多個channel,不過一般情況下並不這樣處理。

EventLoopGroup將多個EventLoop組成了一個Group,通過其中的next方法,可以對Group中的EventLoop進行遍歷。另外EventLoopGroup提供了一些register方法,將Channel註冊到當前的EventLoop中。

從上圖可以看到,register的返回結果是一個ChannelFuture,Future大家都很清楚,可以用來獲得非同步任務的執行結果,同樣的ChannelFuture也是一個非同步的結果承載器,可以通過呼叫sync方法來阻塞Future直到獲得執行結果。

可以看到,register方法還可以傳入一個ChannelPromise物件,ChannelPromise它同時是ChannelFuture和Promise的子類,Promise又是Future的子類,它是一個特殊的可以控制Future狀態的Future。

EventLoopGroup有很多子類的實現,這裡我們使用NioEventLoopGroup,Nio使用Selector對channel進行選擇。還有一個特性是NioEventLoopGroup可以新增子EventLoopGroup。

對於NIO伺服器程式來說,我們需要兩個Group,一個group叫做bossGroup,主要用來監控連線,一個group叫做worker group,用來處理被boss accept的連線,這些連線需要被註冊到worker group中才能進行處理。

將這兩個group傳給ServerBootstrap,就可以從ServerBootstrap啟動服務了,相應的程式碼如下:

//建立兩個EventloopGroup用來處理連線和訊息
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new FirstServerHandler());
                        }
                    })
                    .option(ChannelOption.SO_BACKLOG, 128)
                    .childOption(ChannelOption.SO_KEEPALIVE, true);

            // 繫結埠並開始接收連線
            ChannelFuture f = b.bind(port).sync();

我們最開始建立的FirstServerHandler最作為childHandler的處理器在初始化Channel的時候就被新增進去了。

這樣,當有新建立的channel時,FirstServerHandler就會被用來處理該channel的資料。

上例中,我們還指定了一些ChannelOption,用於對channel的一些屬性進行設定。

最後,我們繫結了對應的埠,並啟動伺服器。

netty的第一個客戶端

上面我們已經寫好了伺服器,並將其啟動,現在還需要一個客戶端和其進行互動。

如果不想寫程式碼的話,可以直接telnet localhost 8000和server端進行互動即可,但是這裡我們希望使用netty的API來構建一個client和Server進行互動。

構建netty客戶端的流程和構建netty server端的流程基本一致。首先也需要建立一個Handler用來處理具體的訊息,同樣,這裡我們也繼承ChannelInboundHandlerAdapter。

上一節講到了ChannelInboundHandlerAdapter裡面有很多方法,可以根據自己業務的需要進行重寫,這裡我們希望當Channel active的時候向server傳送一個訊息。那麼就需要重寫channelActive方法,同時也希望對異常進行一些處理,所以還需要重寫exceptionCaught方法。如果你想在channel讀取訊息的時候進行處理,那麼可以重寫channelRead方法。

建立的FirstClientHandler程式碼如下:

@Slf4j
public class FirstClientHandler extends ChannelInboundHandlerAdapter {

    private ByteBuf content;
    private ChannelHandlerContext ctx;

    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        this.ctx = ctx;
        content = ctx.alloc().directBuffer(256).writeBytes("Hello flydean.com".getBytes(StandardCharsets.UTF_8));
        // 傳送訊息
        sayHello();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        // 異常處理
        log.error("出現異常",cause);
        ctx.close();
    }
    
    private void sayHello() {
        // 向伺服器輸出訊息
        ctx.writeAndFlush(content.retain());
    }
}

上面的程式碼中,我們首先從ChannelHandlerContext申請了一個ByteBuff,然後呼叫它的writeBytes方法,寫入要傳輸的資料。最後呼叫ctx的writeAndFlush方法,向伺服器輸出訊息。

接下來就是啟動客戶端服務了,在服務端我們建了兩個NioEventLoopGroup,是兼顧了channel的選擇和channel中訊息的讀取兩部分。對於客戶端來說,並不存在這個問題,這裡只需要一個NioEventLoopGroup即可。

伺服器端使用ServerBootstrap來啟動服務,客戶端使用的是Bootstrap,其啟動的業務邏輯基本和伺服器啟動一致:

        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap b = new Bootstrap();
            b.group(group)
             .channel(NioSocketChannel.class)
             .handler(new ChannelInitializer<SocketChannel>() {
                 @Override
                 protected void initChannel(SocketChannel ch) throws Exception {
                     ChannelPipeline p = ch.pipeline();
                     p.addLast(new FirstClientHandler());
                 }
             });

            // 連線伺服器
            ChannelFuture f = b.connect(HOST, PORT).sync();

執行伺服器和客戶端

有了上述的準備工作,我們就可以執行了。首先執行伺服器,再執行客戶端。

如果沒有問題的話,應該會輸出下面的內容:

[nioEventLoopGroup-3-1] INFO com.flydean01.FirstServerHandler - 收到訊息:Hello flydean.com

總結

一個完整的伺服器,客戶端的例子就完成了。我們總結一下netty的工作流程,對於伺服器端,首先建立handler用於對訊息的實際處理,然後使用ServerBootstrap對EventLoop進行分組,並繫結埠啟動。對於客戶端來說,同樣需要建立handler對訊息進行處理,然後呼叫Bootstrap對EventLoop進行分組,並繫結埠啟動。

有了上面的討論就可以開發屬於自己的NIO服務了。是不是很簡單? 後續文章將會對netty的架構和背後的原理進行深入討論,敬請期待。

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

本文已收錄於 http://www.flydean.com/01-netty-startup/

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

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

相關文章