Thinking in Netty

Thinking in Netty

By 謝樂

1. 寫在前面

Netty的詞根爲net, 那麼我們就已經猜想到它與網絡有關。官方對Netty的解釋爲:

Netty是一種異步的基於事件驅動的Java網絡應用框架,可用於構建高性能的協議服務器與客戶端。

像我司以及其他互聯網公司,例如Alibaba,Facebook等。有很多內部的中間件,分佈式框架。而這些產品或應用的底層很多都用到了Netty或基於Netty的再開發產品。姑且不用再過多描述Netty是什麼吧,單是出於好奇的原因,我便想一探究竟,本文不會涉及到很多Netty的使用,比如編解碼,協議棧開發,本文是對Netty的底層原理,設計思想的探索。如果需要的話,請參考《Netty in Action>》, 《Netty權威指南》等書籍。

2. Netty快速開始

從一個經典的Echo服務器Demo開始.

package cn.netty;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.FixedLengthFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;

/**
 * @author Xie le
 * @date 2016/4/13
 */
public class EchoServer {

    public static void main(String[] args) throws Exception {

        int port = 8080;
        new EchoServer().bind(port);
    }

    public void bind(int port) throws Exception {

        //配置線程組
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {

            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 1024)
                    .handler(new LoggingHandler(LogLevel.DEBUG))
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {

                            ch.pipeline().addLast(new LineBasedFrameDecoder(128)) // DelimiterBasedFrameDecoder
                                    .addLast(new StringDecoder())
                                    .addLast(new EchoServerHandler());

                        }
                    });

            //綁定端口,同步等待
            ChannelFuture future = bootstrap.bind(port).sync();

            future.channel().closeFuture().sync();

        } finally {

            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }


    private class EchoServerHandler extends ChannelHandlerAdapter {
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            System.out.println("Receive Client : [" + msg + "]");
            ctx.writeAndFlush(msg);
        }

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            cause.printStackTrace();
            ctx.close();
        }
    }
}

如上代碼演示了Netty作爲服務端的示例。啓動EchoServer,然後通過Telnet 輸入命令:

telnet 127.0.0.1 8080

然後觀察Echo行爲,雖然代碼很短,但是五臟俱全。當然此處只是簡單一窺而已,更多內容還在後面。

3. Netty的架構

這裏寫圖片描述

這是http://netty.io 官網上的Netty架構圖。可以看到,Netty包含幾個部分:第一是傳輸服務,第二是協議支持,第三是底層核心部分:具有靈活的事件模型,提供通用通信API,還具有零拷貝的字節緩衝能力。

傳輸服務則包含有Socket和UDP通信服務,HTTP信道,虛擬機管道服務。協議支持則包含HTTP和WebSocket協議,還有壓縮,大文件傳輸,Google Protobuf協議等。

4. Netty的線程模型

Netty的線程模型綜合了Reactor線程模型,說到Reactor模型可以參考我的上一篇文章「Java IO模型&NIO」。
Netty的線程模型與文中的三種Reactor線程模型相似,下面章節我們通過Netty服務端和客戶端的線程處理流程圖來介紹Netty的線程模型。需要說明的是,使用的版本是Netty 5,部分代碼與Netty 4 有出入。

4.1 服務端線程模型

Netty中通用的做法是將服務端監聽線程和IO線程分離,類似於Reactor多線程模型,它的工作原理如圖:
這裏寫圖片描述
注:上圖來源於李林峯的文章,本節的學習思路源於該文,感謝他的分享。

