Netty原始碼分析之一次請求是如何到達channelRead的?

爬蜥發表於2019-03-13

以下分析只講NIO

使用java nio做網路程式設計大致流程如下

Netty原始碼分析之一次請求是如何到達channelRead的?

這個流程有哪些可以優化的空間?

java nio使用簡介
java nio 啟動原始碼分析

Netty是對java網路框架的包裝,它本身肯定也會有類似的處理流程。必定在這個方面做了自己的優化處理

Netty 使用入門
Netty Hello world原始碼分析

獲得Selector

使用Netty的時候都會用到對應的EventLoopGroup,它實際上就完成了Selector的初始化過程

Netty自定義了SelectionKey的集合,做了層包裝,實際將Selector只有1個SelectorKey的集合換成了預設的兩個集合

Netty原始碼分析之一次請求是如何到達channelRead的?

獲得Channel

使用Netty時會執行channel的型別,然後在執行bind方法時,此處就會對channel實行初始化

構建的方式為 class.newInstance(),以NioServerSocketChannel為例,它執行的就是對應的無參建構函式。

 public NioServerSocketChannel() {
 		//newSocket即返回java的ServerSocketChannel
        this(newSocket(DEFAULT_SELECTOR_PROVIDER));
 }
 public NioServerSocketChannel(ServerSocketChannel channel) {
 		//指定當前channel用來接收連線請求,並在父類中指定為非阻塞
        super(null, channel, SelectionKey.OP_ACCEPT);
        //javaChannel()即這裡的引數channel
        config = new NioServerSocketChannelConfig(this, javaChannel().socket());
}
複製程式碼

緊接著Netty開始channel的初始化,在NioServerSocketChannel的pipeline最後新增了一個ChannelInboundHandlerAdapterServerBootstrapAcceptor,它會執有 childGroupchildHandler,childHandler即使用者自定義的channelHandler,而childGroup則是處理請求所用的EventLoop,此時整個pipeline的結構為

childGroup為原始碼中欄位的命名,對應為group中傳遞的worker執行緒池

Netty原始碼分析之一次請求是如何到達channelRead的?

channel的註冊與監聽埠地址關聯

註冊即建立channel和Selector的關係,值得注意的是,註冊使用的執行緒池為group,對應使用者傳入的執行緒池即boss執行緒池,註冊和埠、地址關聯則順著Netty的啟動流程進行

至此可以看到,java nio所需要的準備工作都已經準備好了,剩下的就是等待事件發生以及處理髮生的事件。與普通java nio的不同之處在於

  • Netty準備了兩個執行緒池,channel註冊、埠繫結監聽的只用到了其中同一個執行緒池

等待事件發生

NioEventLoop實現了Executor,意味著它接受其它地方提交任務給它執行,execute的大致結構如下

//判斷當前正在執行的執行緒是否是Netty自己的eventLoop中儲存的執行緒
boolean inEventLoop = inEventLoop();
  if (inEventLoop) {
    //往佇列裡新增任務
  	addTask(task);
  } else {
  	//這裡即執行NioEventLoop自身的run方法
	startThread();
  	addTask(task);
  }
複製程式碼

NioEventLoop啟動執行緒執行run方法,整體結構如下

for (;;) {
 if (hasTasks()) {
    selectNow();
   } else {
    select(oldWakenUp);
   }
  processSelectedKeys();
  runAllTasks();
}
複製程式碼

run迴圈處理的流程如下

Netty原始碼分析之一次請求是如何到達channelRead的?

值得注意的是,這是單個執行緒在執行,而且非本執行緒的任務一概不處理

boss執行緒的啟動時機

在啟動的過程中,有ServerBootstrap來串起整個流程,它的執行執行緒為主執行緒,而註冊事件都是交由執行緒池自己來執行的,用程式表達來講,就是執行了eventLoop自己的execute,此時執行執行緒必定不是EventLoop自己的執行緒,從而boss中的執行緒啟動,在佇列任務中完成註冊

新連線請求的到來