用戶一般通過Web服務器或Main程序來啓動Netty服務(類似於上述示例中的EchoServer代碼)。Netty指定兩個線程池組EventLoopGroup 作爲主從線程池, 一個EventLoopGroup 即爲EventLoop線程組,負責管理EventLoop的創建和回收。EventLoopGroup管理的線程數可以通過構造函數設置,如果沒有設置,默認取-Dio.netty.eventLoopThreads,如果該系統參數也沒有指定,則爲可用的CPU內核數 × 2。
bossGroup線程組實際就是Acceptor線程池,負責處理客戶端的TCP連接請求,如果系統只有一個服務端端口需要監聽,則建議bossGroup線程組線程數設置爲1。
workerGroup是真正負責I/O讀寫操作的線程組,通過ServerBootstrap的group方法進行設置,用於後續的Channel綁定。
但Netty5引入了PipeLine模式,在原有的模型上作了一些改動。下圖是我的理解:
這裏寫圖片描述
Netty從主NIOEventLoop線程池分配好一個線程來作爲Acceptor線程,用以接收連接並監聽網絡事件。但ServerBootstrap在初始化時,會爲創建好的Channel上賦予一個管道ChannelPipeline,並把worker線程池綁定到該Pipeline上。Acceptor線程感知到連接事件後,會先創建SocketChannel,然後通過Pipeline在workerGroup中選取一個EventLoop線程作爲IO處理線程,負責網絡消息的讀寫,並把新建的SocketChannel註冊到Selector上,以監聽其他事件。
說到這裏,需要先說明一下NIO的Server編程步驟,我們遵循源碼就是最好的說明原則,直接show代碼。

    /**
     * 獲得一個ServerSocket通道,並對該通道做一些初始化的工作
     * @param port
     * @throws IOException
     */
    public NIOServer initServer(int port) throws IOException{

        //獲得一個ServerSocket通道
        final ServerSocketChannel serverChannel = ServerSocketChannel.open();

        //設置通道爲非阻塞
        serverChannel.configureBlocking(false);

        //將該通道對應的ServerSocket綁定到port端口
        serverChannel.socket().bind(new InetSocketAddress(port));

        //獲得一個通道管理器
        this.selector =  Selector.open();

        /**
         * 將通道管理器和該通道綁定,併爲該通道註冊服務端接收客戶端連接事件(SelectionKey.OP_ACCEPT)
         * 註冊該事件後,當該事件到達時,selector.select()會返回,如果沒有到達,selector.select()
         * 會一直阻塞
         *
         */
        serverChannel.register(this.selector, SelectionKey.OP_ACCEPT);

        serverChannel.validOps();

        return  this;
    }

NIO的Server編程一般會先初始化一個ServerSocketChannel,表示服務端的SocketChannel,這個類似於BIO中的ServerSocket,其負責在服務端監聽特定端口並作請求接收。然後再打開一個Selector,表示多路複用選擇器,然後把ServerSocketChannel註冊到Selector上,用以在ServerSocketChannel上選擇各種網絡事件。
Netty作爲NIO框架,雖賦予了很多靈活性和便捷性,但其底層實現還是依賴於NIO,我們從Netty的代碼去剖析。
第一、創建ServerSocketChannel與註冊
ServerBootstrap啓動時,在channel()方法中指定Channel類型爲NioServerSocketChannel.class
然後在bind方法中會實例化該Channel,代碼引用棧如下:

    bootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
    ...

    public B channel(Class<? extends C> channelClass) {
        if (channelClass == null) {
            throw new NullPointerException("channelClass");
        }
        return channelFactory(new ReflectiveChannelFactory<C>(channelClass));
    }
   AbstractBootstrap#initAndRegister
   final ChannelFuture initAndRegister() {
        final Channel channel = channelFactory().newChannel();
        try {
            init(channel);
        }
        ChannelFuture regFuture = group().register(channel);
        ....
    }

第二、ServerBootstrap通過group()和register方法把Channel註冊給一個bossGroup中的線程,並以該線程作爲Acceptor線程,然後監聽服務端。ServerBootstrap選擇一個Acceptor線程的主要邏輯如下:

MultithreadEventLoopGroup.java
public ChannelFuture register(Channel channel) {
        return next().register(channel);
}
MultithreadEventExecutorGroup.java
private final class PowerOfTwoEventExecutorChooser implements EventExecutorChooser {
        @Override
        public EventExecutor next() {
            return children[childIndex.getAndIncrement() & children.length - 1];
        }
}

第三、然後在io.netty.channel.SingleThreadEventLoop#register(io.netty.channel.Channel)方法中把Channel註冊到Group中的Acceptor線程,由該Acceptor線程來把Channel註冊到Selector上,並作監聽。代碼調用棧的主要邏輯如下:

io.netty.channel.SingleThreadEventLoop#register(io.netty.channel.Channel)
channel.unsafe().register(this, promise);
protected void doRegister() throws Exception {
        boolean selected = false;
        for (;;) {
            try {
                selectionKey = javaChannel().register(((NioEventLoop) eventLoop().unwrap()).selector, 0, this);
                return;
          }
....

第四、最後的註冊實際上也是調用的java.nio.channels.spi.AbstractSelectableChannel#register方法,意爲由NIO本身的API來完成註冊。

第五、Selector開始在Acceptor線程中在ServerChannel上監聽網絡事件。由於之前我們指定的線程爲NioEventLoop,因此這個職責很有可能在該類中,我們之間看代碼:

io.netty.channel.nio.NioEventLoop#run
 try {
            if (hasTasks()) {
                selectNow();
            } else {
                select(oldWakenUp);
           ....
  }
private static void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
           ....
        try {
            int readyOps = k.readyOps();
            //讀事件,接收連接
            if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
                unsafe.read();
                if (!ch.isOpen()) {
                    // Connection already closed - no need to handle write.
                    return;
                }
            }
            .....
}

第六、處理讀或接收連接事件,調用了unsafe.read()方法。由於服務端我們指定定時NioServerSocketChannel, 而NioServerSocketChannel繼承了AbstractNioMessageChannel, 所以unsafe的read方法在AbstractNioMessageChannel這個類中。

private final class NioMessageUnsafe extends AbstractNioUnsafe {
     public void read() {
         ....
         for (;;) {
             int localRead = doReadMessages(readBuf);
         }
     }
}
再看看doReadMessages方法就知道,NioServerSocketChannel創建了SocketChannel,以建立與客戶端的通道連接。
protected int doReadMessages(List<Object> buf) throws Exception {
        SocketChannel ch = javaChannel().accept();
        try {
            if (ch != null) {
                buf.add(new NioSocketChannel(this, ch));
                return 1;
            }
 }

第七、把新建的SocketChannel註冊到workerGroup線程池中,以開始消息交互。
觸發的入口在AbstractNioMessageChannel類的read方法中:

for (int i = 0; i < size; i ++) {
                    pipeline.fireChannelRead(readBuf.get(i));
 }

需要說明的是,在ServerBootstrap的init方法中把workerGroup線程池綁定到了Pipeline中.

ch.pipeline().addLast(new ServerBootstrapAcceptor(
                        currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));

ServerBootstrapAcceptorchannelRead方法則把新建的SocketChannel賦予到了childGroup中,
其中childGroup就是之前的workerGroup, 從中選擇一個I/O線程負責網絡消息的讀寫。

...
childGroup.register(child)
...

register會把新建的SocketChannel註冊到Selector上,然後開始正式的通信。如果有讀事件來臨,則又會觸發processSelectedKey方法,從而進入數據處理與交互過程。

4.1 客戶端線程模型

客戶端線程模型相對要簡潔一些,如通過閱讀源碼掌握了服務端的線程模型,那麼客戶端的線程模型則會很容易理解。其工作原理大致如下:
這裏寫圖片描述

源碼分析暫就不屠版了。

5. Netty的API設計

其實API需要多用多練才能更好的掌握與熟悉。Netty作爲NIO框架,它的API幾乎都是圍繞NIO的API來封裝或優化的,旨在提供便利,高效率的功用。

我們先看看Netty對NIO的Channel的封裝

和Channel相關的接口及類結構圖如下:
這裏寫圖片描述
1)當前Channel的狀態信息,比如是打開還是關閉等。
2)通過ChannelConfig可以得到的Channel配置信息。
3)Channel所支持的如read、write、bind、connect等IO操作。
4)得到處理該Channel的ChannelPipeline,既而可以調用其做和請求相關的IO操作。

在Channel實現方面,以通常使用的nio socket來說,Netty中的NioServerSocketChannel和NioSocketChannel分別封裝了java.nio中包含的 ServerSocketChannel和SocketChannel的功能。
注:此處內容參考自ImportNew上的文章, 覺得該作者總結的很好