當NioServerSocketChannel繫結了埠之後,NioServerSocketChannel對應的NioEventLoop會等待channel發生事件。整個處理流程如下

Netty原始碼分析之一次請求是如何到達channelRead的?

  1. 讀取訊息的內容,發生在NioServerSocketChannel,對於這個新的連線事件,則包裝成一個客戶端的請求channel作為後續處理

    protected int doReadMessages(List<Object> buf) throws Exception {
     		//1:獲取請求的channel
            SocketChannel ch = javaChannel().accept();
    
            try {
                if (ch != null) {
                	//2:包裝成一個請求,Socket channel返回
                    buf.add(new NioSocketChannel(this, ch));
                    return 1;
                }
            } catch (Throwable t) {
                logger.warn("Failed to create a new channel from an accepted socket.", t);
    
                try {
                    ch.close();
                } catch (Throwable t2) {
                    logger.warn("Failed to close a socket.", t2);
                }
            }
    
            return 0;
        }
    複製程式碼
  2. 返回的NioSocketChannel則完成自身channel的初始化,註冊感興趣的事件

     protected AbstractNioByteChannel(Channel parent, SelectableChannel ch) {
            super(parent, ch, SelectionKey.OP_READ);
    }
    複製程式碼

回想到boss中的下一環即ServerBootstrapAcceptor,而它讀取訊息的處理則是新增使用者自己的handler,並繼續完成註冊事件

 public void channelRead(ChannelHandlerContext ctx, Object msg) {
            final Channel child = (Channel) msg;

            child.pipeline().addLast(childHandler);

            for (Entry<ChannelOption<?>, Object> e: childOptions) {
                try {
                    if (!child.config().setOption((ChannelOption<Object>) e.getKey(), e.getValue())) {
                        logger.warn("Unknown channel option: " + e);
                    }
                } catch (Throwable t) {
                    logger.warn("Failed to set a channel option: " + child, t);
                }
            }

            for (Entry<AttributeKey<?>, Object> e: childAttrs) {
                child.attr((AttributeKey<Object>) e.getKey()).set(e.getValue());
            }

            try {
                childGroup.register(child).addListener(new ChannelFutureListener() {
                    @Override
                    public void operationComplete(ChannelFuture future) throws Exception {
                        if (!future.isSuccess()) {
                            forceClose(child, future.cause());
                        }
                    }
                });
            } catch (Throwable t) {
                forceClose(child, t);
            }
        }
複製程式碼

worker執行緒的啟動時機

worker的註冊發生在boss的執行緒執行中,此刻必定不是同一個執行緒,因而開始啟動worker的執行緒,並在內部完成註冊事件,等待讀訊息的到來

OP_read訊息處理

連線建立後的請求則是交由NioSocketChannel來處理,它將讀到的訊息封裝成ByteBuf,通過InBound處理器fireChannelRead依次傳給其它的地方消費,一直到tailContext訊息處理完畢

此處也可以得知管道的 in 表示資料傳入netty,回寫則是通過 out 一直到Head然後寫入channel

Netty中Nio的處理流程

從上述分析可以得到,Netty的處理流程如下

Netty原始碼分析之一次請求是如何到達channelRead的?

boss是否需要多個執行緒

mainReactor 多執行緒配置 ,對於多個埠監聽是有益的,當然1個也可以處理多埠

Reactor模式

CPU的處理速度快於IO處理速度,在處理事情時,最佳情況是CPU不會由於IO處理而遭到阻塞,造成CPU的”浪費“,當然可以用多執行緒去處理IO請求,但是這會增加執行緒的上下文切換,切換過去可能IO操作也還沒有完成,這也存在浪費的情況。

另一種方式是:當IO操作完成之後,再通知CPU進行處理。那誰來知曉IO操作完成?並將事件講給CPU處理呢?在Reactor模式中,這就是Reactor的作用,它啟動一個不斷執行的執行緒來等待IO發生,並按照事件型別,分發給不同的事先註冊好的事件處理器來處理

Reactor模式抽象如下

Netty原始碼分析之一次請求是如何到達channelRead的?

抽象圖由作者提供
reactor參考

相關文章