其次是ByteBuf
ByteBuf是對NIO中的ByteBuffer的封裝,新增了很多對字節的便利的方法,還提供了很多類型的ByteBuf, 比如是直接分配到堆上的Buf, 還有分配到對象池中的Buf,以節省空間, 還有原生內存Buf, 分配和回收效率更高。實際使用時,根據需要選擇合適的ByteBuf就行。我們借用《Netty權威指南》中的一張圖來加深理解。

這裏寫圖片描述

6. Netty的通信過程

Debug是最好的跟蹤方式,也是最好的源碼閱讀引導。以上文中的EchoServer爲例,跟蹤一下Netty服務端的數據處理以及整個通信過程。啓動EchoServer後,使用telnet 發送一段報文hello, netty.
按照上文分析的邏輯,客戶端對應的SocketChannel由IO線程來處理,Selector感知到讀事件後,
io.netty.channel.nio.NioEventLoop#processSelectedKey()方法中調用NioUnsafe來讀取數據。
Netty按照默認的緩存區來讀取字節數據,並讀入到Bytebuf中,然後開始進入管道,由管道中的Handler層層處理。首先說明,在EchoServer中我們爲客戶端的Channel指定了三個Handler,說明如下:
LineBasedFrameDecoder 換行解碼,
StringDecoder 字符串解碼,
EchoServerHandler 業務處理。
運行中的Debug斷點截圖如下:

這裏寫圖片描述

可以看到,客戶端請求的數據經過Pipeline中的Handler層層編碼,最後把實際報文傳遞到業務Handler中。即在EchoServerHandler的channelRead方法中拿到的數據已經爲hello, netty.

這裏寫圖片描述

在示例中,業務處理很簡單,只做了echo輸出,一般正式的開發中,會自定義很多業務Handler來完成複雜的協議棧開發。我們跟蹤一下echo回寫的過程。

ctx.writeAndFlush -> ChannelHandlerInvokerUtil.invokeFlushNow -> NioSocketChannel.doWrite -> SocketChannel.write

可以看到,最後仍是調用了SocketChannel的寫方法,是爲又回到了NIO的世界中。

7. Netty中的設計模式

Netty作爲一個優秀的框架用到了很多設計模式。我主要分析一下Netty中的ChannelPipeline,每個創建好的ChannelNetty會爲該Channel創建一個Pipeline,用以處理或攔截入站的事件和出站的操作。ChannelPipeline是一種攔截過濾器模式的實現形式,可以讓用戶更好的控制事件以及讓Pipeline中各個ChannelHandler的交互更便利。
攔截過濾器模式J2EE的核心設計模式之一。它可以創建一種可插拔的過濾器,並用一種標準的方式來處理通用的服務,而不用修改核心的請求處理代碼。過濾器攔截入站的請求和出站的響應,並可以作前置處理和後置處理。
過濾攔截器模式的類圖如下:
這裏寫圖片描述

引入參與者和劃分職責後,時序圖如下:
這裏寫圖片描述

FilterManager
FilterManager管理過濾處理。它用相關的過濾器來創建FilterChain,同時規定順序,然後開始處理請求。

FilterChain
FilterChain是一組有序的互相獨立的filters.

FilterOne,FilterTwo,FilterThree
這些是映射到target的獨立的filter,FilterChain會協調他們之間的處理。

*Target
Target是客戶端的請求資源。

Netty中的ChannelPipeline是這種模式的實現。在ChannelPipeline中主要關聯ChannelHandler,可以通過addFirst,addLast,addBeforeremove方法來添加或移除ChannelHandler。DefaultChannelPipeline是ChannelPipeline的默認實現,採用自定義的鏈表結構來組合ChannelHandler。
這個部分分析的是過濾攔截器模式,而ChannelPipeline的更多實現細節可參看官方API和源碼。

8. 最後

Netty很繁雜,層層抽象,層層封裝。閱讀時需要抽絲剝繭,捋一條主線,比如通信過程,或者線程模型這些主線。我閱讀時是花了很多時日,如有錯誤,歡迎指出。

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